<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Tabstack Blog</title><description>Practical guides for adding current web research, clean page content, matching JSON, and public browser tasks to models and applications you control.</description><link>https://tabstack.ai</link><language>en-us</language><item><title>Redact PII before your model searches the web</title><link>https://tabstack.ai/blog/redact-pii-before-web-research</link><guid isPermaLink="true">https://tabstack.ai/blog/redact-pii-before-web-research</guid><description>Build a loop where Rampart redacts personal details on your machine, Tabstack researches the redacted question, and your own model writes the reply. Four short Python files.</description><pubDate>Mon, 12 Oct 2026 14:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Support chats, intake forms, benefits questions, patient messages. The messages people send your app are full of personal details: names, addresses, account numbers, IDs. And many of those questions need a current answer from the web, like a program’s income limits, a bank’s wire rules, or a state’s renewal process.&lt;/p&gt;
&lt;p&gt;The answer depends on what they’re asking, not on who’s asking. This tutorial builds a loop that takes personal details out before the question leaves your machine: Rampart’s open model redacts personal details on your machine, Tabstack runs the search-to-answer loop on the redacted question, your own model in Ollama writes the reply, and the details go back in on your side.&lt;/p&gt;
&lt;h2 id=&quot;rampart-from-national-design-studio&quot;&gt;Rampart, from National Design Studio&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/nationaldesignstudio/rampart&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Rampart&lt;/a&gt; is an open PII redaction library from &lt;a href=&quot;https://ndstudio.gov/posts/say-hello-to-rampart&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;National Design Studio&lt;/a&gt;. It runs in the browser, in the moment between typing a message and sending it, and replaces personal details with placeholders like &lt;code&gt;[GIVEN_NAME_1]&lt;/code&gt; before anything reaches a server. The mapping back to real values never leaves the device.&lt;/p&gt;
&lt;p&gt;At its core is a 14.7 MB MiniLM model that recognizes names, phone numbers, bank account and routing numbers, tax and government IDs, passports, driver’s licenses, and every part of a street address. Rules cover card numbers, SSNs, emails, URLs, and IP addresses. Together they work across seven Latin-script languages and report 98.42% private-term recall on Rampart’s held-out test set. National Design Studio is clear about what it is for: harm reduction for people entering their own information.&lt;/p&gt;
&lt;p&gt;Two choices make it a natural fit for web research. It keeps city, state, and ZIP by default, because location is what most “what applies to me” questions turn on. And the model is published openly on &lt;a href=&quot;https://huggingface.co/nationaldesignstudio/rampart&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Hugging Face&lt;/a&gt;, so it runs from Python on your server as well as in the browser. That is how we use it here.&lt;/p&gt;
&lt;h2 id=&quot;what-ramparts-model-catches&quot;&gt;What Rampart’s model catches&lt;/h2&gt;
&lt;p&gt;Here is what the code in this tutorial sends onward for a few kinds of questions. Everything in brackets stays on the machine.&lt;/p&gt;
&lt;div class=&quot;table-scroll&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Intake&lt;/th&gt;
&lt;th&gt;What leaves your machine&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Benefits&lt;/td&gt;
&lt;td&gt;My name is [GIVEN_NAME_1] [SURNAME_1], I live at [BUILDING_NUMBER_1] [STREET_NAME_1], Brookvale, CT 06482, and I make $1,950 a month. What housing assistance in Connecticut am I eligible for?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Banking&lt;/td&gt;
&lt;td&gt;Hi, this is [GIVEN_NAME_1] [SURNAME_1] from Acme Corp, account [BANK_ACCOUNT_1], routing [ROUTING_NUMBER_1]. Why was my wire held?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Travel documents&lt;/td&gt;
&lt;td&gt;Passport [PASSPORT_1], driver’s license [DRIVERS_LICENSE_1] issued in California. Can I renew my passport by mail from Fresno?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Health&lt;/td&gt;
&lt;td&gt;I’m [GIVEN_NAME_1] [SURNAME_1], DOB 03/14/1988, member ID [GOVERNMENT_ID_1], diagnosed with type 1 diabetes. Does Anthem cover insulin pumps in Ohio?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spanish&lt;/td&gt;
&lt;td&gt;Mi nombre es [GIVEN_NAME_1] [SURNAME_1], vivo en [STREET_NAME_1] [BUILDING_NUMBER_1], Madrid. ¿Qué ayudas hay para alquiler?&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;The names and IDs are gone, and the state, program, amount, and topic are still there, so each question can still be answered. The health row also shows what the model leaves alone, like a date of birth, which we cover in &lt;a href=&quot;#tune-it-for-your-data&quot;&gt;tuning&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;what-youll-build&quot;&gt;What you’ll build&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://tabstack.ai/blog/redact-pii-before-web-research&quot;&gt;View the diagram in the original post.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Four short files:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;redact.py&lt;/code&gt; finds PII with Rampart’s model and swaps it for placeholders.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;research.py&lt;/code&gt; sends the redacted question to Tabstack and gets back a cited report.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;reply.py&lt;/code&gt; has your model write the answer from that report.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ask.py&lt;/code&gt; runs the loop and restores the details.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;set-up&quot;&gt;Set up&lt;/h2&gt;
&lt;p&gt;You need Python 3.9 or later, &lt;a href=&quot;https://ollama.com&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Ollama&lt;/a&gt; with a model pulled, and a Tabstack API key.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;pip&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; install&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; onnxruntime&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; tokenizers&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; huggingface_hub&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; numpy&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; tabstack&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; ollama&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;ollama&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; pull&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; llama3.1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; TABSTACK_API_KEY&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;...&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first run downloads Rampart’s model files from Hugging Face and caches them.&lt;/p&gt;
&lt;h2 id=&quot;step-1-redact-pii-with-ramparts-model&quot;&gt;Step 1: Redact PII with Rampart’s model&lt;/h2&gt;
&lt;p&gt;The model labels each token: &lt;code&gt;B-GIVEN_NAME&lt;/code&gt; starts a name, &lt;code&gt;I-STREET_NAME&lt;/code&gt; continues a street, &lt;code&gt;O&lt;/code&gt; is ordinary text. We join those labels into spans, skip the ones in &lt;code&gt;KEEP&lt;/code&gt;, and run a short list of rules for the classes Rampart handles with rules instead of the model: card numbers, SSNs, IP addresses, emails, and URLs. Long messages run in overlapping 512-token windows, the model’s limit.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# redact.py&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; json&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; re&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; typing &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; Callable, Optional&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; numpy &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;as&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; np&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; onnxruntime &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;as&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; ort  &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# type: ignore[import-untyped]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; huggingface_hub &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; hf_hub_download&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; tokenizers &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; Tokenizer  &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# type: ignore[import-untyped]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;REPO&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;nationaldesignstudio/rampart&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;KEEP&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; {&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;CITY&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;STATE&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;ZIP_CODE&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;}  &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# the answer needs these; Rampart keeps them too&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;MIN_SCORE&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; 0.4&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;  # start a span&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;EXTEND_SCORE&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; 0.15&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;  # continue a span across punctuation, as Rampart does&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;tokenizer &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; Tokenizer.from_file(hf_hub_download(&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;REPO&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;tokenizer.json&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;tokenizer.enable_truncation(&lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;max_length&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;512&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;stride&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;64&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)  &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# long messages run in overlapping windows&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;with&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; open&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(hf_hub_download(&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;REPO&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;config.json&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)) &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;as&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; f:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    id2label &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; json.load(f)[&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;id2label&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;label2id &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; {label: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;int&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(i) &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;for&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; i, label &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; id2label.items()}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;model &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; ort.InferenceSession(hf_hub_download(&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;REPO&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;onnx/model_q4.onnx&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;GAP&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; re.compile(&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;r&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;[\s&lt;/span&gt;&lt;span style=&quot;color:#22863A;font-weight:bold&quot;&gt;\-&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;.()/]&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; luhn_ok&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(number: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;) -&gt; &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;bool&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    digits &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;int&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(d) &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;for&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; d &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; re.sub(&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;r&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;\D&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, number)][::&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    total &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; sum&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(d &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; i &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;%&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; 2&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; ==&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; 0&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; else&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; (d &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; 2&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; -&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; 9&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; if&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; d &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; 4&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; else&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; d &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; 2&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;for&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; i, d &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; enumerate&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(digits))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; total &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;%&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; 10&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; ==&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; 0&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# Rampart catches these with rules rather than the model, so we do too.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# Each rule is (pattern, label, optional check). Append your own for IDs the model doesn&apos;t know.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;Rule &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; tuple[re.Pattern[&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;], &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, Optional[Callable[[&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;], &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;bool&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;]]]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;RULES&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: list[Rule] &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    (re.compile(&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;r&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;\b\d&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;{3}&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;\d&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;{2}&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;\d&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;{4}&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;\b&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;), &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;SSN&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;None&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    (re.compile(&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;r&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;\b(?:\d&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;{4}&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;[ -]&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;?&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;{3}&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;\d&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;{4}&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;\b&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;|&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;\b\d&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;{4}&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;[ -]&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;?&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;\d&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;{6}&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;[ -]&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;?&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;\d&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;{5}&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;\b&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;), &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;CARD_NUMBER&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, luhn_ok),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    (re.compile(&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;r&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;\b(?:\d&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;{1,3}&lt;/span&gt;&lt;span style=&quot;color:#22863A;font-weight:bold&quot;&gt;\.&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;{3}&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;\d&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;{1,3}&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;\b&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;), &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;IP_ADDRESS&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;None&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    (re.compile(&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;r&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;\b[\w.+-]&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;@&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;[\w-]&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;(?:&lt;/span&gt;&lt;span style=&quot;color:#22863A;font-weight:bold&quot;&gt;\.&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;[\w-]&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;\b&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;), &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;EMAIL&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;None&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    (re.compile(&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;r&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;\b&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;https&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;?&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;://&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;^&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;\s&amp;#x3C;&gt;&lt;/span&gt;&lt;span style=&quot;color:#22863A;font-weight:bold&quot;&gt;\&quot;&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;&apos;)&lt;/span&gt;&lt;span style=&quot;color:#22863A;font-weight:bold&quot;&gt;\]&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;]&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;^&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;\s&amp;#x3C;&gt;&lt;/span&gt;&lt;span style=&quot;color:#22863A;font-weight:bold&quot;&gt;\&quot;&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;&apos;).,;:!?&lt;/span&gt;&lt;span style=&quot;color:#22863A;font-weight:bold&quot;&gt;\]&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;]&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;), &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;URL&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;None&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; find_pii&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(text: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;) -&gt; list[tuple[&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;int&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;int&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;]]:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    spans: list[tuple[&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;int&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;int&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;]] &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; []&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    first &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; tokenizer.encode(text)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    for&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; enc &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; [first, &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;first.overflowing]:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;        feeds &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;            &quot;input_ids&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: np.array([enc.ids], &lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;dtype&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;np.int64),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;            &quot;attention_mask&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: np.array([enc.attention_mask], &lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;dtype&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;np.int64),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;            &quot;token_type_ids&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: np.array([enc.type_ids], &lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;dtype&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;np.int64),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;        logits &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; model.run(&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;None&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, feeds)[&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;][&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;        scores &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; np.exp(logits) &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; np.exp(logits).sum(&lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;axis&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=-&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;keepdims&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;True&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;        window: list[tuple[&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;int&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;int&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;]] &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; []&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;        for&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; (start, end), row &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; zip&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(enc.offsets, scores):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;            if&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; start &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; end:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;                continue&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;            label &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; id2label[&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;int&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(row.argmax()))]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;            kind &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; label[&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;2&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;            last &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; window[&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; window &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;and&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; GAP&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.fullmatch(text[window[&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;][&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;]:start]) &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;else&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; None&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;            # Join word pieces and multi-part values (&quot;Cedar Lane&quot;, &quot;203-555-0142&quot;) into one span.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;            if&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; last &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;and&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; (kind &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; last[&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;2&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;or&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; (label &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;O&quot;&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; and&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; row[label2id[&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;I-&quot;&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; +&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; last[&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;2&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;]]] &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; EXTEND_SCORE&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;                window[&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; (last[&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;], end, last[&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;2&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;])&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;            elif&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; label &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;!=&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;O&quot;&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; and&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; row.max() &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; MIN_SCORE&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;                window.append((start, end, kind))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;        spans &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;+=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; window&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    rules &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;        (m.start(), m.end(), label)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;        for&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; pattern, label, check &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; RULES&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;        for&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; m &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; pattern.finditer(text)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; check &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;is&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; None&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; or&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; check(m.group())&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    ]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    # Rule matches win over the model, longer spans win over shorter ones, and none overlap.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    chosen: list[tuple[&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;int&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;int&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;]] &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; []&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    for&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; span &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; sorted&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(rules, &lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;key&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=lambda&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; s: s[&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; s[&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;]) &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; sorted&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(spans, &lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;key&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=lambda&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; s: s[&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; s[&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;]):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; span[&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;2&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;not&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; in&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; KEEP&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; and&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; all&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(span[&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;&amp;#x3C;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; c[&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;or&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; span[&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; c[&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;for&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; c &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; chosen):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;            chosen.append(span)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; sorted&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(chosen)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; Redactor&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;    &quot;&quot;&quot;Swaps PII for numbered placeholders and keeps the mapping in memory.&quot;&quot;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    def&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; __init__&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(self) -&gt; &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;None&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;        self&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.values: dict[&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; {}  &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# placeholder -&gt; original value&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    def&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; redact&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(self, text: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;) -&gt; &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;        out, last &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; [], &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;0&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;        for&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; start, end, kind &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; find_pii(text):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;            value &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; text[start:end]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;            placeholder &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; next&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;((p &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;for&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; p, v &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; self&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.values.items() &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; v &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; value), &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;None&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;            if&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; placeholder &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;is&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; None&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;                count &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; sum&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(p.startswith(&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;f&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;[&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;kind&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;_&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;for&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; p &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; self&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.values) &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; 1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;                placeholder &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; f&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;[&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;kind&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;_&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;count&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;]&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;                self&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.values[placeholder] &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; value&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;            out &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;+=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; [text[last:start], placeholder]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;            last &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; end&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.join(out &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; [text[last:]])&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    def&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; restore&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(self, text: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;) -&gt; &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;        for&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; placeholder, value &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; self&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.values.items():&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;            text &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; text.replace(placeholder, value)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; text&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The placeholder table lives in memory in your process. The same value always gets the same placeholder, so a name that comes up again in a follow-up message keeps its placeholder.&lt;/p&gt;
&lt;h2 id=&quot;step-2-research-the-redacted-question-with-tabstack&quot;&gt;Step 2: Research the redacted question with Tabstack&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;/research&lt;/code&gt; plans searches, reads sources, and returns a Markdown report with the pages it cited, all inside one call. Your model never fetches a page or reads raw HTML, and Tabstack only receives the redacted question.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# research.py&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; tabstack &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; Tabstack&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;client &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; Tabstack()  &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# reads TABSTACK_API_KEY&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; research&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(question: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;) -&gt; tuple[&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, list[dict[&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;]]]:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    stream &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; client.agent.research(&lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;query&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;question, &lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;mode&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;fast&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    for&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; event &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; stream:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; event.event &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;complete&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;            pages &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; event.data.metadata.cited_pages &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;or&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; []&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;            sources &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; [{&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;title&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: page.title &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;or&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;(untitled)&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;url&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: page.url} &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;for&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; page &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; pages]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;            return&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; event.data.report, sources&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; event.event &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;error&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;            raise&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; RuntimeError&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;f&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;Research failed: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;event.data.error.message&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    raise&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; RuntimeError&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;Research stream ended without a result&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On the redacted benefits question, a &lt;code&gt;fast&lt;/code&gt; call came back in about 17 seconds with a report on Connecticut income limits, the Section 8 Housing Choice Voucher Program, and the state Rental Assistance Program, citing five sources including the Connecticut Department of Housing. Use &lt;code&gt;mode=&quot;balanced&quot;&lt;/code&gt; when a question needs more sources.&lt;/p&gt;
&lt;p&gt;Tabstack is built by Mozilla. Every request is covered by two commitments on our &lt;a href=&quot;https://docs.tabstack.ai/trust/data-handling/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Data Handling page&lt;/a&gt;: never trained on, and private by default.&lt;/p&gt;
&lt;h2 id=&quot;step-3-write-the-reply-with-your-model&quot;&gt;Step 3: Write the reply with your model&lt;/h2&gt;
&lt;p&gt;Your model gets the redacted question and the finished report. Tell it to keep placeholders exactly as written, so the details can go back in.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# reply.py&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; ollama&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;MODEL&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;llama3.1&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;SYSTEM_PROMPT&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; (&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;    &quot;Answer the user&apos;s question using only the research report. &quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;    &quot;Keep every placeholder, such as [GIVEN_NAME_1], exactly as written.&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; write_reply&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(question: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, report: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;) -&gt; &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    response &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; ollama.chat(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E36209&quot;&gt;        model&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;MODEL&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E36209&quot;&gt;        messages&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;[&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;            {&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;role&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;system&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;content&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;SYSTEM_PROMPT&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;},&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;            {&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;role&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;user&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;content&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;f&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;Question:&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;\n{&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;question&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;}\n\n&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;Research report:&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;\n{&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;report&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;},&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;        ],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    )&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; response.message.content &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;or&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Any model you run in Ollama works here. Swap &lt;code&gt;MODEL&lt;/code&gt; for Qwen, Mistral, or whatever you already use.&lt;/p&gt;
&lt;h2 id=&quot;step-4-run-the-loop&quot;&gt;Step 4: Run the loop&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# ask.py&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; sys&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; redact &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; Redactor&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; reply &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; write_reply&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; research &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; research&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; ask&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(question: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;) -&gt; &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;None&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    redactor &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; Redactor()  &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# one per conversation; the mapping never leaves this process&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    safe_question &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; redactor.redact(question)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    print&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;f&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;Sent: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;safe_question&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;}\n&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    report, sources &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; research(safe_question)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    reply &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; write_reply(safe_question, report)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    print&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(redactor.restore(reply))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    print&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;\n&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;Sources:&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    for&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; source &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; sources:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;        print&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;f&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;- &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;source[&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&apos;title&apos;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;]&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;source[&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&apos;url&apos;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;]&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; __name__&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; ==&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;__main__&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; len&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(sys.argv) &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; 2&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;        sys.exit(&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&apos;Usage: python ask.py &quot;your question&quot;&apos;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    try&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;        ask(&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot; &quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.join(sys.argv[&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:]))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    except&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; Exception&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; as&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; err:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;        sys.exit(&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;f&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;Could not answer: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;err&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Run it with any question from the table above, or one of your own:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;python&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; ask.py&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;Hi, this is Tom Becker from Acme Corp, account 0042981173, routing 021000021. Why was my wire held?&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first line shows exactly what left your machine. The answer that follows has the real details back in it, with the sources it came from.&lt;/p&gt;
&lt;h2 id=&quot;tune-it-for-your-data&quot;&gt;Tune it for your data&lt;/h2&gt;
&lt;p&gt;Three adjustments cover most intakes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Decide what stays.&lt;/strong&gt; &lt;code&gt;KEEP&lt;/code&gt; is the list of labels that pass through untouched. City, state, and ZIP are the default because local rules depend on them. A question about a federal program may not need the ZIP; a question about a city service needs the city. Set &lt;code&gt;KEEP&lt;/code&gt; per use case, not once for the whole app.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Send structured fields by choice.&lt;/strong&gt; When the intake is a form, you already know which field holds the name. There is no need to guess with a model: send the fields the answer needs, run the model only over the free text, and never send the rest.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# intake.py&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; redact &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; Redactor&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;SEND&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; {&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;state&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;zip_code&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;household_size&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;monthly_income&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;}  &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# fields the answer needs&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;FREE_TEXT&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;question&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;notes&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)  &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# fields people type into&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; build_question&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(form: dict[&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;], redactor: Redactor) -&gt; &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    lines &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;f&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;field.replace(&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&apos;_&apos;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&apos; &apos;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;value&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; for&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; field, value &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; form.items() &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; field &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; SEND&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    lines &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;+=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; [redactor.redact(form[field]) &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;for&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; field &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; FREE_TEXT&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; if&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; field &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; form]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;\n&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.join(lines)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Research a form the same way as a question:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; intake &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; build_question&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; redact &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; Redactor&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; reply &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; write_reply&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; research &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; research&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;form &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;    &quot;full_name&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;Maria Garcia&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;    &quot;email&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;maria@example.com&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;    &quot;state&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;CT&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;    &quot;zip_code&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;06482&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;    &quot;household_size&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;2&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;    &quot;monthly_income&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;$1,950&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;    &quot;question&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;My landlord Dave Kim raised the rent 30%. What help is there?&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;redactor &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; Redactor()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;question &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; build_question(form, redactor)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;report, sources &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; research(question)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;print&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(redactor.restore(write_reply(question, report)))&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The question that goes to Tabstack is:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;state: CT&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;zip code: 06482&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;household size: 2&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;monthly income: $1,950&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;My landlord [GIVEN_NAME_1] [SURNAME_1] raised the rent 30%. What help is there?&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The name and email fields are never sent at all, and the one name inside the free text is redacted.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Add rules for your own identifiers.&lt;/strong&gt; Case numbers, patient IDs, policy numbers, and dates of birth follow formats you know. Append them to &lt;code&gt;RULES&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; re&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; redact &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; RULES&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;RULES&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.append((re.compile(&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;r&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;(?i)(?&amp;#x3C;=&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;dob &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;\d&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;{1,2}&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;\d&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;{1,2}&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;\d&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;{4}&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;), &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;DATE_OF_BIRTH&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;None&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;RULES&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.append((re.compile(&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;r&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;\b&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;CASE-&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;\d&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;{6}&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;\b&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;), &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;CASE_NUMBER&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;None&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;))&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With those added, “I’m Priya Nair, DOB 03/14/1988, case CASE-204871. Does Ohio Medicaid cover insulin pumps?” goes out as “I’m [GIVEN_NAME_1] [SURNAME_1], DOB [DATE_OF_BIRTH_1], case [CASE_NUMBER_1]. Does Ohio Medicaid cover insulin pumps?”&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Send only the question.&lt;/strong&gt; Redaction removes identifiers, but the rest of the message still goes to Tabstack: greetings, background, details the answer doesn’t need. Your model is already running locally, so have it rewrite the redacted message as one standalone research question first.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# focus.py&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; ollama&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; reply &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; MODEL&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;FOCUS_PROMPT&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; (&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;    &quot;Rewrite the user&apos;s message as one standalone research question. &quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;    &quot;Keep only the facts needed to answer it, such as location, program, amounts, dates, or conditions. &quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;    &quot;Drop greetings, names, placeholders like [GIVEN_NAME_1], and any detail the answer does not depend on. &quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;    &quot;Reply with the question only.&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; focus&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(message: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;) -&gt; &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    response &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; ollama.chat(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E36209&quot;&gt;        model&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;MODEL&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E36209&quot;&gt;        messages&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;[&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;            {&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;role&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;system&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;content&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;FOCUS_PROMPT&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;},&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;            {&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;role&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;user&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;content&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: message},&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;        ],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    )&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; (response.message.content &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;or&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; message).strip()&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In &lt;code&gt;ask.py&lt;/code&gt;, import &lt;code&gt;focus&lt;/code&gt;, research and print the focused question, and keep the full redacted message for the reply, so your model can still answer the person directly:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; focus &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; focus  &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# add to the imports at the top of ask.py&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;safe_question &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; redactor.redact(question)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;research_question &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; focus(safe_question)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;print&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;f&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;Sent: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;research_question&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;}\n&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;report, sources &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; research(research_question)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;reply &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; write_reply(safe_question, report)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With the date-of-birth and case rules from above, one run sent Priya’s message to Tabstack as just “Does Ohio Medicaid cover insulin pumps?” and turned the redacted housing question into “What housing assistance programs in Connecticut are available to individuals with a monthly income of $1,950?” The wording changes from run to run.&lt;/p&gt;
&lt;p&gt;Redact first and focus second. The rewrite is a model following instructions, so it can miss. In testing it often kept placeholders like &lt;code&gt;[BANK_ACCOUNT_1]&lt;/code&gt;, which is harmless because a placeholder contains no personal data. It also sometimes added details the person didn’t give, like “for individuals with diabetes”, so check the focused question in the &lt;code&gt;Sent:&lt;/code&gt; line.&lt;/p&gt;
&lt;h2 id=&quot;know-what-rampart-leaves-alone&quot;&gt;Know what Rampart leaves alone&lt;/h2&gt;
&lt;p&gt;Rampart redacts identifiers and leaves the rest of the message alone, which is usually what the answer needs: a diagnosis for a coverage question, an income for an eligibility question, an employer for a benefits question. Dates of birth, ages, usernames, and your own internal IDs also pass through until you add a rule.&lt;/p&gt;
&lt;p&gt;Names in non-Latin scripts are out of scope for this release of Rampart, and so are indirect identifiers: details that only point to someone in combination. National Design Studio describes Rampart as harm reduction for people entering their own information, and that is the right way to use this loop too. This tutorial is a minimal Python port of Rampart’s approach, so Rampart’s published evaluation numbers apply to the library itself, not to this code.&lt;/p&gt;
&lt;h2 id=&quot;where-to-take-it-next&quot;&gt;Where to take it next&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Redact in the browser.&lt;/strong&gt; For a web app, use Rampart’s own JavaScript library so questions are redacted before they reach your server at all: &lt;code&gt;npm install @nationaldesignstudio/rampart @huggingface/transformers&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Redact replies before you log them.&lt;/strong&gt; Run the model’s output through &lt;code&gt;Redactor().redact()&lt;/code&gt; before it goes into logs or analytics.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Keep one redactor per conversation.&lt;/strong&gt; Placeholders stay consistent across turns, so follow-up questions about the same person keep working.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Start with your own question: &lt;a href=&quot;https://console.tabstack.ai&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;get a Tabstack API key&lt;/a&gt; and make a research call.&lt;/p&gt;
</content:encoded></item><item><title>The production-readiness checklist for current-answer features</title><link>https://tabstack.ai/blog/production-readiness-current-answer-features</link><guid isPermaLink="true">https://tabstack.ai/blog/production-readiness-current-answer-features</guid><description>Use seven release gates to check a current-answer feature. Define acceptable answers, test failures, and attach the evidence your team needs to ship.</description><pubDate>Fri, 09 Oct 2026 14:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Before you ship a feature that answers questions from the live web, define which answers it can accept and what happens when a request fails. Review the evidence for answer quality alongside timing, cost, data handling, and the team’s ability to disable the feature.&lt;/p&gt;
&lt;p&gt;A documentation assistant makes the distinction clear. It can cite a real release page and still recommend a runtime that no longer receives support. The request completed, but the answer does not meet the application’s requirements.&lt;/p&gt;
&lt;p&gt;Use the seven gates below to review one workflow. Each gate needs an owner, dated evidence, and a decision. Set thresholds for your application rather than borrowing a universal quality score or latency target.&lt;/p&gt;
&lt;h2 id=&quot;1-define-the-answers-your-feature-can-accept&quot;&gt;1. Define the answers your feature can accept&lt;/h2&gt;
&lt;p&gt;Specify the projects, question types, and source requirements the feature supports. “Answer questions from the web” gives the reviewer no useful target.&lt;/p&gt;
&lt;p&gt;For a documentation assistant, an acceptable answer might need to identify a supported version, explain its relevant limits, and cite the official documentation. An undocumented parameter should produce an evidence gap rather than an invented definition.&lt;/p&gt;
&lt;ul class=&quot;contains-task-list&quot;&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Define the supported questions and the response to requests outside that scope.&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Write the required answer elements and critical failure conditions before testing.&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Separate accepted, review-needed, unanswered, and failed results in the application.&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Name the owner who approves the acceptance rules.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Evaluate the full path from question to displayed answer, including any work your own model performs after receiving a web result.&lt;/p&gt;
&lt;h2 id=&quot;2-check-coverage-and-source-support&quot;&gt;2. Check coverage and source support&lt;/h2&gt;
&lt;p&gt;Coverage asks whether the answer addresses the question. Source support asks whether the cited passages establish its claims. An answer can name the right runtime but omit its support window. It can also include every requested field while attaching a date from the wrong release.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://tabstack.ai/blog/useful-citations-ai-answers&quot;&gt;citation review guide&lt;/a&gt; explains how to inspect a claim’s passage, qualifiers, and version. The &lt;a href=&quot;https://tabstack.ai/blog/trace-question-to-source-backed-answer&quot;&gt;traced Python example&lt;/a&gt; preserves the report and source mapping for that review.&lt;/p&gt;
&lt;ul class=&quot;contains-task-list&quot;&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Review each required answer element and each decision-driving claim.&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Record the source’s relevant date or version, including when it is unknown.&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Keep inaccessible sources, unreviewed claims, and duplicate references visible.&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Apply critical failure rules before considering average scores.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Freeze the question set, expected elements, scoring rules, and implementation configuration before running the evaluation. Keep every attempt, including failures, and separate live results from pilots and synthetic tests. A small sample helps identify defects; stronger reliability claims require a broader evaluation. The &lt;a href=&quot;https://tabstack.ai/blog/twenty-question-live-web-evaluation-set&quot;&gt;20-question evaluation set walkthrough&lt;/a&gt; builds this with a Python kit that hashes the frozen set, keeps every attempt, and labels pilot and synthetic runs.&lt;/p&gt;
&lt;p&gt;Automatic checks can flag missing citations or malformed URLs. Use human review or a separately validated verifier to decide whether a passage supports a claim. In that kit’s pilot, a second blind review caught a claim the first review had scored as fully supported.&lt;/p&gt;
&lt;h2 id=&quot;3-test-the-wait-your-product-allows&quot;&gt;3. Test the wait your product allows&lt;/h2&gt;
&lt;p&gt;Set expectations for first feedback and the final answer. Progress tells the user that work continues; it does not mean the answer is ready.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.tabstack.ai/guides/research&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Tabstack Research&lt;/a&gt; streams progress and returns the report on &lt;code&gt;complete&lt;/code&gt;. It has no overall server-side timeout. The &lt;code&gt;fetch_timeout&lt;/code&gt; parameter limits one page fetch, not the whole request.&lt;/p&gt;
&lt;p&gt;Your client owns the product’s wait policy. A silence timer catches a gap between events. A total deadline limits how long the user waits even when progress continues. The evaluation kit sets both (&lt;code&gt;--silence-timeout&lt;/code&gt; and &lt;code&gt;--deadline&lt;/code&gt;) and records each as its own failure state.&lt;/p&gt;
&lt;ul class=&quot;contains-task-list&quot;&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Define first-feedback and final-result expectations for the interface.&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Measure elapsed time from client dispatch to terminal success or failure.&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Test both a stalled stream and an active request that exceeds the deadline.&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Show a clear timeout state without claiming server-side cancellation.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Stopping the client does not establish that the server stopped processing. Account for that uncertainty before retrying or interpreting usage.&lt;/p&gt;
&lt;h2 id=&quot;4-contain-failures-before-they-reach-the-user&quot;&gt;4. Contain failures before they reach the user&lt;/h2&gt;
&lt;p&gt;Test the interface as well as the exception handler. Exercise HTTP rejection, streamed task errors, interrupted connections, malformed output, missing sources, and a stream that closes without a terminal event.&lt;/p&gt;
&lt;p&gt;For Research, an HTTP success does not establish task completion. Handle both &lt;code&gt;complete&lt;/code&gt; and &lt;code&gt;error&lt;/code&gt;; record an incomplete result if the stream closes without either.&lt;/p&gt;
&lt;ul class=&quot;contains-task-list&quot;&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Give each failure a defined application state and user-visible response.&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Apply the evidence rule when a response has no source list.&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Record SDK and application retries separately, including possibly accepted requests. The evaluation kit sets SDK retries to zero, so one recorded attempt is one request.&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Test fallbacks so they cannot silently present old or unchecked answers as current.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A cached answer is a valid fallback only when its retrieval date remains visible and its age fits the question. A runtime-support feature should not relabel an old recommendation as current.&lt;/p&gt;
&lt;h2 id=&quot;5-measure-cost-from-the-same-requests-you-reviewed&quot;&gt;5. Measure cost from the same requests you reviewed&lt;/h2&gt;
&lt;p&gt;Set a workflow budget and decide what the application does at its limit. Include retries, failed requests, and model work your application performs after receiving the answer.&lt;/p&gt;
&lt;p&gt;Research can use a variable number of actions per task, as the &lt;a href=&quot;https://tabstack.ai/pricing&quot;&gt;pricing page&lt;/a&gt; explains. A per-action rate alone does not establish the cost of a complete request. The Research stream reports token counts per model but no per-request credit record, so attach usage from your account records.&lt;/p&gt;
&lt;ul class=&quot;contains-task-list&quot;&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Attach authoritative usage records to the requests they cover.&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Include billed failures and retries; leave missing usage unavailable rather than zero.&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Set usage limits or alerts appropriate to the rollout.&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Review plan limits, expected volume, and support requirements.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Calculate cost per accepted answer only for a fully reviewed batch with complete usage records. Divide the cost of all attempts in that batch by its accepted answers. Do not combine full-batch spend with a selected subset of successful responses.&lt;/p&gt;
&lt;h2 id=&quot;6-review-where-data-goes-and-what-it-can-do&quot;&gt;6. Review where data goes and what it can do&lt;/h2&gt;
&lt;p&gt;Map which services receive the question, retrieved content, and instructions. Review your application’s logs and access controls as well as the provider’s handling. Running your own model does not make a hosted web call local.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://tabstack.ai/legal/privacy&quot;&gt;Privacy Notice&lt;/a&gt; is the canonical statement of how Tabstack handles your data. It says Tabstack processes your Inputs to run them through third-party models and generate Outputs, and that it stores history, including your content and Outputs, for 90 days or until you delete it. It also asks you to be careful about sharing confidential or sensitive information in your Inputs.&lt;/p&gt;
&lt;ul class=&quot;contains-task-list&quot;&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Check the processing path against the workflow’s privacy requirements.&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Keep credentials and private customer data out of public test artifacts.&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Allowlist logged fields and define who can inspect them.&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Treat retrieved pages as untrusted input, validate source URLs, and restrict downstream actions.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A retrieved page cannot grant an agent permission to run commands or change an account. Set tool permissions and approval rules in the application.&lt;/p&gt;
&lt;p&gt;If the requirement is that data never leave your controlled environment, hosted Research does not fit. Pilo provides self-hosted automation, not a self-hosted Research service.&lt;/p&gt;
&lt;h2 id=&quot;7-assign-an-owner-and-test-the-rollback&quot;&gt;7. Assign an owner and test the rollback&lt;/h2&gt;
&lt;p&gt;Name who responds when a source changes, a dependency fails, quality regresses, or usage exceeds its budget. A managed research service removes work from the application, but your team still owns the feature’s behavior.&lt;/p&gt;
&lt;ul class=&quot;contains-task-list&quot;&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Give every release gate an owner, evidence, and a recorded decision.&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Limit the initial rollout and define when to expand it.&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Test the path that disables the feature or selects a labeled fallback.&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Document escalation contacts and repeatable regression checks.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Keep configuration and dataset versions with the runbook so another engineer can reproduce the result and identify what changed.&lt;/p&gt;
&lt;h2 id=&quot;record-the-release-decision&quot;&gt;Record the release decision&lt;/h2&gt;
&lt;p&gt;Use a compact record that links each decision to its evidence:&lt;/p&gt;
&lt;div class=&quot;table-scroll&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Gate&lt;/th&gt;
&lt;th&gt;Evidence to attach&lt;/th&gt;
&lt;th&gt;Decision&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Scope and acceptance&lt;/td&gt;
&lt;td&gt;Supported workflow and critical failure rules&lt;/td&gt;
&lt;td&gt;What the feature handles or refuses&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Answer quality&lt;/td&gt;
&lt;td&gt;Coverage and claim-support reviews&lt;/td&gt;
&lt;td&gt;Which answers the application accepts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time budget&lt;/td&gt;
&lt;td&gt;Client timings and deadline tests&lt;/td&gt;
&lt;td&gt;Whether the interaction meets its wait policy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failure handling&lt;/td&gt;
&lt;td&gt;Attempt log, failure fixtures, and fallback tests&lt;/td&gt;
&lt;td&gt;How the application contains failures&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost&lt;/td&gt;
&lt;td&gt;Matched usage records and budget controls&lt;/td&gt;
&lt;td&gt;Whether the workload fits the budget&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data and security&lt;/td&gt;
&lt;td&gt;Processing map and reviewed controls&lt;/td&gt;
&lt;td&gt;Whether the architecture meets requirements&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ownership and rollback&lt;/td&gt;
&lt;td&gt;Runbook and disable-path test&lt;/td&gt;
&lt;td&gt;Who acts when behavior changes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;The evaluation kit includes a blank &lt;a href=&quot;https://github.com/Mozilla-Ocho/tabstack-cited-research-python/blob/34bc3bfd709b80f50fbf6335b3c9360a3c35d670/acceptance-evals/release-review-template.csv&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;release-review template&lt;/a&gt; with these seven gates. Mark each gate &lt;code&gt;pass&lt;/code&gt;, &lt;code&gt;fail&lt;/code&gt;, or &lt;code&gt;unknown&lt;/code&gt;. Resolve critical unknowns before expanding the rollout, and retain the evidence behind any decision to accept a remaining risk.&lt;/p&gt;
&lt;p&gt;Start with one public question and one failure case. Review the answer’s source support, then test what the user sees when the request cannot finish. Those two records give the team concrete work to resolve before release.&lt;/p&gt;
&lt;p&gt;Read the &lt;a href=&quot;https://docs.tabstack.ai/guides/research&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Research guide&lt;/a&gt; to check the endpoint contract. When you need to discuss fit, &lt;a href=&quot;https://tabstack.ai/contact&quot;&gt;bring your workflow and constraints to the Tabstack team&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>The 22-line demo and everything it was missing</title><link>https://tabstack.ai/blog/the-22-line-demo</link><guid isPermaLink="true">https://tabstack.ai/blog/the-22-line-demo</guid><description>I asked a 22-line Tabstack Research demo what had changed in the latest Laravel release, and it told me 12.x. Oops. Here&apos;s how I took it to production one commit at a time: routing, deadlines, retries, caching, and checking the answers against the changelog.</description><pubDate>Thu, 08 Oct 2026 14:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Have you ever wanted to ask a question about a framework or package, and just get a simple answer? I have. Something like “what has changed in the latest Laravel release that affects queue workers?”. Important questions that you could answer from the changelog, if you have the time and patience (which I don’t).&lt;/p&gt;
&lt;p&gt;So, 22 lines of Python later (thanks Claude), and 14.7 seconds after asking, it told me “The latest Laravel release, &lt;strong&gt;12.x&lt;/strong&gt;, …”. Oops. Packagist was listing v13.34.0 as the latest stable release when I ran it.&lt;/p&gt;
&lt;p&gt;Nothing crashed, to be fair. It did exactly what I asked: one research call, print whatever comes back. The problem is everything around that call. It had no idea when not to make it, no idea how it ended, kept no record of what it said, and couldn’t tell a good answer from a confidently wrong one (see above).&lt;/p&gt;
&lt;p&gt;So that’s what this tutorial is about. We’re going to take that demo to something I’d actually put in production, one step at a time. It’s all in &lt;a href=&quot;https://github.com/JustSteveKing/what-changed&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;JustSteveKing/what-changed&lt;/a&gt;, and every step below is in the commit history, so you can follow along with the diffs if you like.&lt;/p&gt;
&lt;p&gt;Fair warning before we start: all the results in here come from twenty questions, run on a single day in October. That’s a small sample. It tells you what happened that day with those questions, and I’m not going to pretend it says anything about latency, reliability or correctness in general.&lt;/p&gt;
&lt;h2 id=&quot;what-were-building&quot;&gt;What we’re building&lt;/h2&gt;
&lt;p&gt;A “what changed?” answerer for PHP and Laravel packages. You ask it a question, and you get back an answer plus the sources it came from, in order, so you can check it.&lt;/p&gt;
&lt;p&gt;It knows about Laravel, Livewire, Pest, Filament and Inertia’s Laravel adapter. The code is Python, but if you spend your days in PHP the domain will feel familiar, and none of the patterns care which language you use. You don’t need to know anything about Tabstack.&lt;/p&gt;
&lt;p&gt;The stack is small on purpose: Python with uv, pydantic for the data contracts, pytest and mypy, the official Tabstack SDK, and httpx for talking to Packagist and GitHub. No frameworks.&lt;/p&gt;
&lt;p&gt;If you want to follow along, clone the repo and start at the &lt;code&gt;demo&lt;/code&gt; tag:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; clone&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; https://github.com/JustSteveKing/what-changed.git&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;cd&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; what-changed&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;make&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; install&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; checkout&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; demo&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You’ll need a &lt;code&gt;TABSTACK_API_KEY&lt;/code&gt; for anything that makes a research call (those spend credits). A &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; is optional, but it helps once we get to the GitHub lookups. To see every step after the demo, &lt;code&gt;git log --reverse demo..&lt;/code&gt; lists them in order.&lt;/p&gt;
&lt;h2 id=&quot;step-1-the-demo&quot;&gt;Step 1: the demo&lt;/h2&gt;
&lt;p&gt;Here’s the whole thing:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# The demo: one Research call, print the answer, done.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# Deliberately naive. See the git history for what changes after this.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; sys&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; tabstack &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; Tabstack&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;question &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot; &quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.join(sys.argv[&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:])&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; not&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; question:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    print&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&apos;Usage: uv run src/demo.py &quot;your question&quot;&apos;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;file&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;sys.stderr)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    sys.exit(&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;client &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; Tabstack()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;stream &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; client.agent.research(&lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;query&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;question, &lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;mode&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;fast&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;for&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; event &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; stream:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; event.event &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;complete&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;        print&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(event.data.report)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;        print&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;\n&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;Sources:&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;        for&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; page &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; event.data.metadata.cited_pages &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;or&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; []:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;            print&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;f&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;- &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;page.title &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;or&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &apos;(untitled)&apos;&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;page.url&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; event.event &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;error&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;        raise&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; RuntimeError&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(event.data.error.message)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you’ve looked at the Tabstack Python SDK, that’s very nearly the official example. Run it with a question and it works:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;uv&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; run&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; src/demo.py&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;What changed in the latest Laravel release that affects queue workers?&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And it does work, in the sense that it runs and prints something. That’s about where it stops, though. Every question costs a research call, including the ones a quick metadata lookup could answer and the ones that shouldn’t be answered at all. There’s no deadline. If the stream closes without a &lt;code&gt;complete&lt;/code&gt; or &lt;code&gt;error&lt;/code&gt; event, it prints nothing and exits 0, so whatever called it thinks it succeeded. Nothing gets recorded, the SDK’s default retries are still on, and there’s no way to tell a good answer from a bad one. Which is how we ended up with “12.x”.&lt;/p&gt;
&lt;p&gt;The rest of this tutorial closes those gaps, one at a time.&lt;/p&gt;
&lt;h2 id=&quot;step-2-write-the-questions-before-you-fix-anything&quot;&gt;Step 2: write the questions before you fix anything&lt;/h2&gt;
&lt;p&gt;The obvious next move is to start fixing code. I didn’t, and here’s why: if you change things before you’ve decided what a correct answer looks like, you’ve no way of knowing whether a change helped. You end up judging answers on how convincing they sound (and “12.x” sounded very convincing).&lt;/p&gt;
&lt;p&gt;So the first real change is a set of twenty test questions. Some should be answered with a quick lookup, most need actual research, and a few should be declined. For each one I wrote down where it should go, what a correct answer has to include, where the truth lives (changelogs, release notes, upgrade guides), and when the answer is likely to go stale.&lt;/p&gt;
&lt;p&gt;Here’s the queue-worker question, trimmed down a bit:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;id&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;q06&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;question&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;What changed in the latest Laravel release that affects queue workers?&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;expectedRoute&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;research&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;expectedOutcome&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;answered&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;requiredElements&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    { &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;release&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;&quot;check&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;Identifies which release is the latest (v13.34.0 when written).&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;&quot;source&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;https://github.com/laravel/framework/releases/tag/v13.34.0&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    { &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;max-exceptions&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;&quot;check&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;Mentions that jobs can count worker crashes towards maxExceptions (PR #61737).&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;&quot;source&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;https://github.com/laravel/framework/releases/tag/v13.34.0&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  ],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;freshness&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;Written against v13.34.0 (published 2026-09-29). If a newer laravel/framework release exists at run time, the &apos;latest&apos; is different and these elements no longer apply; the reviewer must score against that release&apos;s notes or mark the question stale.&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I read the ground truth from those official sources rather than from memory. If an answer and the changelog disagree, the changelog wins.&lt;/p&gt;
&lt;p&gt;A few of the questions are there to catch one specific failure:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“What changed in Laravel 14?” should be refused, because there is no Laravel 14.&lt;/li&gt;
&lt;li&gt;“What changed in the latest Livewire release?” is a trap of a different kind. The official release notes for that version are just a compare link, so an honest answer is thin, and only a person reading it can tell.&lt;/li&gt;
&lt;li&gt;One question says “Inertia” and means the Laravel adapter. That ambiguity is deliberate.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Anything pinned to “the latest release” goes stale the day the next one ships, which is what the freshness note is for.&lt;/p&gt;
&lt;h2 id=&quot;step-3-route-before-you-call&quot;&gt;Step 3: route before you call&lt;/h2&gt;
&lt;p&gt;The biggest change, and the one that does most of the work, is a router that decides what to do with a question before anything expensive happens.&lt;/p&gt;
&lt;p&gt;Every question goes one of three ways. A &lt;strong&gt;direct&lt;/strong&gt; question gets answered by one lookup against Packagist or GitHub, with no model involved. A &lt;strong&gt;research&lt;/strong&gt; question needs several sources read and pieced together, so it gets one &lt;a href=&quot;https://docs.tabstack.ai/guides/research/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Tabstack Research&lt;/a&gt; call. Anything else gets a &lt;strong&gt;decline&lt;/strong&gt;: opinions, questions about the future or unreleased versions, and packages it doesn’t know about.&lt;/p&gt;
&lt;p&gt;Declining is a proper route here, with its own reason and its own exit code. Most of the demo’s worst answers were to questions it should have just said no to.&lt;/p&gt;
&lt;p&gt;The router is plain rules, no model. I wanted something I could unit test, explain in an article, run for free and get an answer from instantly, and a handful of regexes and &lt;code&gt;if&lt;/code&gt; statements does all of that. The rules run in order and the first match wins:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Empty, or far too long: decline.&lt;/li&gt;
&lt;li&gt;No supported package found: decline.&lt;/li&gt;
&lt;li&gt;Opinion or comparison (“should I”, “better than”, “vs”): decline.&lt;/li&gt;
&lt;li&gt;Future or unreleased (“upcoming”, “when will”, “next major”): decline.&lt;/li&gt;
&lt;li&gt;More than one package: research.&lt;/li&gt;
&lt;li&gt;One package, asking only for the latest version, date or release notes: direct.&lt;/li&gt;
&lt;li&gt;Change words, or a named version: research.&lt;/li&gt;
&lt;li&gt;Anything else: decline.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Here’s the package list and the patterns behind rules 3 and 4:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# The GitHub repo is listed explicitly because it does not always match the Packagist&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# name: filament/filament&apos;s Packagist source is the filamentphp/panels subsplit, but&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# its GitHub releases are published on filamentphp/filament.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;KNOWN_PACKAGES&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; (&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    KnownPackage(&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;laravel/framework&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;laravel/framework&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, (&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;laravel framework&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;laravel&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    KnownPackage(&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;livewire/livewire&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;livewire/livewire&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, (&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;livewire&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,)),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    KnownPackage(&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;pestphp/pest&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;pestphp/pest&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, (&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;pestphp&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;pest&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    KnownPackage(&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;filament/filament&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;filamentphp/filament&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, (&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;filament&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,)),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    KnownPackage(&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;inertiajs/inertia-laravel&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;inertiajs/inertia-laravel&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, (&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;inertia&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,)),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;OPINION&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; re.compile(&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;r&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;\b(&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;should i&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;|&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;should we&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;|&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;which is better&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;|&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;better than&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;|&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;recommend&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;\w&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;*|&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;vs&lt;/span&gt;&lt;span style=&quot;color:#22863A;font-weight:bold&quot;&gt;\.&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;?|&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;versus&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;|&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;compare&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;\w&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;*|&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;comparison&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;)\b&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;FUTURE&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; re.compile(&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;r&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;\b(&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;upcoming&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;|&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;roadmap&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;|&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;unreleased&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;|&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;planned&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;|&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;when will&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;|&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;be released&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;|&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;next &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;major&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;|&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;minor&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;|&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;release&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;|&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;version&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;))\b&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That comment about Filament is one of those things you only find out by looking. Its Packagist source points at a subsplit repo, but the releases are published on the main one, so the mapping has to be written down.&lt;/p&gt;
&lt;p&gt;The future pattern is narrower than you might expect. “will” on its own doesn’t count, so “Will my workers break when upgrading?” still goes to research. The tests also make sure “Laravel Livewire” counts as one package, and that “PHP 8.5” never gets read as a package version.&lt;/p&gt;
&lt;p&gt;The top of the router looks like this:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; route&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(raw_question: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;) -&gt; RouteDecision:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    question &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; raw_question.strip()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; not&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; 1&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; &amp;#x3C;=&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; len&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(question) &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;&amp;#x3C;=&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; 2000&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; DeclineDecision(&lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;route&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;decline&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;reason&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;The question is empty or longer than 2000 characters.&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    text &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; normalise(question)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    mentions &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; find_packages(text)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; not&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; mentions:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; DeclineDecision(&lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;route&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;decline&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;reason&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;No supported PHP package was identified in the question.&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; OPINION&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.search(text):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; DeclineDecision(&lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;route&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;decline&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;reason&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;The question asks for an opinion or comparison, not what changed.&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; FUTURE&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.search(text):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; DeclineDecision(&lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;route&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;decline&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;reason&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;The question asks about a future or unreleased version.&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Every decline carries a reason, so when the router says no, you can see why.&lt;/p&gt;
&lt;h2 id=&quot;step-4-check-the-version-exists&quot;&gt;Step 4: check the version exists&lt;/h2&gt;
&lt;p&gt;There’s one more guard before research, and it’s tiny. If a question names a version, ask Packagist whether that version exists before going any further:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    # A research call about a version that has never been tagged would answer&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    # from speculation, so check Packagist first.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; decision.version_mentioned &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;is&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; not&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; None&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;        package, version &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; decision.version_mentioned.package, decision.version_mentioned.version&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;        versions &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; fetch_versions(package, config, cancel)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; not&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; version_exists(version, [v.version_normalized &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;for&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; v &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; versions]):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;            return&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; declined(&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;f&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;Packagist lists no tagged release of &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;package&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; matching &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;version&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;.&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;), &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;False&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; research(question, config, cancel), &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;False&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So “What changed in Laravel 14?” now gets declined with “Packagist lists no tagged release of laravel/framework matching 14.” and never reaches the research API. Before this, the same question came back with a confident description of a release that doesn’t exist. One Packagist lookup, and that research call never happens.&lt;/p&gt;
&lt;h2 id=&quot;step-5-one-answer-shape-for-every-route&quot;&gt;Step 5: one answer shape for every route&lt;/h2&gt;
&lt;p&gt;Whatever route a question takes, it comes back in the same shape:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; RouteResult&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;Model&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    terminal_state: TerminalState&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    # True when the call completed but the answer cannot be relied on as a full answer.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    incomplete: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;bool&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    incomplete_reasons: list[&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    text: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;str&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    sources: list[SourceRecord]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    error: AnswerError &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;|&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; None&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    # Only ever a value the API itself reported. Never estimated.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    cost: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;None&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    cost_note: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;str&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; ApplicationAnswer&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;RouteResult&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    question: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;str&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    decision: RouteDecision&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    # True when a direct lookup was served from the cache. Defaults to False so&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    # records written before caching existed still parse.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    cached: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;bool&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; False&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    started_at: AwareDatetime&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    ended_at: AwareDatetime&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The Tabstack event objects never leave the code that talks to Tabstack, so the CLI, the stored records and the scorer don’t have to know what a streamed event is (and they’re much simpler for it).&lt;/p&gt;
&lt;p&gt;And yes, &lt;code&gt;cost&lt;/code&gt; is typed as &lt;code&gt;None&lt;/code&gt;. The API doesn’t report cost, so I made it impossible to put anything else there. No estimates, no guesses.&lt;/p&gt;
&lt;h2 id=&quot;step-6-read-what-the-apis-actually-return&quot;&gt;Step 6: read what the APIs actually return&lt;/h2&gt;
&lt;p&gt;The direct routes look like the easy part. Then you read the actual responses.&lt;/p&gt;
&lt;p&gt;Packagist’s metadata is minified. Each version entry only carries the fields that changed from the one before it, and &lt;code&gt;&quot;__unset&quot;&lt;/code&gt; removes a field. You have to expand it before you can use it, so the code does what Composer itself does:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# The p2 format is minified: each entry only carries the fields that changed from the&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# previous one, and &quot;__unset&quot; removes a field. This mirrors Composer&apos;s own&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# MetadataMinifier::expand (github.com/composer/metadata-minifier).&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; expand_minified&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(versions: list[dict[&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, Any]]) -&gt; list[dict[&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, Any]]:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    expanded: list[dict[&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, Any]] &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; []&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    previous: dict[&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, Any] &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; {}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    for&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; entry &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; versions:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;        current &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; dict&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(previous)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;        for&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; key, value &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; entry.items():&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;            if&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; value &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;__unset&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;                current.pop(key, &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;None&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;            else&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;                current[key] &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; value&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;        expanded.append(current)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;        previous &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; current&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; expanded&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The “stable” metadata also still has pre-release tags in it (233 of them for Filament alone when I checked), so the code only treats plain numeric versions as stable. The order isn’t documented either, so it sorts versions itself.&lt;/p&gt;
&lt;p&gt;GitHub held fewer surprises. The “latest release” endpoint skips drafts and pre-releases, and the unauthenticated rate limit is low, which is why the token is worth setting.&lt;/p&gt;
&lt;p&gt;What I checked, and when, is all written down in &lt;a href=&quot;https://github.com/JustSteveKing/what-changed/blob/main/docs/VERIFIED.md&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;code&gt;docs/VERIFIED.md&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;step-7-measure-the-demo&quot;&gt;Step 7: measure the demo&lt;/h2&gt;
&lt;p&gt;Before improving anything, you need a baseline. So I ran all twenty questions through the demo’s behaviour, every one straight to a research call, and saved the results in the same shape as everything else.&lt;/p&gt;
&lt;p&gt;I ran it twice, once in fast mode and once in balanced mode. Fast mode got through the lot in about four minutes. Balanced mode took nearly three quarters of an hour, with a typical call taking around two minutes and the slowest over four. In both runs, only 13 of the 20 questions ended up where they should have.&lt;/p&gt;
&lt;p&gt;The summary looks fine. Open the stored answers and it looks a lot less fine:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The Laravel 14 question got a description of Laravel 14’s changes in both runs, though no such release existed.&lt;/li&gt;
&lt;li&gt;“When will Pest 6 be released?” opened, in balanced mode, by talking about PEST 6.05, an environmental modelling package from 2003.&lt;/li&gt;
&lt;li&gt;An opinion question got a full comparison instead of a refusal, both times.&lt;/li&gt;
&lt;li&gt;The Inertia question answered about the JavaScript client instead of the Laravel adapter, both times.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Every one of those runs completed, and most of them passed most of the automatic checks. Hang on to that, we’ll come back to it.&lt;/p&gt;
&lt;h2 id=&quot;step-8-put-a-deadline-on-the-whole-call&quot;&gt;Step 8: put a deadline on the whole call&lt;/h2&gt;
&lt;p&gt;The SDK does give research calls a ten minute timeout, but it took reading the source to see what it covers. It applies to each read, so a stream that keeps sending events never hits it.&lt;/p&gt;
&lt;p&gt;The fix is one helper that wraps every outbound call with a deadline on the whole thing:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# Runs `awaitable` with a limit on the whole call, and stops it early if `cancel` is&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# set (Ctrl+C in the CLI). Either way the work in progress is cancelled, which closes&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# any open stream, and the exception says which of the two happened.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;async&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; def&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; with_deadline&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(awaitable: Awaitable[T], seconds: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;float&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, what: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, cancel: asyncio.Event &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;|&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; None&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; None&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;) -&gt; T:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    work &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; asyncio.ensure_future(awaitable)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    stop &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; asyncio.ensure_future(cancel.wait()) &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; cancel &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;is&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; not&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; None&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; else&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; None&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    waiting: set[asyncio.Future[Any]] &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; {work, stop} &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; stop &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;is&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; not&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; None&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; else&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; {work}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    try&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;        done, _ &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; asyncio.wait(waiting, &lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;timeout&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;seconds, &lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;return_when&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;asyncio.&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;FIRST_COMPLETED&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    finally&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; stop &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;is&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; not&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; None&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;            stop.cancel()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; work &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; done:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; work.result()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    work.cancel()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    with&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; contextlib.suppress(asyncio.CancelledError):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;        await&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; work&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; cancel &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;is&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; not&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; None&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; and&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; cancel.is_set():&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;        raise&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; Cancelled(&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;f&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;what&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; was cancelled&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    raise&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; TimedOut(&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;f&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;what&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; timed out after &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;{round&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(seconds &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; 1000&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; ms&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The important line is &lt;code&gt;work.cancel()&lt;/code&gt;. Cancelling the task is what closes the stream, so hitting the deadline actually frees the connection rather than leaving it running in the background. The limits are configurable, with 10 seconds for lookups and five minutes for research by default.&lt;/p&gt;
&lt;p&gt;The test is a local server that sends one event and then hangs, and the call has to give up at the limit.&lt;/p&gt;
&lt;h2 id=&quot;step-9-give-every-ending-a-name&quot;&gt;Step 9: give every ending a name&lt;/h2&gt;
&lt;p&gt;A request can end in more ways than “worked” or “threw”. Name each one, and give each its own exit code so scripts can tell them apart:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;TerminalState &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; Literal[&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;completed&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;failed&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;stream_closed&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;timed_out&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;cancelled&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;declined&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# Each terminal state has its own CLI exit code so scripts can tell them apart.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# 130 follows the shell convention for a process stopped by Ctrl+C (SIGINT).&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;EXIT_CODES&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: dict[TerminalState, &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;int&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;    &quot;completed&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;    &quot;failed&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;    &quot;declined&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;2&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;    &quot;stream_closed&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;3&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;    &quot;timed_out&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;4&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;    &quot;cancelled&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;130&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The one the demo got silently wrong was a stream that just stops. Now it gets a result of its own:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    # The stream ended without a complete or error event.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; RouteResult(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E36209&quot;&gt;        terminal_state&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;stream_closed&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E36209&quot;&gt;        incomplete&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;True&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E36209&quot;&gt;        incomplete_reasons&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;The stream ended without a terminal event.&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E36209&quot;&gt;        text&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E36209&quot;&gt;        sources&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;[],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E36209&quot;&gt;        error&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;None&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E36209&quot;&gt;        cost&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;None&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E36209&quot;&gt;        cost_note&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;COST_NOTE&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    )&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the demo that case printed nothing and exited 0. Now it says what happened.&lt;/p&gt;
&lt;h2 id=&quot;step-10-handle-ctrlc-properly&quot;&gt;Step 10: handle Ctrl+C properly&lt;/h2&gt;
&lt;p&gt;You might have noticed the deadline helper already waits on a cancel event alongside the work. That’s the hook. The CLI sets it on the first Ctrl+C:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;async&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; def&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; ask&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(question: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;) -&gt; ApplicationAnswer:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    # First Ctrl+C cancels the in-flight call and lets it report a cancelled state.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    # A second Ctrl+C exits immediately.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    cancel &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; asyncio.Event()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    def&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; on_interrupt&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;() -&gt; &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;None&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; cancel.is_set():&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;            os._exit(&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;EXIT_CODES&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;cancelled&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;])&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;        print&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;Cancelling. Press Ctrl+C again to exit immediately.&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;file&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;sys.stderr)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;        cancel.set()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    asyncio.get_running_loop().add_signal_handler(signal.&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;SIGINT&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, on_interrupt)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; answer(question, &lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;cancel&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;cancel)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first press lets the call wind down and record a &lt;code&gt;cancelled&lt;/code&gt; answer. The second gets you out immediately. The test runs the real CLI, sends it a Ctrl+C once the request is in flight, and checks it exits with the cancelled code.&lt;/p&gt;
&lt;h2 id=&quot;step-11-store-what-you-answered&quot;&gt;Step 11: store what you answered&lt;/h2&gt;
&lt;p&gt;Every answer is saved exactly as it came back, with its sources in order. It’s a small change, and it’s what made the rest of this possible: everything I’ve said about the baseline came out of these files.&lt;/p&gt;
&lt;h2 id=&quot;step-12-log-every-request&quot;&gt;Step 12: log every request&lt;/h2&gt;
&lt;p&gt;One JSON line per request, with the route, how long it took, how it ended, whether it came from the cache, how many sources, and any error. The question text is left out on purpose, because logs end up in far more places than stored answers do.&lt;/p&gt;
&lt;h2 id=&quot;step-13-retry-the-cheap-things-only&quot;&gt;Step 13: retry the cheap things only&lt;/h2&gt;
&lt;p&gt;Lookups against Packagist and GitHub are cheap and safe to repeat, so they get a few attempts with backoff. Not every failure is worth another go, though:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# Network errors, per-attempt timeouts, 429 and 5xx are worth another try.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# Other 4xx responses will not change on retry.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; is_retryable&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(error: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;Exception&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;) -&gt; &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;bool&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; isinstance&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(error, Cancelled):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; False&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; isinstance&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(error, HttpError):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; error.rate_limit_exhausted:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;            return&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; False&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; error.status &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; 429&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; or&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; error.status &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; 500&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; True&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It also gives up straight away if GitHub’s rate limit is used up, or if the server asks it to wait more than a few seconds.&lt;/p&gt;
&lt;p&gt;Research is never retried. Each call is billed, it takes minutes, and asking again can get you a different answer built from different sources, which is closer to a second opinion than a retry. And this is the bit you’d only find by reading the SDK: it retries twice by default. Leave the client alone and you’re retrying research calls without knowing it, so switch it off:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    # No automatic retries for research. A Research call is billed per call, takes&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    # seconds to minutes, and a second attempt can return a different answer from&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    # different sources. Retrying quietly would hide a failure and could double the&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    # spend, so a failed call is reported and the caller decides whether to ask again.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    # The SDK retries twice by default, so this has to be switched off explicitly.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    client &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; client &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;or&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; AsyncTabstack(&lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;max_retries&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    async&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; def&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; call&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;() -&gt; RouteResult:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;        stream &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; client.agent.research(&lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;query&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;question, &lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;mode&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;config.research_mode)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;        async&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; with&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; stream:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;            return&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; collect_research(stream)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    # The SDK&apos;s timeout is per read, so a stream that keeps sending events is never&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    # cut off by it. This deadline covers the whole call.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    # Setting `cancel` (Ctrl+C in the CLI) stops it early and closes the stream.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; with_deadline(call(), config.research_timeout_s, &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;Research&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, cancel)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There’s a test that fails if the default ever sneaks back in. With it left on, the local test server saw three requests for a single call.&lt;/p&gt;
&lt;h2 id=&quot;step-14-cache-the-lookups-not-the-research&quot;&gt;Step 14: cache the lookups, not the research&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; decision.route &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;direct&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;        key &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; normalise(question)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;        hit &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; cache.get(key)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; hit &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;is&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; not&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; None&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;            return&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; hit, &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;True&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;        result &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; direct(decision.lookup, config, cancel)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;        # Only successful lookups are cached; a failure should be retried next time.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; result.terminal_state &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;completed&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;            cache.set(key, result)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; result, &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;False&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Lookups are cached for a minute, and only when they succeed. Research isn’t cached at all, because the whole point of asking is a current answer. Tabstack does keep its own server-side cache, though, and every run here used the default setting for it.&lt;/p&gt;
&lt;h2 id=&quot;step-15-test-the-stream-without-the-network&quot;&gt;Step 15: test the stream without the network&lt;/h2&gt;
&lt;p&gt;You don’t want your tests making billed calls, but you also don’t want to mock away the bit most likely to break. So the tests point the real SDK at a small local server that replays recorded events. The SDK’s own parsing, streaming and cancelling code all gets exercised, with no API key and no network.&lt;/p&gt;
&lt;p&gt;There are three replay files, and they didn’t all come from the same place. The successful one was recorded from a real call. The “stream just stops” one is that recording with the final event taken off. The error one is real events followed by an error I built by hand, because I never actually saw a real streamed error. Each file says which of those it is, so nobody mistakes the made-up one for a recording later.&lt;/p&gt;
&lt;p&gt;There’s also a test that makes sure neither API key ever ends up in a saved answer or a replay file, which you want in place before you record anything live.&lt;/p&gt;
&lt;p&gt;Back on &lt;code&gt;main&lt;/code&gt;, &lt;code&gt;make check&lt;/code&gt; runs the whole suite plus mypy, without touching the network or spending any credits:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; checkout&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; main&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;make&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; check&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To ask the finished version a question yourself:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; TABSTACK_API_KEY&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;make&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; ask&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; Q=&quot;What is the latest stable version of livewire/livewire?&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That one’s a direct Packagist lookup, so it costs nothing. Only questions that get routed to research spend credits.&lt;/p&gt;
&lt;h2 id=&quot;step-16-run-it-again-and-compare&quot;&gt;Step 16: run it again and compare&lt;/h2&gt;
&lt;p&gt;With everything in place, the same twenty questions went through again in balanced mode, this time through the router.&lt;/p&gt;
&lt;p&gt;Every question went where it was supposed to. Five turned into lookups that came back in well under a second, three were turned away without any call at all, and only twelve needed research instead of twenty. The whole run took about 24 minutes instead of 46.&lt;/p&gt;
&lt;p&gt;None of the safety nets fired, though. No failures, timeouts, dropped streams or cancellations in any live run, so everything I’ve said about deadlines and cancellation is backed by the tests and nothing else.&lt;/p&gt;
&lt;p&gt;The research questions went through exactly the same call both times, so any difference in those answers comes from Tabstack, not from anything I changed. They took about as long as before too, a little under two minutes for a typical one.&lt;/p&gt;
&lt;p&gt;A couple of the automatic checks look worse on paper, but only because a declined answer has no sources by design. Leave the declines out and every answer has sources.&lt;/p&gt;
&lt;p&gt;One thing got worse for real, which you’ll see in the next step: the research answers scored slightly lower than before, with the same call on the same questions. The router didn’t cause that and couldn’t have prevented it.&lt;/p&gt;
&lt;p&gt;The saved answers turned up one more oddity, too. The Livewire question dated Livewire 4’s first release to August 2025. Packagist has v4.0.0 in January 2026.&lt;/p&gt;
&lt;h2 id=&quot;step-17-score-the-answers-separately&quot;&gt;Step 17: score the answers separately&lt;/h2&gt;
&lt;p&gt;Remember those baseline runs that completed and passed most of the checks? Those checks look at things like whether the route was right, whether there are sources, and whether the URLs are valid. All plumbing. The Livewire question passes every one of them in every run without ever saying what changed in that release.&lt;/p&gt;
&lt;p&gt;So I scored the answers on their own, against the things each correct answer had to include, marking each one as fully there, partly there or missing.&lt;/p&gt;
&lt;p&gt;Full disclosure on how that was done: a model (Claude Opus 5.5) drafted the scores, and for each one it had to quote the exact bit of the answer it was judging. A validator rejects any quote that isn’t really in the answer. I went through every judgement call myself (21 of them, every “partly” plus the borderline ones) and agreed with all of them. The rest I accepted as drafted, and the scoring sheets say which is which.&lt;/p&gt;
&lt;p&gt;Here’s how many of the 44 required points each run got fully right:&lt;/p&gt;
&lt;div class=&quot;table-scroll&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Run&lt;/th&gt;
&lt;th&gt;Fully right&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Demo, fast mode&lt;/td&gt;
&lt;td&gt;22 of 44&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Demo, balanced mode&lt;/td&gt;
&lt;td&gt;33 of 44&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;With the router&lt;/td&gt;
&lt;td&gt;35 of 44&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;The improvement comes from the parts the router controls. Direct lookups went from 7 of 9 to all 9, and declines from 1 of 3 to all 3. The research answers went from 25 of 32 to 23, and that’s Tabstack on the day.&lt;/p&gt;
&lt;p&gt;The validator stops the model inventing quotes. It can’t stop it misreading a quote that’s really there, which is why I went through the judgement calls myself, and why I’m telling you which ones I didn’t.&lt;/p&gt;
&lt;h2 id=&quot;step-18-the-production-checklist&quot;&gt;Step 18: the production checklist&lt;/h2&gt;
&lt;p&gt;The last piece is &lt;a href=&quot;https://github.com/JustSteveKing/what-changed/blob/main/docs/CHECKLIST.md&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;a checklist&lt;/a&gt; covering quality, latency, cost, failure handling, privacy and ownership. Each item is a pass, a fail, or “can’t verify from outside”, because some of this you just can’t check from your side of the API.&lt;/p&gt;
&lt;p&gt;Cost is the clearest case. Tabstack’s &lt;a href=&quot;https://docs.tabstack.ai/pricing/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;pricing page&lt;/a&gt; says plans, spend and credit balance are in the console, “not in any API response”. So the app can’t record cost per request at all, and there’s no cost figure anywhere in this tutorial.&lt;/p&gt;
&lt;p&gt;Retention comes from Tabstack’s &lt;a href=&quot;https://tabstack.ai/legal/privacy&quot;&gt;Privacy Notice&lt;/a&gt;, which is the source of truth for it. Research questions are run through third-party models, and history, including your content and outputs, is stored “for 90 days or unless you delete it.” So that’s the number the checklist works from: anything you put in a question is kept for up to 90 days unless you delete it, which is worth knowing before you send anything sensitive.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://docs.tabstack.ai/trust/data-handling/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;data handling page&lt;/a&gt; adds that SOC 2 and ISO 27001 aren’t currently held. Read &lt;a href=&quot;https://tabstack.ai/legal/privacy&quot;&gt;the Privacy Notice itself&lt;/a&gt; rather than relying on my summary.&lt;/p&gt;
&lt;p&gt;The app has fails of its own. Nothing spots a stalled stream before the overall deadline, there’s no spending limit, and Tabstack’s rate-limit headers aren’t checked up front. Saved answers never expire. Nobody is named as the owner, and it’s nobody’s job to refresh the questions as they go stale.&lt;/p&gt;
&lt;p&gt;And on latency, the slowest research call took just over four minutes against a five minute deadline, which doesn’t leave much margin.&lt;/p&gt;
&lt;h2 id=&quot;what-id-take-from-this&quot;&gt;What I’d take from this&lt;/h2&gt;
&lt;p&gt;The demo works. Production is everything around the call, and for this project most of that turned out to be deciding when not to make it.&lt;/p&gt;
&lt;p&gt;If I was starting again, the bits I’d do on day one are: write the questions and the right answers down first, route before you call, check a version exists before asking about it, and give every way a request can end its own name. Then read the SDK source (the default retries and the timeout behaviour weren’t obvious anywhere else), and never retry anything that costs money or can come back with a different answer.&lt;/p&gt;
&lt;p&gt;And be upfront about what you don’t know. Twenty questions on one day is a small sample, a model drafted most of the scores, and some things, like cost, you can’t verify from outside at all.&lt;/p&gt;
&lt;h2 id=&quot;where-it-goes-next&quot;&gt;Where it goes next&lt;/h2&gt;
&lt;p&gt;The checklist fails are basically my to-do list: something to catch a stalled stream well before the overall deadline, a spending limit, actually reading the rate-limit headers instead of finding out the hard way, an expiry for saved answers, a named owner, and a schedule for refreshing stale questions. I also want to try turning off Tabstack’s cache and see what a guaranteed fresh fetch does to the answers.&lt;/p&gt;
&lt;p&gt;If you want to check any of the numbers, &lt;a href=&quot;https://github.com/JustSteveKing/what-changed/blob/main/docs/EVIDENCE.md&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;code&gt;docs/EVIDENCE.md&lt;/code&gt;&lt;/a&gt; has the lot.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Grab the &lt;code&gt;demo&lt;/code&gt; tag, ask it about a package you actually use, and write down the right answer before you read what it says. Then go and have a look at your own demos (mine clearly needed it).&lt;/em&gt;&lt;/p&gt;
</content:encoded></item><item><title>Build a 20-question evaluation set for a live-web feature</title><link>https://tabstack.ai/blog/twenty-question-live-web-evaluation-set</link><guid isPermaLink="true">https://tabstack.ai/blog/twenty-question-live-web-evaluation-set</guid><description>Define expected answer elements, freeze source evidence, and review a live-web feature with a small evaluation set that keeps failures and missing scores visible.</description><pubDate>Wed, 07 Oct 2026 14:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Before you ship a live-web feature, test it on questions your users will ask. Choose 20 from one workflow and define what each answer must include. Then check whether the answer covers the question and whether its sources support the claims.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://github.com/Mozilla-Ocho/tabstack-cited-research-python/tree/460b72e174187796eae5707b7a028aa13b8af09c/acceptance-evals&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Python evaluation kit&lt;/a&gt; handles the run records and review files. It includes 20 candidate questions for an API-documentation assistant and one live pilot run. That pilot shows why coverage and source support need separate checks.&lt;/p&gt;
&lt;h2 id=&quot;define-what-a-useful-answer-includes&quot;&gt;Define what a useful answer includes&lt;/h2&gt;
&lt;p&gt;For a documentation assistant, select questions that test five common problems:&lt;/p&gt;
&lt;div class=&quot;table-scroll&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;What to check&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Source discovery&lt;/td&gt;
&lt;td&gt;Finding the relevant official documentation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exact contract&lt;/td&gt;
&lt;td&gt;Returning the right fields, parameters, and limits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Synthesis&lt;/td&gt;
&lt;td&gt;Combining pages without losing qualifications&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Freshness and version scope&lt;/td&gt;
&lt;td&gt;Using the requested date, version, or plan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unresolved evidence&lt;/td&gt;
&lt;td&gt;Recognizing facts the sources cannot establish&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;We use four questions per category. Adjust the mix to your workflow, including straightforward lookups as well as harder research tasks.&lt;/p&gt;
&lt;p&gt;Write the required answer elements before running each question. For example:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;What fields does Ollama’s web-search API return for each result, and does its &lt;code&gt;content&lt;/code&gt; field contain the full page?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A complete answer must name &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;url&lt;/code&gt;, and &lt;code&gt;content&lt;/code&gt;, explain that &lt;code&gt;content&lt;/code&gt; contains a relevant snippet, and cite the official response definition. The &lt;a href=&quot;https://docs.ollama.com/capabilities/web-search&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Ollama documentation&lt;/a&gt; provides the reference:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;content (string): relevant content snippet from the web page&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The kit stores each question in JSON Lines. Here is Q05, expanded for readability:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;id&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;Q05&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;category&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;exact_contract&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;question&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;What fields does Ollama&apos;s web-search API return for each result, and does its content field contain the full page?&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;required_elements&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;      &quot;id&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;E1&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;      &quot;criterion&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;Name title, url, and content as the result fields.&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;      &quot;id&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;E2&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;      &quot;criterion&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;Describe content as a relevant snippet, not the full page.&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;      &quot;id&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;E3&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;      &quot;criterion&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;Cite the official response definition.&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  ],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;reference_sources&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;      &quot;url&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;https://docs.ollama.com/capabilities/web-search&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;      &quot;passage&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;content (string): relevant content snippet from the web page&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;      &quot;retrieved_at_utc&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;2026-10-06T17:37:54Z&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;      &quot;date_or_version&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;Documentation as retrieved; no version identified in this passage.&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  ],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;evidence_status&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;reviewed&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;critical_failure_conditions&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;    &quot;States that web_search returns full page content.&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  ]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The critical failure condition identifies an error your application should reject even when the rest of the answer is correct. Set these rules yourself and keep them consistent across implementations.&lt;/p&gt;
&lt;h2 id=&quot;review-the-evidence-then-freeze-the-set&quot;&gt;Review the evidence, then freeze the set&lt;/h2&gt;
&lt;p&gt;Open each reference and save the relevant passage, URL, and retrieval time. Record the source’s version or date when available. For time-sensitive questions, specify the date in the question rather than asking only for “current” information.&lt;/p&gt;
&lt;p&gt;Only Q05 has reviewed evidence in the &lt;a href=&quot;https://github.com/Mozilla-Ocho/tabstack-cited-research-python/blob/460b72e174187796eae5707b7a028aa13b8af09c/acceptance-evals/questions.candidate.jsonl&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;candidate set&lt;/a&gt;. The other 19 questions remain pending, so the kit refuses to run the full set. Review their references before including them in scored results.&lt;/p&gt;
&lt;p&gt;Freezing records the dataset version and hash. The kit rejects a changed dataset or evidence older than the freeze record allows, which defaults to 30 days. Once the committed Q05 evidence is more than 30 days old, refresh it and freeze a new version before you run the pilot.&lt;/p&gt;
&lt;p&gt;If a source changes, update the reference under a new dataset version. Do not rewrite the expected answer to fit a result you have already seen.&lt;/p&gt;
&lt;h2 id=&quot;run-a-one-question-pilot&quot;&gt;Run a one-question pilot&lt;/h2&gt;
&lt;p&gt;The kit extends the &lt;a href=&quot;https://tabstack.ai/blog/trace-question-to-source-backed-answer&quot;&gt;traced Python Research example&lt;/a&gt;, which saves the report, ordered sources, and request timeline. Start with one question to check the output and review process before expanding the run.&lt;/p&gt;
&lt;p&gt;The recorded pilot used macOS arm64, Python 3.12.13, uv 0.11.6, and the &lt;code&gt;tabstack&lt;/code&gt; Python SDK 2.8.5. Create a key in the &lt;a href=&quot;https://console.tabstack.ai&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Tabstack console&lt;/a&gt;, then run:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; clone&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; https://github.com/Mozilla-Ocho/tabstack-cited-research-python.git&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;cd&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; tabstack-cited-research-python&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; checkout&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; 460b72e174187796eae5707b7a028aa13b8af09c&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;uv&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; sync&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; --frozen&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; TABSTACK_API_KEY&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;...   &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# Set your key locally; do not commit it.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;cp&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; acceptance-evals/pilot/questions.pilot-q05.jsonl&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; acceptance-evals/pilot/questions.my-pilot.jsonl&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;uv&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; run&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; cited-research-accept&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; freeze&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  --dataset&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; acceptance-evals/pilot/questions.my-pilot.jsonl&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  --version&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; my-pilot-v1&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  --allow-subset&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;uv&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; run&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; cited-research-accept&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; run-one&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  --dataset&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; acceptance-evals/pilot/questions.my-pilot.jsonl&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  --question&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; Q05&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  --run&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; acceptance-evals/runs/my-pilot-q05&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  --mode&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; fast&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  --nocache&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  --silence-timeout&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; 120&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  --deadline&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; 300&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  --pilot&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;run-one&lt;/code&gt; disables SDK and application retries, so each attempt represents one request. It sends the question and a neutral output instruction to Research; the expected answer elements stay in reviewer-side files. The &lt;code&gt;--pilot&lt;/code&gt; flag keeps this run out of scored totals.&lt;/p&gt;
&lt;p&gt;The committed pilot’s terminal output includes:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;complete  report -&gt; acceptance-evals/runs/20261006-pilot-q05/answers/Q05-a1/report.md&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;sources (3) -&gt; acceptance-evals/runs/20261006-pilot-q05/answers/Q05-a1/sources.json&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  1. Web search  https://docs.ollama.com/capabilities/web-search&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  2. Web search  https://docs.ollama.com/capabilities/web-search.md&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  3. Testing Ollama Web Search 🔍 and a Thinking Model 💭  https://dev.to/aairom/testing-ollama-web-search-and-a-thinking-model-1dh7&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;review sheet -&gt; acceptance-evals/runs/20261006-pilot-q05/answers/Q05-a1/review-sheet.csv (unreviewed)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;attempt Q05-a1: complete (exit 0), 3 sources -&gt; acceptance-evals/runs/20261006-pilot-q05/attempts.jsonl&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use a new run directory. The kit refuses to overwrite an existing attempt; &lt;code&gt;--another-attempt&lt;/code&gt; creates a separate record such as &lt;code&gt;Q05-a2&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;check-coverage-and-source-support&quot;&gt;Check coverage and source support&lt;/h2&gt;
&lt;p&gt;Prepare the review files:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;uv&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; run&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; cited-research-accept&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; prepare-review&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  --run&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; acceptance-evals/runs/my-pilot-q05&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In &lt;code&gt;reviews/coverage.csv&lt;/code&gt;, score each required answer element. In &lt;code&gt;reviews/claims.csv&lt;/code&gt;, check individual claims against the cited passages. Both use the same scale:&lt;/p&gt;
&lt;div class=&quot;table-scroll&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Score&lt;/th&gt;
&lt;th&gt;Coverage&lt;/th&gt;
&lt;th&gt;Source support&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Covers the element fully&lt;/td&gt;
&lt;td&gt;Supports the claim at its stated scope&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Covers part or misses a qualifier&lt;/td&gt;
&lt;td&gt;Supports part or requires a qualifier&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Omits or contradicts the element&lt;/td&gt;
&lt;td&gt;Does not support or contradicts the claim&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;U&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Unresolved after review&lt;/td&gt;
&lt;td&gt;Uninspected or inaccessible evidence&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Leave a cell blank if you have not reviewed it yet.&lt;/p&gt;
&lt;p&gt;Record the answer passage and your reason for each score. The &lt;a href=&quot;https://tabstack.ai/blog/useful-citations-ai-answers&quot;&gt;citation review guide&lt;/a&gt; explains the source checks in more detail.&lt;/p&gt;
&lt;p&gt;The Q05 pilot answered the main question correctly:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Crucially, the &lt;code&gt;content&lt;/code&gt; field within each web search result contains a &lt;strong&gt;relevant content snippet&lt;/strong&gt; from the web page, not the full page content [1][2][3].&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The first review scored all three required elements &lt;code&gt;2&lt;/code&gt;. But the answer also added a sentence saying that &lt;code&gt;web_fetch&lt;/code&gt; retrieves the &lt;strong&gt;full content&lt;/strong&gt; of a page. The documentation says &lt;strong&gt;main content&lt;/strong&gt;, so the claim review scored that statement &lt;code&gt;1&lt;/code&gt;. The review split that sentence into two claims.&lt;/p&gt;
&lt;p&gt;That extra claim did not affect coverage because the question did not ask about &lt;code&gt;web_fetch&lt;/code&gt;. It still needs correction before a developer relies on it.&lt;/p&gt;
&lt;p&gt;A second, blind review that could not see the first scores caught one more. The answer says the search API “returns an array” of results, but the documentation says it returns an object containing a &lt;code&gt;results&lt;/code&gt; array. That claim also scored &lt;code&gt;1&lt;/code&gt;, so three of the five claims scored &lt;code&gt;2&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Sources [1] and [2] are HTML and Markdown versions of the same official page, not independent evidence. Source [3] was not opened, and the review records that gap. The CLI preserves source order so each citation number continues to point to the correct page.&lt;/p&gt;
&lt;p&gt;An AI agent did both reviews; the second could not see the first one’s scores. The final decision remains blank because the pilot defined only a critical failure condition, not full release criteria.&lt;/p&gt;
&lt;h2 id=&quot;read-the-summary-without-hiding-failures&quot;&gt;Read the summary without hiding failures&lt;/h2&gt;
&lt;p&gt;After completing the reviews, run:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;uv&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; run&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; cited-research-accept&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; summarize&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  --run&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; acceptance-evals/runs/my-pilot-q05&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The summary separates completed requests from accepted answers. It includes failed requests in the attempt total and shows unfinished reviews separately. For coverage and support scores, it uses inspected rows and leaves &lt;code&gt;U&lt;/code&gt; and blank cells unscored.&lt;/p&gt;
&lt;p&gt;For example, the &lt;strong&gt;synthetic offline test&lt;/strong&gt; has six attempts: three completed and three failed. One answer was accepted, giving &lt;code&gt;1/6&lt;/code&gt; accepted attempts. Coverage was &lt;code&gt;2/4&lt;/code&gt; because two of the four inspected elements were fully covered. The remaining &lt;code&gt;U&lt;/code&gt; and blank cells stayed visible but unscored. These are test-fixture results, not live API performance.&lt;/p&gt;
&lt;p&gt;Cost remains unavailable until you attach authoritative usage records in &lt;code&gt;reviews/usage.csv&lt;/code&gt;. Research streams model token counts but no per-request credit record. Include billed failures and retries; do not treat missing usage as zero.&lt;/p&gt;
&lt;p&gt;Handle the stream’s &lt;code&gt;error&lt;/code&gt; event explicitly. A client timeout stops the wait but does not establish server-side cancellation. The attempt log preserves these outcomes so a failed request cannot disappear from the evaluation.&lt;/p&gt;
&lt;h2 id=&quot;expand-after-the-pilot&quot;&gt;Expand after the pilot&lt;/h2&gt;
&lt;p&gt;The &lt;a href=&quot;https://github.com/Mozilla-Ocho/tabstack-cited-research-python/blob/460b72e174187796eae5707b7a028aa13b8af09c/acceptance-evals/handoff/TEST-OUTPUT.txt&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;test receipt&lt;/a&gt; reports 255 passing tests, with Ruff and Pyright passing. The tests cover dataset validation, terminal states, incomplete reviews, source order, and summary calculations.&lt;/p&gt;
&lt;p&gt;Only one question has run live. All failure tests use offline fixtures. Balanced mode, &lt;code&gt;--fetch-timeout&lt;/code&gt;, and &lt;code&gt;--another-attempt&lt;/code&gt; remain untested against the API, as do Linux, Windows, and reproduction by a second engineer. Cost calculations use synthetic values only. Twenty questions can reveal specific defects, but they cannot establish production reliability or provider superiority.&lt;/p&gt;
&lt;p&gt;Finish reviewing the references for your remaining questions, then use the results to choose the next engineering change. Wrong version scope calls for better version selection. Unsupported claims call for a better evidence path or narrower wording. Slow responses call for a separately evaluated configuration or a different interaction.&lt;/p&gt;
&lt;p&gt;Keep private questions and credentials out of the public dataset. Review the &lt;a href=&quot;https://docs.tabstack.ai/guides/research&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Research guide&lt;/a&gt;, &lt;a href=&quot;https://tabstack.ai/trust&quot;&gt;Trust page&lt;/a&gt;, and &lt;a href=&quot;https://tabstack.ai/legal/privacy&quot;&gt;Privacy Notice&lt;/a&gt; before using hosted processing for a sensitive workflow.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/Mozilla-Ocho/tabstack-cited-research-python/tree/460b72e174187796eae5707b7a028aa13b8af09c/acceptance-evals&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Run the kit&lt;/a&gt; on one question and complete both reviews before expanding to the full set.&lt;/p&gt;
</content:encoded></item><item><title>When should you use search, agentic search, a Research API, or build the loop yourself?</title><link>https://tabstack.ai/blog/search-agentic-search-research-api-or-diy</link><guid isPermaLink="true">https://tabstack.ai/blog/search-agentic-search-research-api-or-diy</guid><description>Compare search APIs, agentic search, Research APIs, and DIY loops. Choose by required output, control, data boundaries, and operational ownership.</description><pubDate>Mon, 05 Oct 2026 14:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Use a search API when your application needs sources. Use agentic search when your model needs to choose what to investigate next. Use a Research API when your application needs a finished answer with citations. Build the loop yourself when control over source selection, evidence handling, or synthesis is part of your product.&lt;/p&gt;
&lt;p&gt;The decision starts with the job. If a coding assistant needs a parameter definition from a known documentation page, fetch that page. If it needs to compare current capabilities across several projects, someone must find the sources, read them, check coverage, and reconcile the evidence before writing the answer.&lt;/p&gt;
&lt;p&gt;Choose where that work belongs and which parts your team needs to control.&lt;/p&gt;
&lt;h2 id=&quot;first-define-what-your-application-must-receive&quot;&gt;First, define what your application must receive&lt;/h2&gt;
&lt;p&gt;“Add web access” leaves the output undefined. Write the result your application actually needs:&lt;/p&gt;
&lt;div class=&quot;table-scroll&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Required output&lt;/th&gt;
&lt;th&gt;Starting point&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Relevant URLs and snippets&lt;/td&gt;
&lt;td&gt;Search&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clean content from a known URL&lt;/td&gt;
&lt;td&gt;Fetch or Markdown extraction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Specific fields from a known page&lt;/td&gt;
&lt;td&gt;Structured extraction, followed by value checks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;An answer supported by current sources&lt;/td&gt;
&lt;td&gt;A research loop, operated by your application, agent, or managed service&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A completed website task&lt;/td&gt;
&lt;td&gt;Browser automation with explicit action boundaries&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;These jobs can exist inside the same application. A pasted link does not need source discovery. A request to find current deployment guidance might. Reading a pricing page and comparing several vendors’ pricing are different workflows even when both begin with a URL.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://tabstack.ai/blog/search-vs-research&quot;&gt;search-versus-research guide&lt;/a&gt; explains the work between finding sources and producing a cited answer. This guide helps you decide who should perform it.&lt;/p&gt;
&lt;h2 id=&quot;what-each-approach-gives-you&quot;&gt;What each approach gives you&lt;/h2&gt;
&lt;h3 id=&quot;a-search-api-returns-candidates-for-your-application-to-use&quot;&gt;A search API returns candidates for your application to use&lt;/h3&gt;
&lt;p&gt;A search API fits a source picker, a documentation finder, or a system that already knows how to build an answer from retrieved material.&lt;/p&gt;
&lt;p&gt;For example, &lt;a href=&quot;https://docs.ollama.com/capabilities/web-search&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Ollama’s Web Search API&lt;/a&gt; returns results with &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;url&lt;/code&gt;, and a relevant &lt;code&gt;content&lt;/code&gt; snippet. Its Web Fetch API returns a page’s main content and links. The two responses support different next steps: a snippet helps identify a source; fetched content gives your application material to inspect.&lt;/p&gt;
&lt;p&gt;If your feature displays links, search can finish the job. If it displays a cited explanation, your application still owns source selection, reading, answer construction, and the connection between claims and sources. That can be a small, bounded implementation when one authoritative page contains the answer.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Choose search when finding sources is the required output, or your existing downstream system already meets the answer requirements.&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&quot;agentic-search-lets-the-model-decide-what-to-do-next&quot;&gt;Agentic search lets the model decide what to do next&lt;/h3&gt;
&lt;p&gt;In model-operated agentic search, the application gives the model search and fetch tools. The model chooses a query, reads the results, and decides whether to open a page, search again, or answer. The application executes those tool calls and enforces their limits.&lt;/p&gt;
&lt;p&gt;Ollama’s &lt;a href=&quot;https://docs.ollama.com/capabilities/web-search#building-a-search-agent&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;search-agent example&lt;/a&gt; demonstrates this pattern. It adds tool results to the conversation and continues until the model stops requesting tools.&lt;/p&gt;
&lt;p&gt;This approach fits a task whose next step depends on what the agent learns. A coding assistant can inspect a local dependency version, retrieve the corresponding documentation, and use it while proposing a change. The application decides which local context the model can use and which information it can send in a public-web query.&lt;/p&gt;
&lt;p&gt;You still maintain tool definitions, context handling, loop limits, source mappings, and failure behavior. Treat web content as untrusted input; it must not grant the agent new permissions or override the application’s instructions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Choose agentic search when the model’s adaptive investigation belongs inside the larger task.&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&quot;a-research-api-returns-the-answer-before-your-model-continues&quot;&gt;A Research API returns the answer before your model continues&lt;/h3&gt;
&lt;p&gt;A managed Research API takes a question and performs the answer-building work inside the service. Your application receives the researched output instead of executing each search and fetch decision itself.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.tabstack.ai/guides/research&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Tabstack Research&lt;/a&gt; plans queries, discovers and reads sources, checks gaps, follows up when needed, and synthesizes a Markdown report with citation metadata. It returns the report on the stream’s terminal &lt;code&gt;complete&lt;/code&gt; event. Your application can give that result to your own model as an input to its next step.&lt;/p&gt;
&lt;p&gt;That boundary suits a briefing feature or assistant that needs a current public-web answer but does not need to control every intermediate retrieval decision.&lt;/p&gt;
&lt;p&gt;Your application still owns acceptance. Check whether the answer covers the question and whether its decision-driving claims have support. Handle task-level &lt;code&gt;error&lt;/code&gt; events, and treat a stream that ends without a terminal event as incomplete. Research always streams; receiving progress is not the same as receiving the finished report.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Choose a Research API when you need the answer as an input and the service’s exposed controls satisfy the job.&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&quot;a-diy-loop-gives-your-team-control-over-the-implementation&quot;&gt;A DIY loop gives your team control over the implementation&lt;/h3&gt;
&lt;p&gt;A custom loop lets you choose the retrieval components, source-selection rules, context limits, reconciliation method, synthesis prompts, and citation construction. You can combine deterministic steps with model decisions.&lt;/p&gt;
&lt;p&gt;Build when those choices create product value or satisfy a requirement a managed service cannot meet. Examples include a prescribed source hierarchy, retrieval across private and public systems, or a requirement to preserve selected evidence passages in your own format.&lt;/p&gt;
&lt;p&gt;Ownership includes maintenance. Your team handles changed pages, failed fetches, provider limits, model changes, incomplete evidence, and malformed output. It also maintains the evaluation that determines whether the loop still works.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Choose DIY when you can name the intermediate decisions you must control and assign the engineering capacity to operate them.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Agentic search and DIY overlap. “Agentic” describes how the system chooses its next action; “DIY” describes what your team builds and owns. A managed Research service can also run an agentic loop internally. Inspect the response contract and control boundary rather than choosing from the label alone.&lt;/p&gt;
&lt;h2 id=&quot;the-decision-matrix&quot;&gt;The decision matrix&lt;/h2&gt;
&lt;div class=&quot;table-scroll&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;What your application receives&lt;/th&gt;
&lt;th&gt;Who chooses follow-up work?&lt;/th&gt;
&lt;th&gt;What your team maintains&lt;/th&gt;
&lt;th&gt;Prefer another path when…&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Search API&lt;/td&gt;
&lt;td&gt;Candidate sources, commonly URLs and snippets&lt;/td&gt;
&lt;td&gt;Your application or model&lt;/td&gt;
&lt;td&gt;Retrieval integration and any downstream answer-building&lt;/td&gt;
&lt;td&gt;You need a finished answer but have no dependable downstream loop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model-operated agentic search&lt;/td&gt;
&lt;td&gt;The configured agent’s answer or task result&lt;/td&gt;
&lt;td&gt;Your model, within application limits&lt;/td&gt;
&lt;td&gt;Tools, prompts, context, orchestration, and acceptance checks&lt;/td&gt;
&lt;td&gt;The web question is a separable subtask and the model does not need intermediate control&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Managed Research API&lt;/td&gt;
&lt;td&gt;The service’s documented report and citation format&lt;/td&gt;
&lt;td&gt;The service, within its exposed controls&lt;/td&gt;
&lt;td&gt;Integration, output acceptance, and provider fit&lt;/td&gt;
&lt;td&gt;Required sources, controls, or processing boundaries fall outside the service’s contract&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DIY research loop&lt;/td&gt;
&lt;td&gt;The output contract you implement&lt;/td&gt;
&lt;td&gt;Your code, models, or both&lt;/td&gt;
&lt;td&gt;Components, orchestration, evidence handling, evaluation, and operations&lt;/td&gt;
&lt;td&gt;The custom loop adds no necessary control and your team cannot justify maintaining it&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;The architecture does not establish which implementation answers better, runs faster, or costs less. Compare the complete configurations on your questions. Include failed attempts and incomplete outputs rather than pricing only the successful demo.&lt;/p&gt;
&lt;h2 id=&quot;where-does-the-work-run&quot;&gt;Where does the work run?&lt;/h2&gt;
&lt;p&gt;Use this process model before selecting a provider:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://tabstack.ai/blog/search-agentic-search-research-api-or-diy&quot;&gt;View the diagram in the original post.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;If a required processing boundary rules out a provider, eliminate it before testing answer quality. A good report cannot make an incompatible data path acceptable.&lt;/p&gt;
&lt;h2 id=&quot;apply-the-guide-to-a-concrete-job&quot;&gt;Apply the guide to a concrete job&lt;/h2&gt;
&lt;p&gt;Suppose a developer assistant receives this question:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;What does Ollama’s Web Search API return, how does that differ from Web Fetch, and can I display the search response as a cited answer?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Before running anything, define the required answer elements:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Name the documented fields in each response.&lt;/li&gt;
&lt;li&gt;Distinguish a relevant snippet from fetched page content.&lt;/li&gt;
&lt;li&gt;Explain the application work required to turn those responses into an answer.&lt;/li&gt;
&lt;li&gt;Cite the documentation for the API facts.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Here is a complete, documentation-derived answer example, not output from a new API run:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Ollama’s Web Search API returns a &lt;code&gt;results&lt;/code&gt; array whose entries contain &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;url&lt;/code&gt;, and a relevant &lt;code&gt;content&lt;/code&gt; snippet. Web Fetch returns &lt;code&gt;title&lt;/code&gt;, the page’s main &lt;code&gt;content&lt;/code&gt;, and &lt;code&gt;links&lt;/code&gt;. Neither response defines a finished cited-answer format. Your application can display search results directly, but it must construct the explanation and attach supporting sources if the feature promises a cited answer. See the &lt;a href=&quot;https://docs.ollama.com/capabilities/web-search&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Web Search and Web Fetch documentation&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;For this job, start by reading the known documentation page.&lt;/strong&gt; It contains the response definitions. If your application can write the explanation and preserve its source, it does not need an open-ended research loop.&lt;/p&gt;
&lt;p&gt;Change the job and the choice can change:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Find official web-search documentation.&lt;/strong&gt; Use search when you do not already know the destination.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Inspect this repository’s dependency version and work out the relevant integration steps.&lt;/strong&gt; Use an agentic path when the model needs to move between local inspection and public documentation. Keep private context out of public queries unless your policy explicitly permits it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Compare current retrieval approaches for a developer assistant using evidence across several projects.&lt;/strong&gt; Evaluate a Research API if your application needs the comparison report before its model continues. Freeze the comparison criteria first.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use only approved sources and preserve the exact passages selected at every stage.&lt;/strong&gt; Build or select a loop that exposes those controls. Do not infer that a report and citation list satisfy an evidence-archive requirement.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The question determines the workflow. A product does not need one architecture for every web request.&lt;/p&gt;
&lt;h2 id=&quot;when-tabstack-is-not-the-answer&quot;&gt;When Tabstack is not the answer&lt;/h2&gt;
&lt;p&gt;We built Tabstack to return finished web output to applications. These requirements point elsewhere:&lt;/p&gt;
&lt;div class=&quot;table-scroll&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Your requirement&lt;/th&gt;
&lt;th&gt;Better starting point&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;You only need ranked links and snippets&lt;/td&gt;
&lt;td&gt;A search API&lt;/td&gt;
&lt;td&gt;Research performs answer-building work the feature does not need&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Your existing assistant or search loop already meets the job&lt;/td&gt;
&lt;td&gt;Keep the existing path&lt;/td&gt;
&lt;td&gt;A new provider needs a concrete reason to enter the architecture&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Your product must control intermediate source selection or synthesis beyond the API’s controls&lt;/td&gt;
&lt;td&gt;A custom loop or service exposing those controls&lt;/td&gt;
&lt;td&gt;The required behavior belongs in the implementation contract&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Your workload must stay inside an air-gapped or exclusively controlled environment&lt;/td&gt;
&lt;td&gt;An architecture compatible with that boundary&lt;/td&gt;
&lt;td&gt;Hosted Tabstack Research processes requests outside your environment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Your workflow needs private sources the public-web service cannot access&lt;/td&gt;
&lt;td&gt;An authorized private-data retrieval path, optionally combined with public research&lt;/td&gt;
&lt;td&gt;Public-web research cannot supply inaccessible evidence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You need a hosted browser task behind a login&lt;/td&gt;
&lt;td&gt;An authenticated automation system you control&lt;/td&gt;
&lt;td&gt;Hosted Tabstack Automate handles public websites and has no credential store or session management&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;A known URL is a separate case: it can fit Tabstack, but usually through &lt;a href=&quot;https://tabstack.ai/structured-extraction&quot;&gt;Extract&lt;/a&gt;, not Research. Ask for Markdown when the next step needs page content, or matching JSON when it needs fields. Validate the values as well as the shape.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/mozilla/pilo&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Pilo&lt;/a&gt; provides a self-hosted automation path. It is not a self-hosted deployment of Tabstack Research.&lt;/p&gt;
&lt;h2 id=&quot;check-the-processing-boundary&quot;&gt;Check the processing boundary&lt;/h2&gt;
&lt;p&gt;Running your own model does not make external web services local. Search queries and research requests follow the data paths of the providers your application calls.&lt;/p&gt;
&lt;p&gt;Tabstack Research runs on hosted infrastructure. Contracted model providers process the query and web content to produce the answer. Our &lt;a href=&quot;https://docs.tabstack.ai/trust/data-handling/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Data Handling documentation&lt;/a&gt; describes that path, and the &lt;a href=&quot;https://tabstack.ai/legal/privacy&quot;&gt;Privacy Notice&lt;/a&gt; is the canonical policy. Confirm workload-specific retention and provider requirements before sending sensitive inputs. Do not interpret “never trained on” as a promise that no hosted processing or storage occurs.&lt;/p&gt;
&lt;h2 id=&quot;choose-a-path-for-one-real-job&quot;&gt;Choose a path for one real job&lt;/h2&gt;
&lt;p&gt;Write down the output your feature promises, the intermediate decisions you must control, and the data it can send outside your environment. Then test the smallest approach that satisfies those requirements.&lt;/p&gt;
&lt;p&gt;Use the &lt;a href=&quot;https://tabstack.ai/blog/useful-citations-ai-answers&quot;&gt;citation review method&lt;/a&gt; to check claim support and answer coverage. The &lt;a href=&quot;https://tabstack.ai/blog/trace-question-to-source-backed-answer&quot;&gt;traced Python example&lt;/a&gt; shows how to preserve the report, cited pages, and request timeline. For a comparison between complete systems, use the &lt;a href=&quot;https://tabstack.ai/blog/search-fetch-model-vs-managed-research-evaluation-plan&quot;&gt;search + fetch + model evaluation plan&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If your job needs a finished answer from public sources, bring that question to the &lt;a href=&quot;https://console.tabstack.ai/playground/research&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Research playground&lt;/a&gt;, which requires sign-in. Write its required answer elements first, then check the returned report against them. If search or a known-page fetch already meets the job, use it.&lt;/p&gt;
</content:encoded></item><item><title>What &quot;private by default&quot; means for a managed web API</title><link>https://tabstack.ai/blog/private-by-default-managed-web-api</link><guid isPermaLink="true">https://tabstack.ai/blog/private-by-default-managed-web-api</guid><description>What a Research call sends, who processes it, what we retain afterward, and the detailed data collection setting that controls it.</description><pubDate>Fri, 02 Oct 2026 14:00:00 GMT</pubDate><content:encoded>&lt;p&gt;If you run your own model, you still need to know what happens to anything you send to an external service. A Tabstack Research call sends your query to our hosted API. Contracted model providers process the query and web content to produce the answer.&lt;/p&gt;
&lt;p&gt;We describe Tabstack as “private by default.” This post explains what we retain after a request and what the detailed data collection setting changes.&lt;/p&gt;
&lt;h2 id=&quot;what-happens-during-a-research-call&quot;&gt;What happens during a Research call&lt;/h2&gt;
&lt;p&gt;Suppose your query is “What changed in the latest Node.js LTS release?” Tabstack receives that query and reads public sources to produce the answer. Your application can then pass the returned report to your own model.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://tabstack.ai/blog/private-by-default-managed-web-api&quot;&gt;View the diagram in the original post.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Your application calls the API with your key over TLS 1.2 or higher. We fetch the public pages the answer needs, and our direct fetches identify themselves as &lt;code&gt;Mozilla-Tabstack/1.0 (+https://tabstack.ai)&lt;/code&gt;. Your query and the page content go to a contracted model provider, which produces the report. The report and the pages it cites come back to your application. All of this runs on our hosted infrastructure, not inside your local model.&lt;/p&gt;
&lt;h2 id=&quot;what-we-retain-after-the-request&quot;&gt;What we retain after the request&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Request metadata&lt;/strong&gt; is recorded on every request, on every plan: the endpoint, success or failure, credits spent, timestamps, and the organization and key that made the call. The Data Handling page doesn’t state how long request metadata is kept.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Payloads&lt;/strong&gt; are the target URL, the request parameters, the response data and the output. For Research, that includes your query. We store payloads only while your organization has detailed data collection on. New organizations start with it on, and an organization admin can turn it off.&lt;/p&gt;
&lt;p&gt;With detailed data collection on, payloads are kept for 90 days and appear in your console, which helps when you’re debugging results or auditing what a task did. With it off, new payloads aren’t stored. If your workload can’t have payloads stored at all, turn the setting off before you send anything.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fetched page content&lt;/strong&gt; is cached by URL, effort and region, with a short time-to-live. The cache isn’t per account, so public page content fetched for one account can be served to another. That matters when you need a fresh copy of a page. Pass &lt;code&gt;nocache: true&lt;/code&gt; to bypass the cache for that request.&lt;/p&gt;
&lt;p&gt;Our &lt;a href=&quot;https://tabstack.ai/legal/privacy&quot;&gt;Privacy Notice&lt;/a&gt; is the canonical statement of what we retain. It applies to the API as well as the website and our other services, and it says: “We store history (including your content and/or Outputs) for 90 days or unless you delete it.” The notice also asks you to be careful about putting confidential or sensitive information in your inputs. For privacy questions, our Data Protection Officer is at &lt;a href=&quot;mailto:dpo@mozilla.com&quot;&gt;dpo@mozilla.com&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;what-never-trained-on-covers&quot;&gt;What “never trained on” covers&lt;/h2&gt;
&lt;p&gt;Our Data Handling page states it directly: your requests and the content Tabstack processes for you are not used to train models.&lt;/p&gt;
&lt;p&gt;Research is one of the endpoints that sends your query and the page content to a contracted model provider. The Privacy Notice names Google Cloud, including Vertex, and OpenAI as contracted providers. Each provider’s own retention and training terms for that traffic are a separate question, and the public documents don’t list them per endpoint.&lt;/p&gt;
&lt;h2 id=&quot;when-a-hosted-api-is-not-suitable&quot;&gt;When a hosted API is not suitable&lt;/h2&gt;
&lt;p&gt;If your workload requires that data stays inside an environment you control, a hosted Research call is the wrong tool, and no setting changes that. There is no self-hosted deployment of the API. &lt;a href=&quot;https://github.com/mozilla/pilo&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Pilo&lt;/a&gt;, our open-source automation engine, runs browser automation in your own environment. It covers automation only, not Research.&lt;/p&gt;
&lt;h2 id=&quot;where-to-review-the-details&quot;&gt;Where to review the details&lt;/h2&gt;
&lt;p&gt;Tabstack is built by Mozilla, which is why we state these limits as plainly as the commitments. Read our &lt;a href=&quot;https://docs.tabstack.ai/trust/data-handling/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Data Handling documentation&lt;/a&gt; for the request flow and retention settings. If your review requires provider-specific terms, &lt;a href=&quot;mailto:support@tabstack.ai&quot;&gt;contact us&lt;/a&gt; before sending sensitive data.&lt;/p&gt;
</content:encoded></item><item><title>Trace a question from request to source-backed answer</title><link>https://tabstack.ai/blog/trace-question-to-source-backed-answer</link><guid isPermaLink="true">https://tabstack.ai/blog/trace-question-to-source-backed-answer</guid><description>Record a Tabstack Research request, preserve its cited pages, and check the answer against its sources with a Python review sheet.</description><pubDate>Wed, 30 Sep 2026 14:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A Tabstack Research call returns a Markdown report and the pages it cites. The &lt;a href=&quot;https://github.com/Mozilla-Ocho/tabstack-cited-research-python&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Python example&lt;/a&gt; saves both, records the request’s progress events, and generates a CSV review sheet that connects each report sentence to its cited pages.&lt;/p&gt;
&lt;p&gt;Use the timeline to inspect the request’s event order and elapsed time. Use the review sheet to check whether a source supports the claim attached to it. For example, the recorded answer describes a search result as page content, while the cited documentation specifies a content snippet. That distinction affects what your application can do with the result.&lt;/p&gt;
&lt;p&gt;This tutorial uses the committed Ollama web-search example. You will run the CLI, inspect its output files, and follow one sentence through the source review. The implementation extends &lt;a href=&quot;https://tabstack.ai/blog/build-cited-live-web-answer-python&quot;&gt;Build a cited live-web answer in Python with one Research call&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;run-the-traced-request&quot;&gt;Run the traced request&lt;/h2&gt;
&lt;p&gt;You need Python 3.9 or later, &lt;a href=&quot;https://docs.astral.sh/uv/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;uv&lt;/a&gt;, and a Tabstack API key. The example’s lockfile pins the &lt;code&gt;tabstack&lt;/code&gt; SDK at 2.8.5. Run the following commands in Bash:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; clone&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; https://github.com/Mozilla-Ocho/tabstack-cited-research-python.git&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;cd&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; tabstack-cited-research-python&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;uv&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; sync&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; --frozen&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# Read the key without echoing it or saving it to shell history.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;read&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; -rs&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; TABSTACK_API_KEY&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; &amp;#x26;&amp;#x26; &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; TABSTACK_API_KEY&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;uv&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; run&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; cited-research&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  --query&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;What are the current ways to add web search to an Ollama-based application, and what output does each approach return?&quot;&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  --mode&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; fast&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; --nocache&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; --silence-timeout&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; 120&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  --output&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; artifacts/my-trace&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The CLI reads &lt;code&gt;TABSTACK_API_KEY&lt;/code&gt; from the environment. The command has no API-key argument. &lt;code&gt;--nocache&lt;/code&gt; bypasses the fetched-page cache, and &lt;code&gt;--silence-timeout 120&lt;/code&gt; stops the client after 120 seconds without an event.&lt;/p&gt;
&lt;p&gt;The command writes your files to &lt;code&gt;artifacts/my-trace&lt;/code&gt;. The committed example below uses &lt;code&gt;artifacts/trace-run&lt;/code&gt;; its paths identify that saved run, not the directory you just selected.&lt;/p&gt;
&lt;p&gt;The recorded run on 29 September 2026 produced this terminal output:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;start Starting research&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;planning:start Planning research strategy&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;planning:end Planning complete: 6 queries&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;iteration:start iteration 1/1 Starting iteration 1 of 1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;searching:start iteration 1 Searching with 6 queries&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;searching:end iteration 1 6 new urls Found 6 URLs&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;iteration:end iteration 1 Iteration 1 complete (fast mode)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;writing:start Writing report&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;writing:end Report draft complete&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;complete report -&gt; artifacts/trace-run/report.md&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;sources (3) -&gt; artifacts/trace-run/sources.json&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  1. Web search https://docs.ollama.com/capabilities/web-search&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  2. Web search https://docs.ollama.com/capabilities/web-search.md&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  3. Subagents and web search in Claude Code https://ollama.com/blog/web-search-subagents-claude-code&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;review sheet -&gt; artifacts/trace-run/review-sheet.csv (unreviewed)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first event arrived after 496 ms, and the request completed in 17.1 seconds. These timings describe the recorded request; they are not a general latency benchmark.&lt;/p&gt;
&lt;h2 id=&quot;open-the-output-files&quot;&gt;Open the output files&lt;/h2&gt;
&lt;p&gt;The example saves the answer, source list, and timeline separately so you can inspect each without reconstructing the stream:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;artifacts/trace-run/&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  question.txt&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  command.txt                 the command as run, no secrets&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  events.sanitized.jsonl      the lifecycle timeline&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  report.md                   the answer&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  sources.json                cited pages in returned order&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  review-sheet.csv            one row per sentence, ready for review&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  trace-diagram.md            the timeline as a diagram, plus its limits&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  run-manifest.json           versions, UTC times, terminal state, caveats&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Start with &lt;code&gt;report.md&lt;/code&gt;, &lt;code&gt;sources.json&lt;/code&gt;, and &lt;code&gt;review-sheet.csv&lt;/code&gt;. The report contains the answer’s numbered citation markers. The source list preserves their positions. The review sheet joins sentences to those entries and leaves evidence and scoring fields for you to complete.&lt;/p&gt;
&lt;p&gt;The sequence diagram shows where that review fits:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://tabstack.ai/blog/trace-question-to-source-backed-answer&quot;&gt;View the diagram in the original post.&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;preserve-the-citation-mapping&quot;&gt;Preserve the citation mapping&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;sources.json&lt;/code&gt; numbers cited pages from 1 in the order the API returns them. Each record keeps the source’s &lt;code&gt;id&lt;/code&gt;, &lt;code&gt;url&lt;/code&gt;, &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;claims&lt;/code&gt;, and &lt;code&gt;source_queries&lt;/code&gt;, along with the CLI’s validation flags. The sample returned a title for every entry; it did not populate the optional &lt;code&gt;relevance&lt;/code&gt; or &lt;code&gt;reliability&lt;/code&gt; fields.&lt;/p&gt;
&lt;p&gt;The report’s &lt;code&gt;[n]&lt;/code&gt; markers refer to the source at position n. Sorting the list or removing an entry changes the targets of subsequent markers. The CLI preserves source order so each citation number continues to point to the correct page.&lt;/p&gt;
&lt;p&gt;The first run returned three entries, but entries 1 and 2 point to the same Ollama documentation page, with and without &lt;code&gt;.md&lt;/code&gt;. The CLI retains both entries and flags the repeated source.&lt;/p&gt;
&lt;p&gt;Its duplicate detector compares URL patterns, including scheme, &lt;code&gt;www.&lt;/code&gt;, trailing slashes, and &lt;code&gt;.md&lt;/code&gt; suffixes. Those comparisons flag possible matches; they do not inspect page contents. The sheet’s “[2] likely same page as [1]” label records that heuristic. In this example, both URLs resolve to the same documentation page.&lt;/p&gt;
&lt;p&gt;The CLI also checks URLs before rendering them as links. It excludes loopback and private addresses, non-HTTP schemes, and URLs containing embedded credentials, and strips credentials from stored URLs.&lt;/p&gt;
&lt;p&gt;Fast mode returns an empty &lt;code&gt;claims&lt;/code&gt; array for each cited page. Use the report’s numbered markers to follow its statements to their source entries. Balanced mode populates the claim lists, as the &lt;a href=&quot;https://docs.tabstack.ai/guides/research&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Research guide&lt;/a&gt; documents. Those lists record the pipeline’s extracted claims; the review still checks them against the page.&lt;/p&gt;
&lt;p&gt;If the response contains no cited pages, the CLI writes &lt;code&gt;[]&lt;/code&gt; to &lt;code&gt;sources.json&lt;/code&gt; and records &lt;code&gt;review_needed_no_sources&lt;/code&gt; in the manifest.&lt;/p&gt;
&lt;h2 id=&quot;check-a-sentence-against-its-source&quot;&gt;Check a sentence against its source&lt;/h2&gt;
&lt;p&gt;The review sheet starts with one row per report sentence. It fills the citation mapping and reserves the evidence fields for the reviewer:&lt;/p&gt;
&lt;div class=&quot;table-scroll&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Fields&lt;/th&gt;
&lt;th&gt;Who fills them&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;claim_id&lt;/code&gt;, &lt;code&gt;answer_text&lt;/code&gt;, &lt;code&gt;citation_ids&lt;/code&gt;, &lt;code&gt;source_url&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The CLI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;passage&lt;/code&gt;, &lt;code&gt;source_date_or_version&lt;/code&gt;, &lt;code&gt;retrieved_at_utc&lt;/code&gt;, &lt;code&gt;support&lt;/code&gt;, &lt;code&gt;reason&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;You, during source review&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Helper columns include cited page IDs, automatic flags, and any claims supplied by the API. The CLI leaves &lt;code&gt;support&lt;/code&gt; blank. Split a sentence into separate rows when its clauses make claims that need different evidence.&lt;/p&gt;
&lt;p&gt;In the recorded answer, one sentence describes results containing &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;url&lt;/code&gt;, and &lt;code&gt;content&lt;/code&gt; “of a relevant web page” and cites entries &lt;code&gt;[1][2]&lt;/code&gt;. Both entries lead to the same documentation. The relevant passage defines the field as:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;content (string): relevant content snippet from the web page&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The answer omits “snippet”. Correct that clause to say the result contains a relevant content snippet from the page. An application that needs the full page must fetch it rather than treat the search snippet as complete page content.&lt;/p&gt;
&lt;p&gt;Record the passage, the source URL, when you inspected it, and why the wording needs correction. The &lt;a href=&quot;https://tabstack.ai/blog/useful-citations-ai-answers&quot;&gt;citation review method&lt;/a&gt; defines the support values: &lt;code&gt;2&lt;/code&gt; for supported at the stated scope, &lt;code&gt;1&lt;/code&gt; for partial support or a missing qualifier, &lt;code&gt;0&lt;/code&gt; for unsupported or contradicted after inspection, and &lt;code&gt;U&lt;/code&gt; for uninspected or inaccessible evidence.&lt;/p&gt;
&lt;p&gt;The original review also identified these specific issues:&lt;/p&gt;
&lt;div class=&quot;table-scroll&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Answer detail&lt;/th&gt;
&lt;th&gt;Source evidence&lt;/th&gt;
&lt;th&gt;Correction&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;An API key requires a free Ollama account, but the sentence has no inline citation&lt;/td&gt;
&lt;td&gt;“A free Ollama account is required.”&lt;/td&gt;
&lt;td&gt;Attach the supporting documentation to that claim.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;“MCP (Multi-Context Processor)”&lt;/td&gt;
&lt;td&gt;The documentation describes a Python MCP server; MCP stands for Model Context Protocol.&lt;/td&gt;
&lt;td&gt;Replace the incorrect expansion with “Model Context Protocol”.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Built-in search applies to “specific” cloud models&lt;/td&gt;
&lt;td&gt;“It works with any model on Ollama’s cloud.”&lt;/td&gt;
&lt;td&gt;Do not turn recommended models into a restriction on supported models.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Keep missing-citation and duplicate-source flags separate from factual support scores. A repeated URL describes the source list; it is not another factual claim. A sentence without a marker needs an evidence check before you classify it as unsupported.&lt;/p&gt;
&lt;p&gt;The question also asks what each approach returns. Check that required element separately for every approach; correcting one sentence does not establish whole-answer coverage. Write down those required elements before reading a new answer.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;cited-research-review&lt;/code&gt; preserves completed review entries and edited rows. It will not regenerate over them unless you pass &lt;code&gt;--force&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;read-the-event-timeline&quot;&gt;Read the event timeline&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;events.sanitized.jsonl&lt;/code&gt; records one event per line using an allowlist of fields. The CLI redacts URLs, email addresses, and credential-like strings from status messages, and omits payload fields such as planned queries.&lt;/p&gt;
&lt;p&gt;This event comes from the second recorded run:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;&quot;elapsed_ms&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;3091&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;&quot;event&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;iteration:end&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;&quot;is_last&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;true&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;&quot;iteration&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;1.0&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;&quot;known_event&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;true&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;&quot;message&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;Iteration 1 complete (fast mode)&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;&quot;received_at_utc&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;2026-09-29T18:58:40.193Z&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;&quot;seq&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;7&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;&quot;stop_reason&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;max_iterations&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;&quot;timestamp_raw&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;1790708320152.0&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;&quot;timestamp_type&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;float&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use &lt;code&gt;seq&lt;/code&gt; to preserve arrival order. Several events share a server timestamp, including &lt;code&gt;start&lt;/code&gt; and &lt;code&gt;planning:start&lt;/code&gt;, so timestamps alone cannot distinguish their sequence.&lt;/p&gt;
&lt;p&gt;The client measures &lt;code&gt;elapsed_ms&lt;/code&gt; from just before it sends the request. That value records the wait your application experienced, not the server’s internal execution time. The CLI also keeps &lt;code&gt;timestamp_raw&lt;/code&gt; and its type. The sample carries a float in Unix epoch milliseconds; the current Research guide documents epoch milliseconds, and the API reference defines the field as a number.&lt;/p&gt;
&lt;p&gt;The first recorded run produced this timeline:&lt;/p&gt;
&lt;div class=&quot;table-scroll&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;seq&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;th&gt;Elapsed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;code&gt;start&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;496 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;code&gt;planning:start&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;496 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;code&gt;planning:end&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1,565 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;code&gt;iteration:start&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1,566 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;&lt;code&gt;searching:start&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1,566 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;&lt;code&gt;searching:end&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;6,843 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;&lt;code&gt;iteration:end&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;6,843 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;&lt;code&gt;writing:start&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;6,843 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;&lt;code&gt;writing:end&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;17,083 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;&lt;code&gt;complete&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;17,084 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;In this run, the client observed roughly 1.1 seconds between the planning events, 5.3 seconds between the searching events, and 10.2 seconds between the writing events. Use these events to update a progress indicator while the request runs.&lt;/p&gt;
&lt;p&gt;The stream exposes request phases, not individual page-fetch timings or internal reasoning. The &lt;code&gt;complete&lt;/code&gt; event supplies the final report and cited-page list; it does not expose the passages the service read.&lt;/p&gt;
&lt;h2 id=&quot;handle-failures-without-duplicating-requests&quot;&gt;Handle failures without duplicating requests&lt;/h2&gt;
&lt;p&gt;Except when &lt;code&gt;TABSTACK_API_KEY&lt;/code&gt; is missing, the CLI records a terminal state in the manifest and returns a corresponding exit code:&lt;/p&gt;
&lt;div class=&quot;table-scroll&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Exit&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;&lt;code&gt;complete&lt;/code&gt; arrived and every file was written&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;The stream sent an &lt;code&gt;error&lt;/code&gt; event&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;The request was rejected before streaming began (401, 429, 5xx)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Connection or transport failure before the stream opened&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;&lt;code&gt;TABSTACK_API_KEY&lt;/code&gt; is not set; no request was made&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;The stream closed before &lt;code&gt;complete&lt;/code&gt; or &lt;code&gt;error&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;No event arrived for &lt;code&gt;--silence-timeout&lt;/code&gt; seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;A second &lt;code&gt;complete&lt;/code&gt; or &lt;code&gt;error&lt;/code&gt; arrived after &lt;code&gt;complete&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;The connection dropped or timed out after the stream opened&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;&lt;code&gt;complete&lt;/code&gt; arrived without a report&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;Any other unexpected failure; the files are still written&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h3 id=&quot;disable-automatic-retries&quot;&gt;Disable automatic retries&lt;/h3&gt;
&lt;p&gt;The CLI disables SDK retries and adds no retry loop. A connection failure can occur after the server accepts a request. Retrying automatically could create another research task and incur another charge. Inspect the failure before deciding whether to send a new request.&lt;/p&gt;
&lt;p&gt;The first recorded run predates this change and records &lt;code&gt;sdk_max_retries: 2&lt;/code&gt;. The second records &lt;code&gt;0&lt;/code&gt;. Check the manifest when comparing saved runs so you know which retry behavior applied.&lt;/p&gt;
&lt;h3 id=&quot;measure-stream-silence&quot;&gt;Measure stream silence&lt;/h3&gt;
&lt;p&gt;The Research API has no overall server-side timeout. The CLI resets its silence timer on every event, including the wait for the first event. A request can continue as long as events arrive within the configured interval.&lt;/p&gt;
&lt;p&gt;A silence timeout stops the client from waiting; it does not establish that the server cancelled the task. The CLI reports that distinction on exit.&lt;/p&gt;
&lt;p&gt;The example includes offline fixtures for clean completion, absent sources, streamed errors, premature closure, duplicate terminal events, HTTP and transport failures, silence timeouts, and a missing API key. These tests exercise client handling without sending Research requests.&lt;/p&gt;
&lt;h2 id=&quot;use-the-trace-in-your-application&quot;&gt;Use the trace in your application&lt;/h2&gt;
&lt;p&gt;Use a managed Research call when your application needs the finished answer and its source record without operating the research loop. Keep orchestration in your own application when you need to control individual searches, fetches, and prompts, or change direction between steps. A search API fits workflows that need links and snippets for the application’s own research process.&lt;/p&gt;
&lt;p&gt;Your application sets the acceptance rules for the returned answer. Check decision-driving claims against their sources and choose how to handle missing evidence. The trace supplies the request record and citation mapping; the review supplies the support decisions.&lt;/p&gt;
&lt;p&gt;Research runs on hosted infrastructure and sends content and instructions to contracted model providers. Use public, non-sensitive questions for this example. Read &lt;a href=&quot;https://tabstack.ai/blog/private-by-default-managed-web-api&quot;&gt;What “private by default” means for a managed web API&lt;/a&gt; and the &lt;a href=&quot;https://docs.tabstack.ai/trust/data-handling/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Data Handling documentation&lt;/a&gt; for the processing path and settings, with the &lt;a href=&quot;https://tabstack.ai/legal/privacy&quot;&gt;Privacy Notice&lt;/a&gt; as the canonical policy.&lt;/p&gt;
&lt;p&gt;The trace does not record billing usage. Check your console for the request’s cost.&lt;/p&gt;
&lt;h2 id=&quot;reproduce-and-review-the-example&quot;&gt;Reproduce and review the example&lt;/h2&gt;
&lt;p&gt;The original sample records Python 3.12.13, uv 0.11.28, and macOS arm64. Its reproducibility references are commit &lt;a href=&quot;https://github.com/Mozilla-Ocho/tabstack-cited-research-python/commit/7fe5f20ce49ba26dd63f2795bef5a0733267f644&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;7fe5f20&lt;/a&gt; and the manifest’s &lt;code&gt;82b9be9&lt;/code&gt;, the same tree before a commit-message change. Keep those recorded versions and commit references when comparing the historical artifacts with a new run.&lt;/p&gt;
&lt;p&gt;Run the &lt;a href=&quot;https://github.com/Mozilla-Ocho/tabstack-cited-research-python&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;traced Python example&lt;/a&gt; with one public question your application answers. Open &lt;code&gt;report.md&lt;/code&gt; beside &lt;code&gt;review-sheet.csv&lt;/code&gt;, follow one citation to its passage, and record your decision. Review the remaining material claims and required answer elements before using the answer downstream.&lt;/p&gt;
&lt;p&gt;Use &lt;a href=&quot;https://tabstack.ai/blog/useful-citations-ai-answers&quot;&gt;What makes a citation useful in an AI-generated answer?&lt;/a&gt; for the review rubric. The &lt;a href=&quot;https://docs.tabstack.ai/guides/research&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Research guide&lt;/a&gt; and &lt;a href=&quot;https://docs.tabstack.ai/api/resources/agent/methods/research&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;API reference&lt;/a&gt; define the stream and response fields.&lt;/p&gt;
</content:encoded></item><item><title>What makes a citation useful in an AI-generated answer?</title><link>https://tabstack.ai/blog/useful-citations-ai-answers</link><guid isPermaLink="true">https://tabstack.ai/blog/useful-citations-ai-answers</guid><description>How to check whether citations in an AI answer support its material claims, scope, and date.</description><pubDate>Mon, 28 Sep 2026 14:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A citation should let you check the claim beside it. If an answer says a library supports Python 3.12, the linked documentation needs to support that version claim. A link to the library’s homepage leaves you to find the evidence yourself.&lt;/p&gt;
&lt;p&gt;We use the same standard for API limits, prices, policy terms, and commands. Find the relevant passage, preserve its qualifiers, and check that it applies to the version or date in the answer.&lt;/p&gt;
&lt;h2 id=&quot;check-the-claim-against-the-passage&quot;&gt;Check the claim against the passage&lt;/h2&gt;
&lt;p&gt;A material claim changes what someone decides or builds. A supported version, a request parameter, or a data-retention policy qualifies. A sentence connecting two sections usually does not need a citation.&lt;/p&gt;
&lt;p&gt;Start with the exact claim. Open its cited page and find the passage that supports it. Check the passage’s scope before using the claim: an enterprise-only feature does not apply to every plan, and a release note describes the version it names.&lt;/p&gt;
&lt;p&gt;One sentence can contain several claims. Separate them when they need different evidence.&lt;/p&gt;
&lt;h2 id=&quot;review-one-sentence-from-a-research-answer&quot;&gt;Review one sentence from a Research answer&lt;/h2&gt;
&lt;p&gt;The &lt;a href=&quot;https://tabstack.ai/blog/build-cited-live-web-answer-python&quot;&gt;Python Research example&lt;/a&gt; includes a recorded fast-mode run that asked:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;What are the current ways to add web search to an Ollama-based application, and what output does each approach return?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The &lt;a href=&quot;https://github.com/Mozilla-Ocho/tabstack-cited-research-python/blob/main/artifacts/sample-run/report.md&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;saved report&lt;/a&gt; contains this sentence:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The Ollama Web Search API, released on September 24, 2025, requires an API key and performs a POST request to &lt;a href=&quot;https://ollama.com/api/web_search&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://ollama.com/api/web_search&lt;/a&gt; with a query and optional max_results (default 5, maximum 10) [1][2][3].&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The &lt;a href=&quot;https://github.com/Mozilla-Ocho/tabstack-cited-research-python/blob/main/artifacts/sample-run/sources.json&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;source record&lt;/a&gt; maps those markers to these entries:&lt;/p&gt;
&lt;div class=&quot;table-scroll&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Marker&lt;/th&gt;
&lt;th&gt;Returned URL&lt;/th&gt;
&lt;th&gt;Page&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[1]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://docs.ollama.com/capabilities/web-search.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Ollama’s Web Search documentation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[2]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;http://docs.ollama.com/capabilities/web-search&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The same documentation page&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[3]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://ollama.com/blog/web-search&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Ollama’s announcement&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Entries &lt;code&gt;[1]&lt;/code&gt; and &lt;code&gt;[2]&lt;/code&gt; point to the same documentation page. The sentence cites two distinct pages, both published by Ollama. Count the pages separately from the citation markers.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://docs.ollama.com/capabilities/web-search&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;documentation&lt;/a&gt; states that the API requires a key, accepts a POST request at the named endpoint, and takes a required &lt;code&gt;query&lt;/code&gt;. It also defines &lt;code&gt;max_results&lt;/code&gt; as optional, with a default of 5 and a maximum of 10.&lt;/p&gt;
&lt;p&gt;That supports the request details. The documentation does not establish the release date, so leave the date out until you verify it against a dated source.&lt;/p&gt;
&lt;p&gt;Here is the revised sentence:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Ollama’s Web Search API requires an API key and accepts a POST request to &lt;code&gt;https://ollama.com/api/web_search&lt;/code&gt; with a required &lt;code&gt;query&lt;/code&gt; and an optional &lt;code&gt;max_results&lt;/code&gt;, which defaults to 5 and has a maximum of 10 [1].&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;We retained the supported details and attached their source. We removed the date rather than carrying an unchecked claim into the revision.&lt;/p&gt;
&lt;p&gt;The report’s opening sentence lists four approaches without attaching citations. That creates a different review task: locate the evidence for each approach. A missing citation tells you the answer has not shown its support; it does not establish that the claim is false.&lt;/p&gt;
&lt;h2 id=&quot;check-support-scope-and-freshness&quot;&gt;Check support, scope, and freshness&lt;/h2&gt;
&lt;p&gt;For each material claim, answer these questions:&lt;/p&gt;
&lt;div class=&quot;table-scroll&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Example failure&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Resolution&lt;/td&gt;
&lt;td&gt;Can you open the cited page and find the relevant passage?&lt;/td&gt;
&lt;td&gt;A documentation homepage links to the parameter’s actual page but does not describe it.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Support&lt;/td&gt;
&lt;td&gt;Does the passage support the claim?&lt;/td&gt;
&lt;td&gt;The answer names a feature the cited page never describes.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scope&lt;/td&gt;
&lt;td&gt;Did the answer preserve the source’s qualifiers?&lt;/td&gt;
&lt;td&gt;The answer describes a paid-plan feature as available on every plan.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time&lt;/td&gt;
&lt;td&gt;Does the source apply to the date or version in the question?&lt;/td&gt;
&lt;td&gt;An older release note supports behavior that has since changed.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;These checks catch different errors. An accessible page can describe the wrong version. A current page can support part of a sentence while leaving another part unchecked.&lt;/p&gt;
&lt;p&gt;If current primary sources disagree, identify the conflicting statements and check their versions and scope. Narrow the answer to what the evidence establishes, or leave the disputed point unresolved.&lt;/p&gt;
&lt;h2 id=&quot;check-whether-the-answer-covers-the-question&quot;&gt;Check whether the answer covers the question&lt;/h2&gt;
&lt;p&gt;Claim support and answer coverage require separate checks.&lt;/p&gt;
&lt;p&gt;The Ollama question asks for both the available approaches and the output each approach returns. An answer that names the approaches but omits their outputs remains incomplete, even if every named approach has a valid citation.&lt;/p&gt;
&lt;p&gt;Write down the required answer elements before reviewing the output. Then mark each element as answered, partially answered, or missing. This keeps a polished paragraph from hiding an unanswered part of the question.&lt;/p&gt;
&lt;h2 id=&quot;keep-a-review-record&quot;&gt;Keep a review record&lt;/h2&gt;
&lt;p&gt;Record one row per material claim. Split a sentence into multiple rows when its clauses need different sources or decisions.&lt;/p&gt;
&lt;div class=&quot;table-scroll&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;What to record&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;claim_id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A stable identifier for the claim.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;answer_text&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The exact sentence or clause, including its qualifiers.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;citation_ids&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The attached citation markers; leave empty when none exist.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;source_url&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The page you inspected.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;passage&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A short supporting or contradicting excerpt.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;source_date_or_version&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The relevant date or version; record unknown when the page does not establish it.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;retrieved_at_utc&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;When you opened the source.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;support&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2&lt;/code&gt;: supported at the stated scope; &lt;code&gt;1&lt;/code&gt;: partially supported or needs a qualifier; &lt;code&gt;0&lt;/code&gt;: unsupported or contradicted after inspection; &lt;code&gt;U&lt;/code&gt;: not inspected or could not inspect.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;reason&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The specific evidence gap or reason for the decision.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Keep citation problems separate from factual decisions. Flag missing markers and duplicate source entries without treating them as proof that a claim is wrong.&lt;/p&gt;
&lt;p&gt;Report supported, partial, and unsupported claims over the claims you inspected. Report &lt;code&gt;U&lt;/code&gt; separately. Measure coverage against the required answer elements, not the number of sentences. If you inspected no claims, report the support rate as unavailable.&lt;/p&gt;
&lt;p&gt;Save the passage and retrieval time so the record shows what you checked if the page later changes. This example demonstrates a review method; it does not measure citation accuracy across Tabstack Research.&lt;/p&gt;
&lt;h2 id=&quot;use-the-source-record-from-tabstack-research&quot;&gt;Use the source record from Tabstack Research&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.tabstack.ai/guides/research&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Tabstack Research&lt;/a&gt; handles source selection and synthesis inside one call, then returns a Markdown report and citation metadata on the &lt;code&gt;complete&lt;/code&gt; event.&lt;/p&gt;
&lt;p&gt;The source list uses &lt;code&gt;citedPages&lt;/code&gt; in the wire format and &lt;code&gt;cited_pages&lt;/code&gt; in the Python SDK. Treat a missing list as empty. In fast mode, each source’s &lt;code&gt;claims&lt;/code&gt; array is empty; use the report’s numbered citation markers to trace statements to their source entries. Balanced mode populates the claim lists.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://tabstack.ai/blog/trace-question-to-source-backed-answer&quot;&gt;traced Python example&lt;/a&gt; preserves source order so each citation number continues to point to the correct page. It generates a review sheet with the answer text, citation markers, and source URLs. You then add the supporting passages and review decisions.&lt;/p&gt;
&lt;p&gt;Your application sets the acceptance rules. Check decision-driving claims before using them downstream, and choose an appropriate review process for lower-risk answers. Automated checks can flag missing markers, out-of-range references, inaccessible links, and matching URL patterns. A verifier still needs to assess whether the passage supports the claim.&lt;/p&gt;
&lt;h2 id=&quot;review-one-answer&quot;&gt;Review one answer&lt;/h2&gt;
&lt;p&gt;Choose a public question your application answers today. List what a complete answer must contain, then check one material claim against its cited passage. Record the evidence and decision before moving to the next claim.&lt;/p&gt;
&lt;p&gt;Use the &lt;a href=&quot;https://tabstack.ai/blog/trace-question-to-source-backed-answer&quot;&gt;traced Research example&lt;/a&gt; to prepare the review sheet and keep the source mapping intact.&lt;/p&gt;
</content:encoded></item><item><title>Where model-operated web research breaks</title><link>https://tabstack.ai/blog/model-operated-web-research-failure-taxonomy</link><guid isPermaLink="true">https://tabstack.ai/blog/model-operated-web-research-failure-taxonomy</guid><description>Nine places web research fails after search has already succeeded, each with a signature you can observe, the evidence that identifies it, and the smallest test that catches it.</description><pubDate>Fri, 25 Sep 2026 14:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Search returned ten results, the interface listed them, and the model answered as though it had never heard of the internet. That failure has at least four possible homes, and the one thing you can be sure of is that it isn’t in the search provider.&lt;/p&gt;
&lt;p&gt;Web research fails after search succeeds more often than it fails at search. Discovery, retrieval, extraction, context assembly, the tool loop, synthesis and citation are separate boundaries with separate failure modes, and a visible success at one of them tells you nothing about the next. What follows is nine classes, each with a signature you can observe, the evidence that separates it from its neighbours, and the smallest test that catches it.&lt;/p&gt;
&lt;p&gt;Every public example below is a single report in a named version and configuration. They show the classes are real and distinct from each other. They say nothing about how often any of them happens, and I’m not using them to make a claim about any project’s current state.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://tabstack.ai/blog/model-operated-web-research-failure-taxonomy&quot;&gt;View the diagram in the original post.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;When you write up an incident, name two boundaries: the last one you can prove worked, and the first one you can prove didn’t.&lt;/p&gt;
&lt;h2 id=&quot;1-scope-and-planning&quot;&gt;1. Scope and planning&lt;/h2&gt;
&lt;p&gt;The answer is about the right topic and doesn’t do the job. A comparison names the products and never says what each one returns. A question about current behaviour comes back built from undated pages. A three-part question gets its easiest part answered well.&lt;/p&gt;
&lt;p&gt;This is the only class that happens before anything runs. Nobody wrote down what a complete answer contains, so the loop stops when it finds relevant text rather than sufficient evidence, and whoever reviews the output invents the criteria after reading it. By then the answer sets the standard it’s judged against, which is exactly backwards.&lt;/p&gt;
&lt;p&gt;Freeze the required elements first, then score each one as answered, partly answered or missing. A well-written answer earns no points for coverage it doesn’t have.&lt;/p&gt;
&lt;h2 id=&quot;2-discovery&quot;&gt;2. Discovery&lt;/h2&gt;
&lt;p&gt;Search comes back with too few candidates, the same domain five times, stale pages, or results that cover one subquestion and ignore the rest. &lt;a href=&quot;https://github.com/open-webui/open-webui/discussions/18302&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Open WebUI discussion #18302&lt;/a&gt; shows the symptom plainly: on v0.6.32 the reporter saw one or two sources fetched for most queries, and short, thin answers as a result.&lt;/p&gt;
&lt;p&gt;The cause is usually one broad query standing in for a multi-part question, or a loop that stops after the first relevant page instead of searching again while coverage is still short.&lt;/p&gt;
&lt;p&gt;Keep every query you issued, the ranked results with their domains, and which required element each candidate might support. Discovery passes when every required element has a plausible candidate. A non-empty result list is not the bar.&lt;/p&gt;
&lt;h2 id=&quot;3-retrieval&quot;&gt;3. Retrieval&lt;/h2&gt;
&lt;p&gt;The URLs are visible and the pages never reach the next stage. The dangerous version is the one where the interface lists the URLs, everyone assumes the pages were read, and the model answers from what it already knew.&lt;/p&gt;
&lt;p&gt;This class is quiet by design, because fetch failures are usually caught and skipped. In the thread on &lt;a href=&quot;https://github.com/open-webui/open-webui/issues/25038&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Open WebUI #25038&lt;/a&gt;, a commenter traced the “No sources found” behaviour on v0.9.5 to a keyword argument passed twice in the page loader. It raised a TypeError on every URL before any network request, and because the loader runs with continue_on_failure=True, the only trace was a warning per skipped fetch. Search worked. The UI listed results. Nothing was read.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.openwebui.com/troubleshooting/web-search/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Open WebUI’s troubleshooting guide&lt;/a&gt; describes a quieter relative: Wikipedia, Cloudflare-protected sites and other large publishers filtering the default Python user agent, so pages come back empty or as 403s.&lt;/p&gt;
&lt;p&gt;Per fetch, keep the requested and final URL, the status or browser outcome, content type, size, redirect chain, duration and a truncation flag. Then test it properly: pick a public page containing a unique sentence, fetch it through your production path, and check the sentence survives into stored content. A fetch counter going up proves nothing at all.&lt;/p&gt;
&lt;h2 id=&quot;4-extraction-and-normalisation&quot;&gt;4. Extraction and normalisation&lt;/h2&gt;
&lt;p&gt;The page was fetched and the useful part is missing, mangled, or buried under navigation.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ollama/ollama/issues/12690&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Ollama issue #12690&lt;/a&gt; shows what happens when nothing selects. On v0.12.6, a web search through Qwen3 in the desktop app returned 102,207 tokens for a simple question, because it passed the full contents of five pages, one of which was a Wikipedia article of roughly 100,000 tokens on its own. The same question through gpt-oss:20b-cloud came back at 768 tokens. For scale, Ollama’s web search docs suggest a context of at least around 32,000 tokens, so one unfiltered page can be three times the whole budget.&lt;/p&gt;
&lt;p&gt;Keep hashes of the raw and normalised content, both lengths, the extraction method and version, and where truncation happened. Test with one expected passage and one known block of boilerplate: the normalised output keeps the passage, loses the boilerplate, and fits a budget you declared before the run.&lt;/p&gt;
&lt;h2 id=&quot;5-context-assembly&quot;&gt;5. Context assembly&lt;/h2&gt;
&lt;p&gt;Search and retrieval both succeed and the model behaves as though it has no evidence at all. This is where I’d put the most monitoring, because every check upstream of it passes.&lt;/p&gt;
&lt;p&gt;Open WebUI produced two versions of it in consecutive releases. In #25038, on v0.9.5, results appeared in the interface, then source processing ended with “No sources found” and a traceback from the web search handler, where a JSONResponse was being indexed as if it were a dictionary. In &lt;a href=&quot;https://github.com/open-webui/open-webui/issues/25585&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;#25585&lt;/a&gt;, after an upgrade to v0.9.6, the logs recorded nine items added to the web search collection, no errors anywhere, and a model answering as though no search had run. Commenters traced that one to source resolution: the function had no branch for web search items, and after an access-control change the fallback treated the collection name as untrusted and dropped it silently. Status history showed zero sources retrieved. A fix was proposed in pull request #25600. #25038 has since been closed, with the closing comment saying the reported problems were fixed by 0.10.2 given correct configuration, though a later comment describes similar behaviour on v0.11.3.&lt;/p&gt;
&lt;p&gt;Both reports are tied to their versions and setups. What makes them worth reading is that a search provider health check would have passed throughout, and in #25585 the logs actively reported success.&lt;/p&gt;
&lt;p&gt;So keep source IDs at every hop: candidates, retrieved, placed in context, present in the final model input. The test is a sentinel. Put one unique, harmless fact into a controlled source and check that the exact fact reaches the model input and can be returned with its source ID. Never infer delivery from a “search complete” event in the UI.&lt;/p&gt;
&lt;h2 id=&quot;6-tool-loop-and-state&quot;&gt;6. Tool loop and state&lt;/h2&gt;
&lt;p&gt;The model calls search, then loses the question, loops on the same tool, or stops with an empty reply.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ollama/ollama/issues/15895&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Ollama issue #15895&lt;/a&gt; is a clean specimen. On v0.22.0, Gemma 4 reasoned for a while, called web search, and once the tool result arrived it produced an empty reply, having lost both the prompt and the chat context. A follow-up on the thread put it down to truncation rather than compaction, with the user’s own message falling outside the window, and another comment noted Qwen3 wasn’t affected in the same setup.&lt;/p&gt;
&lt;p&gt;Keep the ordered message sequence with roles, tool-call IDs matched to their results, context size before and after each call, the stop reason and the final message length. Then test the message adapter away from live search: replay a fixture with the question, one tool call and one small result, and check the model can restate the question, use the result and produce a non-empty answer.&lt;/p&gt;
&lt;h2 id=&quot;7-synthesis-coverage-and-uncertainty&quot;&gt;7. Synthesis, coverage and uncertainty&lt;/h2&gt;
&lt;p&gt;The sources arrived and the answer still misses required elements, blends facts that conflict, or sounds more certain than the evidence allows.&lt;/p&gt;
&lt;p&gt;Most systems have no coverage check between “context assembled” and “start writing”, and a writing prompt that rewards finishing over admitting a gap will always find a way to finish. Two sources describing different versions get treated as interchangeable, and the first plausible story wins.&lt;/p&gt;
&lt;p&gt;Build a fixture with two sources that explicitly disagree and one required element with no evidence at all. A passing answer scopes the conflict and says the evidence is missing. A failing one resolves the disagreement by inventing a reason to prefer one side.&lt;/p&gt;
&lt;h2 id=&quot;8-citation-integrity&quot;&gt;8. Citation integrity&lt;/h2&gt;
&lt;p&gt;There are links, there are numbered references, and a cited page doesn’t support the claim sitting next to it. The variants: important claims with no citation, URL variants of one page counted as separate sources, and a source list with no mapping back to claims.&lt;/p&gt;
&lt;p&gt;Our own &lt;a href=&quot;https://github.com/Mozilla-Ocho/tabstack-cited-research-python&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Week 1 sample&lt;/a&gt; is a fair example, and I’d rather use it than pretend managed output is immune. The committed run used Tabstack Research in fast mode with SDK 2.8.5 on 15 September 2026. It completed cleanly and cited seven pages. Three of those seven were the same Ollama documentation page under different URLs: once over http, once over https, once with a .md suffix. Every claims array came back empty, and the optional relevance and reliability fields weren’t there. The Research guide’s worked example shows populated claims; this run didn’t return them.&lt;/p&gt;
&lt;p&gt;None of that means the answer was wrong. It means three separate measurements were being read as one: whether citations are present, how many distinct sources there are, and whether each claim is supported.&lt;/p&gt;
&lt;p&gt;Check every material claim against its cited page and score the support as direct, partial, absent or inaccessible. Report correctness and completeness separately, and don’t award a pass because the links are there.&lt;/p&gt;
&lt;h2 id=&quot;9-terminal-and-operational&quot;&gt;9. Terminal and operational&lt;/h2&gt;
&lt;p&gt;The interface searches forever, a stream closes before any terminal event, an HTTP failure gets handled as a task failure, or missing output becomes a successful empty answer.&lt;/p&gt;
&lt;p&gt;Most of these live in the consumer. A handler that only listens for success turns every failed run into silence. HTTP errors and in-stream errors sharing a handler lose the information about which phase died. A fixed total timeout kills healthy long runs. Retries can repeat work and cost, and they can be on without anyone choosing them: the Tabstack Python SDK retries a request twice by default on connection errors and on 408, 409, 429 and 5xx responses.&lt;/p&gt;
&lt;p&gt;Replay three fixtures at minimum: a successful complete, an in-stream error, and a stream that ends with neither. Each should produce a distinct result, and both failures should exit non-zero.&lt;/p&gt;
&lt;h2 id=&quot;the-nine-on-one-page&quot;&gt;The nine, on one page&lt;/h2&gt;
&lt;div class=&quot;table-scroll&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Class&lt;/th&gt;
&lt;th&gt;Signature&lt;/th&gt;
&lt;th&gt;Minimum evidence&lt;/th&gt;
&lt;th&gt;Smallest test&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Scope and planning&lt;/td&gt;
&lt;td&gt;Relevant answer omits required parts&lt;/td&gt;
&lt;td&gt;Frozen question and required elements&lt;/td&gt;
&lt;td&gt;Score coverage against criteria written first&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Discovery&lt;/td&gt;
&lt;td&gt;Too few candidates, or a narrow source set&lt;/td&gt;
&lt;td&gt;Queries and result list&lt;/td&gt;
&lt;td&gt;Run fixed queries and inspect candidates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retrieval&lt;/td&gt;
&lt;td&gt;URLs exist, pages absent, blocked or truncated&lt;/td&gt;
&lt;td&gt;Status, redirects, size, truncation&lt;/td&gt;
&lt;td&gt;Fetch a known page through the production path&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Extraction&lt;/td&gt;
&lt;td&gt;Page fetched, evidence missing or malformed&lt;/td&gt;
&lt;td&gt;Raw and normalised content hashes&lt;/td&gt;
&lt;td&gt;Compare an expected passage with the output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Context assembly&lt;/td&gt;
&lt;td&gt;Fetch succeeds, model gets no usable sources&lt;/td&gt;
&lt;td&gt;Source IDs at each hop&lt;/td&gt;
&lt;td&gt;Inject a sentinel fact, inspect model input&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool loop and state&lt;/td&gt;
&lt;td&gt;Question lost, tools repeated, empty stop&lt;/td&gt;
&lt;td&gt;Ordered messages and tool events&lt;/td&gt;
&lt;td&gt;Replay a frozen conversation fixture&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Synthesis and coverage&lt;/td&gt;
&lt;td&gt;Answer incomplete or overconfident&lt;/td&gt;
&lt;td&gt;Required elements and the answer&lt;/td&gt;
&lt;td&gt;Score coverage and uncertainty handling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Citation integrity&lt;/td&gt;
&lt;td&gt;Links don’t support claims, sources duplicated&lt;/td&gt;
&lt;td&gt;Claim-source pairs, normalised URLs&lt;/td&gt;
&lt;td&gt;Check each material claim against its citation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Terminal and operational&lt;/td&gt;
&lt;td&gt;Stream closes, times out, or errors with no result&lt;/td&gt;
&lt;td&gt;Event sequence, timestamps, terminal status&lt;/td&gt;
&lt;td&gt;Replay complete, error and premature-close fixtures&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2 id=&quot;they-cascade&quot;&gt;They cascade&lt;/h2&gt;
&lt;p&gt;Two of the reports above fit together into a chain worth recognising. #12690 shows a single search returning six figures of tokens. #15895 shows what truncation does to the user’s message once the context overflows. They come from different setups, so read this as how the classes connect rather than as one incident:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://tabstack.ai/blog/model-operated-web-research-failure-taxonomy&quot;&gt;View the diagram in the original post.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The visible symptom sits four steps from the cause, and the symptom points at the model. Diagnose from the last boundary with positive evidence instead.&lt;/p&gt;
&lt;h2 id=&quot;triage-order&quot;&gt;Triage order&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Did the application define the required result? If not, freeze the acceptance criteria before touching anything else.&lt;/li&gt;
&lt;li&gt;Did search return plausible candidates for every required element?&lt;/li&gt;
&lt;li&gt;Were the selected pages actually retrieved? Check status, redirects and truncation.&lt;/li&gt;
&lt;li&gt;Did normalisation keep the evidence? Compare content before and after extraction.&lt;/li&gt;
&lt;li&gt;Did that evidence reach the model input? Check source IDs and a sentinel.&lt;/li&gt;
&lt;li&gt;Did the tool conversation keep the question and the message order?&lt;/li&gt;
&lt;li&gt;Did the answer cover the required elements and state its uncertainty?&lt;/li&gt;
&lt;li&gt;Do the citations support the material claims?&lt;/li&gt;
&lt;li&gt;Did the run reach an explicit terminal state? A non-empty UI is not evidence of success.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The order exists to stop you swapping the model when the evidence never reached it. That’s the most expensive wrong move available, and it’s the first one most teams make.&lt;/p&gt;
&lt;h2 id=&quot;moving-the-loop-out-of-the-model&quot;&gt;Moving the loop out of the model&lt;/h2&gt;
&lt;p&gt;A managed Research call takes query planning, retrieval, gap checks, synthesis and citation assembly out of your code. Classes 2 to 6 stop being yours to instrument, which is a real reduction in surface area. Failure still happens, in fewer places you control.&lt;/p&gt;
&lt;p&gt;Class 1 stays with you, because you still own the question. Classes 7 and 8 stay with you as review work, and the Week 1 sample above is the argument for taking that seriously. Class 9 moves into your consumer: request validation, HTTP errors, in-stream errors, a stream that closes early, optional citation metadata that isn’t there.&lt;/p&gt;
&lt;p&gt;I’d use search or model-operated tools when you want links, want control over each step, or the loop itself is the product. I’d use a managed call when the application needs a finished cited result and you’d rather not have the model operating every intermediate stage. Either way, a complete event proves the job terminated and nothing more.&lt;/p&gt;
&lt;h2 id=&quot;write-the-next-one-up-properly&quot;&gt;Write the next one up properly&lt;/h2&gt;
&lt;p&gt;When the next web-enabled answer fails, record the environment and exact versions, the frozen test question or a sanitised hash of it, the answer elements you expected, the last boundary known to have worked, the first known to have failed, sanitised logs or fixtures, whether it reproduces, and whether the workaround changes the architecture or just hides the symptom. Then give it one primary class and note the downstream effects.&lt;/p&gt;
&lt;p&gt;A useful incident report lets another engineer reproduce the boundary. A bad one shows them the answer and asks them to guess.&lt;/p&gt;
&lt;p&gt;Never trained on. Private by default. Built by Mozilla.&lt;/p&gt;
</content:encoded></item><item><title>Add current, cited answers to your model application without touching the model</title><link>https://tabstack.ai/blog/cited-answers-existing-model-application-python</link><guid isPermaLink="true">https://tabstack.ai/blog/cited-answers-existing-model-application-python</guid><description>A second path at the application layer sends current questions to a managed Research call and returns a cited result in your own response shape, with the model left exactly as it is.</description><pubDate>Wed, 23 Sep 2026 14:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Your application already runs a model. Someone asks it about something that happened last month, and it answers confidently from training data that ended long before. The fix people reach for first is a bigger model, or a search tool bolted onto the one they have. Both of those change the thing that currently works.&lt;/p&gt;
&lt;p&gt;There’s a cheaper move. Add a second path at the application layer, send the current question to a managed Research call, and return the result through the response shape your application already uses. The model keeps its prompt, its settings and its context window. It just stops being the component responsible for knowing what happened last month.&lt;/p&gt;
&lt;p&gt;Here’s what that looks like in Python, with the parts that bite.&lt;/p&gt;
&lt;h2 id=&quot;dont-let-the-model-decide&quot;&gt;Don’t let the model decide&lt;/h2&gt;
&lt;p&gt;The first design decision is routing, and I’d make it boring on purpose. The application takes an explicit mode:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;model-web-app&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; chat&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; --prompt&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;Explain our local cache policy&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;model-web-app&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; research&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; --question&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;What are the current ways to add web search to an Ollama-based application?&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Letting a model classify each request as chat or research is the version everyone wants to build, and it’s the version you can’t test deterministically. Every failure then has two possible causes, and the first thing you’ll do when the integration misbehaves is argue about whether the classifier was right. Get the integration working with an explicit mode, prove the model path is untouched, then add classification as its own change with its own tests.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://tabstack.ai/blog/cited-answers-existing-model-application-python&quot;&gt;View the diagram in the original post.&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;the-contract-belongs-to-you&quot;&gt;The contract belongs to you&lt;/h2&gt;
&lt;p&gt;The rest of your application should never see an SDK event object. Give it something you own:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; dataclasses &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; dataclass, field&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; typing &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; Literal&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;@dataclass&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;frozen&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;True&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; SourceRecord&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    id&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;str&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    url: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;str&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    title: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; |&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; None&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; None&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    claims: list[&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; field(&lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;default_factory&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;list&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    source_queries: list[&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; field(&lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;default_factory&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;list&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;@dataclass&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;frozen&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;True&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; ApplicationResponse&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    kind: Literal[&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;chat&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;research&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    content_markdown: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;str&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    sources: list[SourceRecord]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    status: Literal[&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;complete&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;partial&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    provider: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;str&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    retrieved_at_utc: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; |&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; None&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; None&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two fields in there are doing quiet work.&lt;/p&gt;
&lt;p&gt;sources stays ordered. &lt;a href=&quot;https://docs.tabstack.ai/api/resources/agent/methods/research&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Tabstack documents&lt;/a&gt; citedPages as ordered by first citation in the report, and the report’s inline [1], [2] markers refer to that order. Sort it or deduplicate it in the adapter and you’ve broken the only claim-to-source mapping you get in fast mode.&lt;/p&gt;
&lt;p&gt;status has a value other than complete so a partial result has somewhere to live. This integration never produces one, because a Research call ends in either complete or error. It’s there so that when you add your own retrieval later, or when a call gives you a report with half its sources unreachable, the answer to “what do we return?” isn’t “a confident-looking answer with a gap in it”.&lt;/p&gt;
&lt;h2 id=&quot;the-adapter&quot;&gt;The adapter&lt;/h2&gt;
&lt;p&gt;This is the whole of it:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; os&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; typing &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; Literal&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; httpx&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; tabstack&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; tabstack &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; Tabstack&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; run_research&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    question: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    mode: Literal[&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;fast&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;balanced&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;fast&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    nocache: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;bool&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; True&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;) -&gt; ApplicationResponse:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; not&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; os.environ.get(&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;TABSTACK_API_KEY&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;        raise&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; ConfigurationError(&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;TABSTACK_API_KEY is not set&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    with&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; Tabstack(&lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;max_retries&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;as&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; client:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;        try&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;            with&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; client.agent.research(&lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;query&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;question, &lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;mode&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;mode, &lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;nocache&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;nocache) &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;as&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; stream:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;                for&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; event &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; stream:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;                    if&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; event.event &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;error&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;                        raise&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; ResearchTaskError(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E36209&quot;&gt;                            message&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;getattr&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(event.data.error, &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;message&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;None&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;or&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;unknown error&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E36209&quot;&gt;                            activity&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;event.data.activity,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;                        )&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;                    if&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; event.event &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;complete&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;                        return&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; to_application_response(event.data)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;                    report_progress(event)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;        except&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; tabstack.APIStatusError &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;as&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; err:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;            raise&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; ResearchHTTPError(&lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;status&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;err.status_code) &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; None&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;        except&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; tabstack.APIConnectionError:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;            raise&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; ResearchTransportError(&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;could not reach Tabstack&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; None&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;        except&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; httpx.TransportError &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;as&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; err:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;            raise&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; ResearchTransportError(&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;f&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;stream interrupted: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;{type&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(err).&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;__name__}&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; None&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    raise&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; ResearchProtocolError(&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;stream ended without complete or error&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Four lines there aren’t obvious.&lt;/p&gt;
&lt;p&gt;The environment check happens before the client is built. Tabstack() raises a TabstackError from its constructor when neither api_key nor TABSTACK_API_KEY is set, and a missing environment variable deserves its own exit code and its own message rather than arriving as a generic SDK error.&lt;/p&gt;
&lt;p&gt;except httpx.TransportError is there because the SDK’s coverage stops at the stream boundary. Anything that fails before the stream opens comes back as an SDK exception: AuthenticationError, RateLimitError and friends under APIStatusError, or APIConnectionError and its APITimeoutError subclass. Once you’re iterating, the stream iterator in 2.8.5 passes transport failures straight through, so a read timeout or a dropped connection mid-stream arrives as a raw httpx exception. Catch only the SDK types and a stream that dies halfway takes your process with it.&lt;/p&gt;
&lt;p&gt;The error event’s payload carries message, name and an optional stack. Keep the message and the activity, drop the stack. Provider stack traces in your logs are noise at best, and a leak at worst. The defensive read on message is there because the guide warns the error object can arrive unpopulated despite being typed as required.&lt;/p&gt;
&lt;p&gt;from None keeps the SDK’s exception chain out of your tracebacks, which is the cheapest way to stop a key or a header ending up in a log aggregator.&lt;/p&gt;
&lt;p&gt;The final raise is the case people forget. If the stream ends without complete and without error, the loop just finishes and the function returns None unless you make that an error. A silent None becomes an empty answer three layers up, and nobody can tell it apart from a model that had nothing to say.&lt;/p&gt;
&lt;h2 id=&quot;timeouts-and-the-one-everybody-gets-wrong&quot;&gt;Timeouts, and the one everybody gets wrong&lt;/h2&gt;
&lt;p&gt;Research has no server-side timeout on the call as a whole. A fast query usually comes back in under a minute. A broad balanced query consults more sources and can run for around four minutes. So don’t wrap it in a total deadline. A fixed 90-second timeout will kill healthy long runs and you’ll conclude the API is flaky when the problem is your own stopwatch.&lt;/p&gt;
&lt;p&gt;What you want is silence detection, and you already have some. The SDK’s default is httpx.Timeout(connect=5.0, read=60, write=60, pool=60), and httpx applies that read timeout to each read, so on a stream it fires when nothing has arrived for sixty seconds. That’s the behaviour you want, just with a number you chose deliberately rather than inherited. If you add your own silence detection on top, reset it on every event.&lt;/p&gt;
&lt;p&gt;One parameter that is not a total deadline, despite reading like one: fetch_timeout caps how long Research spends fetching a single page.&lt;/p&gt;
&lt;h2 id=&quot;retries-which-are-probably-already-on&quot;&gt;Retries, which are probably already on&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    with&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; Tabstack(&lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;max_retries&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;as&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; client:&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That max_retries=0 is deliberate. The SDK retries twice by default, on connection errors and on 408, 409, 429 and 5xx responses. The committed Week 1 sample run recorded application_retries: 0 in its manifest alongside sdk_max_retries: 2, which is a neat illustration of how “we don’t retry” can be untrue in a codebase nobody has lied in.&lt;/p&gt;
&lt;p&gt;A replayed Research call can repeat billed work, and there’s no idempotency key to make it safe. Turn retries off until somebody has decided which failures are safe to replay and what the ceiling is, then turn them back on with that decision written down.&lt;/p&gt;
&lt;h2 id=&quot;what-actually-comes-back&quot;&gt;What actually comes back&lt;/h2&gt;
&lt;p&gt;The &lt;a href=&quot;https://github.com/Mozilla-Ocho/tabstack-cited-research-python&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Week 1 example repository&lt;/a&gt; has a committed run of this shape: fast mode, SDK 2.8.5, 15 September 2026. It completed in 20.3 seconds with the first event arriving after 1.06 seconds, analysed seven pages, cited seven, and returned a 1,649-character report. Ten events, each once, in order: start, planning, one iteration with a search inside it, writing, complete.&lt;/p&gt;
&lt;p&gt;The interesting part is what the citation metadata did and didn’t contain.&lt;/p&gt;
&lt;p&gt;Three of the seven cited pages were the same Ollama documentation page under different URLs: once over http, once over https, and once with a .md suffix. Your distinct source count is not your cited page count, and normalising scheme, repeated slashes, trailing slashes and file suffixes before counting is a ten-line function that stops you reporting seven sources when you have five.&lt;/p&gt;
&lt;p&gt;Every claims array came back empty. The field is typed as always present and it was present, carrying nothing, because fast mode skips the analysis phases that populate it. The guide’s worked example shows it populated. That’s the gap between a documented shape and a live response, and it’s why the adapter maps claims into the contract without ever depending on its contents. In that mode the report’s inline [n] markers, read against the source order, are the claim-to-source link.&lt;/p&gt;
&lt;p&gt;relevance, reliability and summary were all absent, as the SDK docstrings say they are in fast mode. title was present on every page. And complete.data.timestamp arrived as epoch milliseconds while the guide’s example shows an ISO string, so set retrieved_at_utc from your own clock and store the raw value alongside it.&lt;/p&gt;
&lt;p&gt;None of that makes the output unusable. It does mean you build against the response you get rather than the example you read, and that you read citedPages and gapEvaluations defensively, since both are optional.&lt;/p&gt;
&lt;h2 id=&quot;what-to-log&quot;&gt;What to log&lt;/h2&gt;
&lt;p&gt;Log enough to find the boundary that failed, and nothing that belongs to the person who asked the question. The event names in order, with timestamps. Time to first event and to terminal state. The terminal status and which phase it failed in. Counts of pages analysed, cited pages raw, and distinct URLs after normalising. Whether the model adapter was called, which for this path should always be false. The SDK version, its max_retries and its effective timeout.&lt;/p&gt;
&lt;p&gt;Keep out API keys, authorisation headers, cookies, whole environment dumps, and model reasoning.&lt;/p&gt;
&lt;h2 id=&quot;when-this-is-the-wrong-shape&quot;&gt;When this is the wrong shape&lt;/h2&gt;
&lt;p&gt;If what you need is links, use a search API and skip all of this. If the web layer is what makes your product different from the next one, own the loop yourself and accept the boundaries that come with it. If you need the model to make decisions mid-research based on what it finds, you want a tool loop, not one call.&lt;/p&gt;
&lt;p&gt;This shape fits when your application needs a finished, cited answer to a current question, and the model you run is good at everything except knowing what happened last month. You get to keep the model, and the parts you still own are the question, the review and the decision about whether the answer is good enough to act on.&lt;/p&gt;
&lt;p&gt;A complete event means the job finished. That’s all it means.&lt;/p&gt;
&lt;p&gt;Never trained on. Private by default. Built by Mozilla.&lt;/p&gt;
</content:encoded></item><item><title>The seven stages of web research for a model you run</title><link>https://tabstack.ai/blog/seven-stages-web-research-own-model</link><guid isPermaLink="true">https://tabstack.ai/blog/seven-stages-web-research-own-model</guid><description>A web research system you can trust with a current question does seven separate jobs. It defines what a complete answer looks like, discovers candidate sources, retrieves them, extracts evidence, checks that evidence for gaps, reconciles conflicts, and writes a cited result.</description><pubDate>Mon, 21 Sep 2026 14:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A web research system you can trust with a current question does seven separate jobs. It defines what a complete answer looks like, discovers candidate sources, retrieves them, extracts evidence, checks that evidence for gaps, reconciles conflicts, and writes a cited result.&lt;/p&gt;
&lt;p&gt;Splitting the work up this way won’t make an answer correct on its own. What it does is give every failure an address, so when an answer comes back wrong you can point at the stage that broke instead of blaming “the model”.&lt;/p&gt;
&lt;p&gt;This is for anyone running their own model inside a product, an assistant, an agent or an internal workflow, where users ask about things the training data can’t know.&lt;/p&gt;
&lt;h2 id=&quot;the-loop&quot;&gt;The loop&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://tabstack.ai/blog/seven-stages-web-research-own-model&quot;&gt;View the diagram in the original post.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The arrow from Evaluate back to Discover is the part most implementations leave out, and it’s why I call this a loop and not a pipeline. When one piece of the answer has no evidence behind it, the system has three honest options: search again, narrow the claim, or hand back an incomplete result. Writing confident prose over the hole isn’t on that list.&lt;/p&gt;
&lt;h2 id=&quot;write-the-answer-contract-before-you-search&quot;&gt;Write the answer contract before you search&lt;/h2&gt;
&lt;p&gt;I’ll use one question the whole way through, because it’s the sort of thing people really do ask these systems:&lt;/p&gt;
&lt;p&gt;Which web search approaches can an Ollama-based application use today, and what does each one return?&lt;/p&gt;
&lt;p&gt;Before any searching, write down what a usable answer has to contain:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;question&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;Which web search approaches can an Ollama-based application use today, and what does each one return?&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;required_elements&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;    &quot;approach name&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;    &quot;where the web workflow runs&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;    &quot;what the application receives&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;    &quot;whether the model must still build the answer&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;    &quot;a current source for each material product claim&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  ],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;output&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    &quot;answer_format&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;markdown&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    &quot;citations&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;inline numbered references&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    &quot;source_record&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;machine-readable ordered list&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    &quot;incomplete_evidence&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;must be stated&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This feels like overhead the first time. It’s also the only thing that lets you decide afterwards whether a fluent answer was a complete one. A lot of “relevant but useless” answers come from nobody deciding up front what useful meant.&lt;/p&gt;
&lt;h2 id=&quot;stage-1-define-the-question-and-what-done-looks-like&quot;&gt;Stage 1: Define the question and what done looks like&lt;/h2&gt;
&lt;p&gt;“Web search approaches” is ambiguous. It could mean a raw search API, search plus page fetching, a tool loop the model drives itself, or a managed research call. Each reading is legitimate and each produces a different answer, so the first stage turns the request into a research objective, a set of subquestions, the required elements above, a freshness boundary and an output shape. “Today” in the question is doing real work here: it means current documentation, not a blog post from two years ago.&lt;/p&gt;
&lt;p&gt;The test I’d apply is whether a reviewer could mark the answer complete or incomplete using only what this stage produced. If they’d have to invent criteria after reading the answer, the stage isn’t finished.&lt;/p&gt;
&lt;p&gt;When the ambiguity would materially change the work, stop and ask. A silent guess at this point poisons everything after it, because every later stage will carefully execute the wrong job.&lt;/p&gt;
&lt;h2 id=&quot;stage-2-discover-candidate-sources&quot;&gt;Stage 2: Discover candidate sources&lt;/h2&gt;
&lt;p&gt;Discovery finds pages that might contain evidence. Nothing is established as true yet.&lt;/p&gt;
&lt;p&gt;A multi-part question needs more than one query. For the Ollama example I’d want one aimed at Ollama’s own search and fetch documentation, one at how Open WebUI wires search into a chat, and one at a research API’s response contract. A single broad query tends to return the loudest pages, and they’re rarely the authoritative ones.&lt;/p&gt;
&lt;p&gt;Keep the provenance as you go: which query surfaced each page, and which subquestion it’s supposed to answer. Throw that away and any later debugging turns into guesswork.&lt;/p&gt;
&lt;p&gt;This stage fails in a few recognisable ways. A required element has no candidate at all, the only candidates are derivative (the blog post summarising the docs rather than the docs), the results are duplicates of each other, or everything sits outside the freshness boundary.&lt;/p&gt;
&lt;h2 id=&quot;stage-3-retrieve-readable-source-content&quot;&gt;Stage 3: Retrieve readable source content&lt;/h2&gt;
&lt;p&gt;A search result is a lead, and the page it points at is the evidence. &lt;a href=&quot;https://docs.ollama.com/capabilities/web-search&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Ollama’s web search API&lt;/a&gt; is a tidy illustration of the split: each result is a title, a URL and a content snippet, and a separate web fetch endpoint returns the page’s title, main content and links. The interface is honest about what search gives you.&lt;/p&gt;
&lt;p&gt;Retrieval fetches or renders the chosen pages and turns them into something the next stage can inspect. Record the status, any redirects, a timestamp, the content size, and whether anything was truncated.&lt;/p&gt;
&lt;p&gt;Truncation is the quiet failure. &lt;a href=&quot;https://docs.openwebui.com/troubleshooting/web-search/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Open WebUI’s web search troubleshooting guide&lt;/a&gt; notes that web pages often run to 4,000 to 8,000 tokens or more, and that a model running at Ollama’s 4,096-token default context misses most of that content. Nothing errors when it happens. The model answers from part of a page and sounds exactly as sure of itself as it would with the whole thing.&lt;/p&gt;
&lt;p&gt;A successful search followed by a failed fetch is a partial run with a known boundary, and it should be reported as one.&lt;/p&gt;
&lt;h2 id=&quot;stage-4-extract-claims-with-their-context&quot;&gt;Stage 4: Extract claims with their context&lt;/h2&gt;
&lt;p&gt;A page can be perfectly readable and still useless as evidence. This stage pulls out the statements that bear on the question and keeps the qualifications attached to them.&lt;/p&gt;
&lt;p&gt;At minimum I’d keep this much per claim:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;claim&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;Ollama&apos;s web search API returns a title, URL and content snippet for each result.&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;source_url&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;https://docs.ollama.com/capabilities/web-search&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;source_title&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;Web search&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;supporting_excerpt&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;content (string): relevant content snippet from the web page&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;retrieved_at&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;2026-09-21T00:00:00Z&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;scope&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;documented response fields for the web_search endpoint only&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;supports_required_element&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;what the application receives&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The scope field is the one that earns its place. “The Ollama web search API returns snippets” is true. “Ollama-based applications only get snippets” is false, because the same page documents the fetch endpoint that returns page content. Extraction is where a qualified statement gets quietly promoted to an absolute one if nothing stops it.&lt;/p&gt;
&lt;p&gt;It’s also where citations start meaning something. A list of URLs at the bottom of an answer is a bibliography. A record of which claim came from which page is something you can check.&lt;/p&gt;
&lt;h2 id=&quot;stage-5-evaluate-coverage-source-quality-and-gaps&quot;&gt;Stage 5: Evaluate coverage, source quality and gaps&lt;/h2&gt;
&lt;p&gt;This is the stage that separates research from summarising whatever came back first. Compare the evidence against the contract you wrote at the start:&lt;/p&gt;
&lt;div class=&quot;table-scroll&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Required element&lt;/th&gt;
&lt;th&gt;Evidence found&lt;/th&gt;
&lt;th&gt;Source quality&lt;/th&gt;
&lt;th&gt;Fresh enough&lt;/th&gt;
&lt;th&gt;Decision&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Approach name&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Official&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Keep&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Where workflow runs&lt;/td&gt;
&lt;td&gt;Partial&lt;/td&gt;
&lt;td&gt;Official plus inference&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Narrow wording&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output returned&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Official docs&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Keep&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model still builds answer&lt;/td&gt;
&lt;td&gt;No direct source&lt;/td&gt;
&lt;td&gt;Architectural inference&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;Label as analysis, or search again&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Current source&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Official&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Keep&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;The row worth looking at is “model still builds answer”. You’re unlikely to find a provider page that states it outright, because it’s a conclusion drawn from what each interface returns. That’s fine, provided the answer labels it as analysis. What isn’t fine is attaching a citation to a page that doesn’t say it.&lt;/p&gt;
&lt;p&gt;If a material gap remains, go back to discovery with a narrower query. Don’t pad the context with more loosely related text and hope the volume covers the gap. It won’t, and it makes the next stage harder.&lt;/p&gt;
&lt;h2 id=&quot;stage-6-reconcile-conflicts-and-uncertainty&quot;&gt;Stage 6: Reconcile conflicts and uncertainty&lt;/h2&gt;
&lt;p&gt;Sources disagree for boring reasons. They describe different versions, plans, dates, regions or definitions, and usually neither one is wrong. Reconciliation keeps the conflict visible and works out whether it can be resolved:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Compare publication and update dates.&lt;/li&gt;
&lt;li&gt;Check that both sources describe the same thing at the same version.&lt;/li&gt;
&lt;li&gt;Prefer the source closest to the claim. For product behaviour that’s usually the current official docs.&lt;/li&gt;
&lt;li&gt;Separate a factual disagreement from two sources using the same word differently.&lt;/li&gt;
&lt;li&gt;Narrow the claim when the evidence only supports a limited version of it.&lt;/li&gt;
&lt;li&gt;State the uncertainty when nothing authoritative settles it.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This is the stage I’d trust automation with least. Comparing dates and preferring official documentation is mechanical. Deciding whether two pages are describing the same object, or two objects with the same name, is judgement, and a system that quietly picks the convenient source will look fine in every demo you run. If you’re only adding human review in one place, put it here.&lt;/p&gt;
&lt;p&gt;The failure mode is exactly that quiet pick, or its cousin: two incompatible claims blended into one sentence that neither source supports.&lt;/p&gt;
&lt;h2 id=&quot;stage-7-synthesise-the-answer-and-keep-the-source-record&quot;&gt;Stage 7: Synthesise the answer and keep the source record&lt;/h2&gt;
&lt;p&gt;Only now does the system write the thing the application asked for, and the result should carry more than prose:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;status&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;complete&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;answer&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;The approaches differ mainly in what they return and who operates the work after search. ... [1]&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;sources&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;      &quot;id&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;src_01&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;      &quot;url&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;https://docs.ollama.com/capabilities/web-search&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;      &quot;title&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;Web search&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;      &quot;claims&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;        &quot;Ollama&apos;s web search API returns a title, URL and content snippet for each result.&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;      ]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  ],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;unresolved&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: [],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;retrieved_at&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;2026-09-21T00:00:00Z&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two fields matter more than they look. status needs a value other than complete, otherwise a partial run has nowhere to go but into a confident answer. And an empty unresolved array should be something the system concluded, not the default because nobody wired it up.&lt;/p&gt;
&lt;p&gt;Synthesis fails as a fluent answer with gaps in its support, citations that point at nothing in particular, no way to express a partial result, or output your application can’t parse.&lt;/p&gt;
&lt;h2 id=&quot;the-seven-contracts-on-one-page&quot;&gt;The seven contracts on one page&lt;/h2&gt;
&lt;div class=&quot;table-scroll&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;Takes&lt;/th&gt;
&lt;th&gt;Produces&lt;/th&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;th&gt;Failure state&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Define&lt;/td&gt;
&lt;td&gt;User question, application requirements&lt;/td&gt;
&lt;td&gt;Objective, subquestions, required elements, freshness boundary, output contract&lt;/td&gt;
&lt;td&gt;Could a reviewer judge completeness from this alone?&lt;/td&gt;
&lt;td&gt;Question left broad, undefined or unverifiable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Discover&lt;/td&gt;
&lt;td&gt;Objective and subquestions&lt;/td&gt;
&lt;td&gt;Candidate URLs with the query and subquestion behind each&lt;/td&gt;
&lt;td&gt;Does every required element have a plausible candidate?&lt;/td&gt;
&lt;td&gt;No source, derivative sources only, duplicates, stale candidates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retrieve&lt;/td&gt;
&lt;td&gt;Selected URLs&lt;/td&gt;
&lt;td&gt;Readable content plus status, redirects, timestamps, size, truncation flag&lt;/td&gt;
&lt;td&gt;Did we get the part of the page that holds the claim?&lt;/td&gt;
&lt;td&gt;Blocked, timed out, unrendered, redirected elsewhere, empty or silently truncated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Extract&lt;/td&gt;
&lt;td&gt;Retrieved content and answer criteria&lt;/td&gt;
&lt;td&gt;Claim-to-source records with excerpt and scope&lt;/td&gt;
&lt;td&gt;Does the excerpt support the claim at the same scope?&lt;/td&gt;
&lt;td&gt;Snippet-only evidence, overbroad claims, lost qualifications, no trace back to the page&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Evaluate&lt;/td&gt;
&lt;td&gt;Claim records and required elements&lt;/td&gt;
&lt;td&gt;Coverage matrix, source quality, duplicates, gap list&lt;/td&gt;
&lt;td&gt;Is each element supported, qualified or marked unavailable?&lt;/td&gt;
&lt;td&gt;Relevant pages that don’t cover the actual question&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reconcile&lt;/td&gt;
&lt;td&gt;Supported claims and conflicting evidence&lt;/td&gt;
&lt;td&gt;Resolved claims, scoped readings, unresolved items&lt;/td&gt;
&lt;td&gt;Can a reviewer see why one source won?&lt;/td&gt;
&lt;td&gt;Convenient source chosen quietly, incompatible claims blended&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Synthesise&lt;/td&gt;
&lt;td&gt;Reconciled evidence and output contract&lt;/td&gt;
&lt;td&gt;Answer, citations, source record, limitations&lt;/td&gt;
&lt;td&gt;Does every material claim have inspectable support?&lt;/td&gt;
&lt;td&gt;Unsupported fluency, orphaned citations, no partial state, unparseable output&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2 id=&quot;the-handoffs-are-where-systems-break&quot;&gt;The handoffs are where systems break&lt;/h2&gt;
&lt;p&gt;A visible success at one boundary tells you nothing about the next one.&lt;/p&gt;
&lt;div class=&quot;table-scroll&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Visible event&lt;/th&gt;
&lt;th&gt;What it proves&lt;/th&gt;
&lt;th&gt;What it doesn’t prove&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Search returned URLs&lt;/td&gt;
&lt;td&gt;Discovery worked&lt;/td&gt;
&lt;td&gt;The pages were read, or reached the model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pages were fetched&lt;/td&gt;
&lt;td&gt;Retrieval worked&lt;/td&gt;
&lt;td&gt;The relevant claims were extracted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Context was assembled&lt;/td&gt;
&lt;td&gt;Data reached a context layer&lt;/td&gt;
&lt;td&gt;The model used it correctly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Answer contains links&lt;/td&gt;
&lt;td&gt;Citation strings exist&lt;/td&gt;
&lt;td&gt;The links support the claims beside them&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API emitted complete&lt;/td&gt;
&lt;td&gt;The job terminated successfully&lt;/td&gt;
&lt;td&gt;The answer is correct or complete&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Two public Open WebUI issues from this year show the gap between the first and third rows. In &lt;a href=&quot;https://github.com/open-webui/open-webui/issues/25038&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;#25038&lt;/a&gt;, on v0.9.5 with SearXNG, results came back and appeared in the interface, then source processing ended with “No sources found” and the model claimed web search wasn’t available. In &lt;a href=&quot;https://github.com/open-webui/open-webui/issues/25585&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;#25585&lt;/a&gt;, after an upgrade from v0.9.5 to v0.9.6, the logs recorded nine items added to the web search collection while the model answered as though no search had happened.&lt;/p&gt;
&lt;p&gt;These are individual, version-specific reports and I’m not using them as evidence of how often this happens. What they do show is that “search worked” and “the model used the search” are two different facts, and you need a separate check for each.&lt;/p&gt;
&lt;h2 id=&quot;what-should-your-application-log&quot;&gt;What should your application log?&lt;/h2&gt;
&lt;p&gt;Enough to find the failed boundary, without logging secrets or private source content. I’d capture:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;request ID and UTC timestamps&lt;/li&gt;
&lt;li&gt;a hash of the question, or an approved public test question&lt;/li&gt;
&lt;li&gt;the research objective and required answer elements&lt;/li&gt;
&lt;li&gt;every search query issued&lt;/li&gt;
&lt;li&gt;counts of candidate, fetched, failed, distinct and cited URLs&lt;/li&gt;
&lt;li&gt;redirects, fetch status and truncation flags&lt;/li&gt;
&lt;li&gt;context size and the source IDs passed to synthesis&lt;/li&gt;
&lt;li&gt;terminal status, and the stage it failed at if it failed&lt;/li&gt;
&lt;li&gt;where the answer and source record were written&lt;/li&gt;
&lt;li&gt;dependency and model versions&lt;/li&gt;
&lt;li&gt;duration, and usage figures where the provider reports them&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Leave out API keys, authorisation headers, cookies, environment dumps, model reasoning and anything confidential the user sent.&lt;/p&gt;
&lt;h2 id=&quot;where-should-the-loop-live&quot;&gt;Where should the loop live?&lt;/h2&gt;
&lt;p&gt;It helps to separate the tools first, because they get lumped together. Search returns links and snippets. Fetch or extract reads a URL you already have and gives you clean content or specific fields. Research takes a question and returns a synthesised answer from sources. Automation performs interactions on a public website when the result needs someone to click through to it.&lt;/p&gt;
&lt;div class=&quot;table-scroll&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Best when&lt;/th&gt;
&lt;th&gt;Your application owns&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Search API&lt;/td&gt;
&lt;td&gt;Links or snippets are the result you want&lt;/td&gt;
&lt;td&gt;Any reading, synthesis and citation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model-operated tools&lt;/td&gt;
&lt;td&gt;Your model drives tools reliably and you want the control&lt;/td&gt;
&lt;td&gt;Tool orchestration, context, evaluation, retries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DIY research loop&lt;/td&gt;
&lt;td&gt;The web layer is what differentiates your product&lt;/td&gt;
&lt;td&gt;All seven stages and every boundary between them&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Managed Research API&lt;/td&gt;
&lt;td&gt;You need a cited result without operating the loop&lt;/td&gt;
&lt;td&gt;Question design, acceptance, review, downstream use&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;My default is not to operate all seven stages yourself unless owning the loop is the product, or you need control over a specific stage that nothing managed gives you. Moving the loop to a provider changes who operates it. Deciding whether the output is good enough for your job stays with you either way.&lt;/p&gt;
&lt;h2 id=&quot;what-a-managed-call-shows-you-of-the-seven-stages&quot;&gt;What a managed call shows you of the seven stages&lt;/h2&gt;
&lt;p&gt;Tabstack Research is the one I work on, so here’s how its output lines up against the stages. Per the Research guide, you send a question and it always streams Server-Sent Events: a start, a planning phase, one or more search iterations, a writing phase, then a single complete event carrying the report and its metadata.&lt;/p&gt;
&lt;div class=&quot;table-scroll&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;Where it shows up&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Define&lt;/td&gt;
&lt;td&gt;researchObjective, researchQuestions and researchPlan in metadata, always present&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Discover&lt;/td&gt;
&lt;td&gt;executedQueries, plus sourceQueries on each cited page&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retrieve&lt;/td&gt;
&lt;td&gt;totalPagesAnalyzed, with per-page fetch time bounded by the fetch_timeout parameter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Extract&lt;/td&gt;
&lt;td&gt;claims on each entry in citedPages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Evaluate&lt;/td&gt;
&lt;td&gt;gapEvaluations when populated, plus isLast and stopReason on each iteration:end event&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reconcile&lt;/td&gt;
&lt;td&gt;No dedicated field. You see the outcome in the report, not the working&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Synthesise&lt;/td&gt;
&lt;td&gt;report, as Markdown&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;The reconcile row is the honest gap, and it’s the reason the review step stays with you for anything that matters. citedPages and gapEvaluations are both optional, so read them defensively and treat a missing citedPages as an empty list.&lt;/p&gt;
&lt;p&gt;The operational details are where a first integration usually trips. HTTP errors such as a bad key or a rate limit are raised before the stream opens. Task failures arrive as an error event inside the stream, so a consumer that only listens for complete gets nothing at all from a failed run. You also need to handle a stream that closes without either. There’s no server-side timeout on the request as a whole: fast mode typically finishes in under a minute, while balanced consults more sources, needs a paid plan, and can run for up to about four minutes on the broadest questions. A fixed total timeout will kill healthy long runs, so reset a timer on every event and fail on silence instead.&lt;/p&gt;
&lt;h2 id=&quot;map-your-current-research-loop&quot;&gt;Map your current research loop&lt;/h2&gt;
&lt;p&gt;Take one current question your application has to answer and fill in these seven rows before you change any tools:&lt;/p&gt;
&lt;div class=&quot;table-scroll&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;Current owner&lt;/th&gt;
&lt;th&gt;Input&lt;/th&gt;
&lt;th&gt;Output&lt;/th&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;th&gt;Failure state&lt;/th&gt;
&lt;th&gt;Logged?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Define&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Discover&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retrieve&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Extract&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Evaluate&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reconcile&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Synthesise&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Any blank cell in the owner column is work somebody assumed somebody else was doing. That’s the next thing to instrument, or the next thing to hand off.&lt;/p&gt;
&lt;p&gt;Never trained on. Private by default. Built by Mozilla.&lt;/p&gt;
</content:encoded></item><item><title>Search + fetch + model versus a managed Research call: the evaluation plan</title><link>https://tabstack.ai/blog/search-fetch-model-vs-managed-research-evaluation-plan</link><guid isPermaLink="true">https://tabstack.ai/blog/search-fetch-model-vs-managed-research-evaluation-plan</guid><description>We are comparing two complete ways of answering current public-web questions: a pinned search + fetch + model pipeline the application runs itself, and a single managed Research call. This article publishes the pre-registered method. It does not publish a winner.</description><pubDate>Fri, 18 Sep 2026 14:00:00 GMT</pubDate><content:encoded>&lt;p&gt;We are comparing two complete ways of answering current public-web questions. One is a pinned search + fetch + model pipeline that the application runs itself. The other is a single managed Research call. The questions, the system configurations, the hypotheses, the metrics, the failure rules and the analysis plan all went into Git before either system produced an output anyone looked at. This article publishes that method. It does not publish a winner, because the scored run has not happened yet.&lt;/p&gt;
&lt;p&gt;It is written for people running their own model inside a product, an assistant or an internal workflow, who have to decide where the research loop lives.&lt;/p&gt;
&lt;h2 id=&quot;the-protocol-went-in-first&quot;&gt;The protocol went in first&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://tabstack.ai/blog/search-fetch-model-vs-managed-research-evaluation-plan&quot;&gt;View the diagram in the original post.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Protocol v1 is committed at 116d90191c6d900a97779ac0fc07dc9ffcfc229a. The application-operated system was frozen at 22c1613a8e2546acb429c818d61a5327706004ed. The protocol, question set, rubric, result schema and harness are public in the repository’s evals directory. FULL_EVALUATION_RUN=false.&lt;/p&gt;
&lt;h2 id=&quot;the-question&quot;&gt;The question&lt;/h2&gt;
&lt;p&gt;For current public-web questions, what differences appear between a pinned search + fetch + model pipeline the application operates, and one managed Research call?&lt;/p&gt;
&lt;p&gt;That compares two complete configured systems, and it is worth being blunt about the limit this creates. If the outputs differ, we cannot point at search, page retrieval, model choice, orchestration, source availability or the managed service and say which one caused it. Isolating a component needs a different experiment, and a controlled one.&lt;/p&gt;
&lt;p&gt;The decision underneath is architectural. Does your application own source discovery, retrieval, context assembly, synthesis and citation formatting, or does it call a managed web API and take back the finished result?&lt;/p&gt;
&lt;h2 id=&quot;the-input-and-output-are-held-constant&quot;&gt;The input and output are held constant&lt;/h2&gt;
&lt;p&gt;Every run receives one frozen question and the same output instruction:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code class=&quot;language-plaintext&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Answer the question directly.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Support material factual claims with citations to public source URLs.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Distinguish sourced facts from interpretation.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;State when evidence is incomplete or conflicting.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;End with a Sources section listing only sources used in the answer.&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each run must produce a record with:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;question_id&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;Q01&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;system_id&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;redacted during blind review&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;started_at_utc&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;2026-09-15T20:31:39.716Z&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;completed_at_utc&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;2026-09-15T20:31:56.383Z&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;duration_ms&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;16666&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;terminal_status&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;complete&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;answer_path&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;answer.md&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;sources_path&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;sources.json&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;event_log_path&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;events.sanitized.jsonl&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;usage_receipt_path&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;usage-receipt.json&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;pilot_only&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;true&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;scoring_status&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;unscored&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That is a pilot record, included here to show the harness contract. It is not a scored result.&lt;/p&gt;
&lt;h2 id=&quot;both-systems-were-frozen-before-the-test&quot;&gt;Both systems were frozen before the test&lt;/h2&gt;
&lt;p&gt;We are comparing full paths, not an abstract category against a product.&lt;/p&gt;
&lt;h3 id=&quot;system-a-application-operated-search--fetch--model&quot;&gt;System A: application-operated search + fetch + model&lt;/h3&gt;
&lt;p&gt;System A uses a pinned web-search API, HTTP page retrieval with redirects and a 30-second fetch timeout, standard-library HTML-to-text conversion, and a dated flagship general model behind a chat-completions interface. The planner and writer prompts are committed. There are no application-level retries. Its fixed resource limits are:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#22863A&quot;&gt;max_search_iterations&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;3&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#22863A&quot;&gt;max_queries_per_iteration&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;3&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#22863A&quot;&gt;max_results_per_query&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;5&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#22863A&quot;&gt;max_pages_fetched_total&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;12&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#22863A&quot;&gt;max_chars_per_page&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;12000&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#22863A&quot;&gt;max_total_source_chars&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;60000&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#22863A&quot;&gt;total_wall_clock_timeout_seconds&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;300&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#22863A&quot;&gt;application_retries&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;0&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Model choice is the part of System A most open to challenge, so here is the reasoning behind it. We picked a configuration meant to represent a serious per-query deployment. We did not pick a smaller tier that would quietly weaken the application-operated baseline, and we did not pick a higher-cost specialist tier that a team would never run on every query. The complete provider and version record stays in the committed system configuration.&lt;/p&gt;
&lt;h3 id=&quot;system-b-managed-research&quot;&gt;System B: Managed Research&lt;/h3&gt;
&lt;p&gt;System B uses:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#22863A&quot;&gt;endpoint&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;/research&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#22863A&quot;&gt;mode&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;fast&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#22863A&quot;&gt;nocache&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;true&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#22863A&quot;&gt;fetch_timeout&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;default&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#22863A&quot;&gt;application_retries&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;0&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#22863A&quot;&gt;python_sdk&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;tabstack==2.8.5&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The application makes one &lt;code&gt;/research&lt;/code&gt; call and receives the streamed report and the cited-page metadata.&lt;/p&gt;
&lt;p&gt;Both configurations are part of the public record. If either one changes after anyone has inspected outputs, the protocol version has to change and the full test restarts.&lt;/p&gt;
&lt;h2 id=&quot;the-question-set-includes-work-that-may-not-need-a-research-loop&quot;&gt;The question set includes work that may not need a research loop&lt;/h2&gt;
&lt;p&gt;The frozen set contains 12 questions:&lt;/p&gt;
&lt;div class=&quot;table-scroll&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question type&lt;/th&gt;
&lt;th&gt;Count&lt;/th&gt;
&lt;th&gt;Why it is included&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Simple source discovery&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Test jobs where a list of official sources may be enough&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-source synthesis&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Test questions that require combining several pieces of evidence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Freshness-sensitive&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Test whether sources and claims meet an explicit date requirement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Conflict or incomplete evidence&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Test how each path handles uncertainty and unresolved evidence&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Each question carries two to five required answer elements, written before execution. The set deliberately includes jobs where a list of official sources might be all anyone needs. A comparison built only around the work one system prefers would not answer the architecture question honestly.&lt;/p&gt;
&lt;p&gt;The complete set is in evals/questions.jsonl.&lt;/p&gt;
&lt;h2 id=&quot;the-hypotheses-are-directional-not-conclusions&quot;&gt;The hypotheses are directional, not conclusions&lt;/h2&gt;
&lt;p&gt;Five, registered before the run:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The managed path will have higher median citation completeness, because citations are part of its output contract.&lt;/li&gt;
&lt;li&gt;The managed path will have higher median question coverage on multi-source and conflicting-evidence questions.&lt;/li&gt;
&lt;li&gt;The application-operated path may have lower median wall-clock time on simple questions.&lt;/li&gt;
&lt;li&gt;Cost has no pre-registered direction.&lt;/li&gt;
&lt;li&gt;On pure source-discovery questions, the application-operated path will be no worse on usefulness within this small sample.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Any of these can be wrong. Publishing them beforehand is what makes being wrong visible.&lt;/p&gt;
&lt;h2 id=&quot;separate-metrics-no-composite-score&quot;&gt;Separate metrics, no composite score&lt;/h2&gt;
&lt;p&gt;A single number would conceal the tradeoffs we are trying to see, so every metric is reported on its own.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Answer correctness.&lt;/strong&gt; Per material factual claim: 2 supported and accurate, 1 partly supported, imprecise, or missing a necessary qualification, 0 contradicted or unsupported. If a qualified evaluator or primary-source adjudication is unavailable, correctness stays unavailable rather than being guessed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Citation correctness.&lt;/strong&gt; Per claim-citation pair: 2 the cited source directly supports the claim, 1 the source is relevant but support is partial or indirect, 0 the source does not support the claim or cannot be inspected.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Citation completeness.&lt;/strong&gt; Cited material factual claims divided by material factual claims. Opinions, transitions and interpretation that is clearly marked as interpretation do not require citations.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Question coverage.&lt;/strong&gt; Per pre-listed required element: 2 fully answered, 1 partly answered, 0 missing.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Source quality.&lt;/strong&gt; Per cited source: 3 primary or official source for the claim, 2 strong independent secondary source, 1 weak, derivative, undated or unclear, 0 irrelevant or unusable. We also record source-domain diversity, without assuming that more domains mean better evidence.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Freshness.&lt;/strong&gt; Where a question defines a date window: 2 source and claim meet it, 1 the date is unclear and no contradiction was found, 0 the evidence is outside the window or stale.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Uncertainty handling.&lt;/strong&gt; Whether the answer names material gaps and conflicts and scopes its conclusion, instead of turning incomplete evidence into certainty.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Latency.&lt;/strong&gt; Client-observed wall-clock milliseconds for every attempt, and first-event latency where the interface exposes it. Failures stay in the record. One run is one observation, not typical latency.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cost.&lt;/strong&gt; Authoritative usage and billing evidence only. For the application-operated path, the harness records search calls and model input and output tokens, and those records get combined with the providers’ published rates on the run date. The managed path is the awkward one, because the API does not return usage. Tabstack request telemetry recorded one action for the fast-mode calls made while validating the harness, so the cost of a managed call is recorded action count multiplied by the published action rate, and the customer-visible receipt is a console balance change. Published pricing allows a Research call to spend a variable number of actions. One action on a validation call establishes nothing about what the next question will cost.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Terminal status.&lt;/strong&gt; Every run gets exactly one: complete, partial, unanswered, HTTP failure, transport failure, task failure, or timeout. Provider failures, empty answers and malformed citations stay in the denominator.&lt;/p&gt;
&lt;p&gt;The committed rubric and results template define the full field list.&lt;/p&gt;
&lt;h2 id=&quot;scoring-happens-before-anyone-knows-which-system-wrote-what&quot;&gt;Scoring happens before anyone knows which system wrote what&lt;/h2&gt;
&lt;p&gt;The harness copies each final answer under a random identifier and strips provider names from the wrapper metadata. Evaluators score answer quality before they see system identity, event logs, latency or cost. The engineer who built the systems will not be the sole scorer.&lt;/p&gt;
&lt;p&gt;The blind is incomplete, and we are not going to pretend otherwise. The two systems can produce visibly different answer structures, and an evaluator can infer identity from formatting with every label removed. That limitation ships alongside the results.&lt;/p&gt;
&lt;h2 id=&quot;failures-stay-in-the-denominator&quot;&gt;Failures stay in the denominator&lt;/h2&gt;
&lt;p&gt;The protocol does not allow a cleaner result set to be manufactured after the run.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Do not stop because one system appears better.&lt;/li&gt;
&lt;li&gt;Stop the whole study for security, runaway spend, a provider outage that breaks comparability, corrupted logging, or a protocol violation.&lt;/li&gt;
&lt;li&gt;Exclude a run only when the harness fails before the provider receives the request.&lt;/li&gt;
&lt;li&gt;Keep provider errors, timeouts, empty answers, and malformed citations as results.&lt;/li&gt;
&lt;li&gt;Never rerun only the system that appears to lose.&lt;/li&gt;
&lt;li&gt;If the scoring rules change after outputs are visible, create protocol v2 and rerun the full set.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Operational failure is part of the architecture being evaluated. A path that times out is telling you something about itself, and deleting the row throws that away.&lt;/p&gt;
&lt;h2 id=&quot;the-pilot-tested-the-harness-not-the-systems&quot;&gt;The pilot tested the harness, not the systems&lt;/h2&gt;
&lt;p&gt;We ran one source-discovery question through both configured paths to check that the harness could send the frozen input, run each complete system, preserve answers and sources, record timing and usage fields, mark the rows &lt;code&gt;pilot_only=true&lt;/code&gt;, and package answer text for blind review.&lt;/p&gt;
&lt;p&gt;Both paths reached a terminal complete state. Their outputs, logs and operational records are committed under evals/runs/20260915T203139-pilot.&lt;/p&gt;
&lt;p&gt;Nobody has scored those answers. They are not ranked, and the rows will not enter the full evaluation denominator. The only thing the pilot concludes is that the harness runs end to end with both frozen configurations.&lt;/p&gt;
&lt;h2 id=&quot;what-this-first-evaluation-will-not-establish&quot;&gt;What this first evaluation will not establish&lt;/h2&gt;
&lt;p&gt;The planned 12-question run is exploratory. One run per system per question cannot establish universal superiority, stable provider performance, statistical significance, typical latency or cost, reliability across workloads, or which individual component caused an observed difference.&lt;/p&gt;
&lt;p&gt;It can show the outputs, costs, timing, failures and evaluation scores that two disclosed configurations produced on a frozen question set. That is enough to identify tradeoffs and design a stronger follow-up. It is not enough to declare a category winner.&lt;/p&gt;
&lt;h2 id=&quot;most-teams-should-do-something-smaller-than-this&quot;&gt;Most teams should do something smaller than this&lt;/h2&gt;
&lt;p&gt;If you only need to know whether one workflow meets a product requirement, run an acceptance test instead. Define the required claims, the citation behaviour, the latency ceiling, the cost ceiling and the failure handling, then test that one workflow against them. It answers the question you actually have, and it takes a fraction of the setup.&lt;/p&gt;
&lt;p&gt;If you need causality, run a component-level experiment. Hold the search provider, the fetched pages, the prompt and the model constant, then change one component at a time.&lt;/p&gt;
&lt;p&gt;If you need estimates of variability or stable provider behaviour, you need repeated runs over a larger stratified set, with the sample, the analysis and the stopping rules registered before execution.&lt;/p&gt;
&lt;p&gt;And when the deciding factor is ownership, privacy, deployment boundary or maintainability, a benchmark is the wrong instrument. Do the qualitative architecture review and skip the scores.&lt;/p&gt;
&lt;h2 id=&quot;data-flow&quot;&gt;Data flow&lt;/h2&gt;
&lt;p&gt;Both paths send a public-web question and retrieved content through external services in the frozen configurations. The application-operated path sends data to its chosen search and model providers. The managed path sends the question and retrieved page content to Tabstack, and third-party models may process it under Tabstack’s contracts. Review every provider’s data terms before using confidential inputs. For Tabstack, start with the Privacy Notice.&lt;/p&gt;
&lt;p&gt;Never trained on. Private by default.&lt;/p&gt;
&lt;h2 id=&quot;submit-a-workflow-or-evaluation-question&quot;&gt;Submit a workflow or evaluation question&lt;/h2&gt;
&lt;p&gt;The evaluation is stronger if its questions reflect jobs people are actually trying to ship. Propose a current public-web workflow, a question, and the answer elements that would make the result useful.&lt;/p&gt;
&lt;p&gt;Open the evaluation directory and propose a question&lt;/p&gt;
&lt;p&gt;Related reading and implementation:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Search gives your model sources. What turns them into a cited answer?&lt;/li&gt;
&lt;li&gt;Build a cited live-web answer in Python with one Research call&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.tabstack.ai/guides/research&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Research guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.tabstack.ai/api/resources/agent/methods/research&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Research API reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Public evaluation protocol&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Build a cited live-web answer in Python with one Research call</title><link>https://tabstack.ai/blog/build-cited-live-web-answer-python</link><guid isPermaLink="true">https://tabstack.ai/blog/build-cited-live-web-answer-python</guid><description>Clone the example, set one environment variable, and run one command to send a current question to Tabstack Research. The call streams progress, then writes a Markdown report and a machine-readable list of cited pages.</description><pubDate>Wed, 16 Sep 2026 14:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Clone the example, set one environment variable, and run one command to send a current question to Tabstack Research. The call streams progress, then writes a Markdown report and a machine-readable list of cited pages. The committed sample shows the full path completed in a recorded Python environment, with every artifact included so you can reproduce it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Who this is for:&lt;/strong&gt; People running their own model inside a product, assistant, or internal workflow that needs a current, cited answer from the public web.&lt;/p&gt;
&lt;h2 id=&quot;run-the-example&quot;&gt;Run the example&lt;/h2&gt;
&lt;p&gt;Start with the complete implementation in the &lt;a href=&quot;https://github.com/Mozilla-Ocho/tabstack-cited-research-python&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;cited-research example repo&lt;/a&gt; rather than assembling a partial snippet:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;shell&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; clone&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; https://github.com/Mozilla-Ocho/tabstack-cited-research-python.git&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;cd&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; tabstack-cited-research-python&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;uv&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; sync&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; --frozen&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; TABSTACK_API_KEY&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;uv&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; run&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; cited-research&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  --query&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;What are the current ways to add web search to an Ollama-based application, and what output does each approach return?&quot;&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  --mode&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; fast&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  --nocache&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  --output&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; artifacts/sample-run&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The key comes from the environment. The CLI has no –api-key flag and does not write the key to its output files.&lt;/p&gt;
&lt;p&gt;The repository pins tabstack==2.8.5 in uv.lock. The sample was run with Python 3.12.13 and uv 0.11.6 on macOS Darwin 25.6.0, arm64. The project declares Python 3.9 as its floor, but this sample was not executed on Python 3.9.&lt;/p&gt;
&lt;h2 id=&quot;what-the-program-does&quot;&gt;What the program does&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://tabstack.ai/blog/build-cited-live-web-answer-python&quot;&gt;View the diagram in the original post.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Your application sends the question to /research. The endpoint returns a Server-Sent Events stream. The program displays selected progress events, stops on completion, and saves the report and cited-page metadata as separate files.&lt;/p&gt;
&lt;p&gt;The application remains responsible for reviewing the result and deciding how to use it. A completed Research call gives you a report and a source record to review; how you use the answer stays in your application.&lt;/p&gt;
&lt;h2 id=&quot;the-python-call&quot;&gt;The Python call&lt;/h2&gt;
&lt;p&gt;The repository contains the tested CLI, sanitization, file persistence, and exit-code handling. This standalone version shows the same core API path and can be run as a script after installing tabstack==2.8.5 and setting TABSTACK_API_KEY:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; json&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; os&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; pathlib &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; Path&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; tabstack&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; tabstack &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; Tabstack&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;QUERY&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; (&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;    &quot;What are the current ways to add web search to an Ollama-based &quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;    &quot;application, and what output does each approach return?&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;OUTPUT&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; Path(&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;artifacts/my-run&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;PROGRESS_EVENTS&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;    &quot;start&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;    &quot;planning:start&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;    &quot;planning:end&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;    &quot;iteration:start&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;    &quot;iteration:end&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;    &quot;searching:start&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;    &quot;searching:end&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;    &quot;writing:start&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;    &quot;writing:end&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; not&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; os.environ.get(&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;TABSTACK_API_KEY&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    raise&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; SystemExit&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;TABSTACK_API_KEY is not set&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;OUTPUT&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.mkdir(&lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;parents&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;True&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;exist_ok&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;True&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;final &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; None&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;with&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; Tabstack() &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;as&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; client:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    stream &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; client.agent.research(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E36209&quot;&gt;        query&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;QUERY&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E36209&quot;&gt;        mode&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;fast&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E36209&quot;&gt;        nocache&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;True&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    )&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    for&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; event &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; stream:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; event.event &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; PROGRESS_EVENTS&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;            print&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(event.event, event.data.message)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; event.event &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;error&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;            raise&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; RuntimeError&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(event.data.error.message)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; event.event &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;complete&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;            final &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; event&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;            break&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; final &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;is&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; None&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    raise&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; RuntimeError&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;Research stream ended without a complete event&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;OUTPUT&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; /&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;report.md&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;).write_text(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    final.data.report.rstrip() &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;\n&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E36209&quot;&gt;    encoding&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;utf-8&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;sources &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; []&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;for&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; source &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; final.data.metadata.cited_pages &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;or&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; []:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    sources.append(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;        {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;            &quot;id&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: source.id,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;            &quot;url&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: source.url,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;            &quot;title&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: source.title,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;            &quot;claims&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: source.claims,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;            &quot;source_queries&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: source.source_queries,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;            &quot;relevance&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: source.relevance,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;            &quot;reliability&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: source.reliability,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    )&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;OUTPUT&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; /&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;sources.json&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;).write_text(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    json.dumps(sources, &lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;indent&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;2&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;ensure_ascii&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;False&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;\n&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E36209&quot;&gt;    encoding&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;utf-8&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;print&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;f&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;report -&gt; &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;{OUTPUT&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; /&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &apos;report.md&apos;&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;print&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;f&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;sources (&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;{len&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(sources)&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;) -&gt; &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;{OUTPUT&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; /&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &apos;sources.json&apos;&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;print&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;f&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;tabstack SDK -&gt; &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;tabstack.&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;__version__}&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Python exposes SDK fields in snake case, including cited_pages, source_queries, and total_pages_analyzed. The JSON wire format uses camel case.&lt;/p&gt;
&lt;h2 id=&quot;how-the-stream-terminates&quot;&gt;How the stream terminates&lt;/h2&gt;
&lt;p&gt;/research always returns a Server-Sent Events stream. The sample run received these events, in this order, once each:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code class=&quot;language-plaintext&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;start&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;planning:start&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;planning:end&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;iteration:start&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;searching:start&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;searching:end&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;iteration:end&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;writing:start&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;writing:end&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;complete&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;complete is the successful terminal event. Research does not send a later done event. A task-level failure arrives as an error event inside the stream. That is why the loop must handle both error and complete. HTTP rejections such as 401, 400, or 429 happen before the stream opens and surface as SDK exceptions instead.&lt;/p&gt;
&lt;h2 id=&quot;complete-input&quot;&gt;Complete input&lt;/h2&gt;
&lt;p&gt;The publication run used this question:&lt;/p&gt;
&lt;p&gt;What are the current ways to add web search to an Ollama-based application, and what output does each approach return?&lt;/p&gt;
&lt;p&gt;It used mode=“fast” and nocache=True. The input, exact command, sanitized events, output, and run manifest are committed in &lt;a href=&quot;https://github.com/Mozilla-Ocho/tabstack-cited-research-python/tree/main/artifacts/sample-run&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;artifacts/sample-run&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;complete-output&quot;&gt;Complete output&lt;/h2&gt;
&lt;p&gt;The CLI writes a durable output bundle:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code class=&quot;language-plaintext&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;artifacts/sample-run/&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;├── question.txt&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;├── command.txt&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;├── events.sanitized.jsonl&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;├── report.md&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;├── sources.json&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;├── run-manifest.json&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;├── stdout.txt&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;├── stderr.txt&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;└── terminal.png&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The complete returned report is in report.md. The ordered citation metadata is in sources.json. They remain separate so an application can render the answer while retaining a machine-readable source record.&lt;/p&gt;
&lt;p&gt;The program printed this progress before writing the files:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code class=&quot;language-plaintext&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;start  Starting research&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;planning:start  Planning research strategy&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;planning:end  Planning complete: 6 queries&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;iteration:start  iteration 1/1  Starting iteration 1 of 1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;searching:start  iteration 1  Searching with 6 queries&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;searching:end  iteration 1  8 new urls  Found 8 URLs&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;iteration:end  iteration 1  Iteration 1 complete (fast mode)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;writing:start  Writing report&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;writing:end  Report draft complete&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;complete  report -&gt; artifacts/sample-run/report.md&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;sources (7) -&gt; artifacts/sample-run/sources.json&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;the-run-in-numbers&quot;&gt;The run in numbers&lt;/h2&gt;
&lt;p&gt;The run started at 2026-09-15 19:00:04.556 UTC and completed 20.3 seconds later, with the first progress event arriving after 1.06 seconds. The stream ended on complete and the process exited 0. Tabstack analyzed seven pages, cited seven, and returned a 1,649-character report with inline numbered citations.&lt;/p&gt;
&lt;p&gt;Request telemetry recorded one Research action for the whole call; at the published fast-mode rate that is 250 credits for a planned, multi-source, cited answer. The API response itself carries no usage field, so treat that as the implied cost.&lt;/p&gt;
&lt;p&gt;One run is one data point. Use it to see the shape of a fast-mode result, not to size latency or cost for your workload.&lt;/p&gt;
&lt;h2 id=&quot;what-fast-mode-returns-and-how-to-read-it&quot;&gt;What fast mode returns, and how to read it&lt;/h2&gt;
&lt;p&gt;Fast mode is built for speed: one iteration, no judge pass, sources returned as they were found. That shows up in the output in four specific ways, and each has a straightforward handling pattern.&lt;/p&gt;
&lt;h3 id=&quot;cited-pages-are-counted-by-url-not-by-canonical-page&quot;&gt;Cited pages are counted by URL, not by canonical page&lt;/h3&gt;
&lt;p&gt;Three of the seven entries pointed at the same Ollama documentation page under different URLs (http versus https, and a .md suffix). Normalize scheme and suffix before counting distinct sources, or display the report’s inline citation numbers, which already do that work for the reader.&lt;/p&gt;
&lt;h3 id=&quot;claim-lists-arrive-empty-in-fast-mode&quot;&gt;Claim lists arrive empty in fast mode&lt;/h3&gt;
&lt;p&gt;Each cited page carries a claims array, and in this mode the array is []. The report’s inline [n] markers are the claim-to-source link: they map in order to cited_pages. The example keeps the claims field in sources.json because it is part of the SDK contract and deeper modes can populate it.&lt;/p&gt;
&lt;h3 id=&quot;optional-metadata-stays-optional&quot;&gt;Optional metadata stays optional&lt;/h3&gt;
&lt;p&gt;Title was present on every source. Relevance, reliability, and summary were not; those come from the analysis phases that fast mode skips. Read them defensively and your code works unchanged across modes.&lt;/p&gt;
&lt;h3 id=&quot;source-selection-is-broad-by-design&quot;&gt;Source selection is broad by design&lt;/h3&gt;
&lt;p&gt;Two of the seven sources were tutorials about building your own page-reading tool, and the report used them to describe a fourth implementation path. Fast mode returns what the search surfaced; whether that path belongs in the answer is a judgment your application or reviewer makes. We have not scored this answer, so the article presents it as returned output.&lt;/p&gt;
&lt;h2 id=&quot;failure-handling&quot;&gt;Failure handling&lt;/h2&gt;
&lt;p&gt;The repository gives configuration, HTTP, and task failures different exit codes:&lt;/p&gt;
&lt;div class=&quot;table-scroll&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;Observed or tested behavior&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;TABSTACK_API_KEY missing&lt;/td&gt;
&lt;td&gt;Exit 5, message on stderr, no request made&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Invalid key&lt;/td&gt;
&lt;td&gt;HTTP 401 before the stream opened, exit 3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Streamed error event&lt;/td&gt;
&lt;td&gt;Synthetic fixture produced exit 2 and printed the returned message and phase&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stream closed without complete&lt;/td&gt;
&lt;td&gt;Synthetic fixture produced exit 2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Only the invalid-key case sent a failing request to production. The streamed failure cases use synthetic fixtures rather than trying to break production.&lt;/p&gt;
&lt;p&gt;The full implementation also sanitizes its public event log. It retains event names, timestamps, iteration counters, short status messages, and completion counts. It excludes API keys, authorization headers, cookies, full source text, stack traces, environment dumps, and model reasoning.&lt;/p&gt;
&lt;h2 id=&quot;timeouts-retries-and-cleanup&quot;&gt;Timeouts, retries, and cleanup&lt;/h2&gt;
&lt;p&gt;The SDK applies a 600-second timeout to /research streams. The example adds no shorter total timeout, so it does not turn a longer healthy run into an application timeout.&lt;/p&gt;
&lt;p&gt;The SDK retries certain transport failures twice by default, including 408, 409, 429, and server errors. The example adds zero application-level retries. Repeating a request can repeat work and billing, so retry policy should be an explicit product decision.&lt;/p&gt;
&lt;p&gt;The Tabstack client runs inside a context manager. The response closes when the block exits, including when the stream returns an error.&lt;/p&gt;
&lt;p&gt;A clean uv sync –frozen passed 17 tests. Ruff and Pyright also passed with no reported errors. The quality gate ran on Python 3.12.13, not every declared Python version.&lt;/p&gt;
&lt;h2 id=&quot;when-this-approach-is-sufficient&quot;&gt;When this approach is sufficient&lt;/h2&gt;
&lt;p&gt;Use one managed Research call when your application starts with a question and needs a cited answer from live sources without operating the search, retrieval, and synthesis loop itself.&lt;/p&gt;
&lt;p&gt;Use a search interface instead when links and excerpts are the desired output, or when your own model already operates the remaining loop in a way that meets your requirements.&lt;/p&gt;
&lt;p&gt;Build the loop in your application when source selection, retrieval policy, model behavior, or citation assembly is core to your product and you need control over each component.&lt;/p&gt;
&lt;p&gt;Use known-page extraction when you already have the URL and need page text or fields in a defined shape. Do not use a research workflow for work that a simpler output contract already solves.&lt;/p&gt;
&lt;h2 id=&quot;data-flow&quot;&gt;Data flow&lt;/h2&gt;
&lt;p&gt;A Research call sends your question and the pages it retrieves through Tabstack’s pipeline, including a contracted model provider. Nothing you send is used to train a model. Request payloads are kept for 90 days while detailed data collection is on, which is the default for new organizations, and an organization admin can turn it off. The &lt;a href=&quot;https://docs.tabstack.ai/trust/data-handling/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Data Handling page&lt;/a&gt; spells out what each endpoint processes and where.&lt;/p&gt;
&lt;p&gt;Never trained on. Private by default.&lt;/p&gt;
&lt;h2 id=&quot;run-the-python-example&quot;&gt;Run the Python example&lt;/h2&gt;
&lt;p&gt;The repository contains the lockfile, implementation, fixtures, failure evidence, sample output, and evaluation harness:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/Mozilla-Ocho/tabstack-cited-research-python&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Run the cited Research example on GitHub&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Then continue with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://tabstack.ai/blog/search-vs-research&quot;&gt;Search gives your model sources. What turns them into a cited answer?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://tabstack.ai/blog/search-fetch-model-vs-managed-research-evaluation-plan&quot;&gt;Search + fetch + model versus a managed Research call: the evaluation plan&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.tabstack.ai/guides/research&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Research guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.tabstack.ai/api/resources/agent/methods/research&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Research API reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Mozilla-Ocho/tabstack-python&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Python SDK source&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Search gives your model sources. What turns them into a cited answer?</title><link>https://tabstack.ai/blog/search-vs-research</link><guid isPermaLink="true">https://tabstack.ai/blog/search-vs-research</guid><description>A web search API can return useful titles, URLs, and excerpts. A cited answer requires a research loop after search: define the question, select and read sources, identify gaps, reconcile evidence, write the answer, and preserve the claim-to-source connection.</description><pubDate>Mon, 14 Sep 2026 14:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A web search API can return useful titles, URLs, and excerpts. That is enough when your application needs search results or your model can reliably do the remaining work. A cited answer requires a research loop after search: define the question, select and read sources, identify gaps, reconcile evidence, write the answer, and preserve the claim-to-source connection.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Who this is for:&lt;/strong&gt; People running their own model inside a product, assistant, agent, or internal workflow that needs current answers from the public web.&lt;/p&gt;
&lt;p&gt;In this guide, you will learn:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;what search returns and what it leaves to your system;&lt;/li&gt;
&lt;li&gt;the seven stages between a question and a cited answer;&lt;/li&gt;
&lt;li&gt;the difference between search, fetch, extraction, research, and automation;&lt;/li&gt;
&lt;li&gt;when search alone is sufficient;&lt;/li&gt;
&lt;li&gt;what a research-ready output should contain.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;search-is-access-a-cited-answer-is-a-pipeline&quot;&gt;Search is access. A cited answer is a pipeline.&lt;/h2&gt;
&lt;p&gt;Search and research solve different parts of the same job. A search endpoint usually begins with a query and returns candidates: titles, URLs, snippets, or excerpts. Those results help a person or program decide what to read next. For example, the current Ollama Web Search API documents a response containing title, url, and a relevant content snippet for each result. Brave’s Web Search API similarly returns result objects with a title, URL, description, and optional extra snippets.&lt;/p&gt;
&lt;p&gt;That output may be exactly right. A link finder, source browser, retrieval component, or model with a dependable tool loop can use it directly. But if the application needs an answer it can show, store, or act on, somebody still owns the work after search:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;decide what the question actually requires;&lt;/li&gt;
&lt;li&gt;choose which results are worth reading;&lt;/li&gt;
&lt;li&gt;retrieve the relevant pages;&lt;/li&gt;
&lt;li&gt;extract the useful evidence;&lt;/li&gt;
&lt;li&gt;check whether the evidence is sufficient;&lt;/li&gt;
&lt;li&gt;resolve conflicts and qualify uncertainty;&lt;/li&gt;
&lt;li&gt;write the answer and attach the sources.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;A cited answer is therefore not a formatting option on a list of search results. It is the output of a research system.&lt;/p&gt;
&lt;h2 id=&quot;the-seven-stage-research-loop&quot;&gt;The seven-stage research loop&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://tabstack.ai/blog/search-vs-research&quot;&gt;View the diagram in the original post.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The arrows matter more than the boxes. Research is not always one pass from left to right. If the evidence does not answer part of the question, the system needs another query, another source, or a narrower claim. That loop is what separates research from “summarize the first result.”&lt;/p&gt;
&lt;h3 id=&quot;1-define-the-question-and-success-criteria&quot;&gt;1. Define the question and success criteria&lt;/h3&gt;
&lt;p&gt;The system first turns the user’s request into an answerable research objective. Consider this question:&lt;/p&gt;
&lt;p&gt;What are the main approaches to browser automation for AI agents, and how do they differ?&lt;/p&gt;
&lt;p&gt;A useful answer needs more than definitions. It should establish the comparison dimensions, such as browser control method, deployment model, supported interaction, and who owns the infrastructure. It may also need to distinguish open-source libraries, hosted browser infrastructure, and higher-level task APIs. Without a clear objective, a system can collect relevant pages and still produce an incomplete answer.&lt;/p&gt;
&lt;h3 id=&quot;2-discover-candidate-sources&quot;&gt;2. Discover candidate sources&lt;/h3&gt;
&lt;p&gt;Search finds pages that may contain evidence. One broad query is rarely enough for a multi-part question, so the system may create narrower searches for each comparison dimension. Discovery should preserve where each candidate came from and why it may be relevant. At this stage, a search result is a lead, not yet evidence for a final claim.&lt;/p&gt;
&lt;h3 id=&quot;3-retrieve-and-read-the-pages&quot;&gt;3. Retrieve and read the pages&lt;/h3&gt;
&lt;p&gt;Snippets are useful for triage, but they are not a substitute for the source page. They can omit qualifications, dates, tables, footnotes, or the paragraph that changes the meaning of a claim. The system therefore fetches the selected pages and converts them into content it can inspect. Some pages can be read through a direct HTTP request. Others require rendering before the meaningful content is available. Failed, blocked, truncated, or outdated pages need to be recorded rather than silently treated as supporting evidence.&lt;/p&gt;
&lt;h3 id=&quot;4-extract-claims-with-their-context&quot;&gt;4. Extract claims with their context&lt;/h3&gt;
&lt;p&gt;Reading a page is not the same as identifying the evidence that answers the question. The system must isolate the relevant statement, retain its source URL, and preserve enough context to avoid turning a qualified claim into an absolute one. For a comparison, this may mean extracting:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the specific capability being documented;&lt;/li&gt;
&lt;li&gt;the conditions or plan on which it is available;&lt;/li&gt;
&lt;li&gt;the publication or update date;&lt;/li&gt;
&lt;li&gt;the exact source page;&lt;/li&gt;
&lt;li&gt;the claim the source supports.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The goal is a claim-to-source record, not a pile of page text.&lt;/p&gt;
&lt;h3 id=&quot;5-evaluate-coverage-source-quality-and-gaps&quot;&gt;5. Evaluate coverage, source quality, and gaps&lt;/h3&gt;
&lt;p&gt;The system checks the collected evidence against the original question. Which parts are answered? Which are partial? Which have no useful source yet?&lt;/p&gt;
&lt;p&gt;This stage should also ask whether the sources are appropriate for the claim. Official documentation may be the best source for current API behavior. A maintainer issue may be better evidence for a known failure mode. A third-party comparison can provide a useful lead, but it should not quietly become proof of a competitor’s current limits.&lt;/p&gt;
&lt;p&gt;If a material gap remains, the loop returns to discovery with a more specific query.&lt;/p&gt;
&lt;h3 id=&quot;6-reconcile-conflicts-and-qualify-uncertainty&quot;&gt;6. Reconcile conflicts and qualify uncertainty&lt;/h3&gt;
&lt;p&gt;Sources disagree for ordinary reasons: they cover different dates, product tiers, regions, definitions, or versions. The system should not choose whichever sentence is easiest to quote. Reconciliation means comparing the scope of each claim, preferring the most direct and current evidence, and stating uncertainty when the sources do not support a single conclusion. When the evidence remains incomplete, the answer should say so.&lt;/p&gt;
&lt;h3 id=&quot;7-synthesize-the-answer-and-preserve-citations&quot;&gt;7. Synthesize the answer and preserve citations&lt;/h3&gt;
&lt;p&gt;Only after the evidence is collected and evaluated should the system write the final answer. The result should separate what the sources establish from the system’s interpretation. Citations should let a reader inspect the evidence behind a material claim. A bibliography at the bottom is useful, but it is weaker than a record that connects individual claims to the pages used to support them.&lt;/p&gt;
&lt;h2 id=&quot;what-goes-in-and-what-should-come-out&quot;&gt;What goes in and what should come out?&lt;/h2&gt;
&lt;p&gt;Here is a complete input for a research system:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;query&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;What are the main approaches to browser automation for AI agents, and how do they differ?&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;mode&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;fast&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A useful research output is not just prose. It should contain the answer and enough source metadata to inspect how the answer was built. Tabstack’s current /research response represents that contract with a report plus citation metadata:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;report&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;# Browser automation for AI agents&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;\n\n&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;The main approaches differ in who controls the browser, where it runs, and how much orchestration the application owns. [...]&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;metadata&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    &quot;citedPages&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;      {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;        &quot;id&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;pg_a1b2c3&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;        &quot;url&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;https://source.example/official-documentation&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;        &quot;title&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;Official documentation&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;        &quot;claims&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;          &quot;A specific statement used in the report.&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;        ],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;        &quot;sourceQueries&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;          &quot;browser automation approaches for AI agents&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;        ]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;      }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    ],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    &quot;totalPagesAnalyzed&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;12&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;message&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;Research complete&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;timestamp&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;1789420248496&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Example qualification:&lt;/strong&gt; This response is illustrative and combines the documented /research response fields with placeholder content. It is not the output of a live research run, and the page count is not a benchmark or product claim.&lt;/p&gt;
&lt;p&gt;The output contract matters because different consumers need different artifacts.&lt;/p&gt;
&lt;h2 id=&quot;output-taxonomy-choose-the-artifact-your-application-needs&quot;&gt;Output taxonomy: choose the artifact your application needs&lt;/h2&gt;
&lt;div class=&quot;table-scroll&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Output&lt;/th&gt;
&lt;th&gt;What the application receives&lt;/th&gt;
&lt;th&gt;What still needs to happen&lt;/th&gt;
&lt;th&gt;Good fit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Search results&lt;/td&gt;
&lt;td&gt;Ranked titles, URLs, and excerpts&lt;/td&gt;
&lt;td&gt;Select, read, compare, synthesize, and cite&lt;/td&gt;
&lt;td&gt;Discovery, link finding, model-operated research&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clean page text&lt;/td&gt;
&lt;td&gt;Readable content from one known URL&lt;/td&gt;
&lt;td&gt;Interpret the page and combine it with other sources&lt;/td&gt;
&lt;td&gt;RAG ingestion, reading a page, downstream analysis&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Matching JSON&lt;/td&gt;
&lt;td&gt;Fields returned in a defined schema&lt;/td&gt;
&lt;td&gt;Validate business rules and decide how to use the record&lt;/td&gt;
&lt;td&gt;Known-page extraction, enrichment, pipelines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cited answer&lt;/td&gt;
&lt;td&gt;Synthesized answer plus source references&lt;/td&gt;
&lt;td&gt;Review evidence and apply domain judgment&lt;/td&gt;
&lt;td&gt;Current questions that span multiple sources&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Completed public web task&lt;/td&gt;
&lt;td&gt;Progress and the result of browser interaction&lt;/td&gt;
&lt;td&gt;Confirm the result and handle downstream side effects&lt;/td&gt;
&lt;td&gt;Navigation, clicks, forms, and multi-step public workflows&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;These outputs are not quality levels. They are different contracts. Returning a cited report when an application needs ten links adds unnecessary work. Returning ten links when the application needs an answer transfers the research loop into the model or application.&lt;/p&gt;
&lt;h2 id=&quot;what-is-the-difference-between-search-fetch-extraction-research-and-automation&quot;&gt;What is the difference between search, fetch, extraction, research, and automation?&lt;/h2&gt;
&lt;h3 id=&quot;what-is-web-search&quot;&gt;What is web search?&lt;/h3&gt;
&lt;p&gt;Web search returns candidate sources relevant to a query. A typical result includes a title, URL, and excerpt. Search is sufficient when the list of sources is the desired output or when another component reliably owns the rest of the research loop.&lt;/p&gt;
&lt;h3 id=&quot;what-is-fetch&quot;&gt;What is fetch?&lt;/h3&gt;
&lt;p&gt;Fetch retrieves the content at a known URL. It answers “read this page,” not “answer this open question.” Fetching may be direct or may require rendering, but it does not by itself establish which other pages are needed or reconcile evidence across them.&lt;/p&gt;
&lt;h3 id=&quot;what-is-extraction&quot;&gt;What is extraction?&lt;/h3&gt;
&lt;p&gt;Extraction turns a known page into a cleaner or more structured representation. That may be readable Markdown or JSON matching a supplied schema. Extraction is the right abstraction when the source is known and the application knows what fields or content it needs.&lt;/p&gt;
&lt;h3 id=&quot;what-is-research&quot;&gt;What is research?&lt;/h3&gt;
&lt;p&gt;Research begins with a question and produces a synthesized answer from one or more sources. It includes source discovery, reading, gap detection, reconciliation, and citation. A research API is useful when the application needs the finished answer rather than material for its model to process.&lt;/p&gt;
&lt;h3 id=&quot;what-is-automation&quot;&gt;What is automation?&lt;/h3&gt;
&lt;p&gt;Automation completes a task that requires interaction with a website. It applies when the system must navigate, click, fill forms, or move through several steps before the requested result exists. Reading or researching a public page does not automatically require browser automation.&lt;/p&gt;
&lt;h2 id=&quot;when-is-search-alone-enough&quot;&gt;When is search alone enough?&lt;/h2&gt;
&lt;p&gt;Search alone is often the simpler and better choice when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the user wants links, not an answer;&lt;/li&gt;
&lt;li&gt;your model already performs the tool loop reliably;&lt;/li&gt;
&lt;li&gt;the question is simple and one source is enough;&lt;/li&gt;
&lt;li&gt;you need full control over ranking, retrieval, or synthesis;&lt;/li&gt;
&lt;li&gt;the research loop is part of your product’s core differentiation;&lt;/li&gt;
&lt;li&gt;you are building a source-discovery interface rather than an answer feature.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A managed research loop is more useful when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the application must return a current, cited answer;&lt;/li&gt;
&lt;li&gt;the question spans several sources or sub-questions;&lt;/li&gt;
&lt;li&gt;incomplete coverage needs to trigger another search;&lt;/li&gt;
&lt;li&gt;citations must remain connected to specific claims;&lt;/li&gt;
&lt;li&gt;you do not want the model to spend its context and tool calls operating the loop;&lt;/li&gt;
&lt;li&gt;your team would otherwise maintain search, retrieval, synthesis, and citation orchestration.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Neither path removes the need to evaluate the result. A citation makes a claim inspectable. It does not guarantee that the source is authoritative, that every claim is supported, or that the answer is complete.&lt;/p&gt;
&lt;h2 id=&quot;the-architecture-choice-is-about-ownership&quot;&gt;The architecture choice is about ownership&lt;/h2&gt;
&lt;div class=&quot;table-scroll&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Application receives&lt;/th&gt;
&lt;th&gt;Who owns source selection and reading?&lt;/th&gt;
&lt;th&gt;Who owns gap detection and reconciliation?&lt;/th&gt;
&lt;th&gt;Who owns citation assembly?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Search API&lt;/td&gt;
&lt;td&gt;Results and excerpts&lt;/td&gt;
&lt;td&gt;Your model or application&lt;/td&gt;
&lt;td&gt;Your model or application&lt;/td&gt;
&lt;td&gt;Your model or application&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Search + fetch tools&lt;/td&gt;
&lt;td&gt;Results plus page content&lt;/td&gt;
&lt;td&gt;Your model or application&lt;/td&gt;
&lt;td&gt;Your model or application&lt;/td&gt;
&lt;td&gt;Your model or application&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DIY research loop&lt;/td&gt;
&lt;td&gt;Whatever contract you build&lt;/td&gt;
&lt;td&gt;Your team’s orchestration&lt;/td&gt;
&lt;td&gt;Your team’s orchestration&lt;/td&gt;
&lt;td&gt;Your team’s orchestration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Managed Research API&lt;/td&gt;
&lt;td&gt;Synthesized answer and source metadata&lt;/td&gt;
&lt;td&gt;The API inside the call&lt;/td&gt;
&lt;td&gt;The API inside the call&lt;/td&gt;
&lt;td&gt;The API inside the call&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;A managed API can reduce the orchestration in your codebase, but it does not transfer responsibility for the result. Your team still owns question design, acceptance criteria, domain-specific review, and how the application uses the answer.&lt;/p&gt;
&lt;p&gt;The decision is not “build versus buy” in the abstract. It is where this loop should live, how much control you need, and which output your application can consume safely.&lt;/p&gt;
&lt;h2 id=&quot;a-concrete-managed-research-call&quot;&gt;A concrete managed Research call&lt;/h2&gt;
&lt;p&gt;Tabstack is one implementation of the managed path. The application sends a question to /research; the endpoint streams progress with Server-Sent Events, then emits a complete event containing the report and metadata for cited pages.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; Tabstack &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;@tabstack/sdk&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; client&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; Tabstack&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; stream&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; client.agent.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;research&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  query: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;What are the main approaches to browser automation for AI agents, and how do they differ?&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  mode: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;fast&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;});&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;for&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; event&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; of&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; stream) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;  if&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; (event.event &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;===&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;error&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    throw&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; Error&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(event.data.error.message);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;  if&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; (event.event &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;===&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;complete&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    console.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;log&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(event.data.report);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    for&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; source&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; of&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; event.data.metadata.citedPages &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;??&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; []) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;      console.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;log&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;`${&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;source&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;title&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; ??&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;Untitled source&quot;}: ${&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;source&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;url&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;}`&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Environment:&lt;/strong&gt; Node.js 20 or later; @tabstack/sdk 2.8.6; TABSTACK_API_KEY set in the environment.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Last technically validated:&lt;/strong&gt; September 14, 2026, against the current TypeScript SDK source and &lt;a href=&quot;https://docs.tabstack.ai/guides/research&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;published Research guide&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Validation boundary:&lt;/strong&gt; The snippet’s types and API surface were checked against &lt;a href=&quot;https://github.com/Mozilla-Ocho/tabstack-typescript/commit/366182e084a7d5fd372cde47e8bc2e96e63f5e16&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;SDK commit 366182e084a7d5fd372cde47e8bc2e96e63f5e16&lt;/a&gt;. A live fast-mode request was run on September 14, 2026 against the production API; the complete event matched the shape shown above, including the cited page fields.&lt;/p&gt;
&lt;h2 id=&quot;failure-modes-a-cited-answer-system-must-expose&quot;&gt;Failure modes a cited-answer system must expose&lt;/h2&gt;
&lt;h3 id=&quot;a-relevant-result-is-not-necessarily-a-usable-source&quot;&gt;A relevant result is not necessarily a usable source&lt;/h3&gt;
&lt;p&gt;A search result may point to an outdated, derivative, blocked, or incomplete page. Record source failures and continue rather than treating every candidate as evidence.&lt;/p&gt;
&lt;h3 id=&quot;snippets-can-remove-the-qualification-that-matters&quot;&gt;Snippets can remove the qualification that matters&lt;/h3&gt;
&lt;p&gt;A snippet may mention a price, limit, feature, or policy without its date or plan. Read the source before using the statement in an answer.&lt;/p&gt;
&lt;h3 id=&quot;more-sources-do-not-automatically-improve-the-answer&quot;&gt;More sources do not automatically improve the answer&lt;/h3&gt;
&lt;p&gt;Ten pages repeating the same secondary claim do not provide ten independent confirmations. Source authority, independence, recency, and directness matter.&lt;/p&gt;
&lt;h3 id=&quot;citations-can-be-present-but-poorly-matched&quot;&gt;Citations can be present but poorly matched&lt;/h3&gt;
&lt;p&gt;A link beside a paragraph does not prove that it supports every sentence in the paragraph. Preserve the specific claims associated with each source and test citation correctness separately from citation presence.&lt;/p&gt;
&lt;h3 id=&quot;the-evidence-can-remain-incomplete&quot;&gt;The evidence can remain incomplete&lt;/h3&gt;
&lt;p&gt;Some questions have no public answer. Others have conflicting or rapidly changing evidence. The system needs an explicit partial or unanswered state rather than filling gaps with plausible prose.&lt;/p&gt;
&lt;h3 id=&quot;the-stream-can-fail-before-completion&quot;&gt;The stream can fail before completion&lt;/h3&gt;
&lt;p&gt;For Tabstack /research, HTTP-level failures can occur before the stream opens, while task-level failures arrive as error events inside the stream. A consumer that only listens for completeness can mistake a failed run for an empty answer.&lt;/p&gt;
&lt;h2 id=&quot;trust-and-data-flow&quot;&gt;Trust and data flow&lt;/h2&gt;
&lt;p&gt;A managed research call sends the question and relevant processing data outside your application. Treat that as an architecture decision, not a footnote. Review which providers process inputs and outputs, what telemetry is retained, whether history can be stored, and how personal or confidential information should be handled.&lt;/p&gt;
&lt;p&gt;For Tabstack, the approved summary is: Never trained on. Private by default. Built by Mozilla.&lt;/p&gt;
&lt;p&gt;The current Tabstack Privacy Notice explains that Mozilla operates the service, that inputs and outputs may be processed through third-party models for applicable services, and that users should be careful about placing confidential or sensitive information in inputs. Review the notice for the full data flow rather than interpreting the summary as a promise that nothing is processed or retained.&lt;/p&gt;
&lt;h2 id=&quot;what-should-you-evaluate-before-choosing-a-research-path&quot;&gt;What should you evaluate before choosing a research path?&lt;/h2&gt;
&lt;p&gt;Use one real question from your product, not a generic demo. Then inspect:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Answer correctness: Are the material claims accurate?&lt;/li&gt;
&lt;li&gt;Citation correctness: Does each cited page support the associated claim?&lt;/li&gt;
&lt;li&gt;Citation completeness: Are important factual claims cited?&lt;/li&gt;
&lt;li&gt;Source quality: Are the sources direct, current, and appropriate?&lt;/li&gt;
&lt;li&gt;Coverage: Did the answer address every part of the question?&lt;/li&gt;
&lt;li&gt;Failure behavior: Does the system expose missing evidence, blocked pages, and task errors?&lt;/li&gt;
&lt;li&gt;Operational fit: Can your application handle streaming, retries, latency, and cost for the job?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This is the practical difference between adding search and shipping a current-answer feature. The first gives your system access to sources. The second requires a research loop with an output contract, evidence handling, and failure states.&lt;/p&gt;
&lt;h2 id=&quot;evaluate-a-real-research-question&quot;&gt;Evaluate a real Research question&lt;/h2&gt;
&lt;p&gt;Choose one current question your model or application must answer today. Run it through your existing search-and-fetch path and through a managed Research call. Compare the outputs at the claim level, including missing claims and failed sources, rather than choosing the answer that sounds better.&lt;/p&gt;
&lt;p&gt;Make a Tabstack Research call or &lt;a href=&quot;https://docs.tabstack.ai/guides/research&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;review the Research guide&lt;/a&gt; before you design the evaluation.&lt;/p&gt;
&lt;h2 id=&quot;related-implementation-and-proof-pieces&quot;&gt;Related implementation and proof pieces&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://tabstack.ai/blog/build-cited-live-web-answer-python&quot;&gt;Build a cited live-web answer in Python with one Research call&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://tabstack.ai/blog/search-fetch-model-vs-managed-research-evaluation-plan&quot;&gt;Search + fetch + model versus a managed Research call: the evaluation plan&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.tabstack.ai/guides/research&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Tabstack Research guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.tabstack.ai/api/resources/agent/methods/research&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Research API reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Mozilla-Ocho/tabstack-typescript&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;TypeScript SDK repository&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Give a LlamaIndex.TS agent live web access</title><link>https://tabstack.ai/blog/llamaindex-ts-web-access-tabstack</link><guid isPermaLink="true">https://tabstack.ai/blog/llamaindex-ts-web-access-tabstack</guid><description>Install @tabstack/llamaindex, pass the tools to an agent, and let it read pages, pull typed fields, and research questions across sources on demand.</description><pubDate>Thu, 06 Aug 2026 05:00:00 GMT</pubDate><content:encoded>&lt;p&gt;LlamaIndex is built around grounding answers in real data. Usually that data is yours: documents you have indexed, embedded, and made searchable.&lt;/p&gt;
&lt;p&gt;The awkward half is everything that is not yours and not static. Competitor pricing. A vendor’s changelog. What was published this morning. You cannot index your way to that, because by the time you have, it has moved.&lt;/p&gt;
&lt;p&gt;This tutorial adds the live half. Five tools that read the web on demand, sitting alongside whatever you have already indexed. My honest view is that most RAG systems need less of this than they think and more of it than they have.&lt;/p&gt;
&lt;h2 id=&quot;what-you-need&quot;&gt;What you need&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;A Node project (the package ships dual ESM and CJS builds)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;llamaindex&lt;/code&gt; 0.12.x (the peer dependency is &lt;code&gt;^0.12.0&lt;/code&gt;, so it is pinned to the 0.12 line)&lt;/li&gt;
&lt;li&gt;A Tabstack API key from &lt;a href=&quot;https://console.tabstack.ai&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;console.tabstack.ai&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;npm&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; install&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; @tabstack/llamaindex&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; llamaindex&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; zod&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; @llamaindex/workflow&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; @llamaindex/openai&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; TABSTACK_API_KEY&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;your-key-here&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;llamaindex&lt;/code&gt; and &lt;code&gt;zod&lt;/code&gt; are peer dependencies, so your app’s single instance of each is used. Zod 3 and Zod 4 both work here, since LlamaIndex’s &lt;code&gt;tool()&lt;/code&gt; is Zod-native and accepts either. Nothing to think about on upgrade.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;@llamaindex/workflow&lt;/code&gt; and &lt;code&gt;@llamaindex/openai&lt;/code&gt; are LlamaIndex’s own packages rather than anything to do with Tabstack. The &lt;code&gt;agent()&lt;/code&gt; helper lives in the first and the model provider in the second, and neither is re-exported from the &lt;code&gt;llamaindex&lt;/code&gt; root. Workflow does arrive as a transitive dependency under npm’s hoisted layout, so you can get away with omitting it there, but declare it anyway or the snippet breaks the moment you move to pnpm or Yarn PnP. Swap &lt;code&gt;@llamaindex/openai&lt;/code&gt; for whichever provider you actually use.&lt;/p&gt;
&lt;h2 id=&quot;step-1-an-agent-that-reads-the-web&quot;&gt;Step 1: an agent that reads the web&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; { tabstackTools } &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;@tabstack/llamaindex&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; { agent } &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;@llamaindex/workflow&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; { openai } &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;@llamaindex/openai&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; { Settings } &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;llamaindex&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;Settings.llm &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; openai&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;({ model: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;gpt-4o&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; researcher&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; agent&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;({ tools: tabstackTools });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; result&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; researcher.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;run&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;  &quot;What are Vercel&apos;s current pricing plans? Cite your sources.&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;console.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;log&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(result.data);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;tabstackTools&lt;/code&gt; is a pre-built array, which is the shape &lt;code&gt;agent()&lt;/code&gt; expects. The tools resolve &lt;code&gt;TABSTACK_API_KEY&lt;/code&gt; lazily on first call, so importing the package never requires a key to be set.&lt;/p&gt;
&lt;p&gt;That is a working agent with live web access in about ten lines, and there is nothing running on your side. No Playwright, no browser binary, no headless Chrome to keep patched.&lt;/p&gt;
&lt;h2 id=&quot;step-2-the-five-tools&quot;&gt;Step 2: the five tools&lt;/h2&gt;
&lt;div class=&quot;table-scroll&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Export&lt;/th&gt;
&lt;th&gt;Tool name&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;extractStructuredDataTool&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;extract_structured_data&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Pull specific fields from a URL into a JSON shape you define&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;extractPageContentTool&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;extract_page_content&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fetch a page as clean markdown&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;researchQuestionTool&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;research_question&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Synthesised answer with cited sources across multiple pages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;generateStructuredDataTool&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;generate_structured_data&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fetch a page, then AI-transform it into derived or reshaped JSON&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;automateBrowserTaskTool&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;automate_browser_task&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Run a multi-step, natural language browser task&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Import individually for a subset, or use the array for all of them:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; { researchQuestionTool, extractPageContentTool } &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;@tabstack/llamaindex&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; { agent } &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;@llamaindex/workflow&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; researcher&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; agent&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;({ tools: [researchQuestionTool, extractPageContentTool] });&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Worth doing. Every tool you pass is another option the model weighs on every step, and another way for it to pick wrong. An agent that reads and researches does not need browser automation in its toolbox.&lt;/p&gt;
&lt;p&gt;Two of these are especially worth knowing well.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;extract_page_content&lt;/code&gt; returns clean markdown. That is the format you want when the output is heading somewhere else, whether that is a summarisation step, a prompt, or an ingestion pipeline. It is not HTML you then have to strip.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;research_question&lt;/code&gt; returns a synthesised answer with the sources behind it. This is the one that is genuinely hard to build yourself, and the one to reach for when the answer is not on any single page you can name.&lt;/p&gt;
&lt;h2 id=&quot;step-3-extract-or-generate&quot;&gt;Step 3: extract or generate&lt;/h2&gt;
&lt;p&gt;The inputs, which the model fills in:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;extract_structured_data&lt;/code&gt;: &lt;code&gt;url&lt;/code&gt;, &lt;code&gt;json_schema_json&lt;/code&gt;, a JSON encoded JSON Schema string&lt;/li&gt;
&lt;li&gt;&lt;code&gt;extract_page_content&lt;/code&gt;: &lt;code&gt;url&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;research_question&lt;/code&gt;: &lt;code&gt;query&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;generate_structured_data&lt;/code&gt;: &lt;code&gt;url&lt;/code&gt;, &lt;code&gt;instructions&lt;/code&gt;, &lt;code&gt;json_schema_json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;automate_browser_task&lt;/code&gt;: &lt;code&gt;task&lt;/code&gt;, plus optional &lt;code&gt;url&lt;/code&gt;, &lt;code&gt;guardrails&lt;/code&gt;, &lt;code&gt;data&lt;/code&gt;, &lt;code&gt;country&lt;/code&gt;, &lt;code&gt;max_iterations&lt;/code&gt;, &lt;code&gt;max_validation_attempts&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The distinction people get wrong is between the two structured tools. Extract pulls out what is already written on the page. Generate fetches the page and produces something derived from it, like a category, a sentiment, or a one-line summary nobody wrote down.&lt;/p&gt;
&lt;p&gt;If the field exists in the HTML, extract it. If you are asking for judgement, generate it. Reaching for generate when extract would do costs you time and adds a layer where the answer can drift away from what the page actually said.&lt;/p&gt;
&lt;p&gt;Inputs are validated against the core Zod schema before the SDK call, since LlamaIndex passes the schema straight through as the tool’s &lt;code&gt;parameters&lt;/code&gt;. Malformed model output is caught before a request goes out rather than after the API rejects it.&lt;/p&gt;
&lt;h2 id=&quot;step-4-the-flags-that-matter-in-production&quot;&gt;Step 4: the flags that matter in production&lt;/h2&gt;
&lt;p&gt;The model can pass these itself, and they are sent to Tabstack only when present, so leaving them out keeps the defaults.&lt;/p&gt;
&lt;p&gt;On &lt;code&gt;extract_structured_data&lt;/code&gt;, &lt;code&gt;extract_page_content&lt;/code&gt;, and &lt;code&gt;generate_structured_data&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;effort&lt;/code&gt;: &lt;code&gt;&quot;min&quot;&lt;/code&gt;, &lt;code&gt;&quot;standard&quot;&lt;/code&gt;, or &lt;code&gt;&quot;max&quot;&lt;/code&gt;. Use &lt;code&gt;&quot;max&quot;&lt;/code&gt; for JavaScript-heavy pages, where it does full server-side browser rendering.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nocache&lt;/code&gt;: bypass the cache.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;country&lt;/code&gt;: an ISO 3166-1 alpha-2 code for geotargeted fetches.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;On &lt;code&gt;research_question&lt;/code&gt;: &lt;code&gt;mode&lt;/code&gt; as &lt;code&gt;&quot;fast&quot;&lt;/code&gt; or &lt;code&gt;&quot;balanced&quot;&lt;/code&gt;, plus &lt;code&gt;nocache&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;If a page comes back thin or empty, it is nearly always a client-rendered app and &lt;code&gt;effort: &quot;max&quot;&lt;/code&gt; is the fix. That single argument is the whole workaround, and it costs you a keyword rather than a browser dependency.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;nocache&lt;/code&gt; is the one to remember for this audience specifically. If you are pulling live data precisely because it changes, a cached read defeats the purpose. Set it on anything you are monitoring.&lt;/p&gt;
&lt;h2 id=&quot;step-5-failures&quot;&gt;Step 5: failures&lt;/h2&gt;
&lt;p&gt;When a tool call fails it throws &lt;code&gt;TabstackToolError&lt;/code&gt;, carrying a normalised message plus an HTTP &lt;code&gt;status&lt;/code&gt; for API errors, and LlamaIndex surfaces it in the tool result. The agent sees a readable failure and can retry or explain rather than the run falling over.&lt;/p&gt;
&lt;p&gt;One &lt;code&gt;catch&lt;/code&gt; covers transport problems, parsing problems, and API problems. &lt;code&gt;status&lt;/code&gt; carries the HTTP status for API failures and is undefined for everything else, which is usually enough to decide between retrying and giving up.&lt;/p&gt;
&lt;p&gt;One behaviour to know before shipping: &lt;code&gt;automate_browser_task&lt;/code&gt; runs non-interactively. It never pauses for human-in-the-loop form input, so it cannot block an agent waiting on a person. It returns the final answer plus the data it extracted and the pages it visited. If you genuinely need a human in the middle of a browser session, that is the interactive mode flow in the API rather than this tool.&lt;/p&gt;
&lt;h2 id=&quot;step-6-custom-keys-and-clients&quot;&gt;Step 6: custom keys and clients&lt;/h2&gt;
&lt;p&gt;For a key per tenant, a custom base URL, or reuse of an SDK client you already have:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; { createTabstackLlamaindexTools } &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;@tabstack/llamaindex&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; tools&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; createTabstackLlamaindexTools&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;({ apiKey: process.env.&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;MY_KEY&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; });&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Same array shape, same tool names, so it drops in exactly like the default export.&lt;/p&gt;
&lt;h2 id=&quot;where-to-take-it-next&quot;&gt;Where to take it next&lt;/h2&gt;
&lt;p&gt;The obvious move is combining these tools with your own. An agent holding both a query engine over your documents and &lt;code&gt;research_question&lt;/code&gt; over the live web can answer questions neither source covers alone, which is usually the question someone actually wanted to ask.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;extract_page_content&lt;/code&gt; is also worth a look as an ingestion step rather than an agent tool. Clean markdown is a far better starting point for chunking than raw HTML, and you get it without running a browser or maintaining a parser.&lt;/p&gt;
&lt;p&gt;The tool names are identical in &lt;a href=&quot;https://www.npmjs.com/package/@tabstack/langchain&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;code&gt;@tabstack/langchain&lt;/code&gt;&lt;/a&gt;, &lt;a href=&quot;https://www.npmjs.com/package/@tabstack/ai&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;code&gt;@tabstack/ai&lt;/code&gt;&lt;/a&gt;, &lt;a href=&quot;https://www.npmjs.com/package/@tabstack/eve&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;code&gt;@tabstack/eve&lt;/code&gt;&lt;/a&gt;, and the Python &lt;a href=&quot;https://pypi.org/project/langchain-tabstack/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;code&gt;langchain-tabstack&lt;/code&gt;&lt;/a&gt;, so prompts carry over if this workload moves.&lt;/p&gt;
&lt;p&gt;The package is on npm as &lt;a href=&quot;https://www.npmjs.com/package/@tabstack/llamaindex&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;code&gt;@tabstack/llamaindex&lt;/code&gt;&lt;/a&gt;, and the full API reference is at &lt;a href=&quot;https://docs.tabstack.ai&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;docs.tabstack.ai&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Ready to build? &lt;a href=&quot;https://console.tabstack.ai/signup&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Sign up&lt;/a&gt; for 10,000 free credits, no card required, and query your first live page alongside your index. Full API in the &lt;a href=&quot;https://docs.tabstack.ai&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;docs&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>Add web tools to an OpenAI Agents SDK agent with Tabstack</title><link>https://tabstack.ai/blog/openai-agents-sdk-web-tools-tabstack</link><guid isPermaLink="true">https://tabstack.ai/blog/openai-agents-sdk-web-tools-tabstack</guid><description>A step by step tutorial: install @tabstack/openai-agents, hand the tools to an Agent, and understand why the adapter registers them with strict mode off.</description><pubDate>Wed, 05 Aug 2026 05:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The OpenAI Agents SDK has a clean model. You build an &lt;code&gt;Agent&lt;/code&gt;, you give it tools, you call &lt;code&gt;run&lt;/code&gt;. This tutorial gives it the tools nearly every agent eventually needs: read a page, pull typed fields from it, research across sources, and drive a browser when the data sits behind an interaction.&lt;/p&gt;
&lt;p&gt;It also spends a section on strict mode, because that is the one place where this adapter had to make a real decision. Why are these tools not strict, when the SDK pushes you so hard towards it? The answer is more interesting than it sounds, and knowing it will save you from filing a bug that is not a bug.&lt;/p&gt;
&lt;h2 id=&quot;what-you-need&quot;&gt;What you need&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;A Node project (the package ships dual ESM and CJS builds)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@openai/agents&lt;/code&gt; v0.13 or later&lt;/li&gt;
&lt;li&gt;Zod 4&lt;/li&gt;
&lt;li&gt;A Tabstack API key from &lt;a href=&quot;https://console.tabstack.ai&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;console.tabstack.ai&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;npm&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; install&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; @tabstack/openai-agents&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; @openai/agents&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; zod&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; TABSTACK_API_KEY&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;your-key-here&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Zod 4 is required, not preferred. The Agents SDK depends on it, and the adapter uses Zod 4’s native &lt;code&gt;z.toJSONSchema&lt;/code&gt; to advertise each tool’s parameters. If you are still on Zod 3, that upgrade comes first.&lt;/p&gt;
&lt;h2 id=&quot;step-1-an-agent-with-web-access&quot;&gt;Step 1: an agent with web access&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; { Agent, run } &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;@openai/agents&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; { tabstackTools } &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;@tabstack/openai-agents&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; agent&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; Agent&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  name: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;Research assistant&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  instructions:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;    &quot;You are a research assistant with web intelligence tools. Use research_question for open &quot;&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; +&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;    &quot;questions that need multiple sources, extract_page_content to read a specific URL as &quot;&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; +&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;    &quot;markdown, and the extract tools to pull structured fields from a page. Always cite sources.&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  tools: tabstackTools,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;});&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; result&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; run&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(agent, &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;What are Vercel&apos;s pricing plans, with sources?&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;console.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;log&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(result.finalOutput);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;tabstackTools&lt;/code&gt; is an array of ready-to-use tools, which is exactly what &lt;code&gt;Agent&lt;/code&gt; wants for &lt;code&gt;tools&lt;/code&gt;. No mapping step, no wrapper.&lt;/p&gt;
&lt;p&gt;The tools resolve &lt;code&gt;TABSTACK_API_KEY&lt;/code&gt; lazily on first call, so importing the package never requires a key to be set. Your test suite and your build steps stay clean.&lt;/p&gt;
&lt;h2 id=&quot;step-2-the-five-tools&quot;&gt;Step 2: the five tools&lt;/h2&gt;
&lt;div class=&quot;table-scroll&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool name&lt;/th&gt;
&lt;th&gt;Export&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;extract_structured_data&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;extractStructuredDataTool&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Pull specific fields from a URL into a JSON shape you define&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;extract_page_content&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;extractPageContentTool&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fetch a page as clean markdown&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;research_question&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;researchQuestionTool&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Synthesised answer with cited sources across multiple pages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;generate_structured_data&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;generateStructuredDataTool&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fetch a page, then AI-transform it into derived or reshaped JSON&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;automate_browser_task&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;automateBrowserTaskTool&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Run a multi-step, natural language browser task&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Import them individually when you want a subset. I think building your own array is worth the thirty seconds it costs you. Every tool you pass is another option the model weighs on every step, and another way for it to choose wrong. An agent that only reads and researches should be handed two tools, not five.&lt;/p&gt;
&lt;p&gt;The inputs, which the model fills in:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;extract_structured_data&lt;/code&gt;: &lt;code&gt;url&lt;/code&gt;, &lt;code&gt;json_schema_json&lt;/code&gt;, a JSON encoded JSON Schema string&lt;/li&gt;
&lt;li&gt;&lt;code&gt;extract_page_content&lt;/code&gt;: &lt;code&gt;url&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;research_question&lt;/code&gt;: &lt;code&gt;query&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;generate_structured_data&lt;/code&gt;: &lt;code&gt;url&lt;/code&gt;, &lt;code&gt;instructions&lt;/code&gt;, &lt;code&gt;json_schema_json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;automate_browser_task&lt;/code&gt;: &lt;code&gt;task&lt;/code&gt;, plus optional &lt;code&gt;url&lt;/code&gt;, &lt;code&gt;guardrails&lt;/code&gt;, &lt;code&gt;data&lt;/code&gt;, &lt;code&gt;country&lt;/code&gt;, &lt;code&gt;max_iterations&lt;/code&gt;, &lt;code&gt;max_validation_attempts&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Extract versus generate is the distinction that trips people up. Extract pulls out what is already written on the page. Generate fetches the page and produces something derived from it, like a category or a summary nobody wrote down. If the field exists in the HTML, extract it. If you are asking for judgement, generate it.&lt;/p&gt;
&lt;h2 id=&quot;step-3-why-these-tools-are-not-strict&quot;&gt;Step 3: why these tools are not strict&lt;/h2&gt;
&lt;p&gt;Open the tool definitions and you will find they are registered with &lt;code&gt;strict: false&lt;/code&gt;. That is deliberate, and it is worth understanding rather than treating as a wart.&lt;/p&gt;
&lt;p&gt;The Agents SDK forces strict JSON Schema mode whenever a tool’s &lt;code&gt;parameters&lt;/code&gt; is a Zod schema. Passing &lt;code&gt;strict: false&lt;/code&gt; alongside a Zod schema throws, so it is not a knob you can simply turn. And strict mode cannot represent two constructs the shared core schemas rely on:&lt;/p&gt;
&lt;p&gt;Optional fields. Strict mode requires every property to appear in &lt;code&gt;required&lt;/code&gt;, which rules out &lt;code&gt;.optional()&lt;/code&gt;. All those production flags in the next section are optional by design, because sending them when you do not mean them would override Tabstack’s defaults.&lt;/p&gt;
&lt;p&gt;Open objects. &lt;code&gt;automate_browser_task&lt;/code&gt; accepts a &lt;code&gt;data&lt;/code&gt; object for form-filling context, whose keys you cannot know ahead of time. Strict mode forbids the schema-valued &lt;code&gt;additionalProperties&lt;/code&gt; that an open record needs.&lt;/p&gt;
&lt;p&gt;So the adapter converts each core Zod schema to a JSON Schema and registers the tools with &lt;code&gt;strict: false&lt;/code&gt;. Two things you do not lose in the trade. The model still sees full field descriptions, so tool selection and argument quality are unaffected. And every call is validated against the core Zod schema inside &lt;code&gt;execute&lt;/code&gt; before the request goes out, so malformed model output still fails fast with a clear error rather than reaching the API.&lt;/p&gt;
&lt;p&gt;The shared core schemas are never modified to accommodate this. That is what keeps behaviour identical across every framework and both languages.&lt;/p&gt;
&lt;h2 id=&quot;step-4-the-flags-that-matter-in-production&quot;&gt;Step 4: the flags that matter in production&lt;/h2&gt;
&lt;p&gt;The model can pass these itself, and they are only sent to Tabstack when present, so omitting them keeps the defaults.&lt;/p&gt;
&lt;p&gt;On &lt;code&gt;extract_structured_data&lt;/code&gt;, &lt;code&gt;extract_page_content&lt;/code&gt;, and &lt;code&gt;generate_structured_data&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;effort&lt;/code&gt;: &lt;code&gt;&quot;min&quot;&lt;/code&gt;, &lt;code&gt;&quot;standard&quot;&lt;/code&gt;, or &lt;code&gt;&quot;max&quot;&lt;/code&gt;. Use &lt;code&gt;&quot;max&quot;&lt;/code&gt; for JavaScript-heavy pages, where it does full server-side browser rendering.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nocache&lt;/code&gt;: bypass the cache.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;country&lt;/code&gt;: an ISO 3166-1 alpha-2 code for geotargeted fetches.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;On &lt;code&gt;research_question&lt;/code&gt;: &lt;code&gt;mode&lt;/code&gt; as &lt;code&gt;&quot;fast&quot;&lt;/code&gt; or &lt;code&gt;&quot;balanced&quot;&lt;/code&gt;, plus &lt;code&gt;nocache&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;If a page comes back thin or empty, it is almost always a client-rendered app and &lt;code&gt;effort: &quot;max&quot;&lt;/code&gt; is the fix. That one argument replaces installing Playwright, shipping a browser binary, and keeping both patched. Nothing runs on your side.&lt;/p&gt;
&lt;p&gt;The tidiest way to apply these is through instructions rather than code:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;If a page returns little or no content, retry once with effort set to max. Use fast mode for research while iterating.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;step-5-failures&quot;&gt;Step 5: failures&lt;/h2&gt;
&lt;p&gt;When a tool call fails it throws &lt;code&gt;TabstackToolError&lt;/code&gt;, carrying a normalised message plus an HTTP &lt;code&gt;status&lt;/code&gt; for API errors. The Agents SDK catches tool errors and surfaces them to the model as a tool result, so a failing call does not abort the run by default. The model sees the failure, and can retry with different arguments or explain the problem.&lt;/p&gt;
&lt;p&gt;That is usually what you want. A dead link in step two of a five-step research task should not kill the run.&lt;/p&gt;
&lt;p&gt;One behaviour to know before shipping: &lt;code&gt;automate_browser_task&lt;/code&gt; runs non-interactively. It never pauses for human-in-the-loop form input, so it cannot block waiting on a person. It returns the final answer plus the data it extracted and the pages it visited. If you need a human in the middle of a browser session, that is the interactive mode flow in the API rather than this tool.&lt;/p&gt;
&lt;h2 id=&quot;step-6-custom-keys-and-clients&quot;&gt;Step 6: custom keys and clients&lt;/h2&gt;
&lt;p&gt;For a key per tenant, a custom base URL, or reuse of an SDK client you already have in scope:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; { createTabstackOpenAIAgentsTools } &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;@tabstack/openai-agents&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; tools&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; createTabstackOpenAIAgentsTools&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;({ apiKey: process.env.&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;MY_KEY&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; });&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Same array shape, same tool names, so it drops into &lt;code&gt;tools&lt;/code&gt; exactly like the default export.&lt;/p&gt;
&lt;h2 id=&quot;where-to-take-it-next&quot;&gt;Where to take it next&lt;/h2&gt;
&lt;p&gt;Narrow the toolset per agent. If you are using handoffs, a specialist agent should carry only the tools its speciality needs. A pricing researcher wants &lt;code&gt;extract_structured_data&lt;/code&gt;. A general assistant wants &lt;code&gt;research_question&lt;/code&gt;. Very few agents want all five.&lt;/p&gt;
&lt;p&gt;Show the citations. &lt;code&gt;research_question&lt;/code&gt; returns sources alongside its answer, and surfacing them is what separates an assistant people trust from one they quietly verify.&lt;/p&gt;
&lt;p&gt;The tool names are identical in &lt;a href=&quot;https://www.npmjs.com/package/@tabstack/langchain&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;code&gt;@tabstack/langchain&lt;/code&gt;&lt;/a&gt;, &lt;a href=&quot;https://www.npmjs.com/package/@tabstack/ai&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;code&gt;@tabstack/ai&lt;/code&gt;&lt;/a&gt;, &lt;a href=&quot;https://www.npmjs.com/package/@tabstack/eve&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;code&gt;@tabstack/eve&lt;/code&gt;&lt;/a&gt;, and the Python &lt;a href=&quot;https://pypi.org/project/langchain-tabstack/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;code&gt;langchain-tabstack&lt;/code&gt;&lt;/a&gt;, so your instructions carry over if this workload moves.&lt;/p&gt;
&lt;p&gt;The package is on npm as &lt;a href=&quot;https://www.npmjs.com/package/@tabstack/openai-agents&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;code&gt;@tabstack/openai-agents&lt;/code&gt;&lt;/a&gt;, and the full API reference is at &lt;a href=&quot;https://docs.tabstack.ai&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;docs.tabstack.ai&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Ready to build? &lt;a href=&quot;https://console.tabstack.ai/signup&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Sign up&lt;/a&gt; for 10,000 free credits, no card required, and hand your agent its first typed extraction. Full API in the &lt;a href=&quot;https://docs.tabstack.ai&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;docs&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>Give a Claude Agent SDK app web access</title><link>https://tabstack.ai/blog/claude-agent-sdk-web-access-tabstack</link><guid isPermaLink="true">https://tabstack.ai/blog/claude-agent-sdk-web-access-tabstack</guid><description>Install @tabstack/claude-agent, wire its ready-built in-process MCP server into a query() call, and give Claude extraction, research, and browser tools.</description><pubDate>Tue, 04 Aug 2026 05:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Every other integration in this series hands your framework an array or an object of tools. This one is different, because the Claude Agent SDK expects tools to arrive as an MCP server.&lt;/p&gt;
&lt;p&gt;That sounds like more work, and I assumed it would be. It is actually less, because the package ships a server already built. You import it, name it in your &lt;code&gt;query()&lt;/code&gt; options, and Claude has web access. There is no separate process to run and no MCP config file to maintain.&lt;/p&gt;
&lt;h2 id=&quot;what-you-need&quot;&gt;What you need&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;A Node project (the package ships dual ESM and CJS builds)&lt;/li&gt;
&lt;li&gt;A Tabstack API key from &lt;a href=&quot;https://console.tabstack.ai&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;console.tabstack.ai&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;An Anthropic API key&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;npm&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; install&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; @tabstack/claude-agent&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; @anthropic-ai/claude-agent-sdk&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; zod&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; TABSTACK_API_KEY&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;your-tabstack-key&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; ANTHROPIC_API_KEY&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;your-anthropic-key&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One version constraint to note. The Claude Agent SDK depends on Zod 4, so this package’s &lt;code&gt;zod&lt;/code&gt; peer dependency is &lt;code&gt;^4.0.0&lt;/code&gt;. Unlike the eve and LlamaIndex adapters, Zod 3 is not an option here. If your app is still on Zod 3, that upgrade is a prerequisite rather than a nice-to-have.&lt;/p&gt;
&lt;h2 id=&quot;step-1-wire-in-the-server&quot;&gt;Step 1: wire in the server&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; { query } &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;@anthropic-ai/claude-agent-sdk&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; { tabstackServer, tabstackAllowedTools, tabstackMcpServerName } &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;@tabstack/claude-agent&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;for&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; message&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; of&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; query&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  prompt: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;What are Vercel&apos;s current pricing plans, with sources?&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  options: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    mcpServers: { [tabstackMcpServerName]: tabstackServer },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    allowedTools: tabstackAllowedTools,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;})) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;  if&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; (message.type &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;===&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;result&quot;&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; &amp;#x26;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; message.subtype &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;===&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;success&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    console.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;log&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(message.result);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Three imports, and Claude can read the web.&lt;/p&gt;
&lt;p&gt;Note the &lt;code&gt;for await&lt;/code&gt;. &lt;code&gt;query()&lt;/code&gt; returns a lazy async generator rather than a promise, so nothing runs until you iterate it. Writing &lt;code&gt;await query({ ... })&lt;/code&gt; typechecks fine and does absolutely nothing, which is a quiet half hour to lose.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;tabstackServer&lt;/code&gt; is a ready-built in-process MCP server holding all five tools. In-process is the important word. It runs inside your Node process, so there is no subprocess to spawn, no port to bind, and no server lifecycle to manage. If your only previous experience of MCP was editing a JSON config and restarting something, this is a pleasant surprise.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;tabstackAllowedTools&lt;/code&gt; pre-approves every Tabstack tool. The Claude Agent SDK gates tool use by permission, so without this the tools exist but Claude cannot call them. It is the line people forget, and the symptom is an agent that talks about wanting to look something up and then does not.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;tabstackMcpServerName&lt;/code&gt; is the string &lt;code&gt;&quot;tabstack&quot;&lt;/code&gt;. Use the export rather than typing the literal, because the name becomes part of how Claude addresses each tool.&lt;/p&gt;
&lt;p&gt;The server resolves &lt;code&gt;TABSTACK_API_KEY&lt;/code&gt; lazily on first tool call, so importing the package never requires a key to be set.&lt;/p&gt;
&lt;h2 id=&quot;step-2-understand-what-claude-actually-sees&quot;&gt;Step 2: understand what Claude actually sees&lt;/h2&gt;
&lt;p&gt;Claude does not see &lt;code&gt;research_question&lt;/code&gt;. It sees &lt;code&gt;mcp__tabstack__research_question&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;MCP tools are namespaced by their server, so every tool arrives fully qualified as &lt;code&gt;mcp__tabstack__&amp;#x3C;tool_name&gt;&lt;/code&gt;. This matters in two places. When you read a transcript and want to know which tool ran, that prefix is what you are scanning for. And when you write instructions referring to a tool by name, the unqualified name is what Claude maps onto, but the qualified name is what appears in permission prompts and logs.&lt;/p&gt;
&lt;p&gt;Underneath the prefix, the names are the same everywhere:&lt;/p&gt;
&lt;div class=&quot;table-scroll&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool name&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;extract_structured_data&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Pull specific fields from a URL into a JSON shape you define&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;extract_page_content&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fetch a page as clean markdown&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;research_question&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Synthesised answer with cited sources across multiple pages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;generate_structured_data&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fetch a page, then AI-transform it into derived or reshaped JSON&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;automate_browser_task&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Run a multi-step, natural language browser task&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Those names are shared with the LangChain, Vercel AI SDK, eve, Mastra, and Python packages, so a prompt written for one carries over.&lt;/p&gt;
&lt;p&gt;The distinction worth internalising is extract versus generate. Extract pulls out what is already written on the page. Generate fetches the page and produces something derived from it, like a category or a summary nobody wrote. If the field exists in the HTML, extract it. If you are asking for judgement, generate it.&lt;/p&gt;
&lt;h2 id=&quot;step-3-the-inputs-the-model-fills-in&quot;&gt;Step 3: the inputs the model fills in&lt;/h2&gt;
&lt;p&gt;You will read these in transcripts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;extract_structured_data&lt;/code&gt;: &lt;code&gt;url&lt;/code&gt;, &lt;code&gt;json_schema_json&lt;/code&gt;, a JSON encoded JSON Schema string&lt;/li&gt;
&lt;li&gt;&lt;code&gt;extract_page_content&lt;/code&gt;: &lt;code&gt;url&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;research_question&lt;/code&gt;: &lt;code&gt;query&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;generate_structured_data&lt;/code&gt;: &lt;code&gt;url&lt;/code&gt;, &lt;code&gt;instructions&lt;/code&gt;, &lt;code&gt;json_schema_json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;automate_browser_task&lt;/code&gt;: &lt;code&gt;task&lt;/code&gt;, plus optional &lt;code&gt;url&lt;/code&gt;, &lt;code&gt;guardrails&lt;/code&gt;, &lt;code&gt;data&lt;/code&gt;, &lt;code&gt;country&lt;/code&gt;, &lt;code&gt;max_iterations&lt;/code&gt;, &lt;code&gt;max_validation_attempts&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each tool’s input schema is the core Zod schema’s raw shape, which means the Claude Agent SDK validates Claude’s arguments before the handler runs. Malformed tool calls fail immediately with a clear message rather than travelling to the API and coming back rejected.&lt;/p&gt;
&lt;p&gt;There are optional inputs too, and each is accepted by a different subset of the tools:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;effort&lt;/code&gt;, as &lt;code&gt;&quot;min&quot;&lt;/code&gt;, &lt;code&gt;&quot;standard&quot;&lt;/code&gt;, or &lt;code&gt;&quot;max&quot;&lt;/code&gt;, on the three fetch-based tools&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nocache&lt;/code&gt; to bypass the cache, on the three fetch-based tools and &lt;code&gt;research_question&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;country&lt;/code&gt;, an ISO 3166-1 alpha-2 code, for geotargeted fetches, on the three fetch-based tools and &lt;code&gt;automate_browser_task&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mode&lt;/code&gt;, as &lt;code&gt;&quot;fast&quot;&lt;/code&gt; or &lt;code&gt;&quot;balanced&quot;&lt;/code&gt;, on &lt;code&gt;research_question&lt;/code&gt; only&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The first three are forwarded only when Claude supplies them. &lt;code&gt;mode&lt;/code&gt; is the exception: it defaults to &lt;code&gt;&quot;balanced&quot;&lt;/code&gt; and is always sent.&lt;/p&gt;
&lt;p&gt;If a page comes back thin or empty, it is nearly always a client-rendered app and &lt;code&gt;effort: &quot;max&quot;&lt;/code&gt; is the fix. Full rendering happens server side, so there is no Playwright in your project and no browser binary in your image. Put it in your system prompt and let Claude apply it:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;If a page returns little or no content, retry once with effort set to max. Always cite the sources you used.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;step-4-custom-keys-and-assembling-it-yourself&quot;&gt;Step 4: custom keys, and assembling it yourself&lt;/h2&gt;
&lt;p&gt;For a key per tenant, a custom base URL, or reuse of an SDK client you already have:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; { query } &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;@anthropic-ai/claude-agent-sdk&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  createTabstackClaudeAgentServer,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  tabstackAllowedTools,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  tabstackMcpServerName,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;} &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;@tabstack/claude-agent&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; server&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; createTabstackClaudeAgentServer&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;({ apiKey: process.env.&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;MY_KEY&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;for&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; message&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; of&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; query&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  prompt: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;...&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  options: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    mcpServers: { [tabstackMcpServerName]: server },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    allowedTools: tabstackAllowedTools,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;})) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;  if&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; (message.type &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;===&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;result&quot;&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; &amp;#x26;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; message.subtype &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;===&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;success&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    console.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;log&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(message.result);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you would rather assemble the server yourself, perhaps because you want Tabstack’s tools alongside your own in a single server, &lt;code&gt;createTabstackClaudeAgentTools(config)&lt;/code&gt; returns the raw tool array to pass into your own &lt;code&gt;createSdkMcpServer({ name, version, tools })&lt;/code&gt;. That is also the route to take when you want a subset. Build the array, keep the tools you need, and hand the rest of your own tools in beside them.&lt;/p&gt;
&lt;h2 id=&quot;step-5-failures&quot;&gt;Step 5: failures&lt;/h2&gt;
&lt;p&gt;This adapter handles errors differently from the others, and the difference is deliberate.&lt;/p&gt;
&lt;p&gt;When a tool call fails, the handler normalises it to a &lt;code&gt;TabstackToolError&lt;/code&gt; with a clean message, plus an HTTP &lt;code&gt;status&lt;/code&gt; for API errors, then returns it as an MCP error result with &lt;code&gt;isError: true&lt;/code&gt;. What Claude sees is the message; the &lt;code&gt;status&lt;/code&gt; stays on the error object for your own logging rather than travelling into the transcript. Either way Claude reads a useful message rather than a raw exception, which means it can retry with different arguments or explain the problem to the user. A dead link does not end the run.&lt;/p&gt;
&lt;p&gt;That is the right default for an agent loop. You are not catching these in your own code in the common case, because the model is the one that needs to react.&lt;/p&gt;
&lt;p&gt;One behaviour to know before shipping: &lt;code&gt;automate_browser_task&lt;/code&gt; runs non-interactively. It never pauses for human-in-the-loop form input, so it cannot block a run waiting on someone. It returns the final answer plus the data it extracted and the pages it visited. If you genuinely need a human in the middle of a browser session, that is the interactive mode flow in the API rather than this tool.&lt;/p&gt;
&lt;h2 id=&quot;where-to-take-it-next&quot;&gt;Where to take it next&lt;/h2&gt;
&lt;p&gt;Trim &lt;code&gt;allowedTools&lt;/code&gt;. &lt;code&gt;tabstackAllowedTools&lt;/code&gt; approves all five, which is the fastest way to get started and, I would argue, rarely what you want in production. Narrowing it to the two or three your agent actually needs reduces both cost and the chance of Claude choosing a tool you did not intend.&lt;/p&gt;
&lt;p&gt;Combine it with your own tools. Because this is an in-process MCP server, adding Tabstack does not preclude anything else. Your own &lt;code&gt;createSdkMcpServer&lt;/code&gt; output and &lt;code&gt;tabstackServer&lt;/code&gt; sit side by side under different names in &lt;code&gt;mcpServers&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The package is on npm as &lt;a href=&quot;https://www.npmjs.com/package/@tabstack/claude-agent&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;code&gt;@tabstack/claude-agent&lt;/code&gt;&lt;/a&gt;, and the full API reference is at &lt;a href=&quot;https://docs.tabstack.ai&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;docs.tabstack.ai&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Ready to build? &lt;a href=&quot;https://console.tabstack.ai/signup&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Sign up&lt;/a&gt; for 10,000 free credits, no card required, and hand your Claude agent its first live page. Full API in the &lt;a href=&quot;https://docs.tabstack.ai&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;docs&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>Build a web-connected Mastra agent with Tabstack</title><link>https://tabstack.ai/blog/mastra-web-agent-tabstack</link><guid isPermaLink="true">https://tabstack.ai/blog/mastra-web-agent-tabstack</guid><description>A step by step tutorial: install @tabstack/mastra, spread the tools into an Agent, and give it schema-enforced extraction, research, and browser automation.</description><pubDate>Mon, 03 Aug 2026 05:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Mastra gives you a clean &lt;code&gt;Agent&lt;/code&gt; abstraction and expects you to bring the tools. This tutorial brings the ones nearly every agent ends up needing: read a page, pull typed fields off it, research a question across several sources, and drive a browser when the data is behind an interaction.&lt;/p&gt;
&lt;p&gt;It is one import and one line in your agent definition. That is the entire integration, so most of what follows is about the decisions you make afterwards rather than the wiring.&lt;/p&gt;
&lt;h2 id=&quot;what-you-need&quot;&gt;What you need&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;A Node project (the package ships dual ESM and CJS builds)&lt;/li&gt;
&lt;li&gt;Mastra v1 or later&lt;/li&gt;
&lt;li&gt;A Tabstack API key from &lt;a href=&quot;https://console.tabstack.ai&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;console.tabstack.ai&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;npm&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; install&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; @tabstack/mastra&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; @mastra/core&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; zod&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; TABSTACK_API_KEY&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;your-key-here&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;@mastra/core&lt;/code&gt; and &lt;code&gt;zod&lt;/code&gt; are peer dependencies, so your app’s single instance of each is shared. That is the detail that keeps you out of the genuinely miserable bug where two copies of Zod disagree about whether your schema is a schema.&lt;/p&gt;
&lt;h2 id=&quot;step-1-a-web-connected-agent&quot;&gt;Step 1: a web-connected agent&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; { Agent } &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;@mastra/core/agent&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; { tabstackTools } &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;@tabstack/mastra&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; agent&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; Agent&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  id: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;web-researcher&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  name: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;Web Researcher&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  instructions: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;Answer questions with current information, and always cite your sources.&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  model: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;anthropic/claude-sonnet-4-6&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  tools: tabstackTools,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;});&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; result&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; agent.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;generate&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;What are Vercel&apos;s pricing plans, with sources?&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;console.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;log&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(result.text);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That is a working agent with web access. &lt;code&gt;tabstackTools&lt;/code&gt; reads &lt;code&gt;TABSTACK_API_KEY&lt;/code&gt; from the environment, and the client is created lazily, so importing the package never requires a key to be set.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;tabstackTools&lt;/code&gt; is a named object keyed by tool name, which is exactly the shape Mastra’s &lt;code&gt;tools&lt;/code&gt; expects. No adapter, no mapping step.&lt;/p&gt;
&lt;p&gt;Notice what the instructions are doing. They describe the job, not the tools. Each tool ships with a description written for the model to read, so you do not need to explain in your prompt what &lt;code&gt;research_question&lt;/code&gt; is for. Keep your instructions about the outcome you want and let the descriptions do the routing. It keeps prompts short, and it means description improvements reach you on upgrade rather than requiring a prompt rewrite.&lt;/p&gt;
&lt;h2 id=&quot;step-2-know-your-five&quot;&gt;Step 2: know your five&lt;/h2&gt;
&lt;div class=&quot;table-scroll&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;extract_structured_data&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Pull specific fields from a URL into a JSON shape you define&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;extract_page_content&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fetch a page as clean markdown&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;research_question&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Synthesised answer with cited sources across multiple pages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;generate_structured_data&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fetch a page, then AI-transform it into derived or reshaped JSON&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;automate_browser_task&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Run a multi-step, natural language browser task&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;The model fills in the arguments, but knowing the shapes helps when you are reading a trace and wondering why it chose badly:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;extract_structured_data&lt;/code&gt;: &lt;code&gt;url&lt;/code&gt;, &lt;code&gt;json_schema_json&lt;/code&gt;, a JSON encoded JSON Schema string&lt;/li&gt;
&lt;li&gt;&lt;code&gt;extract_page_content&lt;/code&gt;: &lt;code&gt;url&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;research_question&lt;/code&gt;: &lt;code&gt;query&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;generate_structured_data&lt;/code&gt;: &lt;code&gt;url&lt;/code&gt;, &lt;code&gt;instructions&lt;/code&gt;, &lt;code&gt;json_schema_json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;automate_browser_task&lt;/code&gt;: &lt;code&gt;task&lt;/code&gt;, plus optional &lt;code&gt;url&lt;/code&gt;, &lt;code&gt;guardrails&lt;/code&gt;, &lt;code&gt;data&lt;/code&gt;, &lt;code&gt;country&lt;/code&gt;, &lt;code&gt;max_iterations&lt;/code&gt;, &lt;code&gt;max_validation_attempts&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The one distinction worth internalising is extract versus generate. Extract pulls out what is already written on the page. Generate fetches the page and produces something derived from it, like a category or a one-line summary nobody wrote. If the field exists in the HTML, extract it. If you are asking for judgement, generate it. Reaching for generate when extract would do costs you time and adds a layer where the answer can drift.&lt;/p&gt;
&lt;h2 id=&quot;step-3-give-it-fewer-tools&quot;&gt;Step 3: give it fewer tools&lt;/h2&gt;
&lt;p&gt;Passing all five is the easy default, and often the wrong one. Every tool you expose is another option the model weighs on every step, and another way for it to pick wrong. On smaller models, trimming the toolset is one of the highest-leverage changes available to you.&lt;/p&gt;
&lt;p&gt;Each tool is exported individually, and the package ships a &lt;code&gt;toolNames&lt;/code&gt; map so you do not hand-write the string keys:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; { Agent } &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;@mastra/core/agent&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; { extractPageContentTool, researchQuestionTool, toolNames } &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;@tabstack/mastra&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; agent&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; Agent&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  id: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;web-researcher&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  name: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;Web Researcher&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  instructions: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;Summarize pages and research questions.&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  model: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;anthropic/claude-sonnet-4-6&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  tools: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    [toolNames.researchQuestion]: researchQuestionTool,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    [toolNames.extractPageContent]: extractPageContentTool,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use &lt;code&gt;toolNames&lt;/code&gt; rather than typing &lt;code&gt;&quot;research_question&quot;&lt;/code&gt; by hand. Those strings are the contract shared across every Tabstack integration, including the Python package, and a typo produces a tool your prompt references and your agent does not have.&lt;/p&gt;
&lt;h2 id=&quot;step-4-the-flags-that-matter-in-production&quot;&gt;Step 4: the flags that matter in production&lt;/h2&gt;
&lt;p&gt;The model can pass these itself, and they are only sent to Tabstack when present, so leaving them alone keeps the defaults.&lt;/p&gt;
&lt;p&gt;On &lt;code&gt;extract_structured_data&lt;/code&gt;, &lt;code&gt;extract_page_content&lt;/code&gt;, and &lt;code&gt;generate_structured_data&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;effort&lt;/code&gt;: &lt;code&gt;&quot;min&quot;&lt;/code&gt;, &lt;code&gt;&quot;standard&quot;&lt;/code&gt;, or &lt;code&gt;&quot;max&quot;&lt;/code&gt;. Use &lt;code&gt;&quot;max&quot;&lt;/code&gt; for JavaScript-heavy pages, where it does full server-side browser rendering.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nocache&lt;/code&gt;: bypass the cache.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;country&lt;/code&gt;: an ISO 3166-1 alpha-2 code, such as &lt;code&gt;&quot;US&quot;&lt;/code&gt;, for geotargeted fetches.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;On &lt;code&gt;research_question&lt;/code&gt;: &lt;code&gt;mode&lt;/code&gt; as &lt;code&gt;&quot;fast&quot;&lt;/code&gt; or &lt;code&gt;&quot;balanced&quot;&lt;/code&gt;, plus &lt;code&gt;nocache&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;If a page comes back thin or empty, it is nearly always a client-rendered app and &lt;code&gt;effort: &quot;max&quot;&lt;/code&gt; is the fix. That single argument replaces installing Playwright, shipping a browser binary in your container, and keeping both patched. There is nothing running on your side at all.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;nocache&lt;/code&gt; matters when you are watching something that moves, like prices or availability. &lt;code&gt;country&lt;/code&gt; matters when the page varies by region, which for pricing pages is most of the time.&lt;/p&gt;
&lt;p&gt;The clean way to apply these is in your agent’s instructions rather than in code:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;If a page returns little or no content, retry once with effort set to max. Use fast mode for research while iterating.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;step-5-failures&quot;&gt;Step 5: failures&lt;/h2&gt;
&lt;p&gt;When a tool call fails it throws &lt;code&gt;TabstackToolError&lt;/code&gt;, carrying a normalised message plus an HTTP &lt;code&gt;status&lt;/code&gt; for API errors. Mastra surfaces it in the tool result, so the model sees a readable failure and can retry or explain rather than the run collapsing.&lt;/p&gt;
&lt;p&gt;If you invoke a tool’s &lt;code&gt;execute&lt;/code&gt; directly, catch it yourself:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; { TabstackToolError, tabstackTools, toolNames } &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;@tabstack/mastra&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;try&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;  await&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; tabstackTools[toolNames.extractPageContent].&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;execute&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;?.(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    { url: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;https://example.com&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    {} &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;as&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; never&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  );&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;} &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;catch&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; (err) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;  if&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; (err &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;instanceof&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; TabstackToolError&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    console.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;error&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;`Tabstack failed (${&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;err&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;status&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; ??&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;no status&quot;}): ${&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;err&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;message&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;}`&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One error type instead of separate handling for transport, parsing, and API problems. &lt;code&gt;status&lt;/code&gt; carries the HTTP status for API failures and is undefined for everything else, which is usually enough to decide between retrying and giving up.&lt;/p&gt;
&lt;p&gt;One behaviour to know before you ship: &lt;code&gt;automate_browser_task&lt;/code&gt; runs non-interactively. It never pauses for human-in-the-loop form input, so it cannot block your agent waiting on someone. It returns the final answer along with the data it extracted and the pages it visited. If you need a human in the middle of a browser session, that is the interactive mode flow in the API rather than this tool.&lt;/p&gt;
&lt;h2 id=&quot;step-6-custom-keys-and-clients&quot;&gt;Step 6: custom keys and clients&lt;/h2&gt;
&lt;p&gt;For a key per tenant, a custom base URL, or reuse of an SDK client you already have in scope:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; { createTabstackMastraTools } &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;@tabstack/mastra&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; tools&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; createTabstackMastraTools&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;({ apiKey: process.env.&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;MY_KEY&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; });&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Same object shape, same tool names, so it drops into &lt;code&gt;tools&lt;/code&gt; exactly like the default export.&lt;/p&gt;
&lt;h2 id=&quot;where-to-take-it-next&quot;&gt;Where to take it next&lt;/h2&gt;
&lt;p&gt;If I were starting a Mastra agent from scratch today, I would wire in &lt;code&gt;research_question&lt;/code&gt; and &lt;code&gt;extract_page_content&lt;/code&gt; and nothing else, then add the structured tools when a concrete use case asks for them. Most agents never need all five.&lt;/p&gt;
&lt;p&gt;Render the citations. &lt;code&gt;research_question&lt;/code&gt; returns sources alongside the answer, and showing them is what separates an assistant people trust from one they quietly double-check.&lt;/p&gt;
&lt;p&gt;If part of this workload moves elsewhere, the tool names and inputs are identical in &lt;a href=&quot;https://www.npmjs.com/package/@tabstack/langchain&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;code&gt;@tabstack/langchain&lt;/code&gt;&lt;/a&gt;, &lt;a href=&quot;https://www.npmjs.com/package/@tabstack/ai&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;code&gt;@tabstack/ai&lt;/code&gt;&lt;/a&gt;, and the Python &lt;a href=&quot;https://pypi.org/project/langchain-tabstack/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;code&gt;langchain-tabstack&lt;/code&gt;&lt;/a&gt;. Your instructions carry over, because the model sees the same tools.&lt;/p&gt;
&lt;p&gt;The package is on npm as &lt;a href=&quot;https://www.npmjs.com/package/@tabstack/mastra&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;code&gt;@tabstack/mastra&lt;/code&gt;&lt;/a&gt;, and the full API reference is at &lt;a href=&quot;https://docs.tabstack.ai&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;docs.tabstack.ai&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Ready to build? &lt;a href=&quot;https://console.tabstack.ai/signup&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Sign up&lt;/a&gt; for 10,000 free credits, no card required, and hand your Mastra agent its first live page. Full API in the &lt;a href=&quot;https://docs.tabstack.ai&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;docs&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>Give an eve agent web access with Tabstack</title><link>https://tabstack.ai/blog/eve-agent-web-access-tabstack</link><guid isPermaLink="true">https://tabstack.ai/blog/eve-agent-web-access-tabstack</guid><description>A step by step tutorial: install @tabstack/eve, add one-line tool files, and give your agent schema-enforced extraction, research, and browser automation.</description><pubDate>Fri, 31 Jul 2026 05:00:00 GMT</pubDate><content:encoded>&lt;p&gt;eve does something unusual with tools. It does not ask you to register them, assemble them into an array, or pass them into a constructor. It reads the filenames in &lt;code&gt;agent/tools/&lt;/code&gt;. Add a file, the model can call it. Delete the file, it cannot.&lt;/p&gt;
&lt;p&gt;I like this more than I expected to. It makes this the shortest integration tutorial in the series: giving your agent the ability to read and research the web is going to take five one-line files.&lt;/p&gt;
&lt;h2 id=&quot;what-you-need&quot;&gt;What you need&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;An existing eve project (the package ships dual ESM and CJS builds)&lt;/li&gt;
&lt;li&gt;eve v0.24 or later&lt;/li&gt;
&lt;li&gt;A Tabstack API key from &lt;a href=&quot;https://console.tabstack.ai&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;console.tabstack.ai&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;npm&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; install&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; @tabstack/eve&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; eve&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; zod&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; TABSTACK_API_KEY&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;your-key-here&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;eve&lt;/code&gt; and &lt;code&gt;zod&lt;/code&gt; are peer dependencies, so your app’s single instance of each is what gets used. Zod 3 and Zod 4 both work. On Zod 4 the adapter uses the built-in &lt;code&gt;z.toJSONSchema&lt;/code&gt;, and on Zod 3 it falls back to &lt;code&gt;zod-to-json-schema&lt;/code&gt;. You do not have to care which, but it is worth knowing nothing will break when you upgrade.&lt;/p&gt;
&lt;h2 id=&quot;step-1-add-a-tool&quot;&gt;Step 1: add a tool&lt;/h2&gt;
&lt;p&gt;Create one file under &lt;code&gt;agent/tools/&lt;/code&gt;, named after the tool, and re-export the Tabstack tool as its default export.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// agent/tools/research_question.ts&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; { researchQuestionTool &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;as&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; default&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; } &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;@tabstack/eve&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That is the whole integration. Your agent can now answer open questions using multiple web sources, with citations.&lt;/p&gt;
&lt;p&gt;The filename is load-bearing. eve derives tool identity from the path, so &lt;code&gt;research_question.ts&lt;/code&gt; is what makes the tool &lt;code&gt;research_question&lt;/code&gt;. It has to match exactly, and not only because eve requires it. That name is identical in the LangChain, Vercel AI SDK, Mastra, and Python packages, so a prompt written against one works against all of them. Rename the file to &lt;code&gt;web_search.ts&lt;/code&gt; and you have quietly opted out of that.&lt;/p&gt;
&lt;h2 id=&quot;step-2-add-the-rest&quot;&gt;Step 2: add the rest&lt;/h2&gt;
&lt;p&gt;Same pattern, one file each:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// agent/tools/extract_page_content.ts&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; { extractPageContentTool &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;as&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; default&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; } &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;@tabstack/eve&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// agent/tools/extract_structured_data.ts&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; { extractStructuredDataTool &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;as&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; default&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; } &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;@tabstack/eve&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// agent/tools/generate_structured_data.ts&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; { generateStructuredDataTool &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;as&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; default&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; } &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;@tabstack/eve&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// agent/tools/automate_browser_task.ts&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; { automateBrowserTaskTool &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;as&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; default&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; } &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;@tabstack/eve&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here is what each one buys you:&lt;/p&gt;
&lt;div class=&quot;table-scroll&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File name&lt;/th&gt;
&lt;th&gt;Export&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;extract_structured_data.ts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;extractStructuredDataTool&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Pull specific fields from a URL into a JSON shape you define&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;extract_page_content.ts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;extractPageContentTool&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fetch a page as clean markdown&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;research_question.ts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;researchQuestionTool&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Synthesised answer with cited sources across multiple pages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;generate_structured_data.ts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;generateStructuredDataTool&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fetch a page, then AI-transform it into derived JSON&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;automate_browser_task.ts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;automateBrowserTaskTool&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Run a multi-step, natural language browser task&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;The tools resolve &lt;code&gt;TABSTACK_API_KEY&lt;/code&gt; lazily on first call, so importing the package never requires a key to be set. Your build steps and tests stay happy without secrets.&lt;/p&gt;
&lt;h2 id=&quot;step-3-choose-your-toolset-by-choosing-your-files&quot;&gt;Step 3: choose your toolset by choosing your files&lt;/h2&gt;
&lt;p&gt;This is where eve’s convention pays off. In every other framework, narrowing the toolset means editing the array or object you pass to the agent. Here you just do not create the file.&lt;/p&gt;
&lt;p&gt;So ask yourself what your agent actually needs before you create all five. If it only reads and researches, create two files and stop. The model never sees &lt;code&gt;automate_browser_task&lt;/code&gt;, so it can never decide to try it. Your toolset is visible in a directory listing, which is a more honest description of your agent’s capabilities than most codebases manage.&lt;/p&gt;
&lt;p&gt;That restraint is worth exercising. Every tool you expose is another option the model weighs on every step, and another way for it to choose wrong. Smaller models feel this most.&lt;/p&gt;
&lt;p&gt;One tradeoff worth naming. Unlike the other Tabstack adapters, this package has no aggregate &lt;code&gt;tabstackTools&lt;/code&gt; export. Given the file convention there is nowhere sensible to put one, so I think it is the right call, but if you are moving code over from the LangChain or Vercel AI SDK packages it is the difference you will hit first.&lt;/p&gt;
&lt;h2 id=&quot;step-4-know-what-the-model-is-filling-in&quot;&gt;Step 4: know what the model is filling in&lt;/h2&gt;
&lt;p&gt;You never write these arguments yourself, but you will read them in traces when something goes sideways:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;extract_structured_data&lt;/code&gt;: &lt;code&gt;url&lt;/code&gt;, &lt;code&gt;json_schema_json&lt;/code&gt;, a JSON encoded JSON Schema string&lt;/li&gt;
&lt;li&gt;&lt;code&gt;extract_page_content&lt;/code&gt;: &lt;code&gt;url&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;research_question&lt;/code&gt;: &lt;code&gt;query&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;generate_structured_data&lt;/code&gt;: &lt;code&gt;url&lt;/code&gt;, &lt;code&gt;instructions&lt;/code&gt;, &lt;code&gt;json_schema_json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;automate_browser_task&lt;/code&gt;: &lt;code&gt;task&lt;/code&gt;, plus optional &lt;code&gt;url&lt;/code&gt;, &lt;code&gt;guardrails&lt;/code&gt;, &lt;code&gt;data&lt;/code&gt;, &lt;code&gt;country&lt;/code&gt;, &lt;code&gt;max_iterations&lt;/code&gt;, &lt;code&gt;max_validation_attempts&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The distinction people get wrong is extract versus generate. Extract pulls out what is already written on the page. Generate fetches the page and produces something derived from it, like a category or a summary nobody wrote down. If the field exists in the HTML, extract it. If you are asking for judgement, generate it.&lt;/p&gt;
&lt;p&gt;There are also optional inputs the model can pass for finer control, sent to Tabstack only when present:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;effort&lt;/code&gt;, as &lt;code&gt;&quot;min&quot;&lt;/code&gt;, &lt;code&gt;&quot;standard&quot;&lt;/code&gt;, or &lt;code&gt;&quot;max&quot;&lt;/code&gt;, on the three fetch-based tools. &lt;code&gt;&quot;max&quot;&lt;/code&gt; does full server-side browser rendering.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nocache&lt;/code&gt; to bypass the cache&lt;/li&gt;
&lt;li&gt;&lt;code&gt;country&lt;/code&gt;, an ISO 3166-1 alpha-2 code, for geotargeted fetches&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mode&lt;/code&gt;, as &lt;code&gt;&quot;fast&quot;&lt;/code&gt; or &lt;code&gt;&quot;balanced&quot;&lt;/code&gt;, on &lt;code&gt;research_question&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If a page comes back thin or empty, it is almost always a client-rendered app and &lt;code&gt;effort: &quot;max&quot;&lt;/code&gt; is the fix. That single argument is the entire replacement for running a browser. No Playwright, no binaries in your image, nothing to keep patched.&lt;/p&gt;
&lt;p&gt;You can steer this from your agent’s instructions rather than in code:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;If a page returns little or no content, retry once with effort set to max. Prefer research_question for open questions, and always cite the sources you used.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;step-5-use-a-custom-key-or-client&quot;&gt;Step 5: use a custom key or client&lt;/h2&gt;
&lt;p&gt;The default tools read &lt;code&gt;TABSTACK_API_KEY&lt;/code&gt; from the environment. When you need a key per tenant, a custom base URL, or you want to reuse an SDK client you already have, build the tools explicitly and re-export the one you want:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// agent/tools/research_question.ts&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; { createTabstackEveTools } &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;@tabstack/eve&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; tools&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; createTabstackEveTools&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;({ apiKey: process.env.&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;MY_KEY&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; default&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; tools.research_question;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The file convention still applies. The filename decides the tool name, whatever you did inside.&lt;/p&gt;
&lt;h2 id=&quot;step-6-failures&quot;&gt;Step 6: failures&lt;/h2&gt;
&lt;p&gt;When a tool call fails it throws &lt;code&gt;TabstackToolError&lt;/code&gt;, carrying a normalised message plus an HTTP &lt;code&gt;status&lt;/code&gt; for API errors, and eve surfaces it in the tool result. The model sees a readable failure rather than a stack trace, which means it can retry or explain rather than falling over.&lt;/p&gt;
&lt;p&gt;Inputs are also validated against the core Zod schema before the SDK call runs. When a model produces malformed arguments, it fails fast with a clear message instead of sending a broken request and waiting for the API to reject it.&lt;/p&gt;
&lt;p&gt;One behaviour to know up front: &lt;code&gt;automate_browser_task&lt;/code&gt; runs non-interactively. It never pauses for human-in-the-loop form input, so it cannot block your agent waiting for someone to fill in a field. It returns the final answer along with the data it extracted and the pages it visited. If you genuinely need a human in the middle of a browser session, that is the interactive mode flow in the API rather than this tool.&lt;/p&gt;
&lt;h2 id=&quot;where-to-take-it-next&quot;&gt;Where to take it next&lt;/h2&gt;
&lt;p&gt;Start with two files. &lt;code&gt;research_question.ts&lt;/code&gt; and &lt;code&gt;extract_page_content.ts&lt;/code&gt; cover most of what a general assistant needs, and you can add the structured tools when a real use case asks for them.&lt;/p&gt;
&lt;p&gt;When you add &lt;code&gt;automate_browser_task&lt;/code&gt;, put a guardrails instruction in your agent’s system prompt. It is a plain English constraint on what a run may do, and on anything pointed at a live site it is the difference between a read-only browse and an agent cheerfully clicking a button you did not want clicked.&lt;/p&gt;
&lt;p&gt;The package is on npm as &lt;a href=&quot;https://www.npmjs.com/package/@tabstack/eve&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;code&gt;@tabstack/eve&lt;/code&gt;&lt;/a&gt;, and the full API reference is at &lt;a href=&quot;https://docs.tabstack.ai&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;docs.tabstack.ai&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Ready to build? &lt;a href=&quot;https://console.tabstack.ai/signup&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Sign up&lt;/a&gt; for 10,000 free credits, no card required, and hand your eve agent its first live page. Full API in the &lt;a href=&quot;https://docs.tabstack.ai&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;docs&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>Stream a research assistant with the Vercel AI SDK and Tabstack</title><link>https://tabstack.ai/blog/stream-research-assistant-vercel-ai-sdk-tabstack</link><guid isPermaLink="true">https://tabstack.ai/blog/stream-research-assistant-vercel-ai-sdk-tabstack</guid><description>A step by step tutorial: install @tabstack/ai, give the model web tools, and stream a cited answer token by token instead of making users wait.</description><pubDate>Thu, 30 Jul 2026 05:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Web research is slow. Reading four pages and synthesising an answer takes seconds, sometimes tens of seconds, and there is no clever trick that makes it instant.&lt;/p&gt;
&lt;p&gt;What you can do is stop making people stare at a spinner while it happens. This tutorial builds a research assistant on the Vercel AI SDK that streams its answer as it forms, using Tabstack for the web access. The whole thing is about thirty lines.&lt;/p&gt;
&lt;h2 id=&quot;what-you-need&quot;&gt;What you need&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;A Node project (the package ships dual ESM and CJS builds)&lt;/li&gt;
&lt;li&gt;A Tabstack API key from &lt;a href=&quot;https://console.tabstack.ai&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;console.tabstack.ai&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;An OpenAI key, or any other provider the AI SDK supports&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;npm&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; install&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; @tabstack/ai&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; ai&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; zod&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; @ai-sdk/openai&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; TABSTACK_API_KEY&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;your-key-here&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; OPENAI_API_KEY&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;your-openai-key&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;ai&lt;/code&gt; and &lt;code&gt;zod&lt;/code&gt; are peer dependencies, so your app keeps one instance of each. The package works with AI SDK v5 and later, and is v6 ready.&lt;/p&gt;
&lt;h2 id=&quot;step-1-give-the-model-tools-and-get-out-of-the-way&quot;&gt;Step 1: give the model tools and get out of the way&lt;/h2&gt;
&lt;p&gt;The AI SDK expects &lt;code&gt;tools&lt;/code&gt; to be an object keyed by tool name. That is exactly what &lt;code&gt;tabstackTools&lt;/code&gt; is, so it passes straight through.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; { openai } &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;@ai-sdk/openai&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; { generateText, stepCountIs } &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;ai&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; { tabstackTools } &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;@tabstack/ai&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; { &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;text&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; } &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; generateText&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  model: &lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;openai&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;gpt-4o&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  tools: tabstackTools,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  stopWhen: &lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;stepCountIs&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;5&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  prompt: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;What are Vercel&apos;s pricing plans and how do they compare?&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;});&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;console.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;log&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(text);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That is a working web-connected agent. &lt;code&gt;tabstackTools&lt;/code&gt; reads &lt;code&gt;TABSTACK_API_KEY&lt;/code&gt; from the environment, so there is no client to construct.&lt;/p&gt;
&lt;p&gt;The one line people leave out is &lt;code&gt;stopWhen&lt;/code&gt;. Without it the SDK makes a single call, the model asks for a tool, and you get back a response containing a tool call and no text. &lt;code&gt;stepCountIs(5)&lt;/code&gt; tells it to keep going: call the tool, take the result, and carry on until it has an actual answer. Five is a reasonable ceiling for research work. Raise it if your agent legitimately needs to chain several lookups, lower it if you want a hard cost bound.&lt;/p&gt;
&lt;h2 id=&quot;step-2-stream-it&quot;&gt;Step 2: stream it&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;generateText&lt;/code&gt; waits for everything. For anything user-facing, swap to &lt;code&gt;streamText&lt;/code&gt; and start showing output immediately.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; { openai } &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;@ai-sdk/openai&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; { streamText, stepCountIs } &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;ai&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; { tabstackTools } &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;@tabstack/ai&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; result&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; streamText&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  model: &lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;openai&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;gpt-4o&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  tools: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    research_question: tabstackTools.research_question,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    extract_page_content: tabstackTools.extract_page_content,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  stopWhen: &lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;stepCountIs&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;5&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  prompt: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;Summarize the latest on quantum error correction, with sources.&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;});&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;for&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; chunk&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; of&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; result.textStream) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  process.stdout.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;write&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(chunk);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note what happened to &lt;code&gt;tools&lt;/code&gt;. Because it is a plain object keyed by tool name, narrowing the toolset is just picking keys. Here the assistant only researches and reads, so it gets two tools instead of five.&lt;/p&gt;
&lt;p&gt;That is worth doing deliberately. Every tool you pass is another option the model weighs on every step, and another way for it to pick wrong. On smaller and cheaper models, cutting the toolset to what the job needs is one of the highest-leverage changes you can make.&lt;/p&gt;
&lt;p&gt;One honest note about streaming with tools, and it is the thing I would fix first in any demo. The text stream goes quiet while a tool runs, because the model is waiting on a network call it cannot narrate. Users read that silence as a hang. Surface tool activity in your UI, even if it is just “searching the web”, so the pause has an explanation.&lt;/p&gt;
&lt;h2 id=&quot;step-3-know-your-five-tools&quot;&gt;Step 3: know your five tools&lt;/h2&gt;
&lt;div class=&quot;table-scroll&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;extract_structured_data&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Pull specific fields from a URL into a JSON shape you define&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;extract_page_content&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fetch a page as clean markdown&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;research_question&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A synthesised answer with cited sources across multiple pages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;generate_structured_data&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fetch a page, then AI-transform it into derived or reshaped JSON&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;automate_browser_task&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Run a multi-step, natural language browser task&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;The model fills in the inputs, but knowing the shapes helps when you are debugging why it chose badly:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;extract_structured_data&lt;/code&gt;: &lt;code&gt;url&lt;/code&gt;, &lt;code&gt;json_schema_json&lt;/code&gt;, a JSON encoded JSON Schema string&lt;/li&gt;
&lt;li&gt;&lt;code&gt;extract_page_content&lt;/code&gt;: &lt;code&gt;url&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;research_question&lt;/code&gt;: &lt;code&gt;query&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;generate_structured_data&lt;/code&gt;: &lt;code&gt;url&lt;/code&gt;, &lt;code&gt;instructions&lt;/code&gt;, &lt;code&gt;json_schema_json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;automate_browser_task&lt;/code&gt;: &lt;code&gt;task&lt;/code&gt;, plus optional &lt;code&gt;url&lt;/code&gt;, &lt;code&gt;guardrails&lt;/code&gt;, &lt;code&gt;data&lt;/code&gt;, &lt;code&gt;country&lt;/code&gt;, &lt;code&gt;max_iterations&lt;/code&gt;, &lt;code&gt;max_validation_attempts&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The distinction that matters most is extract versus generate. Extract pulls out what is already written on the page. Generate fetches the page and produces something derived from it, like a category or a summary nobody wrote. If the field exists in the HTML, extract it. If you are asking for judgement, generate it.&lt;/p&gt;
&lt;h2 id=&quot;step-4-the-flags-that-matter-in-production&quot;&gt;Step 4: the flags that matter in production&lt;/h2&gt;
&lt;p&gt;The model can pass these itself, and they are only sent to Tabstack when present, so defaults stay intact when they are omitted.&lt;/p&gt;
&lt;p&gt;On &lt;code&gt;extract_structured_data&lt;/code&gt;, &lt;code&gt;extract_page_content&lt;/code&gt;, and &lt;code&gt;generate_structured_data&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;effort&lt;/code&gt;: &lt;code&gt;&quot;min&quot;&lt;/code&gt;, &lt;code&gt;&quot;standard&quot;&lt;/code&gt;, or &lt;code&gt;&quot;max&quot;&lt;/code&gt;. Reach for &lt;code&gt;&quot;max&quot;&lt;/code&gt; on JavaScript-heavy pages, where it does full server-side browser rendering.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nocache&lt;/code&gt;: bypass the cache when you need a fresh read.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;country&lt;/code&gt;: an ISO 3166-1 alpha-2 code for geotargeted fetches.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;On &lt;code&gt;research_question&lt;/code&gt;: &lt;code&gt;mode&lt;/code&gt; as &lt;code&gt;&quot;fast&quot;&lt;/code&gt; or &lt;code&gt;&quot;balanced&quot;&lt;/code&gt;, plus &lt;code&gt;nocache&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;If a page comes back suspiciously empty, it is nearly always a client-rendered app and &lt;code&gt;effort: &quot;max&quot;&lt;/code&gt; is the fix. That is the whole workaround. No Playwright, no browser binary in your container image, no keeping either patched.&lt;/p&gt;
&lt;p&gt;You can steer these from the system prompt rather than in code, which is often the cleaner move:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; result&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; streamText&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  model: &lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;openai&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;gpt-4o&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  tools: tabstackTools,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  stopWhen: &lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;stepCountIs&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;5&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  system:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;    &quot;You research questions using web tools. &quot;&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; +&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;    &quot;If a page returns little or no content, retry once with effort set to max. &quot;&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; +&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;    &quot;Prefer research_question for open questions and cite every source you use.&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  prompt: userQuestion,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;step-5-failures&quot;&gt;Step 5: failures&lt;/h2&gt;
&lt;p&gt;When a tool call fails it throws &lt;code&gt;TabstackToolError&lt;/code&gt;, carrying a readable message and, for API failures, an HTTP &lt;code&gt;status&lt;/code&gt;. Inside an agent loop the AI SDK surfaces this in the tool result, so the model can see the failure and react. If you are calling a tool’s &lt;code&gt;execute&lt;/code&gt; directly, catch it yourself:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; { TabstackToolError, tabstackTools } &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;@tabstack/ai&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;try&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;  await&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; tabstackTools.extract_page_content.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;execute&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    { url: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;https://example.com&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    { toolCallId: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;1&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, messages: [] },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  );&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;} &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;catch&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; (err) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;  if&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; (err &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;instanceof&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; TabstackToolError&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    console.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;error&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;`Tabstack failed (${&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;err&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;status&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; ??&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;no status&quot;}): ${&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;err&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;message&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;}`&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One error type instead of separate handling for transport, parsing, and API problems.&lt;/p&gt;
&lt;h2 id=&quot;step-6-configuration&quot;&gt;Step 6: configuration&lt;/h2&gt;
&lt;p&gt;The defaults read &lt;code&gt;TABSTACK_API_KEY&lt;/code&gt; from the environment, which is what you want in most apps. When you need a key per tenant, a custom base URL, or you already have an SDK client in scope:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; { createTabstackAiTools } &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;@tabstack/ai&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; tools&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; createTabstackAiTools&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;({ apiKey: process.env.&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;MY_KEY&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; });&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The client is created lazily, so importing the package never requires a key to be set. Your module imports stay safe in tests and in build steps that do not have secrets.&lt;/p&gt;
&lt;h2 id=&quot;where-to-take-it-next&quot;&gt;Where to take it next&lt;/h2&gt;
&lt;p&gt;Render the citations. &lt;code&gt;research_question&lt;/code&gt; returns sources alongside the answer, and showing them is what separates an assistant people trust from one they double-check.&lt;/p&gt;
&lt;p&gt;Narrow the toolset per route. A pricing page reader needs &lt;code&gt;extract_structured_data&lt;/code&gt; and nothing else. A general assistant needs the lot. There is no rule that every surface in your app gets the same five.&lt;/p&gt;
&lt;p&gt;If part of this workload moves to Python or another framework, the tool names and inputs are identical in &lt;a href=&quot;https://www.npmjs.com/package/@tabstack/langchain&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;code&gt;@tabstack/langchain&lt;/code&gt;&lt;/a&gt; and &lt;a href=&quot;https://pypi.org/project/langchain-tabstack/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;code&gt;langchain-tabstack&lt;/code&gt;&lt;/a&gt;. Your prompts carry over, because the model sees the same tools.&lt;/p&gt;
&lt;p&gt;The package is on npm as &lt;a href=&quot;https://www.npmjs.com/package/@tabstack/ai&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;code&gt;@tabstack/ai&lt;/code&gt;&lt;/a&gt;, and the full API reference is at &lt;a href=&quot;https://docs.tabstack.ai&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;docs.tabstack.ai&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Ready to build? &lt;a href=&quot;https://console.tabstack.ai/signup&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Sign up&lt;/a&gt; for 10,000 free credits, no card required, and stream your first cited answer. Full API in the &lt;a href=&quot;https://docs.tabstack.ai&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;docs&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>Build a competitor brief agent with LangChain.js and Tabstack</title><link>https://tabstack.ai/blog/competitor-brief-langchain-js-tabstack</link><guid isPermaLink="true">https://tabstack.ai/blog/competitor-brief-langchain-js-tabstack</guid><description>A step by step TypeScript tutorial: install @tabstack/langchain, call tools directly, then assemble an agent that reads a competitor&apos;s pages and produces a structured brief.</description><pubDate>Wed, 29 Jul 2026 05:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The Python tutorial built a research agent. This one builds something with a shape you can hand to a colleague: a competitor brief. Point it at a company, get back their pricing, their positioning, and what people are saying about them, with sources.&lt;/p&gt;
&lt;p&gt;Same five tools as the Python package, same names, same inputs. If you have read the Python version, the only thing that changes here is the syntax, which I still find slightly surprising every time I move between them.&lt;/p&gt;
&lt;h2 id=&quot;what-you-need&quot;&gt;What you need&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;A Node project (the package ships dual ESM and CJS builds)&lt;/li&gt;
&lt;li&gt;A Tabstack API key from &lt;a href=&quot;https://console.tabstack.ai&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;console.tabstack.ai&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;An OpenAI key, or any other model LangChain.js supports&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;npm&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; install&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; @tabstack/langchain&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; @langchain/core&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; zod&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; langchain&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; @langchain/openai&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; TABSTACK_API_KEY&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;your-key-here&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; OPENAI_API_KEY&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;your-openai-key&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;@langchain/core&lt;/code&gt; and &lt;code&gt;zod&lt;/code&gt; are peer dependencies rather than dependencies. That is intentional. Your app keeps a single instance of each, which avoids the genuinely miserable class of bug where two copies of Zod disagree about whether your schema is a schema.&lt;/p&gt;
&lt;h2 id=&quot;step-1-pull-structured-data-off-a-page&quot;&gt;Step 1: pull structured data off a page&lt;/h2&gt;
&lt;p&gt;Start with the piece the whole brief hangs on. You know the pricing page URL, and you know what you want from it, so describe the shape and ask for it.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; { extractStructuredDataTool } &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;@tabstack/langchain&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; pricing&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; extractStructuredDataTool.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;invoke&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  url: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;https://example.com/pricing&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  json_schema_json: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;JSON&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;stringify&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    type: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;object&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    properties: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;      plans: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;        type: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;array&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;        items: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;          type: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;object&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;          properties: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;            name: { type: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;string&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, description: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;Plan name&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;            price: { type: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;number&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, description: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;Monthly price in USD&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;            features: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;              type: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;array&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;              items: { type: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;string&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;              description: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;Included features&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;            },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;          },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;        },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;      },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  }),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;});&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;for&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; plan&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; of&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; pricing.plans) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  console.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;log&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;`${&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;plan&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;}: $${&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;plan&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;price&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;}`&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two details worth pausing on.&lt;/p&gt;
&lt;p&gt;The schema goes in as &lt;code&gt;json_schema_json&lt;/code&gt;, a JSON encoded string rather than an object. That looks fussy until you remember who else calls this tool. When an agent invokes it, the model has to produce that argument, and a string is a shape models get right far more often than nested objects.&lt;/p&gt;
&lt;p&gt;If you write one thing carefully in this whole tutorial, make it the field descriptions. The &lt;code&gt;description&lt;/code&gt; on each field is not decoration. It is the instruction that tells the extractor which number on the page you meant when you said &lt;code&gt;price&lt;/code&gt;. A pricing page has annual figures, discounted figures, and per-seat figures on it. The description is how you disambiguate. Vague or missing descriptions are the most common reason extraction results disappoint.&lt;/p&gt;
&lt;p&gt;Unlike the Python package, the TypeScript tools return plain objects. No &lt;code&gt;JSON.parse&lt;/code&gt; step.&lt;/p&gt;
&lt;h2 id=&quot;step-2-research-what-is-not-on-any-single-page&quot;&gt;Step 2: research what is not on any single page&lt;/h2&gt;
&lt;p&gt;Pricing lives on a pricing page. Positioning and reputation do not live anywhere in particular, which is what &lt;code&gt;research_question&lt;/code&gt; is for.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; { researchQuestionTool } &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;@tabstack/langchain&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; { &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;answer&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;sources&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; } &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; researchQuestionTool.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;invoke&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  query: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;How do developers describe Example Corp&apos;s developer experience?&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;});&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;console.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;log&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(answer);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;for&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; source&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; of&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; sources) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  console.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;log&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;`- ${&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;source&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;title&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;}: ${&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;source&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;url&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;}`&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;sources&lt;/code&gt; comes back as &lt;code&gt;{ title, url }&lt;/code&gt; objects, which drops straight into a citation list. This is the part that makes a brief defensible rather than just confident. When someone asks where a claim came from, you have the page.&lt;/p&gt;
&lt;p&gt;While iterating, pass &lt;code&gt;mode: &quot;fast&quot;&lt;/code&gt;. Move to &lt;code&gt;&quot;balanced&quot;&lt;/code&gt; when you care more about quality than latency.&lt;/p&gt;
&lt;h2 id=&quot;step-3-generate-what-the-page-never-said&quot;&gt;Step 3: generate what the page never said&lt;/h2&gt;
&lt;p&gt;Extraction pulls out what exists. Sometimes you want something derived, like a category or a one-line summary that nobody wrote down. That is &lt;code&gt;generate_structured_data&lt;/code&gt;: fetch the page, then transform it.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; { generateStructuredDataTool } &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;@tabstack/langchain&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; positioning&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; generateStructuredDataTool.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;invoke&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  url: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;https://example.com&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  instructions:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;    &quot;Identify the primary audience this homepage is written for, &quot;&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; +&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;    &quot;the main problem it claims to solve, and its strongest differentiator.&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  json_schema_json: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;JSON&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;stringify&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    type: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;object&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    properties: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;      audience: { type: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;string&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;      problem: { type: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;string&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;      differentiator: { type: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;string&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  }),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The line between the two tools is simple. If the field is written on the page, extract it. If you are asking for judgement, generate it. Reaching for generate when extract would do costs you time and adds a layer where the answer can drift.&lt;/p&gt;
&lt;h2 id=&quot;step-4-assemble-the-agent&quot;&gt;Step 4: assemble the agent&lt;/h2&gt;
&lt;p&gt;Three tool calls, three pieces of a brief. You could orchestrate that yourself with three awaits, and for a fixed report you probably should. But the moment the shape of the question varies, let the model choose.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; { createAgent } &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;langchain&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; { tabstackTools } &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;@tabstack/langchain&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; agent&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; createAgent&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  model: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;openai:gpt-4o&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  tools: tabstackTools,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  systemPrompt:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;    &quot;You produce competitor briefs. &quot;&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; +&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;    &quot;Use extract_structured_data for facts on a known page such as pricing, &quot;&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; +&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;    &quot;extract_page_content when you need to read a page in full, &quot;&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; +&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;    &quot;generate_structured_data when you need a judgement the page does not state outright, &quot;&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; +&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;    &quot;and research_question for anything spread across multiple sources. &quot;&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; +&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;    &quot;Always cite the sources you used.&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;});&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; result&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; agent.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;invoke&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  messages: [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;      role: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;user&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;      content:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;        &quot;Build me a brief on Example Corp. Their pricing is at https://example.com/pricing.&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  ],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;});&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;console.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;log&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(result.messages.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;at&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)?.content);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;tabstackTools&lt;/code&gt; is all five, ready to pass straight in:&lt;/p&gt;
&lt;div class=&quot;table-scroll&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;When the model reaches for it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;extract_structured_data&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Specific fields from a URL you already know&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;extract_page_content&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The whole page as clean markdown&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;research_question&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;An open question needing several sources&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;generate_structured_data&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fetch a page, then transform it into derived JSON&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;automate_browser_task&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A multi-step browser task described in plain English&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;You do not have to pass all five. &lt;code&gt;tabstackTools&lt;/code&gt; is an array here, so filter it or import individual tools and build your own array if the agent needs two. Fewer tools means fewer chances to pick the wrong one, which matters more the smaller your model is.&lt;/p&gt;
&lt;p&gt;One inconsistency to watch, because it caught me out. The aggregate export shape is not the same across the Tabstack adapters. Here and in the OpenAI Agents and LlamaIndex packages it is an array. In &lt;code&gt;@tabstack/ai&lt;/code&gt; and &lt;code&gt;@tabstack/mastra&lt;/code&gt; it is an object keyed by tool name. That is not sloppiness, it is each package matching what its framework expects, but if you are wiring up two of these in one codebase, check before you spread.&lt;/p&gt;
&lt;p&gt;Notice what the system prompt is doing. It is not describing the tools, since each package ships descriptions written for the model to read. It is describing the job. That division keeps your prompts short and means tool description improvements reach you on upgrade rather than requiring a prompt rewrite.&lt;/p&gt;
&lt;h2 id=&quot;step-5-when-the-data-is-behind-an-interaction&quot;&gt;Step 5: when the data is behind an interaction&lt;/h2&gt;
&lt;p&gt;Some things are not on a page until you do something. A search box, a region selector, a “load more” button. That is &lt;code&gt;automate_browser_task&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; { automateBrowserTaskTool } &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;@tabstack/langchain&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; result&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; automateBrowserTaskTool.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;invoke&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  task: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;Find the top 3 trending repositories and their star counts&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  url: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;https://github.com/trending&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  guardrails: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;browse and extract only, do not submit forms&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;});&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;console.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;log&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(result.answer, result.pagesVisited, result.durationMs);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You get back &lt;code&gt;answer&lt;/code&gt;, &lt;code&gt;success&lt;/code&gt;, &lt;code&gt;iterations&lt;/code&gt;, &lt;code&gt;actions&lt;/code&gt;, &lt;code&gt;durationMs&lt;/code&gt;, &lt;code&gt;extracted&lt;/code&gt;, and &lt;code&gt;pagesVisited&lt;/code&gt;. The last few are what make failures diagnosable. When the answer is wrong, &lt;code&gt;actions&lt;/code&gt; and &lt;code&gt;pagesVisited&lt;/code&gt; usually tell you why in about ten seconds.&lt;/p&gt;
&lt;p&gt;Set &lt;code&gt;guardrails&lt;/code&gt;. It is a plain English constraint on what the run is allowed to do, and on anything pointed at a live site it is the difference between a read-only browse and an agent cheerfully clicking a button you did not want clicked.&lt;/p&gt;
&lt;p&gt;One thing to know up front: this tool runs non-interactively. It never pauses for human input, so it cannot block your agent loop waiting for someone to fill in a form. If you genuinely need a human in the middle of a browser session, that is the interactive mode flow in the API rather than this tool.&lt;/p&gt;
&lt;h2 id=&quot;step-6-flags-and-failures&quot;&gt;Step 6: flags and failures&lt;/h2&gt;
&lt;p&gt;Four optional inputs cover nearly everything. They are only sent when you pass them, so omitting them keeps Tabstack’s defaults.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Render a JS-heavy SPA, fresh, as if from the UK:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;await&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; extractPageContentTool.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;invoke&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  url: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;https://example.com/app&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  effort: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;max&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  nocache: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;true&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  country: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;GB&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;effort&lt;/code&gt; takes &lt;code&gt;&quot;min&quot;&lt;/code&gt;, &lt;code&gt;&quot;standard&quot;&lt;/code&gt;, or &lt;code&gt;&quot;max&quot;&lt;/code&gt;. If a page comes back suspiciously empty, it is almost always a JavaScript-rendered app and &lt;code&gt;&quot;max&quot;&lt;/code&gt; is the fix. That single keyword replaces installing Playwright, shipping a browser binary, and keeping both patched.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;nocache&lt;/code&gt; matters when you are watching something that moves, like prices. &lt;code&gt;country&lt;/code&gt; matters when the page varies by region, which for pricing pages is most of the time.&lt;/p&gt;
&lt;p&gt;Every tool throws one error type:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; { TabstackToolError, extractPageContentTool } &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;@tabstack/langchain&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;try&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;  await&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; extractPageContentTool.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;invoke&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;({ url: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;https://example.com&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;} &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;catch&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; (err) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;  if&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; (err &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;instanceof&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; TabstackToolError&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    console.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;error&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;`Tabstack failed (${&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;err&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;status&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; ??&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;no status&quot;}): ${&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;err&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;message&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;}`&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One &lt;code&gt;catch&lt;/code&gt; instead of separate handling for transport, parsing, and API errors. &lt;code&gt;status&lt;/code&gt; carries the HTTP status for API failures and is undefined for everything else, which is usually enough to decide between retrying and giving up.&lt;/p&gt;
&lt;h2 id=&quot;configuration-if-you-need-it&quot;&gt;Configuration, if you need it&lt;/h2&gt;
&lt;p&gt;The default tools read &lt;code&gt;TABSTACK_API_KEY&lt;/code&gt; from the environment. If you need a different key per tenant, a custom base URL, or you already have an SDK client you want reused:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; { createTabstackLangchainTools } &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;@tabstack/langchain&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; tools&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; createTabstackLangchainTools&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;({ apiKey: process.env.&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;MY_KEY&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; });&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;where-to-take-it-next&quot;&gt;Where to take it next&lt;/h2&gt;
&lt;p&gt;Turn the brief into a scheduled job. &lt;code&gt;nocache: true&lt;/code&gt; plus a diff against last week’s run is a change monitor, and it is maybe twenty more lines.&lt;/p&gt;
&lt;p&gt;If you are moving this to a streaming UI, the same five tools exist in &lt;code&gt;@tabstack/ai&lt;/code&gt; for the Vercel AI SDK, under identical names. Your system prompt carries over unchanged.&lt;/p&gt;
&lt;p&gt;The package is on npm as &lt;a href=&quot;https://www.npmjs.com/package/@tabstack/langchain&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;code&gt;@tabstack/langchain&lt;/code&gt;&lt;/a&gt;, and the full API reference is at &lt;a href=&quot;https://docs.tabstack.ai&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;docs.tabstack.ai&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Ready to build? &lt;a href=&quot;https://console.tabstack.ai/signup&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Sign up&lt;/a&gt; for 10,000 free credits, no card required, and generate your first competitor brief. Full API in the &lt;a href=&quot;https://docs.tabstack.ai&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;docs&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>Build a research agent that cites its sources with LangChain and Tabstack</title><link>https://tabstack.ai/blog/research-agent-langchain-tabstack</link><guid isPermaLink="true">https://tabstack.ai/blog/research-agent-langchain-tabstack</guid><description>A step by step tutorial: install langchain-tabstack, call a tool directly, then wrap the whole toolset in a LangChain agent that reads the web and shows its working.</description><pubDate>Tue, 28 Jul 2026 05:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Most agent tutorials stop at the point where the model says something plausible. I want to go one step further, because the interesting part is never the answer. It is the list of URLs underneath it.&lt;/p&gt;
&lt;p&gt;We are going to build a small research agent in Python. By the end you will have an agent that can read a specific page, extract typed fields from it, and answer open questions using multiple sources with citations attached. Roughly forty lines of code, no browser to install.&lt;/p&gt;
&lt;p&gt;If you have written LangChain before, this will feel familiar. If you have not, everything you need is here.&lt;/p&gt;
&lt;h2 id=&quot;what-you-need&quot;&gt;What you need&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Python 3.9 or later&lt;/li&gt;
&lt;li&gt;A Tabstack API key from &lt;a href=&quot;https://console.tabstack.ai&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;console.tabstack.ai&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;An OpenAI key, or any other chat model LangChain supports&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;pip&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; install&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; langchain-tabstack&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; langchain&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; langchain-openai&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; TABSTACK_API_KEY&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;your-key-here&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; OPENAI_API_KEY&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;your-openai-key&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;langchain-tabstack&lt;/code&gt; pulls in the official Tabstack SDK for you. There is nothing else to install, and specifically there is no Playwright and no browser binary. Page rendering happens on Tabstack’s side.&lt;/p&gt;
&lt;h2 id=&quot;step-1-call-a-tool-on-its-own&quot;&gt;Step 1: call a tool on its own&lt;/h2&gt;
&lt;p&gt;Before wiring anything into an agent, prove the plumbing works. Every tool in the package is exported individually and can be invoked directly, which makes debugging much easier later.&lt;/p&gt;
&lt;p&gt;Create &lt;code&gt;research.py&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; json&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; langchain_tabstack &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; research_question_tool&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;raw &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; research_question_tool.invoke(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    {&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;query&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;What are the latest developments in quantum error correction?&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;result &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; json.loads(raw)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;print&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(result[&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;answer&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;])&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;print&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;for&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; source &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; result[&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;sources&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;]:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    print&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;f&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;- &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;source[&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&apos;title&apos;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;]&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;source[&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&apos;url&apos;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;]&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Run it:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;python&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; research.py&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You get a synthesised answer, then the pages it was built from. That second part is the bit worth pausing on. The tool did not just fetch a search results page and hand the model a wall of text. It read across multiple sources and returned the answer alongside the specific pages behind it, so you can put those in front of a user.&lt;/p&gt;
&lt;p&gt;Two things about the return shape. It is a JSON string, not a dict, so &lt;code&gt;json.loads&lt;/code&gt; is doing real work. That is the LangChain Python convention for tool output rather than a Tabstack quirk. And &lt;code&gt;sources&lt;/code&gt; is a list of &lt;code&gt;{&quot;title&quot;: ..., &quot;url&quot;: ...}&lt;/code&gt; objects, which drops straight into a citation list in whatever you are building.&lt;/p&gt;
&lt;p&gt;The tool read &lt;code&gt;TABSTACK_API_KEY&lt;/code&gt; from the environment on its own. The client is created lazily, so importing the package without a key set does not blow up, which keeps your imports safe in tests.&lt;/p&gt;
&lt;h2 id=&quot;step-2-extract-typed-data-from-a-known-page&quot;&gt;Step 2: extract typed data from a known page&lt;/h2&gt;
&lt;p&gt;Research is for open questions. When you already know which page holds the data, use &lt;code&gt;extract_structured_data&lt;/code&gt; instead. You describe the shape you want, and you get that shape back.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; json&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; langchain_tabstack &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; extract_structured_data_tool&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;schema &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;    &quot;type&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;object&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;    &quot;properties&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;        &quot;stories&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;            &quot;type&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;array&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;            &quot;items&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;                &quot;type&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;object&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;                &quot;properties&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;                    &quot;title&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: {&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;string&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;},&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;                    &quot;points&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: {&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;number&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;description&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;Story points&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;},&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;                },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;            },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;raw &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; extract_structured_data_tool.invoke(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;        &quot;url&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;https://news.ycombinator.com&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;        &quot;json_schema_json&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: json.dumps(schema),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;for&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; story &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; json.loads(raw)[&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;stories&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;]:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    print&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(story[&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;points&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;], story[&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;title&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;])&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note &lt;code&gt;json_schema_json&lt;/code&gt;. The schema goes in as a JSON encoded string rather than a dict, which looks odd for about ten seconds until you remember who else calls this tool. When an agent invokes it, the model has to produce that argument, and a string is a shape models handle reliably.&lt;/p&gt;
&lt;p&gt;Write real descriptions on your fields. &lt;code&gt;&quot;description&quot;: &quot;Story points&quot;&lt;/code&gt; is not decoration, it is the instruction that tells the extractor which number on the page you actually meant. Schemas with vague or missing descriptions are the single most common cause of disappointing extraction results.&lt;/p&gt;
&lt;h2 id=&quot;step-3-hand-the-whole-toolset-to-an-agent&quot;&gt;Step 3: hand the whole toolset to an agent&lt;/h2&gt;
&lt;p&gt;Now the part you came for. Instead of deciding which tool to call, let the model decide.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; langchain.agents &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; create_agent&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; langchain_tabstack &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; TABSTACK_TOOLS&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;agent &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; create_agent(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;    &quot;openai:gpt-4o&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E36209&quot;&gt;    tools&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;TABSTACK_TOOLS&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E36209&quot;&gt;    system_prompt&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;        &quot;You are a research assistant with web intelligence tools. &quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;        &quot;Use extract_structured_data for specific fields from a URL, &quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;        &quot;extract_page_content for full page text, and research_question &quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;        &quot;for multi-source research. Always cite the sources you used.&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    ),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;result &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; agent.invoke(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    {&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;messages&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: [{&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;role&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;user&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;content&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;What are Vercel&apos;s pricing plans?&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;}]}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;print&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(result[&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;messages&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;][&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;].content)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One version note, because it is the first thing that will bite you. &lt;code&gt;create_agent&lt;/code&gt; is the LangChain 1.x replacement for &lt;code&gt;AgentExecutor&lt;/code&gt; plus &lt;code&gt;create_tool_calling_agent&lt;/code&gt;. If you are still on 0.x you will get an import error here, and the fix is upgrading LangChain rather than anything to do with Tabstack.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;TABSTACK_TOOLS&lt;/code&gt; is all five tools, ready to pass straight in:&lt;/p&gt;
&lt;div class=&quot;table-scroll&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;When the model reaches for it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;extract_structured_data&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Specific fields from a URL you already know&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;extract_page_content&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The whole page as clean markdown&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;research_question&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;An open question needing several sources&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;generate_structured_data&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fetch a page, then AI-transform it into derived JSON&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;automate_browser_task&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A multi-step browser task described in plain English&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;You do not have to pass all five. &lt;code&gt;TABSTACK_TOOLS&lt;/code&gt; is a list, so slice it or import individual tools if your agent only needs two. Fewer tools means fewer chances for the model to pick the wrong one, and on smaller models that matters.&lt;/p&gt;
&lt;p&gt;Ask it something that needs more than one page and watch which tool it selects. That decision is driven by the tool descriptions shipped in the package, which is why they are written for the model to read rather than for you.&lt;/p&gt;
&lt;h2 id=&quot;step-4-the-flags-you-will-need-in-production&quot;&gt;Step 4: the flags you will need in production&lt;/h2&gt;
&lt;p&gt;Four optional inputs cover almost everything you will hit. They are only sent when you pass them, so leaving them out keeps Tabstack’s defaults.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; langchain_tabstack &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; extract_page_content_tool&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;markdown &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; extract_page_content_tool.invoke(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;        &quot;url&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;https://example.com/app&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;        &quot;effort&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;max&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,      &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# full server-side browser rendering&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;        &quot;nocache&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;True&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,      &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# skip the cache, fetch fresh&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;        &quot;country&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;GB&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,      &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# fetch as if from the UK&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;effort&lt;/code&gt; takes &lt;code&gt;&quot;min&quot;&lt;/code&gt;, &lt;code&gt;&quot;standard&quot;&lt;/code&gt;, or &lt;code&gt;&quot;max&quot;&lt;/code&gt;. If a page comes back suspiciously empty, it is almost always a JavaScript-rendered app and &lt;code&gt;&quot;max&quot;&lt;/code&gt; is the fix. This is the flag that replaces an entire Playwright setup, and it costs you one keyword argument instead of a browser binary in your Dockerfile.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;nocache&lt;/code&gt; matters when you are monitoring something that changes, like prices or stock. &lt;code&gt;country&lt;/code&gt; matters when the page you want varies by region, which for pricing pages is most of the time.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;research_question&lt;/code&gt; takes &lt;code&gt;mode&lt;/code&gt; as &lt;code&gt;&quot;fast&quot;&lt;/code&gt; or &lt;code&gt;&quot;balanced&quot;&lt;/code&gt; instead of &lt;code&gt;effort&lt;/code&gt;. Start with &lt;code&gt;&quot;fast&quot;&lt;/code&gt; while you are iterating, move to &lt;code&gt;&quot;balanced&quot;&lt;/code&gt; when quality matters more than latency.&lt;/p&gt;
&lt;h2 id=&quot;step-5-handle-failures-like-an-adult&quot;&gt;Step 5: handle failures like an adult&lt;/h2&gt;
&lt;p&gt;Pages go away. Sites rate limit. Networks wobble. Every tool in the package raises one error type:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; langchain_tabstack &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; TabstackToolError, extract_page_content_tool&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;try&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    markdown &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; extract_page_content_tool.invoke({&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;url&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;https://example.com&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;})&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;except&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; TabstackToolError &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;as&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; err:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    print&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;f&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;Tabstack failed (&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;err.status &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;or&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &apos;no status&apos;&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;): &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;err&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One &lt;code&gt;except&lt;/code&gt; clause instead of catching transport errors, parse errors, and API errors separately. &lt;code&gt;err.status&lt;/code&gt; carries the HTTP status for API failures and is &lt;code&gt;None&lt;/code&gt; for everything else, which is usually enough to decide between retrying and giving up.&lt;/p&gt;
&lt;h2 id=&quot;step-6-go-async&quot;&gt;Step 6: go async&lt;/h2&gt;
&lt;p&gt;If you are fetching several pages, do not do it one at a time. Every tool supports &lt;code&gt;ainvoke&lt;/code&gt; natively, backed by an async Tabstack client, so it runs in your event loop rather than bouncing through a thread pool:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; asyncio&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; langchain_tabstack &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; extract_page_content_tool&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;async&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; def&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; fetch_all&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(urls):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; asyncio.gather(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;        *&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(extract_page_content_tool.ainvoke({&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;url&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: url}) &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;for&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; url &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; urls)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    )&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;pages &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; asyncio.run(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    fetch_all(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;        [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;            &quot;https://example.com/one&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;            &quot;https://example.com/two&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;            &quot;https://example.com/three&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;        ]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    )&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Three pages in the time of the slowest one. This is the difference between an agent that feels responsive and one people abandon.&lt;/p&gt;
&lt;h2 id=&quot;where-to-take-it-next&quot;&gt;Where to take it next&lt;/h2&gt;
&lt;p&gt;You have the whole surface now. The obvious next moves:&lt;/p&gt;
&lt;p&gt;Swap &lt;code&gt;research_question&lt;/code&gt; output into your UI as a real citation list rather than printing it. The &lt;code&gt;sources&lt;/code&gt; array is already the right shape for that.&lt;/p&gt;
&lt;p&gt;Try &lt;code&gt;generate_structured_data&lt;/code&gt; when the field you want is not written on the page. Extraction pulls out what exists. Generation produces what does not, like a category or a one-line summary, in a schema you define.&lt;/p&gt;
&lt;p&gt;If you move this agent to TypeScript later, the tool names and inputs are identical in &lt;code&gt;@tabstack/langchain&lt;/code&gt; and &lt;code&gt;@tabstack/ai&lt;/code&gt;. Your prompts carry over unchanged, because the model sees exactly the same tools.&lt;/p&gt;
&lt;p&gt;The full API reference lives at &lt;a href=&quot;https://docs.tabstack.ai&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;docs.tabstack.ai&lt;/a&gt;, and the package is on PyPI as &lt;a href=&quot;https://pypi.org/project/langchain-tabstack/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;code&gt;langchain-tabstack&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Ready to build? &lt;a href=&quot;https://console.tabstack.ai/signup&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Sign up&lt;/a&gt; for 10,000 free credits, no card required, and give your agent its first cited answer. Full API in the &lt;a href=&quot;https://docs.tabstack.ai&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;docs&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>Turn any page into clean markdown or typed JSON, cheaply</title><link>https://tabstack.ai/blog/extract-most-used-endpoint</link><guid isPermaLink="true">https://tabstack.ai/blog/extract-most-used-endpoint</guid><description>Most web-data jobs are just fetch-and-clean, not an agent. Turn any URL into clean markdown or typed JSON, cheaply. Why it&apos;s the right default.</description><pubDate>Mon, 20 Jul 2026 05:00:00 GMT</pubDate><content:encoded>&lt;p&gt;If I had to bet on which Tabstack endpoint you will call most often, I would put my money on &lt;code&gt;/extract&lt;/code&gt; without thinking twice. It is not the one people get excited about in the demo. Research and automate get the “wow.” Extract is the one that ends up in production, running thousands of times a day, doing the unglamorous work that everything else is built on top of.&lt;/p&gt;
&lt;p&gt;Here is the thing about extract: it is deterministic, it is cheap, and it does exactly one job per call. Those are not limitations. They are the reason it is the right default for most web data work. Before you reach for an agent, ask whether the answer is just “fetch this page and give me the clean version.” A lot of the time, it is.&lt;/p&gt;
&lt;p&gt;Extract has two methods. They solve two different problems, so let me take them one at a time.&lt;/p&gt;
&lt;h2 id=&quot;extractmarkdown-the-page-minus-the-noise&quot;&gt;extract.markdown: the page, minus the noise&lt;/h2&gt;
&lt;p&gt;Every web page is wrapped in stuff you do not want. Navigation, ads, cookie banners, sidebars, footers, the same boilerplate on every page of the site. When all you need is the actual content, &lt;code&gt;extract.markdown&lt;/code&gt; fetches the URL, strips the cruft, and hands you clean markdown.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; Tabstack &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &apos;@tabstack/sdk&apos;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; client&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; Tabstack&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; result&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; client.extract.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;markdown&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  url: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&apos;https://example.com/blog/article&apos;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;});&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;console.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;log&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(result.content);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;By default, the page’s metadata (title, author, description, and so on) comes embedded as YAML frontmatter at the top of &lt;code&gt;content&lt;/code&gt;. That is handy if you are feeding a static site generator that already expects frontmatter. For most programmatic use, though, you want the metadata as a structured object, not a string you have to parse. Add &lt;code&gt;metadata: true&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; result&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; client.extract.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;markdown&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  url: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&apos;https://example.com/blog/article&apos;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  metadata: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;true&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;});&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;console.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;log&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(result.content);          &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// pure markdown, no frontmatter&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;console.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;log&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(result.metadata?.title);  &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// &quot;Example Article Title&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now &lt;code&gt;content&lt;/code&gt; is clean markdown and &lt;code&gt;metadata&lt;/code&gt; is a typed object with &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;author&lt;/code&gt;, &lt;code&gt;publisher&lt;/code&gt;, &lt;code&gt;image&lt;/code&gt;, &lt;code&gt;site_name&lt;/code&gt;, &lt;code&gt;type&lt;/code&gt;, and friends. No YAML parsing, which means no broken parse the day a title contains a colon or a quote.&lt;/p&gt;
&lt;p&gt;Where does this earn its keep? Mostly two places. The first is anything that feeds an LLM: RAG pipelines, summarization jobs, content classification. Models work better on clean markdown than on a soup of HTML, and you are not paying for tokens you will throw away. The second is content systems: read-it-later apps, aggregators, archives, anything that wants a stable, storable version of a page instead of the live HTML.&lt;/p&gt;
&lt;p&gt;A couple of parameters worth knowing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;effort&lt;/code&gt; controls how hard the renderer works, with values &lt;code&gt;min&lt;/code&gt;, &lt;code&gt;standard&lt;/code&gt; (the default), and &lt;code&gt;max&lt;/code&gt;. Static pages are fine on &lt;code&gt;standard&lt;/code&gt;. Pages that build their content with JavaScript may need &lt;code&gt;max&lt;/code&gt;. Pages that are already plain HTML can drop to &lt;code&gt;min&lt;/code&gt;. This is a direct cost and speed lever, so it is worth tuning per source rather than leaving on default everywhere.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nocache&lt;/code&gt; defaults to &lt;code&gt;false&lt;/code&gt;. Extract caches for a short window, which is a win for blog posts and docs. For breaking news or anything that changes minute to minute, set &lt;code&gt;nocache: true&lt;/code&gt; to force a fresh fetch.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;geo_target&lt;/code&gt; takes a country code (&lt;code&gt;{ country: &apos;US&apos; }&lt;/code&gt;) when the page serves different content by region.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;extractjson-the-shape-you-asked-for-every-time&quot;&gt;extract.json: the shape you asked for, every time&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;extract.markdown&lt;/code&gt; gives you the whole page. Often you do not want the whole page. You want three fields out of it, typed, ready to drop into your code. That is &lt;code&gt;extract.json&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;You pass a URL and a JSON schema. You get back data in exactly that shape.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; result&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; client.extract.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;json&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  url: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&apos;https://news.ycombinator.com&apos;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  json_schema: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    type: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&apos;object&apos;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    properties: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;      stories: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;        type: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&apos;array&apos;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;        items: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;          type: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&apos;object&apos;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;          properties: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;            title: { type: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&apos;string&apos;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;            points: { type: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&apos;number&apos;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;          },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;        },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;      },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;});&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// { stories: [ { title: &quot;...&quot;, points: 342 }, ... ] }&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The response matches your schema. Not “close to” your schema. You get strings where you asked for strings and numbers where you asked for numbers, instead of everything arriving as text that you then coerce by hand. There is no HTML parsing on your side, no brittle selectors that break the next time the site ships a redesign.&lt;/p&gt;
&lt;p&gt;Two behaviors worth internalizing:&lt;/p&gt;
&lt;p&gt;First, a missing field does not throw. A single field the extractor cannot fill, on one page in a batch of a thousand, will not blow up the whole run, which is exactly what you want in production. What comes back for that field is not guaranteed to be &lt;code&gt;null&lt;/code&gt;, though: depending on the type, you may get &lt;code&gt;null&lt;/code&gt; or a best-effort value the extractor inferred. So validate the values you get back, do not just check for &lt;code&gt;null&lt;/code&gt;, and mark a field &lt;code&gt;required&lt;/code&gt; when you want the extractor to work harder to find it.&lt;/p&gt;
&lt;p&gt;Second, your schema descriptions are doing real work. Adding a &lt;code&gt;description&lt;/code&gt; to a property gives the extractor context about what to look for. Start with a simple schema, run it, and add descriptions to the fields that come back wrong. It is the highest-leverage tuning you can do.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;json_schema&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;  type&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&apos;object&apos;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;  properties&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;    price&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;      type&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&apos;number&apos;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;      description&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&apos;The current sale price in USD, not the original or crossed-out price.&apos;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That one description is the difference between extracting the right number and the struck-through one.&lt;/p&gt;
&lt;h2 id=&quot;where-extract-is-the-wrong-tool&quot;&gt;Where extract is the wrong tool&lt;/h2&gt;
&lt;p&gt;This is the honest part, and it is the same boundary I drew for generate. Extract pulls what is on the page. It does not form opinions. If the field you want is “is this pricing aggressive” or “summarize this in two sentences,” that is interpretation, and it belongs to &lt;code&gt;/generate/json&lt;/code&gt;, which puts a model in the loop on purpose.&lt;/p&gt;
&lt;p&gt;The reason to keep that line clean is cost and determinism. Extract is the cheaper, repeatable workhorse. Reach for it whenever the data is literally on the page, which is more often than you would guess. Save the AI endpoints for the jobs that actually need judgment.&lt;/p&gt;
&lt;p&gt;If you are deciding between extract and research: extract is for when you have the URL. Research is for when you have a question and do not yet know which page holds the answer. Different problems, different endpoints.&lt;/p&gt;
&lt;h2 id=&quot;getting-started&quot;&gt;Getting started&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;npm&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; install&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; @tabstack/sdk&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# or&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;pip&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; install&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; tabstack&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The SDK reads &lt;code&gt;TABSTACK_API_KEY&lt;/code&gt; from your environment, so a first call is two lines:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; tabstack &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; Tabstack&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;client &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; Tabstack()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;result &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; client.extract.markdown(&lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;url&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;https://example.com/blog/article&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;print&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(result.content)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Start with &lt;code&gt;extract.markdown&lt;/code&gt; to confirm your key works and see what a clean page looks like. Then move to &lt;code&gt;extract.json&lt;/code&gt; the moment you know which fields you actually want. Most of the web data problems you think need a pipeline are one extract call and a schema.
Ready to build? &lt;a href=&quot;https://console.tabstack.ai/signup&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Sign up&lt;/a&gt; for 10,000 free credits, no card required, and make your first &lt;code&gt;extract&lt;/code&gt; call. Full API in the &lt;a href=&quot;https://docs.tabstack.ai&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;docs&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>Tabstack on Stripe Projects</title><link>https://tabstack.ai/blog/stripe-projects-integration</link><guid isPermaLink="true">https://tabstack.ai/blog/stripe-projects-integration</guid><description>Provision Tabstack API keys natively via the Stripe CLI. Programmatic billing, automated account lifecycle management, and browsing infrastructure built for autonomous AI agents.</description><pubDate>Wed, 15 Jul 2026 05:00:00 GMT</pubDate><content:encoded>&lt;p&gt;At Tabstack, we build the advanced web-browsing, automation, and research toolchains that power autonomous AI agents. Yet, every agent project eventually hits a setup wall: your agent needs an API key. That means leaving whatever you were building, going to another dashboard, creating an account, adding billing, copying a credential, and wiring it into your environment. For a human developer, this is friction. For an AI agent trying to bootstrap its own tooling, it is a hard stop.&lt;/p&gt;
&lt;p&gt;Stripe Projects resolves this exact architectural bottleneck.&lt;/p&gt;
&lt;h2 id=&quot;stripe-projects-commercial-routing-for-machines&quot;&gt;Stripe Projects: Commercial Routing for Machines&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://stripe.com/docs/projects&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Stripe Projects&lt;/a&gt; is a new workflow in the Stripe CLI. Currently in developer preview, it lets developers provision third-party services directly from within a Stripe project. Once provisioned, credentials inject automatically into the project environment, and billing flows natively through Stripe. Think of it as a marketplace where an AI agent can walk in, select the required tools, and exit with functional credentials and an established billing relationship.&lt;/p&gt;
&lt;p&gt;When you add a service through Stripe Projects, Stripe Projects coordinates account creation or linking with Tabstack, provisions the resource, and drops the credentials directly into your &lt;code&gt;.env&lt;/code&gt; file. Your agent moves from zero to a working API key with just a few terminal commands.&lt;/p&gt;
&lt;h2 id=&quot;terminal-first-workspace-setup&quot;&gt;Terminal-First Workspace Setup&lt;/h2&gt;
&lt;p&gt;Starting today, Tabstack is participating in the Stripe Projects developer preview. As a co-design partner on the underlying Agentic Provisioning Protocol, we worked closely with Stripe to ensure that provisioning and credential handoff work reliably and deterministically across providers.&lt;/p&gt;
&lt;p&gt;With Stripe Projects, you can initialize your app workspace and attach the Tabstack toolchain directly from the terminal:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;shell&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# Initialize the project environment&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;stripe&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; projects&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; init&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; my-app&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# Provision the Tabstack resource&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;stripe&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; projects&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; add&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; tabstack/api&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You will then be prompted to select a Tabstack plan for your account (you can also view our full tier breakdown on the &lt;a href=&quot;https://tabstack.ai/pricing&quot;&gt;Tabstack pricing page&lt;/a&gt;). The Stripe CLI triggers an interactive menu directly in your terminal to make your selection:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;shell&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;Plan&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;        Price&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;             Description&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;trial&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;       Free&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;              10,000&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; credits&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; included.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;individual&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;  Usage&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; based&lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;       $0&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;.35&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; per&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; 1,000&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; credits.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;team&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;        $99&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;/month&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;         500,000&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; credits&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; included,&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; then&lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt; $0&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;.30&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; per&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; 1,000.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;pro&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;         $499&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;/month&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;        3,000,000&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; credits&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; included,&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; then&lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt; $0&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;.25&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; per&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; 1,000.&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once you select a plan and confirm, the CLI provisions your Tabstack account while injecting &lt;code&gt;TABSTACK_API_KEY&lt;/code&gt; into your project environment. On paid plans, billing details are set up once and shared securely with your SaaS stack through a Shared Payment Token, eliminating separate invoice management. Resources continue to live under your own Tabstack account with standard access and dashboard configurations intact.&lt;/p&gt;
&lt;h2 id=&quot;unlocking-the-toolchain&quot;&gt;Unlocking the Toolchain&lt;/h2&gt;
&lt;p&gt;Once provisioning is complete, your Tabstack API key gives you or your agent full access to all Tabstack features:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;/extract/markdown:&lt;/strong&gt; Converts any URL into clean Markdown, built for an LLM context window.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;/extract/json:&lt;/strong&gt; Converts web pages into structured JSON matching a defined schema.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;/generate/json:&lt;/strong&gt; Generates structured data from web content using natural language payloads.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;/automate:&lt;/strong&gt; Navigates, clicks, fills forms, and completes multi-step web workflows.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;/research:&lt;/strong&gt; Runs a multi-pass agentic research loop and outputs a synthesized, cited report.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Initialize the SDK once the credential populates your environment variables:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; { Tabstack } &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &apos;@tabstack/sdk&apos;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; client&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; Tabstack&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  apiKey: process.env.&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;TABSTACK_API_KEY&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;});&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Target the markdown extraction backend directly&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; result&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; client.extract.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;markdown&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  url: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&apos;https://example.com/docs&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;});&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;console.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;log&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(result.content);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;lifecycle-management&quot;&gt;Lifecycle Management&lt;/h2&gt;
&lt;p&gt;Beyond provisioning, you can manage your entire Tabstack resource lifecycle directly from the terminal, making it simple to upgrade tiers, rotate credentials, or log in to the Tabstack dashboard without leaving your workflow:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;shell&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# Rotate your API key&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;stripe&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; projects&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; rotate&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; tabstack/api&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# Open the Tabstack dashboard for your account&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;stripe&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; projects&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; open&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; tabstack&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# Upgrade to a higher plan&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;stripe&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; projects&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; upgrade&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; tabstack/api&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# Remove the service and deprovision the account&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;stripe&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; projects&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; remove&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; tabstack/api&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;designing-for-autonomy&quot;&gt;Designing for Autonomy&lt;/h2&gt;
&lt;p&gt;This integration is about more than just developer convenience; it is about building infrastructure native to machines.&lt;/p&gt;
&lt;p&gt;Manual credentials break autonomous systems. When an AI agent relies on one-off dashboard navigation or hardcoded keys, it stalls. Eliminating this manual setup allows the agent to provision its own browsing infrastructure dynamically as dependencies scale.&lt;/p&gt;
&lt;p&gt;When an automated deployment script triggers the workspace setup, a functional key drops in thirty seconds later with billing wired instantly. This unlocks entirely new operational patterns: automated project initialization, self-healing runtimes that swap out broken tools, and dynamic infrastructure scaling based on task complexity.&lt;/p&gt;
&lt;p&gt;Tabstack handles the browsing. Stripe handles the billing. The agent handles the execution.&lt;/p&gt;
&lt;h2 id=&quot;next-steps&quot;&gt;Next Steps&lt;/h2&gt;
&lt;p&gt;This integration is in developer preview, and we want your feedback. To get started, install the &lt;a href=&quot;https://stripe.com/docs/stripe-cli&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Stripe CLI here&lt;/a&gt;, initialize your app workspace, and attach Tabstack to your environment:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;shell&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;stripe&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; projects&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; init&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; my-app&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;stripe&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; projects&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; add&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; tabstack/api&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Review the full API documentation at &lt;a href=&quot;https://docs.tabstack.ai&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;docs.tabstack.ai&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>Introducing Schema Source: Programmatic Blueprinting for Web Data Extraction</title><link>https://tabstack.ai/blog/introducing-schema-source-web-data-extraction</link><guid isPermaLink="true">https://tabstack.ai/blog/introducing-schema-source-web-data-extraction</guid><description>Generate a structured JSON schema from any URL with Schema Source by Tabstack, then hand it to Extract for matching JSON with no manual schema work.</description><pubDate>Fri, 26 Jun 2026 05:00:00 GMT</pubDate><content:encoded>&lt;p&gt;We built &lt;strong&gt;Tabstack&lt;/strong&gt; to replace traditional parsing configurations with a declarative model. Instead of telling a scraper &lt;em&gt;how&lt;/em&gt; to navigate a page, you define &lt;em&gt;what&lt;/em&gt; data you want using a JSON schema, and our &lt;code&gt;/extract/json&lt;/code&gt; and &lt;code&gt;/generate/json&lt;/code&gt; endpoints handle the execution.&lt;/p&gt;
&lt;p&gt;Schema-first extraction bypasses structural web shifts, but introduces a predictable bottleneck: &lt;strong&gt;manual schema design&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Manually building a valid, production-grade JSON schema for a complex web page is tedious. It requires inspecting the DOM, identifying data hierarchies, mapping out types, and writing detailed string descriptions so the underlying LLMs understand the context of each field.&lt;/p&gt;
&lt;p&gt;To automate this setup phase, we built &lt;a href=&quot;https://schema.tabstack.ai?utm_source=tabstack.ai&amp;#x26;utm_medium=blog&amp;#x26;utm_campaign=blog_post&amp;#x26;utm_content=schema_intro&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;strong&gt;Schema Source&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;what-is-schema-source&quot;&gt;&lt;strong&gt;What is Schema Source?&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Schema Source&lt;/strong&gt; is an independent API that reverse-engineers a webpage’s visual and semantic layout to instantly generate a structured data schema. Pass a URL via a raw &lt;code&gt;GET&lt;/code&gt; request to instantly retrieve a structured JSON blueprint.&lt;/p&gt;
&lt;p&gt;For example, if you point it at a content-heavy community page like &lt;code&gt;https://reddit.com/r/nba&lt;/code&gt;, Schema Source analyzes the page components and automatically outputs an optimized JSON schema mapping the posts, metrics, and metadata.&lt;/p&gt;
&lt;p&gt;You can then pass this schema directly into Tabstack’s extraction pipelines to begin pulling live data immediately.&lt;/p&gt;
&lt;h2 id=&quot;how-schema-source-fits-into-the-tabstack-workflow&quot;&gt;&lt;strong&gt;How Schema Source Fits into the Tabstack Workflow&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;In a schema-first architecture, &lt;strong&gt;object descriptions act as system prompts&lt;/strong&gt;. If your schema has a field called score, an LLM needs to know if that means a sporting event’s box score, a user upvote count, or a sentiment rating.&lt;/p&gt;
&lt;p&gt;Schema Source handles this contextual work by generating explicit metadata alongside the data types.&lt;/p&gt;
&lt;h3 id=&quot;step-1-bootstrapping-the-schema-via-schema-source&quot;&gt;&lt;strong&gt;Step 1: Bootstrapping the Schema via Schema Source&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;To generate a structural baseline for a target page, append your target URL directly to the Schema Source endpoint:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;async&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; function&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; generateSchema&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;url&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;	const&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; res&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; fetch&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;	  `https://schema.tabstack.ai/get/${&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;encodeURIComponent&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;url&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;}?format=json`&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;	);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; { &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;schema&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; } &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; res.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;json&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;  return&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; schema;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The API analyzes the target page and returns a clean, production-ready JSON schema tailored to its core data elements:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;   &quot;type&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;object&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;   &quot;properties&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;      &quot;posts&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;         &quot;type&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;array&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;         &quot;description&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;List of popular posts on Reddit&apos;s front page&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;         &quot;items&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;            &quot;type&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;object&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;            &quot;properties&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;               &quot;title&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;                  &quot;type&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;string&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;                  &quot;description&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;The title of the Reddit post&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;               },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;               &quot;author&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;                  &quot;type&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;string&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;                  &quot;description&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;The username of the post&apos;s author&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;               },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;               &quot;score&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;                  &quot;type&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;number&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;                  &quot;description&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;The score (upvotes) of the post&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;               },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;               &quot;comment_count&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;                  &quot;type&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;number&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;                  &quot;description&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;The number of comments on the post&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;               },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;               &quot;permalink&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;                  &quot;type&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;string&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;                  &quot;description&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;The direct link to the Reddit post discussion page&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;               },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;               &quot;published_at&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;                  &quot;type&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;string&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;                  &quot;description&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;The timestamp when the post was published&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;               }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;            },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;            &quot;required&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:[&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;               &quot;title&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;               &quot;author&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;               &quot;score&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;               &quot;comment_count&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;               &quot;permalink&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;               &quot;published_at&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;            ]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;         },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;         &quot;maxItems&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;10&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;      }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;   },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;   &quot;required&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:[&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;      &quot;posts&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;   ]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;step-2-running-a-direct-extraction-with-tabstack&quot;&gt;&lt;strong&gt;Step 2: Running a Direct Extraction with Tabstack&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Deploying the schema into a production pipeline requires zero manual layout configuration. Pass the generated schema directly into the official &lt;code&gt;@tabstack/sdk&lt;/code&gt; to execute a literal data extraction.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; Tabstack &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &apos;@tabstack/sdk&apos;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; client&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; Tabstack&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;({ apiKey: process.env.&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;TABSTACK_API_KEY&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; url&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &apos;https://reddit.com/r/nba&apos;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; json_schema&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; generateSchema&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(url);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;async&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; function&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; extractLiteralData&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; response&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; client.extract.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;json&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    url,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    json_schema,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  console.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;log&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&apos;Extracted Data:&apos;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;JSON&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;stringify&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(response.data, &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;null&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;2&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;step-3-layering-ai-transformation-with-generatejson&quot;&gt;&lt;strong&gt;Step 3: Layering AI Transformation with generate.json&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;If your pipeline requires transforming, filtering, or analyzing data rather than just mirroring what is literally on the page, you can use the exact same schema with Tabstack’s &lt;code&gt;/generate/json&lt;/code&gt; endpoint.&lt;/p&gt;
&lt;p&gt;While &lt;code&gt;/extract/json&lt;/code&gt; returns literal data from the DOM, generate allows you to pass custom prompt instructions (like summarization or synthesis) while guaranteeing the engine wraps those modified results perfectly inside your schema structure:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;async&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; function&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; extractAndTransformData&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; response&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; client.generate.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;json&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    url,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    json_schema,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    instructions: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;Filter out any threads that aren&apos;t about live game updates. For the remaining posts, rewrite the title to be a strictly objective 5-word summary.&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  console.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;log&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&apos;Transformed Data:&apos;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;JSON&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;stringify&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(response.data, &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;null&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;2&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;decoupling-configuration-from-execution&quot;&gt;&lt;strong&gt;Decoupling Configuration from Execution&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;We decoupled Schema Source to isolate structural data discovery from production data ingestion pipelines.&lt;/p&gt;
&lt;p&gt;This architecture eliminates the need to manually draft schemas or guess layout hierarchies during the initial development cycle. Teams can use Schema Source programmatically to seed baseline data definitions, then rely entirely on Tabstack’s infrastructure to fetch, parse, and validate those payloads at scale.&lt;/p&gt;
&lt;p&gt;Try generating your first schema at &lt;a href=&quot;https://schema.tabstack.ai?utm_source=tabstack.ai&amp;#x26;utm_medium=blog&amp;#x26;utm_campaign=blog_post&amp;#x26;utm_content=schema_cta&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;schema.tabstack.ai&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>Hardening AI Web Agents: How We&apos;re Securing Tabstack Against Indirect Prompt Injection</title><link>https://tabstack.ai/blog/securing-ai-agents-indirect-prompt-injection</link><guid isPermaLink="true">https://tabstack.ai/blog/securing-ai-agents-indirect-prompt-injection</guid><description>A technical look at the prompt injection vulnerability in Tabstack and the code changes made to fix it.</description><pubDate>Fri, 12 Jun 2026 05:00:00 GMT</pubDate><content:encoded>&lt;p&gt;At Mozilla, we believe that building a useful AI ecosystem requires radical transparency, especially when it comes to security.&lt;/p&gt;
&lt;p&gt;Recently, security researchers at Brave reached out to us regarding an Indirect Prompt Injection (IPI) vulnerability they identified in Tabstack’s &lt;code&gt;/v1/automate&lt;/code&gt; endpoint, which they have since detailed in their public blog post on the flaw. Because Tabstack is built to act as an autonomous web agent that can browse, click, and interact with the live web on behalf of a user, the implications of IPI are a critical design challenge.&lt;/p&gt;
&lt;p&gt;The vulnerability has been patched, and the fix was independently verified by the Brave team before their public write-up. We want to share a transparent look at the exploit, how our model handled it, and the architecture we’ve implemented to harden our automation engine against this entire class of attacks.&lt;/p&gt;
&lt;h2 id=&quot;the-vulnerability-bypassing-the-scope-of-the-task&quot;&gt;The Vulnerability: Bypassing the Scope of the Task&lt;/h2&gt;
&lt;p&gt;The attack discovered by Brave highlights the unique risks associated with “agentic” AI tools. During a controlled test, researchers passed a standard, routine prompt to the &lt;code&gt;/v1/automate&lt;/code&gt; endpoint: “Summarize this page.”&lt;/p&gt;
&lt;p&gt;However, the target page contained hidden, malicious instructions (rendered in white-on-white text, invisible to a human but fully readable in the page’s text layer ingested by the AI). The injected text instructed the model to ignore its previous task, grab the user’s full conversation history, paste it into an external web form, and hit submit.&lt;/p&gt;
&lt;p&gt;Because Large Language Models mix user intent and third-party data into a single, flat text window, Tabstack didn’t view this as a security conflict. Instead, it confidently followed the instructions step-by-step:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Navigated away from the target page to an external domain.&lt;/li&gt;
&lt;li&gt;Copied the user’s conversation context.&lt;/li&gt;
&lt;li&gt;Submitted the form, actively exfiltrating the data to the researcher’s server.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;When we analyzed the agent’s internal reasoning traces, the challenge became even clearer. The model wasn’t “tricked” or confused; it was executing what it genuinely believed to be a legitimate workflow continuation.&lt;/p&gt;
&lt;h2 id=&quot;how-we-fixed-it-moving-beyond-a-flat-context&quot;&gt;How We Fixed It: Moving Beyond a Flat Context&lt;/h2&gt;
&lt;p&gt;Prompt injection is a dynamic, shifting threat vector across the entire AI industry. While it is functionally impossible to guarantee any LLM is 100% immune to prompt injection, we can severely limit an agent’s ability to act on malicious inputs. That was our north star: assume the model will occasionally be fooled by injected text, and make sure that being fooled cannot translate into exfiltrated data.&lt;/p&gt;
&lt;p&gt;Following the disclosure, our engineering team confirmed the gap and shipped a series of changes to our underlying browser automation engine, Mozilla Pilo. Rather than trying to detect malicious prompts (a losing game of pattern-matching), we focused on shrinking the agent’s “blast radius” with structural guardrails that hold regardless of what the page says.&lt;/p&gt;
&lt;h3 id=&quot;a-structural-action-firewall-for-forms&quot;&gt;A Structural Action Firewall for Forms&lt;/h3&gt;
&lt;p&gt;The core of the fix is a new action firewall that sits between the agent’s decisions and the browser’s execution of them. Critically, it does not work by scanning text for “suspicious” instructions. Instead, it classifies every form interaction using DOM field metadata and reference provenance: where a value came from, what kind of field it is targeting, and whether a human ever approved it.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The agent is free to fill operational controls it legitimately needs to do its job (search boxes, date pickers, range sliders, comboboxes).&lt;/li&gt;
&lt;li&gt;It is structurally blocked from auto-filling freeform or sensitive fields, and from submitting any form that contains agent-filled data that was never approved, before the action reaches the browser.&lt;/li&gt;
&lt;li&gt;Even operational submissions are now restricted to the same host as the current page. An attacker page can label its collector field as a “search box,” but it cannot make the agent submit that data to an attacker-controlled domain. Unknown page hosts fail closed.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When a block fires in non-interactive mode, the CLI prints a remediation footer explaining how to proceed. That footer is user-facing only: the model never sees it, so injected page content cannot instruct the agent to talk the user into disabling the firewall.&lt;/p&gt;
&lt;h3 id=&quot;external-content-isolation&quot;&gt;External Content Isolation&lt;/h3&gt;
&lt;p&gt;We also closed the more fundamental gap that made the original exploit possible: the agent treated web-page text and user instructions as one undifferentiated stream.&lt;/p&gt;
&lt;p&gt;Web-sourced content returned by the agent’s tools (page extracts, fetched markdown, search results, even the completion validator’s own feedback) is now wrapped in explicit &lt;code&gt;&amp;#x3C;EXTERNAL-CONTENT&gt;&lt;/code&gt; tags at the point the data enters the conversation, with an inline warning on every block and a matching directive in the system prompt. This mirrors the trust-framing we already applied to raw page snapshots, so untrusted page text is consistently delineated from the user’s actual task.&lt;/p&gt;
&lt;p&gt;This wrapping is structural, not learned: the boundary is enforced by how we construct the context, not by hoping the model “decides” to distrust the right paragraph. The same change also clips stale external content out of conversation history after a turn, which incidentally shut down a cost-amplification angle of the same attack.&lt;/p&gt;
&lt;h3 id=&quot;caller-controlled-trust-boundaries-now-per-request&quot;&gt;Caller-Controlled Trust Boundaries, Now Per Request&lt;/h3&gt;
&lt;p&gt;Some legitimate workflows genuinely need the agent to enter data into third-party forms. Rather than weaken the firewall globally, we exposed explicit, opt-in trust controls:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;trustedHostnames&lt;/code&gt;:&lt;/strong&gt; A caller-supplied allowlist that bypasses the fill and submit gates, but only when the current page host and every form-action target (including submitter &lt;code&gt;formaction&lt;/code&gt; overrides) are all on the list.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;unsafeMode&lt;/code&gt;:&lt;/strong&gt; A deliberate, fully-documented global opt-out, with prominent data-risk warnings on every surface.&lt;/li&gt;
&lt;li&gt;A caller-provided start URL is treated as consent to interact with that specific site, but planner-chosen or agent-navigated URLs are not, since those are influenced by the model and the page.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Originally configured via local environment variables and core configuration files, we have exposed these parameters directly to our automation engine’s request layer. This allows the Tabstack API to pass &lt;code&gt;trustedHostnames&lt;/code&gt; and &lt;code&gt;unsafeMode&lt;/code&gt; dynamically inside the request body, establishing strict trust boundaries per request rather than per deployment.&lt;/p&gt;
&lt;h3 id=&quot;defense-in-depth&quot;&gt;Defense in Depth&lt;/h3&gt;
&lt;p&gt;Alongside the firewall and content isolation, we tightened how the agent reports completion. A new data-grounding rule requires every value the agent returns to trace back to a page snapshot, tool result, or task input (no filling in answers from the model’s own training), backed by a pre-completion verification checklist. We also wired the completion validator’s conversation history and outcome into the agent loop, and hardened the agent against getting stuck repeating injected actions across changing element references.&lt;/p&gt;
&lt;h2 id=&quot;the-responsible-disclosure-timeline&quot;&gt;The Responsible Disclosure Timeline&lt;/h2&gt;
&lt;p&gt;We are incredibly grateful to the Brave research team for their clean, professional coordination on this bug. Their outreach allowed us to identify, patch, and verify a critical gap before it could ever affect users in production.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;May 13, 2026:&lt;/strong&gt; Brave reports the IPI vulnerability to the Mozilla Tabstack team.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;May 14, 2026:&lt;/strong&gt; Mozilla engineers review the traces, confirm the vulnerability, and begin engineering defenses.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;June 1, 2026:&lt;/strong&gt; Mozilla deploys the updated Pilo browser engine changes, and the Brave team independently verifies the fix.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Securing autonomous web agents is one of the most complex engineering challenges in AI right now. By treating third-party web content as inherently untrusted, creating structural boundaries, and cutting off the pathways for unauthorized data exfiltration, we are building a safer foundation for agentic workflows. We will continue to harden Tabstack out in the open, and we welcome the global security community’s continued scrutiny.&lt;/p&gt;
</content:encoded></item><item><title>Upgrading Pilo to Support Human-in-the-Loop Browser Automation</title><link>https://tabstack.ai/blog/pilo-interactive-mode</link><guid isPermaLink="true">https://tabstack.ai/blog/pilo-interactive-mode</guid><description>Stop your web agents from guessing. Pilo Interactive Mode brings human-in-the-loop capabilities to browser automation, allowing agents to pause and ask for the exact context they need to complete complex tasks.</description><pubDate>Thu, 16 Apr 2026 05:00:00 GMT</pubDate><content:encoded>&lt;p&gt;When we open-sourced Pilo, our goal was to provide a robust execution engine for AI web agents capable of navigating the chaos of the modern web. By leveraging the accessibility tree and intelligent reasoning loops, Pilo handles complex browser orchestration reliably. But autonomous execution has a hard limit: missing context.&lt;/p&gt;
&lt;p&gt;Today, we are excited to introduce &lt;strong&gt;Interactive Mode (Beta)&lt;/strong&gt; for Pilo. This new feature allows the Pilo agent to pause its execution loop and ask the caller, whether that is a human user or a parent agent, for the specific information it needs to complete a task.&lt;/p&gt;
&lt;h2 id=&quot;the-problem-of-missing-context&quot;&gt;The Problem of Missing Context&lt;/h2&gt;
&lt;p&gt;In traditional web automation, agents are expected to operate entirely on their own once given a prompt. But what happens when an agent encounters a step that requires specific, localized knowledge?&lt;/p&gt;
&lt;p&gt;Imagine giving an agent the prompt: &lt;em&gt;“Sign up for the mozilla newsletter.”&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The agent can successfully generate a plan, navigate to the Mozilla website, locate the newsletter signup page, and identify the required input fields. But when it tries to submit the form, it hits a roadblock: it doesn’t know your email address or your subscription preferences. Previously, this would result in a failed task or a hallucinated input. The agent was trapped in a silo, unable to request the missing piece of the puzzle.&lt;/p&gt;
&lt;h2 id=&quot;enter-interactive-mode&quot;&gt;Enter Interactive Mode&lt;/h2&gt;
&lt;p&gt;Interactive Mode transforms Pilo from a purely autonomous executor into a collaborative agent. Instead of failing when it lacks information, Pilo can now dynamically halt its loop and prompt the caller for guidance.&lt;/p&gt;
&lt;p&gt;For our first iteration of Interactive Mode, we are focusing specifically on form completion. When Pilo encounters a form requiring personal data, credentials, or preferences it doesn’t possess, it will output a request for input. Once the caller provides the missing information, Pilo directly injects it into the form and resumes the agentic loop to complete the task.&lt;/p&gt;
&lt;h2 id=&quot;how-it-works-across-the-stack&quot;&gt;How It Works Across the Stack&lt;/h2&gt;
&lt;p&gt;Interactive Mode integrates deeply into Pilo’s core execution engine rather than relying on the LLM to decide when to ask for help. When Pilo encounters a form, it uses a “fill gate” mechanism to inspect the required fields. If it detects that user input is needed, Pilo bypasses the LLM entirely to pause task execution and request the data.&lt;/p&gt;
&lt;p&gt;Because Pilo can be run in several different environments, we designed the interactive feedback loop to adapt to how you are using it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Pilo Core:&lt;/strong&gt; When you are building directly on top of the Pilo library, the core engine expects a simple callback function. When the execution loop pauses, it triggers your callback with the required fields, waiting for your application logic to return the necessary data before resuming.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pilo CLI:&lt;/strong&gt; For developers testing locally, the Pilo CLI handles this automatically. When the core requests information, the CLI pauses the terminal output and interactively prompts the user for the missing fields right in the console.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pilo Server:&lt;/strong&gt; For remote execution, the Pilo Server emits an &lt;code&gt;interactive:form_data:request&lt;/code&gt; event containing the full field data. It utilizes a dedicated WebSocket endpoint (&lt;code&gt;/pilo/run&lt;/code&gt;), enabling real-time, bidirectional communication to exchange user data while the task is suspended.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Furthermore, Pilo captures form validation states directly within its ARIA tree snapshots. If a user submits an invalid email, the agent detects the field error in the snapshot and will automatically trigger a re-prompt for the corrected information.&lt;/p&gt;
&lt;h2 id=&quot;seeing-it-in-action-with-tabstack&quot;&gt;Seeing it in Action with Tabstack&lt;/h2&gt;
&lt;p&gt;The Tabstack &lt;strong&gt;/automate&lt;/strong&gt; endpoint is built directly on top of the Pilo Server. This means you can test the WebSocket-driven Interactive Mode today using the Tabstack SDK.&lt;/p&gt;
&lt;p&gt;Here is what it looks like to catch those interactive requests and supply the missing context:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; tabstack &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; Tabstack&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# Initialize the Tabstack client&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;client &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; Tabstack(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E36209&quot;&gt;    api_key&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;YOUR_TABSTACK_API_KEY&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# Start a task with Interactive Mode enabled&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;stream &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; client.agent.automate(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E36209&quot;&gt;    task&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;signup for the mozilla newsletter&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E36209&quot;&gt;    interactive&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;True&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# Listen for events in the stream&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;for&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; event &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; stream:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    print&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(event)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    # Catch the specific event where the agent asks for form data&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; event.event &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;interactive:form_data:request&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;        request_id &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; event.data.get(&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;requestId&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;        fields &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; event.data.get(&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;fields&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, [])&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;        print&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;f&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;\n&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;--- Interactive input requested (requestId: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;request_id&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;) ---&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;        &lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;        field_values &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; []&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;        &lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;        # Dynamically prompt the user for the missing fields&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;        for&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; field &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; fields:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;            ref &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; field.get(&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;ref&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;            label &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; field.get(&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;label&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;            required &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; field.get(&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;required&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;False&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;            &lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;            prompt &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; f&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;  &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;label&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;}{&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&apos;*&apos;&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; if&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; required &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;else&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &apos;&apos;&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;: &quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;            value &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; input&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(prompt)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;            field_values.append({&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;ref&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: ref, &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;value&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: value})&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;        # Submit the user&apos;s input back to the agent to resume the task&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;        response &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; client.agent.automate_input(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;            request_id,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E36209&quot;&gt;            fields&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;field_values,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;        )&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;        print&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;f&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;\n&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;--- Input response: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;response&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; ---&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;\n&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;By decoupling the reasoning engine from the data source, Interactive Mode allows you to build more adaptable, resilient pipelines. Whether you are typing in the terminal or wiring Pilo into a complex backend where a master orchestrator agent automatically supplies the missing data, the engine easily adapts.&lt;/p&gt;
&lt;h2 id=&quot;just-the-first-step&quot;&gt;Just the First Step&lt;/h2&gt;
&lt;p&gt;It is important to note that Interactive Mode is currently in &lt;strong&gt;Beta&lt;/strong&gt;. This initial release is highly optimized for form-filling scenarios, but this is only the first step in our vision for collaborative agents.&lt;/p&gt;
&lt;p&gt;As we continue to test and build out this functionality, Pilo’s interactive capabilities will become much more advanced. Future iterations will support complex scenarios like resolving ambiguous instructions mid-task, requesting visual confirmations before executing destructive actions, and handling multi-step interactive workflows.&lt;/p&gt;
&lt;p&gt;We built Pilo to be the most reliable open-source foundation for web agents, and giving those agents the ability to ask questions is a massive step forward in making them truly useful.&lt;/p&gt;
&lt;p&gt;Check out the updated documentation on the &lt;a href=&quot;https://github.com/mozilla/pilo&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Mozilla Pilo GitHub repository&lt;/a&gt; or try out the SDK at &lt;a href=&quot;https://tabstack.ai&quot;&gt;Tabstack.ai&lt;/a&gt; today.&lt;/p&gt;
</content:encoded></item><item><title>Tabstack&apos;s New Pricing Plans</title><link>https://tabstack.ai/blog/updated-pricing-plans</link><guid isPermaLink="true">https://tabstack.ai/blog/updated-pricing-plans</guid><description>Tabstack&apos;s new pricing is here! Scale your workflow without interruption using our new free Trial, Individual Pay As You Go plan, or predictable monthly subscriptions.</description><pubDate>Mon, 06 Apr 2026 05:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;unlocking-flexibility&quot;&gt;Unlocking Flexibility&lt;/h2&gt;
&lt;p&gt;Since day one, our goal at Tabstack has been to build tools that make your workflow smoother and more efficient. We have loved watching our community grow and build amazing things using our platform. But as your projects have evolved, we realized our pricing model needed to evolve alongside them.&lt;/p&gt;
&lt;p&gt;Until now, Tabstack has operated exclusively on prepaid credit packs. While this worked for many, we heard your feedback loud and clear: you want &lt;strong&gt;clearer, consistent billing&lt;/strong&gt;, and you want choices that adapt to the way your team actually works.&lt;/p&gt;
&lt;p&gt;Today, we are thrilled to announce a complete overhaul of our pricing structure, designed to give you ultimate control, predictability, and flexibility.&lt;/p&gt;
&lt;h2 id=&quot;what-is-new&quot;&gt;What Is New?&lt;/h2&gt;
&lt;p&gt;We are moving away from the strict advanced credit model and introducing a tiered system that grows with you, taking you from your very first API call to enterprise deployments.&lt;/p&gt;
&lt;h3 id=&quot;the-new-trial-tier&quot;&gt;The New Trial Tier&lt;/h3&gt;
&lt;p&gt;We want everyone to experience the power of Tabstack without any friction. Every new user will now start with a &lt;strong&gt;Trial Tier that includes 10,000 free credits&lt;/strong&gt;. This allows you to test the waters, build a proof of concept, and see exactly how Tabstack fits into your stack before spending a dime.&lt;/p&gt;
&lt;h3 id=&quot;the-individual-plan-pay-as-you-go&quot;&gt;The Individual Plan (Pay As You Go)&lt;/h3&gt;
&lt;p&gt;Got a project with unpredictable volume? The Individual plan is for you. Once you graduate from the Trial Tier, you can opt to simply pay for what you use. You will be billed monthly based strictly on your exact usage. There are no upfront commitments, and you are never paying for credits you do not end up using.&lt;/p&gt;
&lt;h3 id=&quot;predictable-monthly-subscriptions&quot;&gt;Predictable Monthly Subscriptions&lt;/h3&gt;
&lt;p&gt;For teams with consistent or high volume needs, we are introducing two new subscription plans. These plans offer a set monthly price, a generous default credit allowance, and a lower cost per credit ratio.&lt;/p&gt;
&lt;div class=&quot;table-scroll&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plan Tier&lt;/th&gt;
&lt;th&gt;Monthly Price&lt;/th&gt;
&lt;th&gt;Included Monthly Credits&lt;/th&gt;
&lt;th&gt;Ideal For&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Team&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$99 / month&lt;/td&gt;
&lt;td&gt;500,000 credits&lt;/td&gt;
&lt;td&gt;Growing startups and mid-sized teams with consistent workloads.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Pro&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$499 / month&lt;/td&gt;
&lt;td&gt;3,000,000 credits&lt;/td&gt;
&lt;td&gt;High-volume applications and established companies scaling fast.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2 id=&quot;overages-and-spend-protection&quot;&gt;Overages and Spend Protection&lt;/h2&gt;
&lt;p&gt;We know that traffic can spike unexpectedly. If you are on the Team or Pro plan and exceed your monthly credit allowance, your workflow will not break. You will seamlessly transition to overage billing so your services keep running smoothly.&lt;/p&gt;
&lt;p&gt;However, we also want to protect you from unexpected bills. You now have the power to configure your own custom usage limits directly from your dashboard. You can set up alerts to notify you when you reach a certain threshold, or set your own hard caps to pause operations automatically.&lt;/p&gt;
&lt;p&gt;Additionally, to provide an extra layer of security against massive, surprise bills, we have implemented a platform-level hard cap on all accounts. While you cannot edit this system cap directly, you can easily request an increase from our support team as your project scales.&lt;/p&gt;
&lt;h2 id=&quot;important-details-for-existing-users&quot;&gt;Important Details for Existing Users&lt;/h2&gt;
&lt;p&gt;If you are already building with Tabstack, we want to make sure your transition is as smooth as possible. We are providing a 90 day grace period for all existing accounts.&lt;/p&gt;
&lt;p&gt;During the next 90 days, you will continue to receive your legacy monthly allowance of 50,000 credits, and you can still purchase credit packs just like before. You can choose to upgrade to the Individual plan or one of the new subscription plans at any time during this window.&lt;/p&gt;
&lt;p&gt;When you upgrade, any existing credit balance you have will be carried over to the new system, and &lt;strong&gt;those credits will not expire&lt;/strong&gt;. If you have not transitioned after 90 days, your account will be updated automatically, and your remaining credit balance will still be safely preserved without an expiration date.&lt;/p&gt;
&lt;h2 id=&quot;updated-credit-costs&quot;&gt;Updated Credit Costs&lt;/h2&gt;
&lt;p&gt;Along with the new plans, we are updating the credit cost per operation to better reflect the scale and processing power of our tools.&lt;/p&gt;
&lt;p&gt;If you are an existing user in the 90 day grace period, you will continue to enjoy the old pricing. All new users, existing users who choose to upgrade early, and all users after the 90 day window will be subject to the new pricing structure.&lt;/p&gt;
&lt;div class=&quot;table-scroll&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;Old Cost (Credits/Action)&lt;/th&gt;
&lt;th&gt;New Cost (Credits/Action)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Extract Markdown&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Extract JSON&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Generate&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;57&lt;/td&gt;
&lt;td&gt;250&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Automate&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;75&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Research Fast&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Research Balanced&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;1,000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2 id=&quot;why-we-made-the-switch-and-why-it-is-good-for-you&quot;&gt;Why We Made the Switch (And Why It Is Good For You)&lt;/h2&gt;
&lt;p&gt;We did not just change our pricing to add more options; we redesigned it to solve real pain points we noticed in how users were interacting with the platform. Here is how the new system works in your favor:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Clearer, Consistent Billing:&lt;/strong&gt; You can now budget effectively. Whether you choose the predictability of the Team and Pro tiers or the exactness of the Individual plan, your finance team will thank you.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Goodbye, “Top-Up Anxiety”:&lt;/strong&gt; Under the old credit pack system, running out of credits meant your operations paused until someone manually logged in and bought more. Now, your workflow remains entirely uninterrupted.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scale at Your Own Pace:&lt;/strong&gt; Start for free, scale to the Individual plan as you gain traction, and lock in savings with a subscription tier once your volume is established. You are never forced into a box that does not fit your current size.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No Bill Shock:&lt;/strong&gt; With customizable alerts and platform safety limits, you get the peace of mind that your budget is protected even if your application goes viral overnight.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lower Barrier to Entry:&lt;/strong&gt; The 10k credit Trial Tier allows developers and creators to experiment freely without needing to pull out a company credit card right away.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;ready-to-explore-the-new-plans&quot;&gt;Ready to Explore the New Plans?&lt;/h2&gt;
&lt;p&gt;These new plans are live today! Head over to your dashboard to view your current usage, explore the new tiers, and select the plan that makes the most sense for your next big project.&lt;/p&gt;
&lt;p&gt;As always, if you have any questions, our support team is ready to help you find the perfect fit. Happy building!&lt;/p&gt;
</content:encoded></item><item><title>Not Every Page Needs a Browser</title><link>https://tabstack.ai/blog/fetch-effort-parameter</link><guid isPermaLink="true">https://tabstack.ai/blog/fetch-effort-parameter</guid><description>The new effort parameter lets you control speed vs. rendering depth when fetching content with Tabstack API. Start fast, escalate when needed.</description><pubDate>Tue, 17 Mar 2026 05:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;the-problem-with-one-size-fits-all&quot;&gt;The Problem with One-Size-Fits-All&lt;/h2&gt;
&lt;p&gt;When you point an extraction API at a URL, you typically have no say in how the page gets fetched. A static documentation page and a JavaScript-heavy single-page application go through the same pipeline, with the same overhead and the same latency. You end up paying a time penalty on simple pages because the system has to assume every page might be complex.&lt;/p&gt;
&lt;p&gt;The reality is that most pages on the web are straightforward. A news article, a product listing on a server-rendered site, a wiki page: none of these need a full headless browser to extract. But a React dashboard behind client-side rendering absolutely does.&lt;/p&gt;
&lt;p&gt;You know your target sites. We should let you tell us.&lt;/p&gt;
&lt;h2 id=&quot;what-changed&quot;&gt;What Changed&lt;/h2&gt;
&lt;p&gt;We made two changes to how Tabstack handles page fetching.&lt;/p&gt;
&lt;p&gt;First, we rearchitected the default fetch path to be significantly faster and more reliable. The &lt;code&gt;&quot;standard&quot;&lt;/code&gt; effort level that every request uses by default is now smarter about how it retrieves content, with better fallback mechanisms and reduced overhead.&lt;/p&gt;
&lt;p&gt;Second, we added the &lt;code&gt;effort&lt;/code&gt; parameter to the &lt;code&gt;/extract/json&lt;/code&gt;, &lt;code&gt;/extract/markdown&lt;/code&gt;, and &lt;code&gt;/generate/json&lt;/code&gt; endpoints. This gives you explicit control over how hard Tabstack works to fetch a page before extraction begins.&lt;/p&gt;
&lt;p&gt;There are three levels:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;&quot;min&quot;&lt;/code&gt;&lt;/strong&gt;: a lightweight fetch with no fallback. This is the fastest option usually under 1 second, ideal for static or server-rendered pages where you know the content is available without JavaScript execution.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;&quot;standard&quot;&lt;/code&gt;&lt;/strong&gt;: the default. A balanced approach with enhanced reliability and fallback mechanisms, typically completing in less than 10 seconds. This will handle most pages, including ones with some JavaScript. Start here for any new integration.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;&quot;max&quot;&lt;/code&gt;&lt;/strong&gt;: full browser rendering. This spins up a headless browser, executes JavaScript, waits for dynamic content to load, and then extracts. It takes 15 to 60 seconds but is necessary for SPAs, pages behind client-side hydration, or sites that load content dynamically. Try this when the other levels are not getting the content you want.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;using-effort&quot;&gt;Using Effort&lt;/h2&gt;
&lt;p&gt;Adding the parameter is straightforward. Here is a basic example that extracts markdown from a JavaScript-heavy page:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; os&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; tabstack &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; Tabstack&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;client &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; Tabstack(&lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;api_key&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;os.environ.get(&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;TABSTACK_API_KEY&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# Full browser rendering for a JS-heavy page&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;result &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; client.extract.markdown(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E36209&quot;&gt;    url&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;https://example.com/spa-dashboard&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E36209&quot;&gt;    effort&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;max&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;print&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(result.content)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For a static docs site where speed matters, drop to &lt;code&gt;&quot;min&quot;&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;result &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; client.extract.markdown(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E36209&quot;&gt;    url&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;https://docs.example.com/api-reference&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E36209&quot;&gt;    effort&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;min&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;adaptive-effort-start-fast-escalate-when-needed&quot;&gt;Adaptive Effort: Start Fast, Escalate When Needed&lt;/h2&gt;
&lt;p&gt;The most powerful pattern with the effort parameter is adaptive escalation. Instead of guessing the right level upfront, you start with the fastest option and only escalate when the result is insufficient. This gives you the best possible latency on easy pages while still handling complex ones reliably.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; os&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; tabstack &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; Tabstack&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;client &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; Tabstack(&lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;api_key&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;os.environ.get(&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;TABSTACK_API_KEY&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; extract_with_adaptive_effort&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(url, min_length&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;100&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;    &quot;&quot;&quot;Extract markdown, escalating effort until we get a good result.&quot;&quot;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    efforts &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;min&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;standard&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;max&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    for&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; effort &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; efforts:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;        result &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; client.extract.markdown(&lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;url&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;url, &lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;effort&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;effort)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; result.content &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;and&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; len&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(result.content) &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; min_length:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;            return&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; result&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; result  &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# Return whatever we got at max effort&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;result &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; extract_with_adaptive_effort(&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;https://example.com/products&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;print&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(result.content)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This pattern works well in production pipelines where you process a mix of sites. Simple pages resolve in a couple of seconds at &lt;code&gt;&quot;min&quot;&lt;/code&gt;, and only the pages that genuinely need browser rendering incur the additional latency. Your average response time drops significantly while your success rate stays high.&lt;/p&gt;
&lt;h2 id=&quot;when-to-use-each-level&quot;&gt;When to Use Each Level&lt;/h2&gt;
&lt;p&gt;Here is a quick guide for choosing the right effort level:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Use &lt;code&gt;&quot;min&quot;&lt;/code&gt; when&lt;/strong&gt; you are scraping well-known static sites, server-rendered pages, or any URL where you have verified that lightweight fetching returns complete content. Great for high-volume pipelines where speed is critical.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use &lt;code&gt;&quot;standard&quot;&lt;/code&gt; when&lt;/strong&gt; you are integrating with a new site or processing a mix of URLs. It handles most of the web reliably without the overhead of full rendering.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use &lt;code&gt;&quot;max&quot;&lt;/code&gt; when&lt;/strong&gt; content is missing from lower effort levels, the target is a known SPA or JavaScript-heavy application, or the page loads data asynchronously after initial render.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;get-started&quot;&gt;Get Started&lt;/h2&gt;
&lt;p&gt;The effort parameter is available now on all extract and generate endpoints. Update to the latest SDK version to use it, or pass &lt;code&gt;effort&lt;/code&gt; directly in your request body if you are calling the API without an SDK.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;API Reference:&lt;/strong&gt; &lt;a href=&quot;https://docs.tabstack.ai&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;docs.tabstack.ai&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tabstack Dashboard:&lt;/strong&gt; &lt;a href=&quot;https://tabstack.ai&quot;&gt;tabstack.ai&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you are building pipelines that process diverse URLs, try the adaptive effort pattern. Start fast, escalate smart, and let your code decide how hard to try.&lt;/p&gt;
</content:encoded></item><item><title>Scaling Web Automation with Pilo and Agentic AI</title><link>https://tabstack.ai/blog/introducing-pilo-browser-automation</link><guid isPermaLink="true">https://tabstack.ai/blog/introducing-pilo-browser-automation</guid><description>Meet Pilo: an open source agentic web automation engine by Tabstack. Build reliable, self-healing AI web agents that truly understand the modern web.</description><pubDate>Tue, 24 Feb 2026 06:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;the-chaos-of-the-modern-web&quot;&gt;The Chaos of the Modern Web&lt;/h2&gt;
&lt;p&gt;Building reliable web automation is notoriously difficult. Modern websites are dynamic, heavily reliant on JavaScript, and constantly changing. When you add Large Language Models to the mix to create autonomous web agents, the complexity multiplies. You suddenly have to manage browser orchestration, keep token costs down while maintaining full page context, handle flaky networks, and build complex reasoning loops just to get an AI to reliably extract data or click a button. You end up managing massive amounts of infrastructure just to handle a basic reasoning loop.&lt;/p&gt;
&lt;p&gt;We didn’t just stumble upon these problems; we ran into them headfirst while building Tabstack. We needed a reliable engine to power our own /automate endpoint, but nothing off the shelf could handle the chaos of the modern web without constant breakage. So we built our own solution.&lt;/p&gt;
&lt;p&gt;We call it Pilo. It became the bedrock of our platform, and we realized that developers everywhere need this same robust, transparent foundation. Today, we are very excited to share Pilo as an open source project, enabling anyone to run this powerful web automation engine completely on their own infrastructure.&lt;/p&gt;
&lt;p&gt;You can find the full source code and documentation on &lt;a href=&quot;https://github.com/mozilla/pilo&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Github here&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;giving-ai-the-steering-wheel&quot;&gt;Giving AI the Steering Wheel&lt;/h2&gt;
&lt;p&gt;At its core, Pilo moves away from rigid scripts and embraces an agentic loop built around Playwright’s accessibility tree. Instead of telling a program exactly where to click, you give Pilo a natural language goal. Pilo then takes over and makes decisions at every step based on what it actually sees on the screen.&lt;/p&gt;
&lt;p&gt;The system operates in a continuous, intelligent loop:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Observe:&lt;/strong&gt; Pilo captures the current page state using the browser’s accessibility tree. This provides a semantic, stable structure of the page rather than a chaotic mess of raw HTML tags.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Decide:&lt;/strong&gt; Pilo passes this state to an LLM provider of your choice. The LLM evaluates the context and selects a specific tool to use next.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Act:&lt;/strong&gt; Pilo executes the chosen action, whether that is navigating to a URL, clicking a button, filling out a form, or extracting structured data.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Pilo handles all the gritty details under the hood. It features layered retries for flaky navigation, automatic context truncation to prevent context window bloat during long tasks, and a robust error recovery philosophy. If an action fails, Pilo does not simply crash. It feeds the error back to the LLM so the agent can adapt and try a new approach.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tabstack.ai/images/blog/browser-ref2.png&quot; alt=&quot;Pilo Agent Loop&quot; title=&quot;By utilizing the accessibility tree, Pilo turns a chaotic webpage into a structured roadmap of labeled elements, making complex browser automation faster and more reliable.&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;anatomy-of-an-autonomous-action&quot;&gt;Anatomy of an Autonomous Action&lt;/h2&gt;
&lt;p&gt;To understand how Pilo handles the complexities of the web, let’s look under the hood at a task like “find the best pizza restaurants in Seattle”.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. The Planning Phase:&lt;/strong&gt; Before launching a browser, Pilo forces the LLM to pause and strategize. It calls a create_plan tool to generate a step-by-step path and, crucially, defines strict success criteria. For a search-heavy task like this, the planner might smartly set the starting URL to about:blank, signaling the agent to immediately trigger its search workflow rather than wasting time navigating to a specific homepage.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Layered Navigation:&lt;/strong&gt; Navigating to a URL is the single most failure-prone operation in browser automation. Pilo wraps navigation in a layered defense system. It starts with timeout escalation, doubling the allowed wait time on each failure. If the network is truly flaky and throws a DNS error, Pilo will automatically kill and restart the entire browser instance to clear any bad state before retrying.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Compressing the Matrix:&lt;/strong&gt; Once the page loads, Pilo needs to show it to the LLM. Passing raw HTML is too expensive and noisy, so Pilo captures the accessibility tree, a semantic map of the page’s structure.&lt;/p&gt;
&lt;p&gt;It pipes this tree through a compression engine that maps verbose tags like listitem to li, shortens reference IDs, and deduplicates repetitive text. This process slashes token usage by 60 to 80 percent while keeping every interactive element accessible.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. The Decision Loop:&lt;/strong&gt; The LLM reviews the compressed snapshot and selects a tool, such as click. Pilo resolves the reference ID (e.g., e45) to a live Playwright locator and executes the action. If the page shifted and e45 is gone, Pilo catches the InvalidRefException and feeds it back to the LLM as a recoverable error. The agent sees the failure, realizes the page state has changed, and naturally requests a fresh snapshot to try again.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5. Quality Control:&lt;/strong&gt; When the agent thinks it’s finished and calls done, Pilo doesn’t just take its word for it. It triggers an isolated validation step where a second LLM acts as a grader. It compares the final results against the success criteria defined in step one. If the agent found only two restaurants when the plan demanded five, the validator marks it as “partial” quality and sends the agent back to work with specific feedback on what’s missing.&lt;/p&gt;
&lt;h2 id=&quot;seeing-is-believing-pilo-in-your-browser&quot;&gt;Seeing is Believing: Pilo in Your Browser&lt;/h2&gt;
&lt;p&gt;While Pilo is a powerful backend engine, we wanted to make its capabilities completely tangible. Because the event system decouples the core logic from where it runs, Pilo is built to work seamlessly in different contexts, including a browser extension. You can install the extension, give it a natural language prompt, and literally watch the agentic loop drive your browser in real time. It is one thing to read about accessibility trees and LLM decision making; it is entirely another to sit back and watch an AI navigate, click, and extract data across the live web right in front of your eyes. It provides a perfect sandbox to test tasks and understand exactly how Pilo thinks and operates.&lt;/p&gt;
&lt;h2 id=&quot;the-engine-inside-tabstack&quot;&gt;The Engine Inside Tabstack&lt;/h2&gt;
&lt;p&gt;We built Pilo to be completely standalone. You can run it locally, plug in your own API keys, and use it to power your own applications without ever signing up for Tabstack. Our goal is to provide developers with a reliable, open source foundation for web agents.&lt;/p&gt;
&lt;p&gt;However, building and scaling this kind of infrastructure in production is incredibly resource intensive. You have to manage compute pools, handle persistent browser sessions, and scale concurrent tasks. This exact challenge is why we built Tabstack in the first place.&lt;/p&gt;
&lt;p&gt;Pilo is the core engine that underlies our /automate endpoint. If you want to leverage the power and resilience of Pilo but do not want to manage the underlying infrastructure, browser orchestration, or scaling logistics, Tabstack provides all of this out of the box.&lt;/p&gt;
</content:encoded></item><item><title>Tabstack Research: Verified Answers from the Open Web</title><link>https://tabstack.ai/blog/tabstack-research-verified-answers</link><guid isPermaLink="true">https://tabstack.ai/blog/tabstack-research-verified-answers</guid><description>Offload browser orchestration and agentic loops to Tabstack. Bridge the synthesis gap with a research primitive that delivers verified, cited web data.</description><pubDate>Tue, 03 Feb 2026 06:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Web browsing is the hidden tax of AI development. When you connect an agent to the open web, you are forced to stop building AI and start managing the overhead of browser orchestration. You end up debugging JS-rendering, rotating proxies, and writing brittle selectors just to turn the chaotic web into clean inputs. This is why we built Tabstack: to turn browsing into a reliable infrastructure layer.&lt;/p&gt;
&lt;p&gt;But even with a stable browser fleet, developers face a secondary bottleneck: &lt;strong&gt;The Synthesis Gap&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Fetching raw data from a single URL is an infrastructure problem. Answering a complex question like “Compare Slack vs. Teams retention policies” is a state management and reasoning problem. To answer that, an agent must spawn a fleet of parallel searches, navigate dozens of unvetted URLs, filter out 90% marketing noise, and reconcile conflicting data.&lt;/p&gt;
&lt;p&gt;Processing this at scale creates a direct conflict between latency and context window density. If you feed your model every raw byte, you burn your token budget on noise. If you truncate too early, you lose the ground truth.&lt;/p&gt;
&lt;p&gt;Today we are launching &lt;strong&gt;Tabstack Research&lt;/strong&gt;. It is a high-level research primitive that moves the autonomous reasoning loop into the infrastructure layer. You give us a goal. We execute the coordinated workflow of discovery, extraction, and verification to return a synthesized report backed by source citations.&lt;/p&gt;
&lt;h2 id=&quot;the-drudgery-of-scale&quot;&gt;The Drudgery of Scale&lt;/h2&gt;
&lt;p&gt;Most developers begin by wrapping a basic scraper in a loop, but they quickly hit an infrastructure wall. Scaling a research task does not just increase your bandwidth requirements. It multiplies your &lt;strong&gt;orchestration complexity&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;When an agent tackles a non-trivial query, it rarely performs a single lookup. It spawns a fan-out of parallel sub-queries. In this scenario, “ten blue links” quickly explode into 100 unvetted URLs.&lt;/p&gt;
&lt;p&gt;Processing this volume in production requires you to solve three high-stakes problems:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;The Concurrency Bottleneck:&lt;/strong&gt; Running 100 parallel headless sessions to handle modern, JS-heavy sites requires massive compute overhead. Managing the memory leaks, zombie processes, and proxy rotation needed to avoid rate limits turns into a full-time DevOps job.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Token Tax:&lt;/strong&gt; Consumption is not comprehension. Shoveling 50,000 tokens of raw HTML boilerplate into your model is a recipe for high latency and “lost in the middle” reasoning errors. Without a pre-processing layer, you are paying to process navigation menus and footer links instead of actual data.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Resolution Logic:&lt;/strong&gt; You are forced to build custom logic to reconcile conflicting data points across different domains. You end up spending more time on data cleaning and deduplication than on your core agent logic.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The result is a fragile pipeline where engineering cycles are wasted managing a browser fleet instead of refining the product.&lt;/p&gt;
&lt;h2 id=&quot;an-adaptive-research-loop&quot;&gt;An Adaptive Research Loop&lt;/h2&gt;
&lt;p&gt;We built &lt;strong&gt;Tabstack Research&lt;/strong&gt; to move this orchestration logic out of your application and into our specialized browsing layer. When you send us a request, we initiate a multi-phase agentic loop designed to mimic the recursive nature of human research at machine speed.&lt;/p&gt;
&lt;p&gt;By offloading the “discovery and verification” cycle to our infrastructure, you avoid the complexity of building custom state machines to handle search branching and error correction.&lt;/p&gt;
&lt;p&gt;Behind the scenes, the system executes a coordinated workflow that treats research as an iterative process rather than a linear fetch:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Planning and Decomposition:&lt;/strong&gt; The system mimics a researcher’s initial “mental map” by breaking your goal into targeted sub-questions. It identifies that a true comparison requires hitting distinct data silos: official documentation, enterprise pricing tables, and compliance whitepapers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Parallel Execution:&lt;/strong&gt; We visit these sites in parallel using our core browsing infrastructure. We perform real-time content extraction, filtering out the DOM noise and marketing fluff that typically bloat a context window.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Gap Evaluation:&lt;/strong&gt; This is the recursive heart of the system. The system evaluates the collected data against the original intent. If it identifies a missing variable or a conflicting date, it detects the gap and triggers a new iteration to hunt down that specific information.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Verification and Termination&lt;/strong&gt;: The loop concludes when the system determines the claims are sufficiently verified against the source text or when it reaches the iteration limit for the selected mode. For example, while the system stops early if it has all the required information, &lt;strong&gt;Balanced Mode&lt;/strong&gt; allows for up to three rounds of recursive discovery to ensure the final output is grounded in retrieved evidence rather than model weights.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;anatomy-of-a-request&quot;&gt;Anatomy of a Request&lt;/h2&gt;
&lt;p&gt;To see how this works in production, consider a complex research task: &lt;strong&gt;“Compare enterprise data retention policies for Slack vs. Microsoft Teams.”&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; os &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; tabstack &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; Tabstack&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# Initialize the client&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;tabs &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; Tabstack(&lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;api_key&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;os.getenv(&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&apos;TABSTACK_API_KEY&apos;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# Ask a research question&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;result &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; tabs.agent.research(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E36209&quot;&gt;    query&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;Compare enterprise data retention policies for Slack vs. Microsoft Teams.&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E36209&quot;&gt;    mode&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;balanced&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;print&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(result)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you pass this query to a standard LLM or a basic search-based agent, you will likely get generic advice or a surface-level summary of the marketing pages. Here is how the &lt;strong&gt;Tabstack Research&lt;/strong&gt; loop executes it:&lt;/p&gt;
&lt;h3 id=&quot;the-planning-phase&quot;&gt;The Planning Phase&lt;/h3&gt;
&lt;p&gt;The system deconstructs the high-level prompt into a series of technical primitives. It identifies that “retention” is not a single value. It generates sub-queries for “Teams Purview chat retention,” “Slack Enterprise Grid file storage limits,” and “M365 E5 compliance overrides.”&lt;/p&gt;
&lt;h3 id=&quot;the-discovery-phase&quot;&gt;The Discovery Phase&lt;/h3&gt;
&lt;p&gt;It executes these searches in parallel. It prioritizes official documentation like &lt;code&gt;learn.microsoft.com&lt;/code&gt; and &lt;code&gt;slack.com&lt;/code&gt;. Critically, our browsing layer ignores the SEO-optimized “Top 10” blog posts from 2023 that no longer reflect the 2026 pricing and policy landscape.&lt;/p&gt;
&lt;h3 id=&quot;the-recursive-pivot&quot;&gt;The Recursive Pivot&lt;/h3&gt;
&lt;p&gt;During extraction, the system hits a common stumbling block. Initial results for Teams mention a 30-day default retention. However, our evaluation step detects a critical gap: &lt;strong&gt;Files shared in Teams are actually stored in SharePoint and OneDrive, which often have conflicting retention policies.&lt;/strong&gt; A standard agent would miss this nuance. Tabstack Research detects the ambiguity and triggers a targeted, second-pass search to clarify the “Conflict Resolution” logic between these interdependent Microsoft services. The engine identifies that while messages appear in Teams, the actual preservation happens in a hidden “SubstrateHolds” folder within the “Exchange Recoverable Items” folder. This is a nuance that dictates how legal holds are actually applied and accessed.&lt;/p&gt;
&lt;h3 id=&quot;the-verification-phase&quot;&gt;The Verification Phase&lt;/h3&gt;
&lt;p&gt;The final report is compiled with specific, time-sensitive data points that are verified against the source text to mitigate the risk of model hallucinations:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Service Interdependence:&lt;/strong&gt; It clarifies that while Teams manages the message metadata, SharePoint manages the physical file retention.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Price Adjustments:&lt;/strong&gt; It catches upcoming M365 price increases scheduled for July 1, 2026 (e.g., E3 moving from $36 to $39).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Infrastructure Transitions:&lt;/strong&gt; It identifies technical shifts scheduled for late 2025 and 2026, such as Slack implementing a two-year rolling retention policy for audit logs and Teams migrating private channel messages to group mailboxes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Retention Granularity:&lt;/strong&gt; It identifies that Slack offers channel-level control, whereas Microsoft favors a centralized “longest-period-wins” logic.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Every single claim is returned with an inline citation and a direct link to the specific documentation used for the extraction.&lt;/p&gt;
&lt;h2 id=&quot;verifiable-claims&quot;&gt;Verifiable Claims&lt;/h2&gt;
&lt;p&gt;The output is not a block of prose. It is a structured synthesis designed for downstream application consumption. We prioritize high-fidelity data over general summaries.&lt;/p&gt;
&lt;h3 id=&quot;a-note-on-ai-generated-research&quot;&gt;A Note on AI-Generated Research&lt;/h3&gt;
&lt;p&gt;LLMs can be remarkably confident even when they are incorrect. While Tabstack Research is built to minimize errors through recursive browsing and cross-referencing, no automated system is perfect. This is why we provide thorough, inline citations for every claim. We believe the value of an AI agent is not in replacing human judgment, but in providing the clear, sourced evidence required for you to make an informed decision.&lt;/p&gt;
&lt;h3 id=&quot;grounded-attribution&quot;&gt;Grounded Attribution&lt;/h3&gt;
&lt;p&gt;In an era of black box AI, we have moved the burden of proof from the model to the evidence. Every claim in a Tabstack Research report is backed by an inline citation. If the system reports that Slack Enterprise Grid requires a custom quote for the Discovery API, it provides the specific URL and the text fragment used to verify that fact. This allows your application to provide “click to verify” functionality for your end users.&lt;/p&gt;
&lt;h3 id=&quot;conflict-resolution&quot;&gt;Conflict Resolution&lt;/h3&gt;
&lt;p&gt;The system does not just aggregate data. It reconciles it. The loop often encounters conflicting information. For example, one source may claim Slack retention is limited to 90 days while another cites indefinite storage for paid tiers. The system resolves this by verifying the context, such as Free tier visibility limits versus Enterprise Grid storage policies. The result is a reconciled data point that prioritizes the most authoritative source.&lt;/p&gt;
&lt;h3 id=&quot;structured-data-primitives&quot;&gt;Structured Data Primitives&lt;/h3&gt;
&lt;p&gt;Because we strip the DOM noise and marketing boilerplate during the execution phase, the final synthesis is high-density. You receive clean and actionable data points:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Logic Mapping:&lt;/strong&gt; You learn that Slack offers granular channel level control while Microsoft favors a centralized “longest period wins” conflict logic.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Temporal Accuracy:&lt;/strong&gt; You get specific price points like Slack’s ~$15 to $45 per user per month (custom pricing) versus Microsoft 365’s $36 for E3 and $60 for E5 list price, which accounts for upcoming 2026 shifts.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Infrastructure Insights:&lt;/strong&gt; You receive technical specifics that impact compliance. This includes the fact that Teams video clips and embedded images are retained, but code snippets and voice memos are often excluded from standard retention policies.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;By the time the data reaches your application, the heavy lifting of discovery and extraction is complete. You are left with a verified asset that is ready to be stored in a database or presented in a UI.&lt;/p&gt;
&lt;h2 id=&quot;continuum-of-execution-choosing-your-speed&quot;&gt;Continuum of Execution: Choosing Your Speed&lt;/h2&gt;
&lt;p&gt;Research is not a one-size-fits-all operation. Different use cases require different levels of recursion and computational depth. We offer two primary modes to help you manage the balance between latency and comprehensiveness.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Fast Mode:&lt;/strong&gt; This is optimized for instant answers and populating UI tooltips. It uses lightweight fetches and high-priority search excerpts to return an answer in &lt;strong&gt;10 to 30 seconds&lt;/strong&gt;. It is ideal for “what is” questions where the answer is likely available in the primary search result or a single documentation page.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Balanced Mode:&lt;/strong&gt; This triggers our full agentic loop. The system visits, renders, and analyzes pages deeply. It performs the multi-pass gap evaluation we described earlier. Typically delivering a comprehensive report in &lt;strong&gt;1 to 2 minutes&lt;/strong&gt;, this mode is designed for complex comparisons and multi-source verification.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;built-for-trust&quot;&gt;Built for Trust&lt;/h2&gt;
&lt;p&gt;In an era of opaque AI, we have taken a different path. As part of the &lt;strong&gt;Mozilla&lt;/strong&gt; ecosystem, Tabstack is built on a foundation of privacy and responsibility. This is not just a marketing claim. It is a technical constraint on how we handle your data.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Zero Training:&lt;/strong&gt; We do not train AI models on your research queries or the data we collect for you. Your intellectual property remains yours.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ephemeral by Design:&lt;/strong&gt; Your research data is treated as ephemeral. It is used to execute the specific task and is then discarded from our active processing memory.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Full Verifiability:&lt;/strong&gt; By providing full citations and source metadata, we move the burden of proof from the model to the evidence. This allows you to build applications where the AI can be audited in real time.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;what-you-can-finally-ask&quot;&gt;What You Can Finally Ask&lt;/h2&gt;
&lt;p&gt;When you stop worrying about how to get the data, you can finally focus on asking the right questions. By moving the orchestration of web browsing into the infrastructure layer, the scope of what your agents can accomplish expands significantly.&lt;/p&gt;
&lt;p&gt;You can build agents that handle:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Competitive Intelligence:&lt;/strong&gt; Compare enterprise pricing and data retention policies for Slack versus Teams.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Regulatory Compliance:&lt;/strong&gt; Extract mandatory data residency requirements for healthcare SaaS in the European Union.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Due Diligence:&lt;/strong&gt; Identify strategic supply chain risks in the latest 10-K filings from NVIDIA.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Strategic Analysis:&lt;/strong&gt; Summarize the major commercial partnerships announced by Salesforce in the last year.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We take care of the hard parts. We handle the browsing, the parsing, and the noise reduction so you can focus on the reasoning that makes your agent unique.&lt;/p&gt;
&lt;p&gt;Ready to build? &lt;a href=&quot;https://console.tabstack.ai/signup&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Sign up&lt;/a&gt; to get started and start with 10,000 free credits, or explore the technical implementation in our &lt;a href=&quot;https://docs.tabstack.ai/api/resources/agent/methods/research&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;documentation&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>Tabstack: Browsing Infrastructure for AI Agents</title><link>https://tabstack.ai/blog/intro-browsing-infrastructure-ai-agents</link><guid isPermaLink="true">https://tabstack.ai/blog/intro-browsing-infrastructure-ai-agents</guid><description>Tabstack is the web execution layer for AI agents. Automate workflows and turn web content into clean data with a developer API backed by Mozilla.</description><pubDate>Wed, 14 Jan 2026 06:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;the-web-access-challenge-for-agentic-ai&quot;&gt;&lt;strong&gt;The Web Access Challenge for Agentic AI&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Every AI engineer eventually hits the same infrastructure wall. You build an agent that reasons perfectly in a sandbox, but the moment you connect it to the open web to perform a complex task, you are forced to stop building AI and start managing the massive overhead of browser orchestration.&lt;/p&gt;
&lt;p&gt;What starts as a simple script to fetch a URL quickly spirals into a full-time job. You find yourself managing headless browsers just to handle client-side rendering. You spend hours writing brittle selectors to parse raw HTML, only to watch them break the moment a site updates its layout. Instead of getting clean data, you are stuck debugging a chaotic mess of tags and attributes.&lt;/p&gt;
&lt;p&gt;This is the hidden tax of autonomous systems. The web is messy, unpredictable, and designed for human eyes rather than software agents. When teams try to bring browsing infrastructure to production themselves, they end up spending more cycles handling session reliability and patching browser fleets than they do improving their agent’s intelligence.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Suddenly, you are not building AI anymore. You are maintaining a fragile, expensive web layer.&lt;/strong&gt; We built Tabstack to turn that chaotic infrastructure into a solved problem.&lt;/p&gt;
&lt;h2 id=&quot;web-browsing-as-infrastructure&quot;&gt;&lt;strong&gt;Web Browsing as Infrastructure&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Today, we are publicly launching &lt;strong&gt;Tabstack&lt;/strong&gt;, the web execution layer for AI systems.&lt;/p&gt;
&lt;p&gt;Tabstack is a developer-focused API that enables AI agents to extract, generate, and automate web content. It acts as a single abstraction layer that collapses the complexity of web automation. Tabstack gives your agents the ability to navigate pages, interact with real user flows, and turn the chaotic web into clean, actionable inputs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;We treat browsing as a distinct infrastructure problem.&lt;/strong&gt; Developers should not have to manually assemble network layers, proxies, and rendering engines just to reliably interact with a website.&lt;/p&gt;
&lt;p&gt;Whether you need to render complex single-page applications or navigate multi-step user workflows, Tabstack handles the heavy lifting of orchestration and reliability. We provide the stability needed to operate at scale so you can focus on building the intelligence that drives your agents.&lt;/p&gt;
&lt;h2 id=&quot;a-continuum-of-execution&quot;&gt;&lt;strong&gt;A Continuum of Execution&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Under the hood, Tabstack dynamically routes every request to the most efficient extraction method. It does not just blindly fire up a heavy browser for every request. Instead, it intelligently selects the lightest viable method by starting with raw HTTP fetches and escalating to full browser sessions only when edge cases or complex interactions demand it.&lt;/p&gt;
&lt;p&gt;This adaptive approach delivers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Speed:&lt;/strong&gt; Process simple pages instantly via lightweight fetches.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reliability:&lt;/strong&gt; Handle complex single-page apps (SPAs) with a robust automation engine that manages scrolling, rendering, and wait times.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Resilience:&lt;/strong&gt; Maintain high success rates with &lt;strong&gt;intelligent routing and retry logic&lt;/strong&gt;, without your team needing to manage the underlying infrastructure.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;turning-the-web-into-data&quot;&gt;&lt;strong&gt;Turning the Web into Data&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Tabstack provides a high-level interface for both understanding and acting on web content. By filtering out the noise before it reaches your model, &lt;strong&gt;Tabstack prevents you from burning your budget on parsing raw HTML.&lt;/strong&gt; We ensure every token in your context window is spent on reasoning, not rendering.&lt;/p&gt;
&lt;p&gt;Whether you are feeding a RAG (Retrieval-Augmented Generation) pipeline or building an autonomous agent, we have an endpoint for the job:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;/markdown:&lt;/strong&gt; Instantly convert any URL’s page content into clean Markdown, perfect for feeding into LLM context windows.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;/json:&lt;/strong&gt; Transform unstructured web pages into structured JSON objects using a schema you provide. This is ideal for turning product listings or news articles into database-ready records.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;/automate:&lt;/strong&gt; Instruct Tabstack to navigate, click, type, and handle complex interactions to complete multi-step workflows. This allows your agent to execute tasks just like a human user.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;giving-your-agent-eyes&quot;&gt;&lt;strong&gt;Giving Your Agent Eyes&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;We designed our API to be dead simple. Just import the SDK and tell Tabstack what to do.&lt;/p&gt;
&lt;p&gt;Here is how you can automate a complex task, like researching the top posts on Hacker News, in just a few lines of Python:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; os &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; tabstack &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; Tabstack&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# Initialize the client&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;tabs &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; Tabstack(&lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;api_key&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;os.getenv(&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&apos;TABSTACK_API_KEY&apos;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# Run an autonomous task&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;result &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; tabs.agent.automate(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E36209&quot;&gt;    url&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;https://news.ycombinator.com&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E36209&quot;&gt;    task&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;Go through 5 pages of the top posts. For each post, determine the website it is from and group all the posts by website. Return the list of the 10 websites with the most posts.&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;print&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(result)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the background, Tabstack’s engine navigates pagination, interprets the content, aggregates the results, and returns the final answer. Your agent stays focused on reasoning while we handle the browsing.&lt;/p&gt;
&lt;h2 id=&quot;built-for-production&quot;&gt;&lt;strong&gt;Built for Production&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Tabstack is designed for teams building AI systems that need to work reliably beyond a prototype. You can start quickly with a simple API and SDKs, then scale without re-architecting your stack or taking on new operational burden. There is no need to manage browser fleets, network layers, or constantly shifting edge cases.&lt;/p&gt;
&lt;p&gt;We are seeing developers use Tabstack to build incredible things that go far beyond simple data retrieval:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Autonomous Research:&lt;/strong&gt; Agents that “read” the news or documentation and synthesize answers rather than hallucinating from outdated training sets.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Action-Oriented Agents:&lt;/strong&gt; Systems that can book reservations, check flight statuses, or fill out forms on your behalf.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Live Market Analysis:&lt;/strong&gt; Tools that navigate e-commerce sites to track pricing and availability in real-time.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;privacy-you-can-trust-backed-by-mozilla&quot;&gt;&lt;strong&gt;Privacy You Can Trust (Backed by Mozilla)&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;We know that giving an external service access to your web traffic requires trust. &lt;strong&gt;Tabstack is built by Mozilla.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In an era where data is often harvested without consent, we are building this capability in a way that reflects Mozilla’s values.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No Training on Your Data:&lt;/strong&gt; Mozilla does &lt;strong&gt;not&lt;/strong&gt; train AI systems on the data that is collected by Tabstack.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ephemeral by Default:&lt;/strong&gt; We practice strict data minimization. Internally, we use the returned data solely to execute the requested task. Customer data is treated as ephemeral.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Secure by Design:&lt;/strong&gt; We utilize end-to-end TLS and scoped API keys to support the secure handling of sensitive information, giving you control and peace of mind.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We are building a tool for developers to access the web responsibly, rather than a mass data harvester for model training.&lt;/p&gt;
&lt;h2 id=&quot;get-started&quot;&gt;&lt;strong&gt;Get Started&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;The era of static, isolated AI is over. It is time to let your agents browse.&lt;/p&gt;
&lt;p&gt;Tabstack is now in public early access. We want to see what you can build when &lt;strong&gt;the entire web is an API.&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Website:&lt;/strong&gt; &lt;a href=&quot;https://tabstack.ai&quot;&gt;https://tabstack.ai&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Documentation:&lt;/strong&gt; &lt;a href=&quot;https://docs.tabstack.ai&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://docs.tabstack.ai&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Twitter/X:&lt;/strong&gt; &lt;a href=&quot;https://x.com/tabstack&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;@tabstack&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Let’s see what you build.&lt;/p&gt;
</content:encoded></item></channel></rss>