[{"data":1,"prerenderedAt":6440},["ShallowReactive",2],{"docs-nav":3,"docs-article-engineering\u002Fsystem-design\u002Fagentic-platform\u002Fcapabilities\u002Ftools-and-integrations":797},[4,17,27,44,55,67,75,82,94,106,114,122,129,135,144,153,161,169,177,189,202,211,218,229,240,248,260,268,276,286,296,305,314,323,331,337,343,350,356,364,371,378,383,393,401,410,415,422,432,439,444,451,458,462,467,475,487,499,509,516,525,533,539,545,551,557,563,567,579,593,603,614,621,626,633,640,646,653,658,665,673,678,686,692,699,704,710,721,730,740,747,753,761,767,776,782,791],{"path":5,"title":6,"description":7,"group":8,"section":6,"order":9,"tags":10,"lastUpdated":16},"\u002Fagents\u002Fagentic-crm","Agentic CRM","Research brief and build plan for an AgencyCore agentic CRM layer, rendered as an interactive page — the core operating loop, the target architecture, the typed-tool risk gateway, the proposed-actions review queue, and the four-slice MVP.","Agents",0,[11,12,13,14,15],"crm","agents","ai","architecture","research","2026-06-12",{"path":18,"title":19,"description":20,"group":8,"section":21,"order":22,"tags":23,"lastUpdated":26},"\u002Fagents\u002Fchat","Chat agent","High-level system design of the AgencyCore chat agent — core components, data flow, and the two abstractions that hold it together.","Reference",1,[12,14,24,25],"chat","system-design","2026-05-13",{"path":28,"title":29,"description":30,"group":8,"section":31,"order":32,"tags":33,"lastUpdated":43},"\u002Fagents\u002Fcompany-enrichment","Company Enrichment","The company enrichment workflow - a cache-first read in front of the company intelligence database that fills firmographic, contact and technographic facts via a fixed-order provider waterfall, and writes every resolved fact back with provenance so the first org pays once and every later search rides free.","Enrichment",2,[12,34,35,36,37,38,39,40,41,42],"workflow","enrichment","companies","waterfall","cache","intelligence-database","firmographics","provenance","sonar","2026-06-10",{"path":45,"title":46,"description":47,"group":8,"section":48,"order":9,"tags":49,"lastUpdated":54},"\u002Fagents\u002Fcompany-sonar","Company Signals","Signal-first company discovery for marketing agencies, on the Claude Agent SDK, with a global intelligence cache and deterministic composite scoring.","Company Sonar",[12,34,42,50,51,52,35,53,14],"company-search","signals","agent-sdk","scoring","2026-06-08",{"path":56,"title":57,"description":58,"group":8,"section":48,"order":22,"tags":59,"lastUpdated":66},"\u002Fagents\u002Fcompany-sonar\u002Fsignal-monitoring","Company Signals Monitoring","Realtime signal capture layer on top of the data graph. Detects hot events, scores them with a Claude managed agent against each agency's ICP, fans out alerts.",[14,51,60,61,62,63,64,65],"intel","icp","alerts","monitoring","sse","managed-agents","2026-06-09",{"path":68,"title":69,"description":70,"group":8,"section":71,"order":22,"tags":72,"lastUpdated":74},"\u002Fagents\u002Fconcepts\u002Fchat-agent-design-principles","Designing chat agents","The 2026 playbook for production chat agents that reach into internal systems via tools — context engineering, memory, tool design, when to add complexity.","Concepts",[12,14,24,73],"context-engineering","2026-05-14",{"path":76,"title":77,"description":78,"group":8,"section":71,"order":32,"tags":79,"lastUpdated":74},"\u002Fagents\u002Fconcepts\u002Fsystem-prompt-architecture","System prompt architecture","How to structure a production chat agent system prompt — eight sections, what each one does, and the rules vendors converge on.",[12,80,81],"prompt-engineering","system-prompt",{"path":83,"title":84,"description":85,"group":8,"section":84,"order":9,"tags":86,"lastUpdated":54},"\u002Fagents\u002Fenvoy","Envoy","High-level system design for the AI outreach engine — the sequence step state machine, the human-in-the-loop draft approval gate, multi-source context enrichment, and the inbox sentiment flow, rendered as an interactive page.",[12,87,88,89,90,91,92,93,14],"envoy","outreach","sales-engagement","sequences","state-machine","human-in-the-loop","nylas",{"path":95,"title":96,"description":97,"group":8,"section":98,"order":9,"tags":99,"lastUpdated":16},"\u002Fagents\u002Fheadhunter","Headhunter","The AI talent-search pipeline on one page - the production six-step design with its current-title relevance gate, and the 2.0 system design with internal-first waterfall sourcing, a pluggable source registry, automatic entity resolution, and a people intelligence graph that compounds every run.","General Search",[12,34,100,101,14,25,102,37,103,104,105],"headhunter","recruiting","multi-source","entity-resolution","people-intelligence","flywheel",{"path":107,"title":108,"description":109,"group":8,"section":21,"order":32,"tags":110,"lastUpdated":113},"\u002Fagents\u002Fpaperclip","Paperclip","Architecture deep dive into the Paperclip orchestration system.",[12,14,111,112],"orchestration","paperclip","2026-04-20",{"path":115,"title":116,"description":117,"group":8,"section":31,"order":22,"tags":118,"lastUpdated":16},"\u002Fagents\u002Fpeople-enrichment","People Enrichment","The people enrichment workflow - a cache-first read in front of the people intelligence database that fills profile, contact and employment facts via a fixed-order provider waterfall, keyed on the LinkedIn URL, and writes every resolved fact back with provenance so the first org pays once and every later search rides free. The fill step Headhunter and People Signals both call.",[12,34,35,119,37,38,39,120,41,100,121],"people","linkedin","people-sonar",{"path":123,"title":124,"description":125,"group":8,"section":126,"order":9,"tags":127,"lastUpdated":54},"\u002Fagents\u002Fpeople-sonar","People Signals","Signal-first people discovery for marketing agencies, built on the headhunter pipeline, with a composite score weighted by signal strength, source reputation, recency, and ICP fit.","People Sonar",[12,34,121,128,51,100,35,53,14],"people-search",{"path":130,"title":131,"description":132,"group":8,"section":126,"order":22,"tags":133,"lastUpdated":54},"\u002Fagents\u002Fpeople-sonar\u002Fpeople-signal-monitoring","People Signals Monitoring","Forward-looking design for the push layer that tracks known people - champions, past contacts, target-company decision-makers - and fires a warm lead the moment they change jobs, get promoted, or their company has an event.",[14,51,60,119,63,134],"warm-leads",{"path":136,"title":137,"description":138,"group":139,"section":140,"order":22,"tags":141,"lastUpdated":143},"\u002Fengineering\u002Fguides\u002Fagent-execution-stack","The Agent Execution Stack","Durable workflows over pluggable agent backends — how AgencyCore runs AI agents on Inngest over a webhook-driven Claude Managed Agents backend.","Engineering","Guides",[12,142,14,25],"inngest","2026-06-25",{"path":145,"title":146,"description":147,"group":139,"section":140,"order":9,"tags":148,"lastUpdated":143},"\u002Fengineering\u002Fguides\u002Fagent-runtime","Agent runtime","How AgencyCore runs AI agents on a provider-neutral runtime — the abstraction layer that lets us swap the agent backend, with Claude managed agents as the current provider.",[12,149,14,150,151,152,25],"runtime","anthropic","claude","providers",{"path":154,"title":155,"description":156,"group":139,"section":21,"order":157,"tags":158,"lastUpdated":160},"\u002Fengineering\u002Freference\u002Fagno-to-agent-sdk-migration","Agno → Claude Agent SDK migration","System-design spec for moving the ac-python-api workflow engine off Agno onto Anthropic's Claude Agent SDK \u002F Managed Agents, tiered by control-flow shape.",10,[12,14,159,52,65],"migration","2026-06-06",{"path":162,"title":163,"description":164,"group":139,"section":21,"order":22,"tags":165,"lastUpdated":54},"\u002Fengineering\u002Freference\u002Fcloudflare-agent-sandbox","Cloudflare agent sandbox","Cloudflare's Workers-based agent platform, evaluated as an alternative sandbox for our Agno workflows.",[12,166,167,168,159],"sandbox","cloudflare","workers",{"path":170,"title":171,"description":172,"group":139,"section":21,"order":32,"tags":173,"lastUpdated":176},"\u002Fengineering\u002Freference\u002Fvirtual-filesystem-rag","Virtual filesystem for AI assistants","How ChromaFs provides AI agents with structured file access.",[12,174,14,175],"rag","chromafs","2026-04-18",{"path":178,"title":179,"description":180,"group":139,"section":181,"order":182,"tags":183,"lastUpdated":188},"\u002Fengineering\u002Fsystem-design\u002Fagentic-platform\u002Fcapabilities\u002Fstate-and-knowledge","State and knowledge","What a run may know. One deterministic context builder over application state, knowledge and memory, one owner for every fact, and memory that is written through a tool.","Agentic platform",11,[184,185,186,11,187],"context","memory","knowledge","pgvector","2026-08-31",{"path":190,"title":191,"description":192,"group":139,"section":181,"order":157,"tags":193,"lastUpdated":201},"\u002Fengineering\u002Fsystem-design\u002Fagentic-platform\u002Fcapabilities\u002Ftools-and-integrations","Tools and integrations","A tool is the one way an agent reaches the world. AgencyCore owns the model facing contract, the invoke path, the credentials and the result boundary.",[194,195,196,197,198,199,200],"tools","integrations","mcp","agno","policy","security","idempotency","2026-09-04",{"path":203,"title":204,"description":205,"group":139,"section":181,"order":22,"tags":206,"lastUpdated":210},"\u002Fengineering\u002Fsystem-design\u002Fagentic-platform\u002Fcontract","Platform contract","One platform behind chat, interactive channels, triggers, approvals and background runs, with one Agno runtime, one tool layer, one state layer, and three cross-cutting planes.",[12,14,197,142,194,207,149,208,198,209],"skills","channels","observability","2026-09-02",{"path":212,"title":181,"description":213,"group":139,"section":214,"order":22,"tags":215,"lastUpdated":201},"\u002Fengineering\u002Fsystem-design\u002Fagentic-platform","The whole agentic platform on one page - who starts a run, the one boundary every run passes, how the work executes, and what comes back.","System design",[12,14,216,197,142,217,198],"overview","runs",{"path":219,"title":220,"description":221,"group":139,"section":181,"order":222,"tags":223,"lastUpdated":228},"\u002Fengineering\u002Fsystem-design\u002Fagentic-platform\u002Finterfaces\u002Fagent-access","Agent access (CLI and MCP)","How an outside AI agent reaches AgencyCore. The ac CLI works today as a user seat. An MCP server is planned and not designed.",6,[224,196,12,151,225,226,227],"cli","access","auth","todo","2026-08-18",{"path":230,"title":231,"description":232,"group":139,"section":181,"order":233,"tags":234,"lastUpdated":239},"\u002Fengineering\u002Fsystem-design\u002Fagentic-platform\u002Finterfaces\u002Fchannel-gateway","Channel gateway","The only layer that knows both an interactive channel and the platform. One message shape converges inbound, one intent shape diverges outbound, and no model call happens here.",3,[208,235,236,237,238,199],"slack","web","identity","sessions","2026-08-30",{"path":241,"title":242,"description":243,"group":139,"section":181,"order":244,"tags":245,"lastUpdated":210},"\u002Fengineering\u002Fsystem-design\u002Fagentic-platform\u002Finterfaces\u002Ffront-door","Front door","The conversational control layer. It turns a request into one structured decision, then deterministic application code answers or hands work to RunManager.",4,[246,247,197,184,198,217],"front-door","routing",{"path":249,"title":250,"description":251,"group":139,"section":181,"order":32,"tags":252,"lastUpdated":259},"\u002Fengineering\u002Fsystem-design\u002Fagentic-platform\u002Finterfaces\u002Fsurfaces","Surfaces","Every product surface and its API contract. Web chat goes through the gateway; every schema-native surface calls the domain API.",[253,254,24,255,256,257,258,217,64],"surfaces","api","approvals","prospects","saved-searches","builder","2026-09-03",{"path":261,"title":262,"description":263,"group":139,"section":181,"order":264,"tags":265,"lastUpdated":188},"\u002Fengineering\u002Fsystem-design\u002Fagentic-platform\u002Finterfaces\u002Ftriggers","Triggers","A Run with no person. Every producer emits one Event, matching is deterministic, and dispatch reuses RunManager, Policy and Inngest.",5,[266,267,142,200],"triggers","events",{"path":269,"title":270,"description":271,"group":139,"section":181,"order":272,"tags":273,"lastUpdated":259},"\u002Fengineering\u002Fsystem-design\u002Fagentic-platform\u002Fplanes\u002Fidempotency","Idempotency","One durable PostgreSQL key service prevents duplicate effects and freezes mutable input before selected Run starts. A Run start is guarded by a unique index on the Run row.",14,[200,217,194,274,275],"webhooks","reliability",{"path":277,"title":278,"description":279,"group":139,"section":181,"order":280,"tags":281,"lastUpdated":285},"\u002Fengineering\u002Fsystem-design\u002Fagentic-platform\u002Fplanes\u002Fobservability-and-operations","Observability and operations","One run row, one span tree and one usage meter. Sentry reports system failure; AgencyCore spans explain what the agent did.",13,[209,217,282,283,64,284],"spans","usage","sentry","2026-08-26",{"path":287,"title":288,"description":289,"group":139,"section":181,"order":290,"tags":291,"lastUpdated":295},"\u002Fengineering\u002Fsystem-design\u002Fagentic-platform\u002Fplanes\u002Fpolicy-and-governance","Policy and governance","One deterministic plane answers may this happen, at three checkpoints, with one grant model, one approval model and one decision log.",12,[198,292,255,293,294],"permissions","limits","governance","2026-08-25",{"path":297,"title":6,"description":298,"group":139,"section":299,"order":22,"tags":300,"lastUpdated":210},"\u002Fengineering\u002Fsystem-design\u002Fagentic-platform\u002Fproducts\u002Fagentic-crm","The AgencyCore CRM loop for turning signals and discovery into qualified organization prospects, CRM relationships and outreach.","Agentic products",[11,301,51,302,256,35,303,304,87],"lead-generation","intelligence","signals-search","email-sequence",{"path":306,"title":307,"description":308,"group":139,"section":299,"order":264,"tags":309,"lastUpdated":188},"\u002Fengineering\u002Fsystem-design\u002Fagentic-platform\u002Fproducts\u002Fbuilder-chat","Front door builder chat","Conversational authoring for organization-specific Agent and Workflow definitions, entered through the normal Front Door and backed by the existing DefinitionService.",[310,311,246,12,312,313,198],"authoring","definitions","workflows","templates",{"path":315,"title":316,"description":317,"group":139,"section":181,"order":318,"tags":319,"lastUpdated":201},"\u002Fengineering\u002Fsystem-design\u002Fagentic-platform\u002Fproducts\u002Fcapability-contracts","Company, People and Signals contracts","The five Phase 7 product capabilities, their bounded inputs, stable references, permissions and results.",21,[320,321,119,51,322],"capabilities","company","contracts",{"path":324,"title":325,"description":326,"group":139,"section":181,"order":327,"tags":328,"lastUpdated":330},"\u002Fengineering\u002Fsystem-design\u002Fagentic-platform\u002Fproducts\u002Fcapability-scenarios","Capability design scenarios","Normal, failure and recovery cases for the Phase 7 capability contracts, with implementation owners.",22,[320,329,321,119,51],"validation","2026-09-05",{"path":332,"title":333,"description":334,"group":139,"section":299,"order":233,"tags":335,"lastUpdated":210},"\u002Fengineering\u002Fsystem-design\u002Fagentic-platform\u002Fproducts\u002Femail-sequence","Email sequence workflow","Envoy durable outreach for one or many people, with fresh context, approvals, reply waits, follow-ups and Nylas transport.",[336,87,34,142,93,255],"email",{"path":338,"title":339,"description":340,"group":139,"section":299,"order":244,"tags":341,"lastUpdated":188},"\u002Fengineering\u002Fsystem-design\u002Fagentic-platform\u002Fproducts\u002Fgeneral-chat","Front door general chat","The default conversational answer path for AgencyCore. It answers from supplied context, cites what it used, asks when context is insufficient, and delegates real work through the normal Front Door.",[24,246,186,184,247,342],"citations",{"path":344,"title":345,"description":346,"group":139,"section":299,"order":222,"tags":347,"lastUpdated":188},"\u002Fengineering\u002Fsystem-design\u002Fagentic-platform\u002Fproducts\u002Fhuman-review","Human review inbox","One product page for every agentic action that is paused because a person must authorize an exact proposal. It is a view over the shared approval primitive, not a second review system.",[348,255,349,198,12],"human-review","inbox",{"path":351,"title":352,"description":353,"group":139,"section":299,"order":32,"tags":354,"lastUpdated":259},"\u002Fengineering\u002Fsystem-design\u002Fagentic-platform\u002Fproducts\u002Fsignals-search","Signals Search","One bounded discovery workflow that finds companies, verifies signals, finds relevant people, and produces evidence-backed organization prospects without prematurely creating CRM records.",[303,355,36,119,51,302,256,11,35],"discovery",{"path":357,"title":358,"description":359,"group":139,"section":299,"order":360,"tags":361,"lastUpdated":188},"\u002Fengineering\u002Fsystem-design\u002Fagentic-platform\u002Fproducts\u002Fworkflow-visualizer","Workflow visualizer","One constrained workflow graph, reused to author a draft, read a published definition, and watch a Run. Build mode edits the draft; run mode overlays Run and span state on the frozen snapshot.",7,[312,362,258,311,217,282,363,255],"visualizer","graph",{"path":365,"title":366,"description":367,"group":139,"section":181,"order":368,"tags":369,"lastUpdated":201},"\u002Fengineering\u002Fsystem-design\u002Fagentic-platform\u002Fruntime\u002Fdefinitions","Runtime definitions","Editable drafts, one published configuration per definition, template forks, deterministic validation, and the Run snapshot that keeps in flight work stable.",8,[149,311,329,370],"publishing",{"path":372,"title":373,"description":374,"group":139,"section":181,"order":375,"tags":376,"lastUpdated":259},"\u002Fengineering\u002Fsystem-design\u002Fagentic-platform\u002Fruntime\u002Fexecution","Runtime execution","The Run record, the Inngest step boundaries, agent segments, workflow nodes, approvals, cancellation, failure handling and live events.",9,[149,217,197,142,255,377,64],"cancellation",{"path":379,"title":380,"description":381,"group":139,"section":181,"order":360,"tags":382,"lastUpdated":285},"\u002Fengineering\u002Fsystem-design\u002Fagentic-platform\u002Fruntime","Agentic runtime","One Run contract, one Agno agent runtime, one deterministic workflow model, and the component boundaries that keep the framework replaceable.",[149,217,197,312,207,142],{"path":384,"title":385,"description":386,"group":139,"section":387,"order":244,"tags":388,"lastUpdated":392},"\u002Fengineering\u002Fsystem-design\u002Fmission-control\u002Fcompany-context","Company context","L3. Company state, knowledge and memory are three different things. One deterministic builder turns them into one brief.","Mission Control",[389,390,186,185,184,391,11],"mission-control","company-state","retrieval","2026-08-12",{"path":394,"title":395,"description":396,"group":139,"section":387,"order":22,"tags":397,"lastUpdated":392},"\u002Fengineering\u002Fsystem-design\u002Fmission-control\u002Fexperience","Experience","L6. Where a person observes and controls the company, and the one rule that keeps the UI out of the business.",[389,398,399,255,400],"ui","control-plane","activity",{"path":402,"title":403,"description":404,"group":139,"section":387,"order":222,"tags":405,"lastUpdated":392},"\u002Fengineering\u002Fsystem-design\u002Fmission-control\u002Ffoundation","Foundation","L1. Generic infrastructure with no business logic in it. The test is that another product could run on it unchanged.",[389,406,407,408,267,409,226,209],"infrastructure","database","queue","storage",{"path":411,"title":387,"description":412,"group":139,"section":214,"order":233,"tags":413,"lastUpdated":392},"\u002Fengineering\u002Fsystem-design\u002Fmission-control","The internal Company OS. Six layers and one policy plane put a person in control of company state and of autonomous execution.",[389,414,14,12,312,198,399],"company-os",{"path":416,"title":417,"description":418,"group":139,"section":387,"order":32,"tags":419,"lastUpdated":392},"\u002Fengineering\u002Fsystem-design\u002Fmission-control\u002Fintelligence","Intelligence","L5. The agent is the primitive. A skill is how it works, a tool is how it reaches the world, and the two are never the same thing.",[389,12,207,420,421],"planning","reasoning",{"path":423,"title":424,"description":425,"group":139,"section":387,"order":368,"tags":426,"lastUpdated":392},"\u002Fengineering\u002Fsystem-design\u002Fmission-control\u002Fmetrics-and-connectors","Metrics and connectors","A worked example across every layer. Three vendors, one metric pipeline, three views, and the rule that decides what we store.",[389,427,195,428,429,284,430,431],"metrics","stripe","posthog","ingest","dashboards",{"path":433,"title":434,"description":435,"group":139,"section":387,"order":233,"tags":436,"lastUpdated":392},"\u002Fengineering\u002Fsystem-design\u002Fmission-control\u002Forchestration","Orchestration","L4. Workflow, run, step, trigger and event. Five nouns that turn a decision into durable execution.",[389,312,217,266,267,437,438],"durability","retry",{"path":440,"title":288,"description":441,"group":139,"section":387,"order":360,"tags":442,"lastUpdated":392},"\u002Fengineering\u002Fsystem-design\u002Fmission-control\u002Fpolicy-and-governance","A plane, not a layer. One place decides what an agent may do, under what conditions, and how much. Human approval is one of its three answers.",[389,198,294,255,292,293,443],"audit",{"path":445,"title":191,"description":446,"group":139,"section":387,"order":264,"tags":447,"lastUpdated":392},"\u002Fengineering\u002Fsystem-design\u002Fmission-control\u002Ftools-and-integrations","L2. One contract for every capability. The tool is the only route to the world, and it is where policy, audit and tenancy meet.",[389,194,195,448,449,450],"adapters","registry","credentials",{"path":452,"title":453,"description":454,"group":139,"section":455,"order":22,"tags":456,"lastUpdated":210},"\u002Fengineering\u002Fsystem-design\u002Fworkflows\u002Fcompany-search","Company search","Implementation notes for company.search. Search resolves and gates company identities; enrichment is a separate capability.","Workflows",[321,457,142,42],"search",{"path":459,"title":31,"description":460,"group":139,"section":455,"order":233,"tags":461,"lastUpdated":210},"\u002Fengineering\u002Fsystem-design\u002Fworkflows\u002Fenrichment","Reusable company and people enrichment workflows with canonical Intelligence write-back, existing tier freshness and bounded asynchronous email.",[35,321,119,142],{"path":463,"title":464,"description":465,"group":139,"section":455,"order":32,"tags":466,"lastUpdated":259},"\u002Fengineering\u002Fsystem-design\u002Fworkflows\u002Fpeople-search","People search","Implementation notes for people.search. Bounded company scope and persona gates return selectable person identities without enrichment.",[119,457,142,100],{"path":468,"title":469,"description":470,"group":139,"section":455,"order":244,"tags":471,"lastUpdated":474},"\u002Fengineering\u002Fsystem-design\u002Fworkflows\u002Fsignals-search","Signals search","Superseded. The earlier on-demand buying-signal search component, kept as a record of the design that the agentic platform Signals Search workflow replaces.",[51,12,142,472,473],"intelligence-databases","superseded","2026-08-28",{"path":476,"title":477,"description":478,"group":479,"section":480,"order":481,"tags":482,"lastUpdated":66},"\u002Flearnings\u002Fagentic-sdlc","The agentic SDLC","How AI agents move from autocomplete to owning the loop across the software lifecycle, and why that shifts the bottleneck from coding to verification.","Learnings",null,30,[12,483,484,485,486],"sdlc","engineering","verification","review",{"path":488,"title":489,"description":490,"group":479,"section":480,"order":491,"tags":492,"lastUpdated":498},"\u002Flearnings\u002Fagi-to-asi","From AGI to ASI","What lies beyond human-level AI. The four technological pathways from AGI to artificial superintelligence, the formal ceiling that bounds them, and the six bottlenecks that could stall the climb - distilled from the DeepMind report.",50,[493,494,495,496,497],"ai-futures","asi","agi","scaling","recursive-self-improvement","2026-06-19",{"path":500,"title":501,"description":502,"group":479,"section":480,"order":503,"tags":504,"lastUpdated":66},"\u002Flearnings\u002Fai-native-company-playbook","AI native company playbook","Why AI should be the operating system your company runs on, not a tool it uses, and the concrete practices that follow - closed loops, a queryable org, software factories, and token maxing.",40,[505,506,12,507,508],"ai-native","company-building","gtm","founders",{"path":510,"title":511,"description":512,"group":479,"section":480,"order":157,"tags":513,"lastUpdated":54},"\u002Flearnings\u002Fbuying-intent-signals","Buying intent signals","How buyers leak their intent before they ever fill in a form, and how to read those signals before the window closes.",[514,51,507,515],"intent","sales",{"path":517,"title":518,"description":519,"group":479,"section":480,"order":520,"tags":521,"lastUpdated":54},"\u002Flearnings\u002Fcold-outbound-system","Cold outbound system","A high-level study of an open-source 29-skill cold email system, organized into five sequential tracks from ICP to iteration.",20,[522,523,507,524],"outbound","cold-email","systems",{"path":526,"title":527,"description":528,"group":479,"section":480,"order":529,"tags":530,"lastUpdated":532},"\u002Flearnings\u002Fswan-gtm-skills-architecture","Swan GTM skills architecture","A research note on Swan AI's foundations and maps model for GTM agents, with ASCII diagrams and ideas AgencyCore can borrow.",60,[507,12,73,531,14],"swan","2026-07-01",{"path":534,"title":535,"description":536,"group":387,"section":480,"order":272,"tags":537,"lastUpdated":43},"\u002Fmission-control\u002Fciops-agent","CIOps agent","High-level system architecture and design notes for the Mission Control CIOps agent.",[389,12,538,14],"ciops",{"path":540,"title":541,"description":542,"group":387,"section":480,"order":182,"tags":543,"lastUpdated":43},"\u002Fmission-control\u002Fcostops-agent","CostOps agent","High-level system architecture and design notes for the Mission Control CostOps agent.",[389,12,544,14],"finops",{"path":546,"title":547,"description":548,"group":387,"section":480,"order":520,"tags":549,"lastUpdated":54},"\u002Fmission-control\u002Fdashboard","Dashboard","The Mission Control product UI - a dark cockpit with a fleet-nav rail, company-state grid, a working escalation queue, live ledger and a global kill switch.",[389,12,550,398],"dashboard",{"path":552,"title":553,"description":554,"group":387,"section":480,"order":280,"tags":555,"lastUpdated":43},"\u002Fmission-control\u002Fproduct-analytics-agent","ProductAnalytics agent","High-level system architecture and design notes for the Mission Control ProductAnalytics agent.",[389,12,556,14],"product-analytics",{"path":558,"title":559,"description":560,"group":387,"section":480,"order":290,"tags":561,"lastUpdated":43},"\u002Fmission-control\u002Frevenueops-agent","RevenueOps agent","High-level system architecture and design notes for the Mission Control RevenueOps agent.",[389,12,562,14],"revops",{"path":564,"title":214,"description":565,"group":387,"section":480,"order":157,"tags":566,"lastUpdated":54},"\u002Fmission-control\u002Fsystem-design","One screen for the whole company, watched by a guardrailed fleet of ops agents that explain, propose, act and learn overnight.",[389,12,544,14],{"path":568,"title":569,"description":570,"group":571,"section":480,"order":32,"tags":572,"lastUpdated":578},"\u002Fproduct-design\u002Fonboarding-flow","Onboarding flow","Product design for the signup wizard and how TAM building folds into it. Analyzes the flow today (account, profile, company), the gap (no ICP, empty dashboard), and the integration of a new \"who you sell to\" ICP step plus a build-and-reveal screen that lands the user on a populated, ranked list.","Product Design",[573,61,574,575,576,577],"onboarding","tam","activation","ux","user-journey","2026-06-11",{"path":580,"title":581,"description":582,"group":571,"section":480,"order":233,"tags":583,"lastUpdated":592},"\u002Fproduct-design\u002Fpricing-entitlements","Pricing tiers, entitlements and usage credits","Specification for subscription tiers with gated platform access: composable plan entitlements, a unified usage-credit currency, plan-sourced limits, per-module trials and a two-ticket delivery plan built on the Stripe billing foundation. Written for discussion; the Linear document is the canonical copy with ticket links.",[584,585,586,587,588,589,590,591],"pricing","entitlements","billing","credits","subscriptions","plans","seats","trials","2026-07-06",{"path":594,"title":595,"description":596,"group":571,"section":480,"order":233,"tags":597,"lastUpdated":578},"\u002Fproduct-design\u002Fsales-signals-ux","Designing Signals","Product design for the sales-signals experience in ac-frontend: the 14-type taxonomy and its color system, the anatomy of a signal card across four densities, the 0-10 lead score scale, the origin tag (sonar pull vs proactive push), the seven surfaces where signals render (launchpad, sonar app, company detail, timeline, activities, data layer, Envoy), and the interaction rules that keep them consistent.",[51,576,598,11,42,599,600,601,602],"design-system","lead-score","origin","pull","push",{"path":604,"title":605,"description":606,"group":607,"section":608,"order":244,"tags":609,"lastUpdated":43},"\u002Fproprietary-data\u002Fcrm\u002Factivities","Activities","Deep dive on crm_activities, the interaction + task log of the CRM — where it is served from, how a row is born and read, and its full schema, relationships and rules.","Proprietary data","CRM",[11,610,611,612,613],"activities","tasks","data-model","schema",{"path":615,"title":616,"description":617,"group":607,"section":608,"order":264,"tags":618,"lastUpdated":43},"\u002Fproprietary-data\u002Fcrm\u002Fcommunications","Communications","Deep dive on crm_communications and crm_communication_events, the unified email\u002Fcall\u002Fmessage log and its per-message engagement tracking — where it is served from, the outbound message lifecycle, and the full schema, relationships and rules.",[11,619,336,620,612],"communications","engagement",{"path":622,"title":623,"description":624,"group":607,"section":608,"order":22,"tags":625,"lastUpdated":43},"\u002Fproprietary-data\u002Fcrm\u002Fcompanies","Companies","Deep dive on crm_companies, the account record at the centre of the CRM — where it is served from, how a row is born and read, and its full schema, relationships and rules.",[11,36,612,613,14],{"path":627,"title":628,"description":629,"group":607,"section":608,"order":233,"tags":630,"lastUpdated":43},"\u002Fproprietary-data\u002Fcrm\u002Fdeals","Deals","Deep dive on the deal pipeline — crm_deals, crm_pipeline_stages and crm_pipeline_config. Where it is served from, the life of a deal, and its full schema, relationships and rules.",[11,631,632,612,613],"deals","pipeline",{"path":634,"title":635,"description":636,"group":607,"section":608,"order":222,"tags":637,"lastUpdated":43},"\u002Fproprietary-data\u002Fcrm\u002Flists","Lists","Deep dive on crm_lists and crm_list_members, the static or dynamic member collections of the CRM — where they are served from, how a list and its members come to be and are read, and their schema, relationships and rules.",[11,638,639,612,613],"lists","segments",{"path":641,"title":642,"description":643,"group":607,"section":608,"order":32,"tags":644,"lastUpdated":43},"\u002Fproprietary-data\u002Fcrm\u002Fpeople","People","Deep dive on crm_people, the contact record of the CRM — where it is served from, how a row is born and read, and its full schema, relationships and rules.",[11,119,645,612,613],"contacts",{"path":647,"title":648,"description":649,"group":607,"section":608,"order":368,"tags":650,"lastUpdated":43},"\u002Fproprietary-data\u002Fcrm\u002Fsaved-filters","Saved filters","Deep dive on crm_saved_filters, the named reusable filter snapshots over the company, person and signal list views — where it is served from, how a saved view is born and applied, and its full schema, relationships and rules.",[11,651,652,612,613],"saved-filters","views",{"path":654,"title":655,"description":656,"group":607,"section":608,"order":360,"tags":657,"lastUpdated":578},"\u002Fproprietary-data\u002Fcrm\u002Fsignals","Signals","Deep dive on the signals tables - signals, company_signals and person_signals, the CRM's sales-intelligence layer. Where signals are served from, how one is born and attached, and the full schema, relationships and rules.",[11,51,302,612,613],{"path":659,"title":660,"description":661,"group":607,"section":662,"order":22,"tags":663,"lastUpdated":43},"\u002Fproprietary-data\u002Fintelligence-databases\u002Fcompany-intelligence-database","Company Intelligence Database","Decided architecture for ENG-669, the cross-org company intelligence layer that acts as a read-through cache in front of enrichment providers, with public-facts-only privacy and provenance-tracked write-back.","Intelligence databases",[14,60,36,51,38,664],"eng-669",{"path":666,"title":667,"description":668,"group":607,"section":662,"order":244,"tags":669,"lastUpdated":578},"\u002Fproprietary-data\u002Fintelligence-databases\u002Forg-signal-feed","Org Signal Feed","The per-org activation layer on top of the shared signals store. One immutable intel_signals row fans out to many orgs through scoring (signal-type weight times ICP fit times recency decay) and materializes as ranked, tiered rows in intel_org_signal_feed - the only org-scoped, RLS-per-org table of the signal stack, the door the launchpad, inbox and digest all read through. Signals enter by two ingest classes - a user's sonar pull (ungated) or an automated push (gated by threshold plus an optional competitor-ICP check) - logged in intel_signal_ingests, and each feed row records its origin.",[14,60,51,670,53,671,672,575,430,601,602,600],"feed","decay","rls",{"path":674,"title":675,"description":676,"group":607,"section":662,"order":32,"tags":677,"lastUpdated":578},"\u002Fproprietary-data\u002Fintelligence-databases\u002Fpeople-intelligence-database","People Intelligence Database","Decided architecture for the cross-org people intelligence layer - a read-through cache in front of headhunter research and Hunter email lookups, with LinkedIn-URL identity, append-only employment edges, per-tier freshness stamps on the flat profile, shared intel_sources provenance, unified intel_signals, and a GDPR erasure path.",[14,60,119,51,38,100],{"path":679,"title":680,"description":681,"group":607,"section":662,"order":233,"tags":682,"lastUpdated":578},"\u002Fproprietary-data\u002Fintelligence-databases\u002Fsignals-intelligence-database","Signals Intelligence Database","Decided v1 architecture for the unified signal store - one polymorphic append-only intel_signals table that holds both company and person signals, with a shared taxonomy, source-ranked provenance, an intel_signal_ingests log that records which pipeline found each signal, decay at read time, and a person-to-company rollup so a champion job change surfaces on the company feed.",[14,60,51,683,671,684,670,685,41,601,602],"polymorphic","taxonomy","ingests",{"path":687,"title":688,"description":689,"group":607,"section":480,"order":9,"tags":690,"lastUpdated":16},"\u002Fproprietary-data\u002Foverview","Data Layer Overview","The AgencyCore data layer in one map - the org-scoped CRM plane in production today and the global intelligence plane designed to sit in front of it, with interactive diagrams of both, the end-to-end data flow, freshness and precedence rules, the privacy seam, and the rollout path.",[691,14,60,11,51,38,25,216],"data-layer",{"path":693,"title":694,"description":695,"group":696,"section":480,"order":9,"tags":697,"lastUpdated":54},"\u002Froadmap","Roadmap - June 2026","June 2026 product plan across four themes. The spine is moving our agents onto an isolated sandbox runtime and rebuilding the core agents and workflows on it, then standing up a read-through intelligence data store and shipping the Stripe billing system. Knowledge base, assistant, and credit tracking carry into the July roadmap.","Roadmap",[698,420],"roadmap",{"path":700,"title":701,"description":702,"group":696,"section":480,"order":22,"tags":703,"lastUpdated":54},"\u002Froadmap\u002Fjuly-2026","Roadmap - July 2026","July 2026 product plan across three themes, all carried over from June. Building on June's sandbox runtime, July grounds the agents in a knowledge base, launches the AI chat assistant, and meters every action with per-action credit tracking that reconciles into the Stripe billing system shipped in June.",[698,420],{"path":705,"title":706,"description":707,"group":696,"section":480,"order":32,"tags":708,"lastUpdated":532},"\u002Froadmap\u002Fjune-2026-slides","Roadmap slides - June 2026","Board-review slide deck for the June 2026 product roadmap, rendered directly from the original PPTX in the docs site.",[698,420,709],"slides",{"path":711,"title":712,"description":713,"group":714,"section":8,"order":520,"tags":715,"lastUpdated":16},"\u002Fsymphony\u002Fagents\u002Fdevops-agent","DevOps agent","Interactive design for a Slack-first Symphony DevOps agent that wraps production promotion, rollback, audit, and operational jobs behind policy gates, typed runbooks, and an auditable ledger.","Symphony",[716,235,717,718,719,720],"symphony","devops","production","runbooks","operations",{"path":722,"title":723,"description":724,"group":714,"section":8,"order":157,"tags":725,"lastUpdated":16},"\u002Fsymphony\u002Fagents\u002Foncall-agent","Oncall agent","Interactive design for a Symphony oncall agent that turns Sentry incidents into rich Linear tickets, investigates with Codex, opens fix PRs, and resolves Sentry after merge.",[716,284,726,727,728,729],"linear","oncall","incident-response","codex",{"path":731,"title":732,"description":733,"group":714,"section":734,"order":157,"tags":735,"lastUpdated":16},"\u002Fsymphony\u002Fhousekeeping\u002Fcodex-vacuum","Codex vacuum","Interactive design for the Symphony housekeeping timer that checkpoints and vacuums Codex sqlite stores on the VPS.","Housekeeping",[716,736,737,729,738,739],"timed-jobs","housekeeping","sqlite","vps",{"path":741,"title":742,"description":743,"group":714,"section":734,"order":481,"tags":744,"lastUpdated":16},"\u002Fsymphony\u002Fhousekeeping\u002Fhost-cleanup","Host cleanup","Interactive design for the Symphony housekeeping timer that removes stale \u002Ftmp debris, vacuums the journal, and optionally cleans the apt package cache.",[716,736,737,739,745,746],"disk","cleanup",{"path":748,"title":749,"description":750,"group":714,"section":734,"order":520,"tags":751,"lastUpdated":16},"\u002Fsymphony\u002Fhousekeeping\u002Fworkspace-cleanup","Workspace cleanup","Interactive design for the Symphony housekeeping timer that prunes idle per-issue workspaces after their TTL.",[716,736,737,752,746,739],"workspaces",{"path":754,"title":755,"description":756,"group":714,"section":480,"order":9,"tags":757,"lastUpdated":66},"\u002Fsymphony","Symphony orchestration","How AgencyCore runs OpenAI Symphony as a long-running daemon that turns Linear tickets into isolated, autonomous Codex runs, reviewed by Claude and merged by humans. High-level workflow, system architecture, and the engineer playbook.",[716,729,726,758,111,739,759,760],"claude-review","qa","automation",{"path":762,"title":763,"description":764,"group":714,"section":214,"order":22,"tags":765,"lastUpdated":392},"\u002Fsymphony\u002Fsystem-design\u002Fhigh-level-design","High-level design","The Symphony daemon end to end — the standing agent workforce and its label-routed workflows, then the runtime that polls, dispatches, runs and writes back.",[716,14,111,12,729,726,766],"systemd",{"path":768,"title":769,"description":770,"group":714,"section":771,"order":503,"tags":772,"lastUpdated":16},"\u002Fsymphony\u002Ftimed-jobs\u002Fdaily-security-agent","Daily security agent","Interactive design for a report-only Symphony timed job that reviews the last 24h of commits, scans the system for vulnerabilities, and opens focused follow-up tickets.","Timed jobs",[716,199,736,729,773,774,775],"semgrep","threat-model","ownership",{"path":777,"title":778,"description":779,"group":714,"section":771,"order":481,"tags":780,"lastUpdated":16},"\u002Fsymphony\u002Ftimed-jobs\u002Fdaily-sentry-triage","Daily Sentry triage","Interactive design for the Symphony timed job that performs read-only Sentry triage, deduplicates existing tracked clusters, and creates focused ENG bugs for new actionable errors.",[716,736,284,209,781,726],"triage",{"path":783,"title":784,"description":785,"group":714,"section":771,"order":157,"tags":786,"lastUpdated":16},"\u002Fsymphony\u002Ftimed-jobs\u002Fnightly-local-staging-e2e","Nightly local staging E2E","Interactive design for the Symphony timed job that seeds local Supabase, runs ac-frontend Playwright E2E against the local staging stack, uploads evidence, and cleans artifacts.",[716,736,787,788,789,790],"e2e","playwright","staging","frontend",{"path":792,"title":793,"description":794,"group":714,"section":771,"order":520,"tags":795,"lastUpdated":16},"\u002Fsymphony\u002Ftimed-jobs\u002Fnightly-staging-qa","Nightly staging QA","Interactive design for the Symphony timed job that seeds a staging QA Linear issue, runs an agent-browser crawl, validates feature-map coverage, and files focused follow-up work.",[716,736,789,759,796,726],"agent-browser",{"id":798,"title":191,"body":799,"customComponent":480,"description":192,"extension":6428,"group":139,"lastUpdated":201,"meta":6429,"navigation":1224,"order":157,"path":190,"related":6430,"section":181,"seo":6436,"stem":6437,"tags":6438,"__hash__":6439},"docs\u002Fengineering\u002Fsystem-design\u002Fagentic-platform\u002Fcapabilities\u002Ftools-and-integrations.md",{"type":800,"value":801,"toc":6371},"minimark",[802,806,810,818,828,833,838,885,892,896,959,962,973,977,983,990,1010,1016,1032,1156,1159,1332,1345,1348,1352,1355,1394,1416,1423,1430,1435,1442,1459,1465,1479,1489,1492,1497,1503,1528,1531,1534,1541,1555,1565,1582,1585,1598,1610,1616,1709,1712,1737,1756,1769,1781,1806,1868,1871,1891,1908,1918,1924,1941,1951,1961,1967,1972,1987,1990,1996,2002,2028,2032,2035,2041,2052,2058,2064,2067,2077,2101,2107,2117,2123,2127,2136,2143,2150,2156,2159,2165,2171,2184,2196,2200,2206,2215,2221,2226,2229,2235,2239,2251,2274,2283,2289,2307,2313,2316,2330,2335,2438,2456,2478,2482,2488,2496,2503,2516,2523,2529,2539,2551,2555,2569,2575,2582,2609,2633,2644,2661,2728,2744,2749,2756,2861,2887,2893,2899,2902,2906,2917,2923,2926,2933,2942,2946,2951,2958,2961,2967,2974,2980,2990,2999,3015,3020,3026,3042,3058,3077,3094,3111,3127,3130,3136,3158,3162,3266,3291,3313,3323,3338,3341,3353,3356,3362,3390,3393,3405,3411,3417,3425,3431,3445,3463,3479,3492,3497,3500,3506,3549,3580,3590,3593,3597,3703,3706,3712,3726,3729,3733,3739,3769,3795,3805,3828,3850,3855,3923,3937,3946,3955,3977,3986,4016,4045,4055,4063,4067,4083,4089,4098,4102,4111,4114,4117,4123,4141,4144,4147,4150,4160,4164,4170,4173,4179,4188,4198,4250,4254,4261,4267,4279,4285,4298,4301,4307,4310,4356,4362,4368,4384,4387,4391,4397,4400,4406,4416,4435,4438,4441,4445,4560,4568,4574,4581,4584,4592,4618,4621,4639,4655,4660,4677,4681,4691,4694,4700,4704,4710,4716,4728,4732,4750,4761,4774,4781,4793,4796,4800,4810,4820,4824,4827,4840,4846,4852,4855,4865,4871,4877,4916,4926,4935,4945,4954,4958,4964,4985,4995,5001,5008,5012,5021,5027,5030,5039,5043,5057,5074,5077,5087,5105,5111,5117,5133,5137,5140,5345,5349,5352,5382,5388,5405,5418,5425,5433,5437,5451,5464,5471,5474,5480,5484,5489,5499,5502,5594,5597,5601,5825,5829,6058,6062,6086,6090,6367],[803,804,191],"h1",{"id":805},"tools-and-integrations",[807,808,809],"p",{},"A tool is one atomic capability an agent may call. It is the only way an agent changes anything.",[807,811,812,813,817],{},"The model proposes. ",[814,815,816],"code",{},"ToolInvoker"," decides, executes and bounds the answer.",[819,820,826],"pre",{"className":821,"code":823,"language":824,"meta":825},[822],"language-text","Agno agent\n   -> AgnoToolAdapter declaration\n   -> proposed tool call\n   -> ToolInvoker\n        -> live tool state\n        -> input schema\n        -> journal read (write and send)\n        -> policy action checkpoint (stage 1 grant, stage 2 rules)\n        -> idempotency claim (write and send)\n        -> connection + handler\n        -> result boundary\n        -> span + usage\n   -> ToolResult\n   -> Agno agent continues\n","text","",[814,827,823],{"__ignoreMap":825},[829,830],"doc-diagram",{"caption":831,"name":832},"The capabilities layer on one page. Reading is the top half: a deterministic builder reads four sources concurrently, and a packer resolves them by precedence into one brief — no model call anywhere in it. Acting is the bottom half: the model proposes, and ToolInvoker walks a fixed twelve-step path where the journal read is step four, policy is step six, and the claim is step eight. A denial comes back as a value so the agent adapts; an approval raises, because a value would let the model route around the gate. Below that, the four checks that must never collapse into one, and the four stores that hold it all.","3-capabilities",[834,835,837],"h2",{"id":836},"tool-skill-service","Tool, skill, service",[839,840,841,854],"table",{},[842,843,844],"thead",{},[845,846,847,851],"tr",{},[848,849,850],"th",{},"Concept",[848,852,853],{},"Meaning",[855,856,857,869,877],"tbody",{},[845,858,859,863],{},[860,861,862],"td",{},"Tool",[860,864,865,866],{},"One callable business action, such as ",[814,867,868],{},"email.send",[845,870,871,874],{},[860,872,873],{},"Skill",[860,875,876],{},"Reusable guidance for how an agent should use the tools it has",[845,878,879,882],{},[860,880,881],{},"Service",[860,883,884],{},"The implementation behind a tool",[807,886,887,888,891],{},"A skill teaches. A tool acts. Never expose a raw REST route, a generic ",[814,889,890],{},"api_request",", a repository or SQL to a model.",[834,893,895],{"id":894},"four-checks","Four checks",[839,897,898,911],{},[842,899,900],{},[845,901,902,905,908],{},[848,903,904],{},"Check",[848,906,907],{},"Owner",[848,909,910],{},"Question",[855,912,913,926,937,948],{},[845,914,915,918,923],{},[860,916,917],{},"Visibility",[860,919,920],{},[814,921,922],{},"ToolRegistry",[860,924,925],{},"Which tools does this agent see?",[845,927,928,931,934],{},[860,929,930],{},"Action decision",[860,932,933],{},"Policy",[860,935,936],{},"May this call happen now, with these arguments?",[845,938,939,942,945],{},[860,940,941],{},"Business validation",[860,943,944],{},"Tool handler",[860,946,947],{},"Are the arguments and the business state valid?",[845,949,950,953,956],{},[860,951,952],{},"Tenancy",[860,954,955],{},"The store's explicit tenant guard",[860,957,958],{},"Which rows belong to this organization?",[807,960,961],{},"Do not collapse the four. Visibility is not authorization.",[807,963,964,965,968,969,972],{},"For public service-key reads, ",[814,966,967],{},"scoped_db(organization_id)"," is the tenant guard. Agent-schema reads filter ",[814,970,971],{},"organization_id"," explicitly. RLS remains a guard for user-key paths, but it does not filter the API service key.",[834,974,976],{"id":975},"core-code","Core code",[819,978,981],{"className":979,"code":980,"language":824,"meta":825},[822],"services\u002Ftools\u002F\n  models.py             ToolSpec, ToolResult, ToolError,\n                        NAME_PATTERN, model_tool_name\n  registry.py           ToolRegistry, build_registry\n  declarations.py       code declared specs + MCP discovered specs\n  invoker.py            ToolInvocation, ToolInvoker, ApprovalRequired\n  default_invoker.py    policy, approval, accrual, idempotency and execution order\n  output_validation.py  output normalization and JSON Schema validation\n  result_boundary.py    redaction, bounding, untrusted content envelope\n  handlers\u002F\n    base.py             ToolHandler protocol\n    crm\u002F\n      common.py         shared CRM handler helpers\n      companies.py      company reads, updates and canonical upsert\n      people.py         people search and canonical upsert\n      lists.py          idempotent list membership\n    memory.py           durable observation and preference memory\n    research.py         Exa, Parallel, Firecrawl\n",[814,982,980],{"__ignoreMap":825},[807,984,985,986,989],{},"The connection resolver, Nylas email handler and generic MCP handler are\nplanned extensions. They are not modules in ",[814,987,988],{},"agentic-platform"," yet.",[807,991,992,999,1000,1003,1004,1009],{},[993,994,995,998],"strong",{},[814,996,997],{},"AgnoToolAdapter"," is not in this package."," It lives beside the runtime it\nserves, at ",[814,1001,1002],{},"runtime\u002Fagent\u002Fagno\u002Fadapter.py",", because it declares an Agno callable\nand raises an Agno exception. Keeping it here would put Agno imports in two\npackages, and the import contract could then name neither of them. Nothing else\nin this page moves: the registry, the invoker, the policy and the handlers stay\nframework neutral. See ",[1005,1006,1008],"a",{"href":1007},"\u002Fengineering\u002Fsystem-design\u002Fagentic-platform\u002Fruntime\u002Findex","the Agno boundary",".",[834,1011,1013],{"id":1012},"toolspec",[814,1014,1015],{},"ToolSpec",[807,1017,1018,1020,1021,1023,1024,1027,1028,1031],{},[814,1019,1015],{}," is the framework neutral contract. ",[814,1022,997],{}," shows the model\nthe ",[814,1025,1026],{},"description"," and the ",[814,1029,1030],{},"input_schema",", under a derived name.",[819,1033,1037],{"className":1034,"code":1035,"language":1036,"meta":825,"style":825},"language-python shiki shiki-themes github-dark","class ToolSpec(BaseModel):\n    name: str                       # the platform name: domain.verb, globally unique and stable\n    description: str                # the model reads this\n    input_schema: dict              # JSON Schema\n    output_schema: dict             # JSON Schema\n    version: int = 1                # bumped whenever a schema changes\n    schema_hash: str                # of input_schema + output_schema; the resync compares it\n    kind: Literal['internal', 'vendor', 'mcp']\n    binding: HandlerBinding | RemoteBinding\n    side_effects: Literal['read', 'write', 'send']\n    connection: str | None = None   # provider key the ConnectionResolver must satisfy\n    timeout_s: int = 30\n    retry: Literal['none', 'safe'] = 'none'\n    metered: bool = False           # the handler writes a usage row\n    repeatable: bool | None = None  # required and false for write\u002Fsend; absent for read\n    approval_preview: bool = False  # the handler renders the approval summary\n    policy_facts: list[str] = []    # target facts a rule may read about this call\n    batch_argument: str | None = None\n    max_batch_size: int | None = None\n    max_results: int | None = None\n    max_output_bytes: int | None = None\n    item_kind: str | None = None    # the ResourceRef kind of one item of `data`\n","python",[814,1038,1039,1046,1051,1056,1061,1066,1071,1076,1081,1086,1091,1096,1101,1106,1111,1117,1123,1129,1135,1141,1146,1151],{"__ignoreMap":825},[1040,1041,1043],"span",{"class":1042,"line":22},"line",[1040,1044,1045],{},"class ToolSpec(BaseModel):\n",[1040,1047,1048],{"class":1042,"line":32},[1040,1049,1050],{},"    name: str                       # the platform name: domain.verb, globally unique and stable\n",[1040,1052,1053],{"class":1042,"line":233},[1040,1054,1055],{},"    description: str                # the model reads this\n",[1040,1057,1058],{"class":1042,"line":244},[1040,1059,1060],{},"    input_schema: dict              # JSON Schema\n",[1040,1062,1063],{"class":1042,"line":264},[1040,1064,1065],{},"    output_schema: dict             # JSON Schema\n",[1040,1067,1068],{"class":1042,"line":222},[1040,1069,1070],{},"    version: int = 1                # bumped whenever a schema changes\n",[1040,1072,1073],{"class":1042,"line":360},[1040,1074,1075],{},"    schema_hash: str                # of input_schema + output_schema; the resync compares it\n",[1040,1077,1078],{"class":1042,"line":368},[1040,1079,1080],{},"    kind: Literal['internal', 'vendor', 'mcp']\n",[1040,1082,1083],{"class":1042,"line":375},[1040,1084,1085],{},"    binding: HandlerBinding | RemoteBinding\n",[1040,1087,1088],{"class":1042,"line":157},[1040,1089,1090],{},"    side_effects: Literal['read', 'write', 'send']\n",[1040,1092,1093],{"class":1042,"line":182},[1040,1094,1095],{},"    connection: str | None = None   # provider key the ConnectionResolver must satisfy\n",[1040,1097,1098],{"class":1042,"line":290},[1040,1099,1100],{},"    timeout_s: int = 30\n",[1040,1102,1103],{"class":1042,"line":280},[1040,1104,1105],{},"    retry: Literal['none', 'safe'] = 'none'\n",[1040,1107,1108],{"class":1042,"line":272},[1040,1109,1110],{},"    metered: bool = False           # the handler writes a usage row\n",[1040,1112,1114],{"class":1042,"line":1113},15,[1040,1115,1116],{},"    repeatable: bool | None = None  # required and false for write\u002Fsend; absent for read\n",[1040,1118,1120],{"class":1042,"line":1119},16,[1040,1121,1122],{},"    approval_preview: bool = False  # the handler renders the approval summary\n",[1040,1124,1126],{"class":1042,"line":1125},17,[1040,1127,1128],{},"    policy_facts: list[str] = []    # target facts a rule may read about this call\n",[1040,1130,1132],{"class":1042,"line":1131},18,[1040,1133,1134],{},"    batch_argument: str | None = None\n",[1040,1136,1138],{"class":1042,"line":1137},19,[1040,1139,1140],{},"    max_batch_size: int | None = None\n",[1040,1142,1143],{"class":1042,"line":520},[1040,1144,1145],{},"    max_results: int | None = None\n",[1040,1147,1148],{"class":1042,"line":318},[1040,1149,1150],{},"    max_output_bytes: int | None = None\n",[1040,1152,1153],{"class":1042,"line":327},[1040,1154,1155],{},"    item_kind: str | None = None    # the ResourceRef kind of one item of `data`\n",[807,1157,1158],{},"Seven fields deserve an explanation.",[1160,1161,1162,1189,1197,1253,1265,1281,1297],"ul",{},[1163,1164,1165,1171,1172,1174,1175,1180,1181,1184,1185,1188],"li",{},[993,1166,1167,1170],{},[814,1168,1169],{},"name"," is the platform name, and the model never sees it."," OpenAI and Anthropic both accept a letter, a digit, an underscore and a hyphen alone in a function name, so either provider refuses ",[814,1173,868],{}," on the first model call. OpenAI documents a cap of 64 characters, and the platform takes the lowest cap a provider documents. ",[1005,1176,1178],{"href":1177},"#agnotooladapter",[814,1179,997],{}," derives the model facing name when it declares the tool, and the adapter owns that map alone. A read call carries the platform name in its own body. A paused write or send reaches the drain loop under the model facing name, and the adapter maps that one back. Every one of these keeps the dot: ",[814,1182,1183],{},"Principal.scopes",", ",[814,1186,1187],{},"declared_scopes",", every published definition, every frozen snapshot, every policy rule, every idempotency scope and every live run event.",[1163,1190,1191,1196],{},[993,1192,1193,1195],{},[814,1194,1030],{}," is JSON Schema, not a Python type."," An internal or vendor tool declares a Pydantic model in code, and registration generates the schema from it. An MCP tool carries the schema its remote server published. One field then serves both, and the catalogue row stays storable.",[1163,1198,1199,1205,1241,1244,1245,1248,1249,1252],{},[993,1200,1201,1204],{},[814,1202,1203],{},"binding"," never carries code, and it is not a string.",[819,1206,1208],{"className":1034,"code":1207,"language":1036,"meta":825,"style":825},"class HandlerBinding(BaseModel):\n    handler_key: str          # resolves in the trusted handler registry\n\nclass RemoteBinding(BaseModel):\n    connection_id: UUID\n    remote_name: str          # the tool name the remote server published\n",[814,1209,1210,1215,1220,1226,1231,1236],{"__ignoreMap":825},[1040,1211,1212],{"class":1042,"line":22},[1040,1213,1214],{},"class HandlerBinding(BaseModel):\n",[1040,1216,1217],{"class":1042,"line":32},[1040,1218,1219],{},"    handler_key: str          # resolves in the trusted handler registry\n",[1040,1221,1222],{"class":1042,"line":233},[1040,1223,1225],{"emptyLinePlaceholder":1224},true,"\n",[1040,1227,1228],{"class":1042,"line":244},[1040,1229,1230],{},"class RemoteBinding(BaseModel):\n",[1040,1232,1233],{"class":1042,"line":264},[1040,1234,1235],{},"    connection_id: UUID\n",[1040,1237,1238],{"class":1042,"line":222},[1040,1239,1240],{},"    remote_name: str          # the tool name the remote server published\n",[1242,1243],"br",{},"A single ",[814,1246,1247],{},"'\u003Cconnection_id>:\u003Cremote_name>'"," string needs a parser, and a parser gets written twice and disagrees with itself once. A database row still cannot introduce executable Python: ",[814,1250,1251],{},"handler_key"," resolves in a registry the deploy owns.",[1163,1254,1255,1264],{},[993,1256,1257,1259,1260,1263],{},[814,1258,438],{}," is ",[814,1261,1262],{},"safe"," only for a read."," A write or a send retries through Inngest and the idempotency claim, never inside the handler.",[1163,1266,1267,1273,1274,1277,1278,1009],{},[993,1268,1269,1272],{},[814,1270,1271],{},"policy_facts"," is how a rule asks a question the arguments cannot answer."," A rule such as ",[814,1275,1276],{},"email.send AND recipient_is_new"," needs a CRM lookup. Policy may not query domain data, and the handler runs after the decision, so the tool declares the fact and the invoker resolves it first. See ",[1005,1279,1280],{"href":287},"policy and governance",[1163,1282,1283,1293,1294,1296],{},[993,1284,1285,1288,1289,1292],{},[814,1286,1287],{},"batch_argument"," names the public list that ",[814,1290,1291],{},"max_batch_size"," bounds."," The two fields appear together or not at all. The name must resolve to a top-level array in ",[814,1295,1030],{},". An optional batch that is absent counts as zero items. The invoker does not guess from every list in the argument tree. A filter list is not necessarily a batch, and two lists can form a Cartesian product.",[1163,1298,1299,1312,1313,1316,1317,1320,1321,1324,1325,1328,1329,1331],{},[993,1300,1301,1304,1305,1308,1309,1009],{},[814,1302,1303],{},"item_kind"," is what lets the invoker turn a ",[814,1306,1307],{},"Dropped"," marker into a ",[814,1310,1311],{},"ResourceRef"," ",[814,1314,1315],{},"bound()"," writes a marker and never a ref, because a pure function has no row to name. The invoker does hold the rows, and it is the one caller that substitutes — but a marker carries a size and a reason, not a kind. So the tool declares the kind of one item of its own ",[814,1318,1319],{},"data",", such as ",[814,1322,1323],{},"crm.person",", and the invoker reads the ",[814,1326,1327],{},"id"," off the item it replaced. A tool whose ",[814,1330,1319],{}," is not a collection of rows leaves the field unset, and its markers stay markers.",[807,1333,1334,1335,1338,1339,1312,1342,1344],{},"There is no ",[814,1336,1337],{},"scopes"," field. ",[993,1340,1341],{},"V1 has one scope vocabulary: the platform name.",[814,1343,1183],{}," holds the tool names the run may call, and it is already the intersection of the agent grant and the caller rights. A second vocabulary adds a mapping table and answers no question the tool name does not answer.",[807,1346,1347],{},"V1 needs no tool revision history. A run freezes the contract it showed the model, and reads the live enabled state, the policy, the credentials and the handler at each call.",[834,1349,1351],{"id":1350},"where-a-spec-comes-from","Where a spec comes from",[807,1353,1354],{},"Two sources, one catalogue.",[839,1356,1357,1370],{},[842,1358,1359],{},[845,1360,1361,1364,1367],{},[848,1362,1363],{},"Source",[848,1365,1366],{},"Declared",[848,1368,1369],{},"Refreshed",[855,1371,1372,1383],{},[845,1373,1374,1377,1380],{},[860,1375,1376],{},"Internal and vendor",[860,1378,1379],{},"In code, next to the handler",[860,1381,1382],{},"On deploy",[845,1384,1385,1388,1391],{},[860,1386,1387],{},"MCP",[860,1389,1390],{},"Listed from the connected server",[860,1392,1393],{},"On connect, and on a scheduled resync",[807,1395,1396,1399,1400,1403,1404,1407,1408,1411,1412,1415],{},[814,1397,1398],{},"internal_tool()"," and ",[814,1401,1402],{},"vendor_tool()"," generate schemas from the same Pydantic models and create the same ",[814,1405,1406],{},"HandlerBinding",". The internal factory sets ",[814,1409,1410],{},"kind='internal'",". The vendor factory sets ",[814,1413,1414],{},"kind='vendor'",". The vendor factory does not infer a connection, metering, retries, or limits. A caller declares each supported field explicitly. The MCP resync builds its own remote declaration when that binding lands.",[807,1417,1418,1419,1422],{},"An MCP tool is named ",[814,1420,1421],{},"mcp.\u003Cconnection_slug>.\u003Cremote_name>",". The prefix keeps a remote server from claiming a platform name. Registration rejects a platform name collision instead of shadowing.",[807,1424,1425,1426,1429],{},"An MCP server may drop or change a tool between runs. The invoker therefore checks that the bound remote tool still exists before it executes, and returns ",[814,1427,1428],{},"tool_unavailable"," when it does not.",[1431,1432,1434],"h3",{"id":1433},"schema-drift-is-a-version-check-not-a-presence-check","Schema drift is a version check, not a presence check",[807,1436,1437,1438,1441],{},"Presence is not enough. A server that keeps a tool and changes its input schema accepts the call and rejects the arguments, so a run fails with ",[814,1439,1440],{},"invalid_input"," on a call that was valid when the snapshot froze. The model is shown the frozen contract and cannot know.",[807,1443,1444,1455,1456,1458],{},[993,1445,1446,1447,1450,1451,1454],{},"Every spec carries a ",[814,1448,1449],{},"schema_hash"," and a ",[814,1452,1453],{},"version",", and the resync compares the first to decide whether to bump the second."," Both are fields on ",[814,1457,1015],{}," above, because the check reads them from a frozen snapshot and from the live registry, and a value that lives in neither cannot be compared.",[819,1460,1463],{"className":1461,"code":1462,"language":824,"meta":825},[822],"on connect and on resync\n  hash the remote input_schema and output_schema\n  unchanged  -> nothing happens\n  changed    -> write the new spec, and bump ToolSpec.version\n\nat invoke time\n  snapshot.version == registry.version   -> execute\n  differ                                 -> ToolResult error 'tool_changed'\n",[814,1464,1462],{"__ignoreMap":825},[807,1466,1467,1470,1471,1474,1475,1478],{},[814,1468,1469],{},"tool_changed"," is a stable error code and a value, not a raise, so the agent reads it and adapts exactly as it does for ",[814,1472,1473],{},"denied",". A workflow tool node fails its node, and ",[814,1476,1477],{},"continue_on_error"," decides the rest.",[807,1480,1481,1482,1485,1486,1488],{},"The resync runs ",[993,1483,1484],{},"hourly"," per connection, and on every connect. An hour is the exposure window, and ",[814,1487,1469],{}," closes it at the moment of the call rather than at the moment of the sync.",[807,1490,1491],{},"This answers what was open decision 1: a changed schema neither disables the tool nor forks it. The new spec wins for a new run, and a run holding the old contract stops cleanly instead of sending wrong arguments.",[834,1493,1495],{"id":1494},"toolregistry",[814,1496,922],{},[807,1498,1499,1500,1009],{},"The registry answers what a tool ",[993,1501,1502],{},"is",[819,1504,1506],{"className":1034,"code":1505,"language":1036,"meta":825,"style":825},"class ToolRegistry:\n    async def get(self, name: str) -> ToolSpec: ...\n    async def resolve(self, names: list[str]) -> list[ToolSpec]: ...\n    async def list_for_organization(self, organization_id: UUID) -> list[ToolSpec]: ...\n",[814,1507,1508,1513,1518,1523],{"__ignoreMap":825},[1040,1509,1510],{"class":1042,"line":22},[1040,1511,1512],{},"class ToolRegistry:\n",[1040,1514,1515],{"class":1042,"line":32},[1040,1516,1517],{},"    async def get(self, name: str) -> ToolSpec: ...\n",[1040,1519,1520],{"class":1042,"line":233},[1040,1521,1522],{},"    async def resolve(self, names: list[str]) -> list[ToolSpec]: ...\n",[1040,1524,1525],{"class":1042,"line":244},[1040,1526,1527],{},"    async def list_for_organization(self, organization_id: UUID) -> list[ToolSpec]: ...\n",[807,1529,1530],{},"The registry answers catalogue resolution and visibility. It never answers per call permission.",[807,1532,1533],{},"The full catalogue never reaches a model. An agent sees the tools its definition names, and nothing else.",[1431,1535,1334,1537,1540],{"id":1536},"there-is-no-toolfactory-and-there-was-one",[814,1538,1539],{},"ToolFactory",", and there was one",[807,1542,1543,1544,1546,1547,1550,1551,1554],{},"The factory closed each spec over ",[814,1545,816],{}," and answered a ",[814,1548,1549],{},"BoundTool"," list, and\n",[814,1552,1553],{},"AgentExecutor"," called it once per segment. Nothing calls it now, because two later\nchanges took both of its jobs.",[807,1556,1557,1560,1561,1564],{},[993,1558,1559],{},"The snapshot took the visibility half."," A definition names its tools, publication\nfreezes those contracts, and the run reads them as ",[814,1562,1563],{},"AgentExecutionRequest.tools",". A model\ntherefore sees exactly the frozen list, and a factory filtering the same list a second\ntime answers the same list.",[807,1566,1567,1312,1570,1574,1575,1578,1579,1581],{},[993,1568,1569],{},"The adapter took the calling half.",[1005,1571,1572],{"href":1177},[814,1573,997],{}," turns one\nfrozen contract into one framework callable, and ",[814,1576,1577],{},"ToolInvocation"," carries the ambient\nidentity a closure used to hold. A ",[814,1580,1549],{}," between them is a third shape for one fact.",[807,1583,1584],{},"So visibility is the snapshot, and authority is stage 1 of the policy engine. Two\nmechanisms, in two layers, and neither restates the other.",[807,1586,1587,1588,1593,1594,1597],{},"⚠️ ",[993,1589,1590,1592],{},[814,1591,816],{}," holds no scope check of its own."," Stage 1 reads the grant, live, at\nthe action checkpoint. A frozen read in the invoker would pass a tool the live read\nstripped, and it would refuse before the engine ran, so a scope refusal would reach no\n",[814,1595,1596],{},"agent.policy_decisions"," row.",[807,1599,1587,1600,1606,1607,1009],{},[993,1601,1602,1605],{},[814,1603,1604],{},"principal.scopes"," must therefore hold the definition's tool names."," The principal\nis the intersection of the agent grant and the caller rights, and stage 1 refuses a call\nwhose name it does not find. A principal minted with an empty scope list refuses\nevery call a run makes, and the refusal reads as a policy decision rather than as an\nunfilled field. The same set is what a child run intersects against, so an empty parent\ngrant empties every grant below it too. See ",[1005,1608,1609],{"href":372},"runtime execution",[834,1611,1613,1615],{"id":1612},"toolinvocation-and-handlers",[814,1614,1577],{}," and handlers",[819,1617,1619],{"className":1034,"code":1618,"language":1036,"meta":825,"style":825},"@dataclass(frozen=True)\nclass ToolInvocation:\n    \"\"\"The ambient identity of one call. It is not model context.\"\"\"\n    organization_id: UUID\n    user_id: UUID | None\n    run_id: UUID\n    definition_id: UUID\n    step_path: str                  # workflow node ID, or the literal 'agent'\n    principal: Principal\n    ceilings: RunCeilings           # the REMAINDER for this segment, not the frozen budget\n    deadline: datetime\n    source: RunSourceKind\n\nclass ToolHandler(Protocol):\n    async def execute(self, inv: ToolInvocation, args: BaseModel) -> ToolResult: ...\n\nclass ApprovalPreviewHandler(Protocol):\n    async def preview(self, inv: ToolInvocation, args: BaseModel) -> str: ...\n",[814,1620,1621,1626,1631,1636,1641,1646,1651,1656,1661,1666,1671,1676,1681,1685,1690,1695,1699,1704],{"__ignoreMap":825},[1040,1622,1623],{"class":1042,"line":22},[1040,1624,1625],{},"@dataclass(frozen=True)\n",[1040,1627,1628],{"class":1042,"line":32},[1040,1629,1630],{},"class ToolInvocation:\n",[1040,1632,1633],{"class":1042,"line":233},[1040,1634,1635],{},"    \"\"\"The ambient identity of one call. It is not model context.\"\"\"\n",[1040,1637,1638],{"class":1042,"line":244},[1040,1639,1640],{},"    organization_id: UUID\n",[1040,1642,1643],{"class":1042,"line":264},[1040,1644,1645],{},"    user_id: UUID | None\n",[1040,1647,1648],{"class":1042,"line":222},[1040,1649,1650],{},"    run_id: UUID\n",[1040,1652,1653],{"class":1042,"line":360},[1040,1654,1655],{},"    definition_id: UUID\n",[1040,1657,1658],{"class":1042,"line":368},[1040,1659,1660],{},"    step_path: str                  # workflow node ID, or the literal 'agent'\n",[1040,1662,1663],{"class":1042,"line":375},[1040,1664,1665],{},"    principal: Principal\n",[1040,1667,1668],{"class":1042,"line":157},[1040,1669,1670],{},"    ceilings: RunCeilings           # the REMAINDER for this segment, not the frozen budget\n",[1040,1672,1673],{"class":1042,"line":182},[1040,1674,1675],{},"    deadline: datetime\n",[1040,1677,1678],{"class":1042,"line":290},[1040,1679,1680],{},"    source: RunSourceKind\n",[1040,1682,1683],{"class":1042,"line":280},[1040,1684,1225],{"emptyLinePlaceholder":1224},[1040,1686,1687],{"class":1042,"line":272},[1040,1688,1689],{},"class ToolHandler(Protocol):\n",[1040,1691,1692],{"class":1042,"line":1113},[1040,1693,1694],{},"    async def execute(self, inv: ToolInvocation, args: BaseModel) -> ToolResult: ...\n",[1040,1696,1697],{"class":1042,"line":1119},[1040,1698,1225],{"emptyLinePlaceholder":1224},[1040,1700,1701],{"class":1042,"line":1125},[1040,1702,1703],{},"class ApprovalPreviewHandler(Protocol):\n",[1040,1705,1706],{"class":1042,"line":1131},[1040,1707,1708],{},"    async def preview(self, inv: ToolInvocation, args: BaseModel) -> str: ...\n",[807,1710,1711],{},"The model supplies the public arguments only. The organization, user, run,\ndefinition, principal, source and every credential are runtime owned.",[807,1713,1714,1720,1721,1723,1724,1727,1728,1730,1731,1734,1735,1009],{},[993,1715,1334,1716,1719],{},[814,1717,1718],{},"span_id"," field, and there was one."," One invocation is built once per segment and shared by every call in it, so an invocation is older than every call made through it: one ",[814,1722,1718],{}," would name one span for thirty tool calls. Span identity is ambient for exactly this reason, read through ",[814,1725,1726],{},"current_span_id()",", and ",[814,1729,816],{}," opens the ",[814,1732,1733],{},"tool"," span itself as a child of whatever is current. Two mechanisms for one fact disagree the first time either moves, and this pair disagreed on its first call. See ",[1005,1736,209],{"href":277},[807,1738,1739,1312,1745,1747,1748,1751,1752,1755],{},[993,1740,1741,1744],{},[814,1742,1743],{},"ceilings"," carries the remainder for four bounds, and the frozen budget for the fifth.",[814,1746,1553],{}," subtracts the turns, the tool calls and the wall clock a run already spent, so a metered call reads what is left of each. It subtracts nothing from ",[814,1749,1750],{},"max_cost_cents",", because the meter answers the spend of the ",[993,1753,1754],{},"whole tree"," and a total compares against a total. Subtract there as well and the run stops at half its budget.",[807,1757,1758,1761,1762,1765,1766,1768],{},[814,1759,1760],{},"preview()"," is optional. A tool that sets ",[814,1763,1764],{},"approval_preview"," renders the line a person reads in the approval inbox, because only the handler knows that ",[814,1767,868],{}," means \"one message to 3 new recipients\". Without it the inbox falls back to a generic argument render.",[807,1770,1771,1776,1777,1780],{},[993,1772,1773,1775],{},[814,1774,1760],{}," must be read only, and it runs on a call nobody has allowed yet."," Policy has just answered ",[814,1778,1779],{},"require_approval",", so the action is explicitly not authorized, and the invoker calls the handler anyway to render the line. Five rules keep that safe.",[1160,1782,1783,1786,1789,1796,1799],{},[1163,1784,1785],{},"It performs no write, no send, and no metered call. A preview that spent money would spend it on a request a person then rejects.",[1163,1787,1788],{},"It reads under the same principal, so it can see nothing the run could not.",[1163,1790,1791,1792,1795],{},"It runs under the shorter of ",[814,1793,1794],{},"timeout_s"," and the run deadline. A slow summary cannot hold the approval path without a bound.",[1163,1797,1798],{},"It returns a string. The invoker trims it and replaces each whitespace run with one space, so the stored value is one non-empty line.",[1163,1800,1801,1802,1805],{},"The normalized line is at most 512 UTF-8 bytes. A non-string, an empty line, an oversized line, a timeout, or an exception stores ",[814,1803,1804],{},"preview=None",". Human Review then keeps its argument view. The invoker logs the fallback, and the approval still exists.",[839,1807,1808,1821],{},[842,1809,1810],{},[845,1811,1812,1815,1818],{},[848,1813,1814],{},"Handler family",[848,1816,1817],{},"Reaches",[848,1819,1820],{},"Credential",[855,1822,1823,1836,1847,1858],{},[845,1824,1825,1828,1831],{},[860,1826,1827],{},"Internal, organization data",[860,1829,1830],{},"The AgencyCore application boundary",[860,1832,1833,1834],{},"Run scoped principal, and ",[814,1835,967],{},[845,1837,1838,1841,1844],{},[860,1839,1840],{},"Internal, global Intelligence",[860,1842,1843],{},"Dedicated Intelligence domain stores",[860,1845,1846],{},"Service-role client used only by those stores",[845,1848,1849,1852,1855],{},[860,1850,1851],{},"Vendor",[860,1853,1854],{},"Nylas, Exa, Parallel, Firecrawl",[860,1856,1857],{},"Platform or organization credential from the vault",[845,1859,1860,1862,1865],{},[860,1861,1387],{},[860,1863,1864],{},"An approved remote MCP server",[860,1866,1867],{},"Organization connection and vault credential",[807,1869,1870],{},"Vendor and MCP output is untrusted input.",[807,1872,1873,1312,1876,1879,1880,1883,1884,1886,1887,1890],{},[993,1874,1875],{},"An internal handler is not protected by RLS, and calling it that is the dangerous shorthand.",[814,1877,1878],{},"ac-python-api"," reads ",[814,1881,1882],{},"public"," on the service key, which bypasses every policy, so the tenancy guard is ",[814,1885,967],{}," applying an explicit filter against ",[814,1888,1889],{},"ORG_SCOPED_TABLES",". A handler that reaches for the admin client directly compiles, passes review and reads every tenant.",[807,1892,1893,1312,1896,1899,1900,1902,1903,1905,1906,1009],{},[993,1894,1895],{},"Global Intelligence is the narrow exception.",[814,1897,1898],{},"public.intel_*"," has no ",[814,1901,971],{},", is absent from ",[814,1904,1889],{},", and denies every user role through RLS. Its dedicated domain stores own the service-role client. An Intelligence handler calls those stores and never imports the admin domain or the client. A test must prove that the write does not include ",[814,1907,971],{},[807,1909,1587,1910,1917],{},[993,1911,1912,1913,1916],{},"The guard is ",[814,1914,1915],{},"scoped_db",", not \"go through the domain service\"."," Both CRM services reach the legacy agent stack:",[819,1919,1922],{"className":1920,"code":1921,"language":824,"meta":825},[822],"crm.companies.service -> crm.activities.service\n                      -> domains.envoy.sequences.steps.step_orchestrator\n                      -> domains.envoy.sequences.service\n                      -> workflow_engine.services.workflow_execution_service\n",[814,1923,1921],{"__ignoreMap":825},[807,1925,1926,1929,1930,1933,1934,1936,1937,1940],{},[814,1927,1928],{},"src.agentic never imports the legacy agent stack"," is an ",[814,1931,1932],{},"import-linter"," contract. The legacy stack stays a working fallback until cutover. Importing either service breaks the contract and pulls ",[814,1935,197],{}," into a second package. ",[814,1938,1939],{},"crm.people.service"," reaches the same edge.",[807,1942,1943,1944,1946,1947,1950],{},"So an internal handler takes ",[814,1945,1915],{}," directly, exactly as\n",[814,1948,1949],{},"crm.search.service"," already does, and never the admin client. It does not\nimport a legacy domain service or repository.",[807,1952,1953,1954,1957,1958,1960],{},"A platform-local capability service under ",[814,1955,1956],{},"src.agentic"," can share write rules\nwith a platform surface. The service takes the same ",[814,1959,1915],{}," client and\ndoes not cross into the legacy stack. Do not add this layer when the handler is\nthe only caller.",[807,1962,1963,1966],{},[993,1964,1965],{},"The handler or its platform-local service carries what the legacy service\nwas carrying",": the soft delete filter and the column projection. A tool must\nnot answer a row the product hides. Both rules belong in a test, because\nneither rule is visible in a review of the query alone.",[834,1968,1970],{"id":1969},"toolinvoker",[814,1971,816],{},[819,1973,1975],{"className":1034,"code":1974,"language":1036,"meta":825,"style":825},"class ToolInvoker:\n    async def invoke(self, tool_name: str, args: dict, inv: ToolInvocation) -> ToolResult: ...\n",[814,1976,1977,1982],{"__ignoreMap":825},[1040,1978,1979],{"class":1042,"line":22},[1040,1980,1981],{},"class ToolInvoker:\n",[1040,1983,1984],{"class":1042,"line":32},[1040,1985,1986],{},"    async def invoke(self, tool_name: str, args: dict, inv: ToolInvocation) -> ToolResult: ...\n",[807,1988,1989],{},"The path is fixed.",[819,1991,1994],{"className":1992,"code":1993,"language":824,"meta":825},[822],"open the `tool` span, as a child of current_span_id()\n -> resolve the snapshotted contract\n -> check the live enabled state, the remote tool presence, and the spec version\n -> validate the input schema and max_batch_size\n -> read the journal when write or send\n      absent     -> continue\n      completed  -> return the stored result, and tag the span replayed\n      processing -> raise ClaimInFlight; another worker holds a live lease\n      conflict   -> ToolResult error 'conflict'\n -> read the authorized_by the runtime handed this segment\n      covers the call, approved -> the action checkpoint is already decided\n      covers the call, rejected -> ToolResult error 'rejected'\n      names another proposal    -> ignored; the call is decided live\n -> resolve the policy_facts a matching rule needs\n -> policy action checkpoint\n      stage 1 reads the grant, live, so a right stripped mid run is gone here\n      fault            -> ToolResult error '&lt;fault_code&gt;', stop 'fault'\n      deny             -> ToolResult error 'denied'\n      require_approval -> the claim already holds a rejection of this call\n                            -> ToolResult error 'rejected'\n                          otherwise\n                            -> persist the approval, raise ApprovalRequired\n      allow            -> continue\n -> accrual checkpoint, when the tool is metered\n      deny             -> ToolResult error 'budget_exhausted', stop 'budget_exhausted',\n                          meta.partial_reason 'budget_exhausted'\n      fault            -> ToolResult error \u003Cthe fault code>, stop 'fault'\n -> claim the idempotency key when write or send\n      claimed    -> continue\n      completed  -> return the stored result, and tag the span replayed\n      processing -> raise ClaimInFlight; another worker holds a live lease\n      conflict   -> ToolResult error 'conflict'\n -> resolve the connection, then the handler\n -> execute under timeout_s and the run deadline\n -> validate the output schema\n -> redact, bound and tag the result\n -> complete the claim, or release it when the failure is retryable\n -> close the span, link the usage row\n -> ToolResult\n",[814,1995,1993],{"__ignoreMap":825},[807,1997,1998,1999,2001],{},"Only ",[814,2000,816],{}," calls a handler from an execution path. Nothing else does.",[807,2003,2004,1312,2007,1184,2009,2012,2013,1184,2015,1184,2017,1399,2020,2023,2024,2027],{},[993,2005,2006],{},"The span opens first, so a refused call is still in the tree.",[814,2008,1473],{},[814,2010,2011],{},"rejected",",\n",[814,2014,1469],{},[814,2016,1440],{},[814,2018,2019],{},"conflict",[814,2021,2022],{},"budget_exhausted"," all return before the handler runs,\nand each one is a thing the model tried to do. A span opened after the checkpoints would record the calls\nthat succeeded and hide the calls that policy stopped, which is the opposite of what an\nauditor opens the tree for. The refusal closes the span with ",[814,2025,2026],{},"status = 'error'"," and its\nstable code.",[1431,2029,2031],{"id":2030},"the-journal-is-read-before-the-decision-and-claimed-after-it","The journal is read before the decision, and claimed after it",[807,2033,2034],{},"The journal read and the claim are two steps, and the decision sits between them. A design\nthat read the journal inside the claim would put the whole answer after the policy\ncheckpoint. A turn that holds two gated calls would then never finish.",[819,2036,2039],{"className":2037,"code":2038,"language":824,"meta":825},[822],"requirement 1  email.send(to='jo@acme.test')    gated\nrequirement 2  email.send(to='sam@other.test')  gated\n\npass 1    call 1 raises. Nothing ran\nresume A  call 1 is approved and runs. Call 2 raises\nresume B  approval B names call 2, so call 1 reaches the invoker unauthorized\n",[814,2040,2038],{"__ignoreMap":825},[807,2042,2043,2044,2047,2048,2051],{},"At ",[814,2045,2046],{},"resume B"," the journal answers ",[814,2049,2050],{},"completed"," for call 1. The pass moves on and decides\ncall 2.",[807,2053,2054,2055,2057],{},"Read the journal after the checkpoint instead, and policy answers ",[814,2056,1779],{}," for\ncall 1 a second time. The inbox then shows a person a message the platform already sent.\nEvery resume repeats that, and the run ends on the tool call ceiling.",[807,2059,2060,2063],{},[993,2061,2062],{},"A decision about an effect that already happened is not a decision."," Policy cannot\nunsend the message. The platform already recorded the answer, so the journal answers\nfirst.",[807,2065,2066],{},"The claim itself stays after the checkpoints. A call that stops for a person takes no\nclaim, and the claim it would have taken is the one the resume needs.",[807,2068,1587,2069,2072,2073,2076],{},[993,2070,2071],{},"The read can go stale, so the claim decides again."," The checkpoints take time, and a\nhuman round trip takes days. Another attempt of the same segment can complete the very key\nthis one read as ",[814,2074,2075],{},"absent",". The claim is therefore the authority.",[807,2078,2079,2082,2083,2086,2087,2089,2090,2086,2093,2096,2097,2100],{},[993,2080,2081],{},"The two steps answer four outcomes each, and the four are not the same four."," Only\n",[814,2084,2085],{},"read()"," answers ",[814,2088,2075],{},", and only ",[814,2091,2092],{},"claim()",[814,2094,2095],{},"claimed",". One shared branch helper\ngives the claim path an ",[814,2098,2099],{},"absent -> continue"," arm. A claim that raced and lost then reaches\nthe handler, and that is the second message this step exists to stop.",[819,2102,2105],{"className":2103,"code":2104,"language":824,"meta":825},[822],"read()    absent   completed   processing   conflict\nclaim()   claimed  completed   processing   conflict\n",[814,2106,2104],{"__ignoreMap":825},[807,2108,2109,2112,2113,2116],{},[993,2110,2111],{},"The key is derived once, before the read."," Both steps use that one value. V1 refuses\n",[814,2114,2115],{},"repeatable = True",", so no ordinal can make the read and claim name different keys.",[807,2118,1587,2119,2122],{},[993,2120,2121],{},"A replay is answered before the checkpoint, and policy is not asked."," The effect\nalready happened, so policy is never asked to decide it again. Only a write or a send\njournals, and the key names one organization, one run, one step and one arguments hash,\nso a replay is always one run reading back its own effect. The first call this run has\nnot already made meets the live grant and the current rules.",[1431,2124,2126],{"id":2125},"a-metered-call-checks-the-budget","A metered call checks the budget",[807,2128,2129,2131,2132,2135],{},[814,2130,1553],{}," checks accrual before each segment, and ",[814,2133,2134],{},"WorkflowStepExecutor"," before each node. That is enough while a node costs about the same as a model turn. It is not enough for a fan out.",[807,2137,2138,2139,2142],{},"Signals Search searches people across every company that survived pruning, inside ",[993,2140,2141],{},"one"," step. Between the accrual check at the top of that node and the check at the top of the next one, the run can make hundreds of paid vendor calls. The ceiling is read once, and the overshoot is the whole fan out.",[807,2144,2145,2146,2149],{},"So a tool that declares ",[814,2147,2148],{},"metered"," passes the accrual checkpoint before it executes.",[819,2151,2154],{"className":2152,"code":2153,"language":824,"meta":825},[822],"ToolSpec.metered = True   -> the invoker calls accrual first\nToolSpec.metered = False  -> no extra read; the node boundary is enough\n",[814,2155,2153],{"__ignoreMap":825},[807,2157,2158],{},"This adds no fourth checkpoint. Accrual keeps one definition and gains a second caller, exactly as the action checkpoint has one definition and many callers.",[807,2160,2161,2164],{},[993,2162,2163],{},"A metered call reads the run tree only."," The organization day ceiling is checked at the segment and node boundaries, and not on every paid call. A run cannot pass the organization ceiling faster than it passes its own, so the second read buys nothing and costs a whole-day aggregate per call inside a fan out.",[819,2166,2169],{"className":2167,"code":2168,"language":824,"meta":825},[822],"before a metered tool call     the run tree total          indexed on root_run_id, one run's rows\nbefore a segment or a node     the run tree AND the day    rare, so the cost does not matter\n",[814,2170,2168],{"__ignoreMap":825},[807,2172,2173,2174,2177,2178,2181,2182,1009],{},"Both read ",[814,2175,2176],{},"ai_usage_log",", the canonical meter. Neither reads ",[814,2179,2180],{},"ai_usage_daily",": a rollup lags, so a run's own in flight spend is missing from it and the ceiling would never fire. See ",[1005,2183,1280],{"href":287},[807,2185,2186,2187,2190,2191,2193,2194,1009],{},"A run stopped this way still ",[993,2188,2189],{},"succeeds with a partial reason",". The invoker returns ",[814,2192,2022],{}," as a value, so the agent or the step can finish with the items it already has. See ",[1005,2195,1280],{"href":287},[1431,2197,2199],{"id":2198},"a-metered-call-the-meter-cannot-see","A metered call the meter cannot see",[807,2201,2202,2203,1009],{},"Two things stop the checkpoint before it reads anything. No accrual checker is wired, or the call runs outside a run scope. Both are wiring faults, and both answer ",[814,2204,2205],{},"internal_error",[807,2207,2208,2209,2211,2212,2214],{},"They never answer ",[814,2210,2022],{},". The money code sends an operator to ",[814,2213,2176],{}," and to the run ceilings, and both show headroom.",[807,2216,2217,2220],{},[993,2218,2219],{},"Sentry receives the fault one time. The log receives every call."," The fault runs on the path of one tool call. A fan out node makes hundreds of calls inside one step. A report per call gives the Sentry issue one repeated message. That message then hides every other defect. The invoker holds no durable step, so it holds a set of the faults it reported.",[807,2222,2223,2225],{},[814,2224,2134],{}," bounds the same shape by a different amount. Its durable step reports one time for each step execution, so a later node and a later run report again. The invoker's set reports one time for the life of the invoker.",[807,2227,2228],{},"The key names the tool and the cause. It holds no run id. Both causes are static. A missing checker is a defect of the deploy. A missing run scope is a defect of the call site. Neither one varies by run, so a run id in the key gives one report for each run of a fan out.",[807,2230,1587,2231,2234],{},[993,2232,2233],{},"An operator who resolves the Sentry issue receives no second event."," That invoker stays silent for its life. Both causes change only on a deploy, and a deploy builds a new invoker with an empty set. The log carries every call until then, and a retry in another worker process gives one more report.",[1431,2236,2238],{"id":2237},"the-refusal-carries-its-own-stop-beside-the-code","The refusal carries its own stop, beside the code",[807,2240,2241,2243,2244,2247,2248,2250],{},[814,2242,816],{}," answers a refusal it made and a tool that failed as the same type. The two need different answers, and ",[993,2245,2246],{},"the code cannot separate them",". A remote tool may answer ",[814,2249,2022],{}," about a vendor budget. A caller that read the code would turn a run that really failed into a partial success.",[807,2252,2253,2254,2257,2258,2261,2262,2265,2266,2269,2270,2273],{},"So ",[814,2255,2256],{},"ToolResult"," carries the field ",[814,2259,2260],{},"stop",". Only ",[814,2263,2264],{},"DefaultToolInvoker"," writes it, ",[814,2267,2268],{},"_bound"," drops a value a handler wrote, and ",[814,2271,2272],{},"model_payload"," never renders it.",[819,2275,2277],{"className":1034,"code":2276,"language":1036,"meta":825,"style":825},"ToolStop = Literal['budget_exhausted', 'fault']\n",[814,2278,2279],{"__ignoreMap":825},[1040,2280,2281],{"class":1042,"line":22},[1040,2282,2276],{},[819,2284,2287],{"className":2285,"code":2286,"language":824,"meta":825},[822],"budget_exhausted   the run ends partial under that reason; the work done stays done\nfault              the run ends failed under the result's own error code\nNone               an answer the model adapts to, or a success\n",[814,2288,2286],{"__ignoreMap":825},[807,2290,2291,2292,2295,2296,2298,2299,2302,2303,2306],{},"Six refusals on the metered path set ",[814,2293,2294],{},"fault",". Each one is a defect of the deploy or of the call site, and no model can act on any of them. Other ",[814,2297,2205],{}," refusals outside that path still set nothing. ",[814,2300,2301],{},"PLATFORM_STOP_CODES"," stops a ",[993,2304,2305],{},"workflow tool node"," for them, because the code is a member. The agent path reads the field alone, so those refusals return to the model there.",[819,2308,2311],{"className":2309,"code":2310,"language":824,"meta":825},[822],"the meter did not answer, or the caller passed an argument accrual cannot read\nno accrual checker is wired\nthe call ran outside a run scope\na write or send tool reached no idempotency journal\npolicy asked for an approval and no approval service is wired\nan approval needs a run scope, and the call carried none\n",[814,2312,2310],{"__ignoreMap":825},[807,2314,2315],{},"The invoker reports four of the six to Sentry. Those four share one dedup set, so a fan out node reports each one time rather than one time per call.",[807,2317,2318,1184,2320,1184,2322,1399,2324,2326,2327,2329],{},[814,2319,1473],{},[814,2321,2011],{},[814,2323,1440],{},[814,2325,1428],{}," set nothing, because each one tells the model to do something else. ",[814,2328,1469],{}," will not either, when the connection resolver lands.",[807,2331,2332],{},[993,2333,2334],{},"Three callers read it, and each already owns the answer.",[839,2336,2337,2355],{},[842,2338,2339],{},[845,2340,2341,2344,2348,2352],{},[848,2342,2343],{},"Caller",[848,2345,2346],{},[814,2347,2022],{},[848,2349,2350],{},[814,2351,2294],{},[848,2353,2354],{},"How it leaves",[855,2356,2357,2375,2408],{},[845,2358,2359,2364,2369,2372],{},[860,2360,2361],{},[814,2362,2363],{},"WorkflowStepExecutor.run_tool",[860,2365,2366],{},[814,2367,2368],{},"NodeOutcome.partial_reason",[860,2370,2371],{},"a platform stop under the result's code",[860,2373,2374],{},"it returns the outcome",[845,2376,2377,2386,2395,2402],{},[860,2378,2379,2381,2382,2385],{},[814,2380,997],{}," (a ",[814,2383,2384],{},"read"," tool)",[860,2387,2388,2391,2392],{},[814,2389,2390],{},"record_stop"," → ",[814,2393,2394],{},"record_ceiling(COST_CEILING)",[860,2396,2397,2391,2399],{},[814,2398,2390],{},[814,2400,2401],{},"record_failed(ToolStopFault)",[860,2403,2404,2405],{},"the body raises ",[814,2406,2407],{},"StopAgentRun",[845,2409,2410,2423,2428,2432],{},[860,2411,2412,2381,2415,2418,2419,2422],{},[814,2413,2414],{},"AgnoAgentRuntime._resolve",[814,2416,2417],{},"write"," or ",[814,2420,2421],{},"send"," tool at the pause)",[860,2424,2425,2426],{},"the same ",[814,2427,2390],{},[860,2429,2425,2430],{},[814,2431,2390],{},[860,2433,2434,2437],{},[814,2435,2436],{},"_resolve"," returns False",[807,2439,2440,2443,2444,2447,2448,2451,2452,2455],{},[993,2441,2442],{},"Both agent rows call the one method."," A second copy of the mapping would let the two\npaths end one run two ways, so ",[814,2445,2446],{},"AgnoToolAdapter.record_stop"," is public and holds it once.\n",[814,2449,2450],{},"ToolStopFault"," carries the code the invoker named. It is terminal: ",[814,2453,2454],{},"_answer"," ends the run\nunder that code rather than re-raising, because the same wiring answers the same way on\nevery replay.",[807,2457,1587,2458,2461,2462,2418,2464,2466,2467,2470,2471,2474,2475,2477],{},[993,2459,2460],{},"The drain loop is the third caller, and it is the one a reader forgets."," A ",[814,2463,2417],{},[814,2465,2421],{}," tool never reaches the adapter body. The framework pauses it, and the drain loop invokes it outside anything the framework wraps. A refusal delivered as that call's result lets the model propose the same send on the next turn, and the run then ends on ",[814,2468,2469],{},"max_agent_turns"," under ",[814,2472,2473],{},"limit_reached",", which names the wrong clock. ",[814,2476,2390],{}," is public for that reason. Both paths call the one method, so one refusal ends one run one way.",[1431,2479,2481],{"id":2480},"the-claim-ends-with-the-call","The claim ends with the call",[819,2483,2486],{"className":2484,"code":2485,"language":824,"meta":825},[822],"ok=True                          -> complete the claim with the stored result\nok=False, retryable=False        -> complete the claim; the answer is stable\nok=False, retryable=True         -> release the claim; nothing happened\nthe handler raised               -> release the claim; the segment retries\nthe call timed out               -> HOLD the claim; nothing is known\n",[814,2487,2485],{"__ignoreMap":825},[807,2489,2490,2493,2494,1009],{},[993,2491,2492],{},"A retryable failure must not be remembered."," A vendor 429 makes no effect, and a completed claim would return that 429 to every later attempt in the same run. The agent would then be locked out of a vendor that recovered a minute later. See ",[1005,2495,200],{"href":269},[807,2497,2498,2499,2502],{},"A release is safe because the claim is taken ",[993,2500,2501],{},"before"," the handler runs. Nothing outside the platform saw the call.",[807,2504,1587,2505,2508,2509,2511,2512,2515],{},[993,2506,2507],{},"A timeout is the one ending that release does not fit, so it is its own row."," The other four are knowable. A success and a stable failure are what the handler observed; a 429 is a rejection the handler read; a raise is a decision the handler made. A timeout is none of those: ",[814,2510,816],{}," cancelled the handler mid ",[814,2513,2514],{},"await",", and the request it had already sent may still reach the vendor. Release it and the very next attempt sends the second email.",[807,2517,2518,2519,2522],{},"So a timed-out call ",[993,2520,2521],{},"completes nothing and releases nothing",". The lease is what bounds it.",[819,2524,2527],{"className":2525,"code":2526,"language":824,"meta":825},[822],"retry inside the lease   the claim reads `processing`, the invoker raises,\n                         and Inngest replays the step after the lease\nretry past the lease     the claim is reclaimed, exactly as it is for a\n                         worker that died mid call\n",[814,2528,2526],{"__ignoreMap":825},[807,2530,2531,2532,2536,2537,1009],{},"The second line is a real duplicate-effect window, and it is the same one a crashed worker already has. Only a downstream vendor idempotency key closes it, which is why ",[1005,2533,2535],{"href":2534},"#idempotency-and-the-replay-journal","passing the key downstream"," matters for a ",[814,2538,2421],{},[807,2540,2541,1312,2547,2550],{},[993,2542,2543,2544,1009],{},"The deadline case is the opposite answer, and it is also a ",[814,2545,2546],{},"TimeoutError",[814,2548,2549],{},"_execute"," refuses before the handler runs when the Run is already past its deadline. Nothing outside the platform saw that call, so it releases. The two are told apart by type, not by message: the pre-call refusal raises a subclass, and the invoker branches on it before the general timeout.",[1431,2552,2554],{"id":2553},"the-approval-is-a-pause-not-a-raise","The approval is a pause, not a raise",[807,2556,2557,2558,2561,2562,1879,2565,2568],{},"A write or send tool is declared ",[814,2559,2560],{},"external_execution=True"," when the adapter builds it.\nAgno then stops its loop before that call runs, and ",[814,2563,2564],{},"RunOutput.status",[814,2566,2567],{},"PAUSED",".\nThe declaration is static, and it says only \"this call is worth a decision\". The decision\nstays live: the runtime reads the pending call at the pause and invokes it with the\narguments in hand.",[819,2570,2573],{"className":2571,"code":2572,"language":824,"meta":825},[822],"model proposes email.send(to='jo@acme.test', ...)\n  -> Agno pauses. The callable never runs.\n  -> RunOutput.status = PAUSED, and each proposal is a RunRequirement\n  -> the runtime reads requirement.tool_execution.tool_args\n  -> ToolInvoker.invoke(...)          \u003C- our code, at the pause, outside the loop\n       allow            -> set_external_execution_result(\u003Cthe ToolResult>)\n       deny             -> set_external_execution_result(\u003Ca rejected ToolResult>)\n       require_approval -> ApprovalRequired; the segment returns needs_approval\n  -> acontinue_run(...), unless the segment stopped\n",[814,2574,2572],{"__ignoreMap":825},[807,2576,2577,2578,2581],{},"The expiry still travels with the raise. ",[814,2579,2580],{},"AgentExecutionResult.approval_expires_at"," is\nwhat the Inngest wait computes its timeout from, and reading it back from the approval row\nafterwards would be a second query for a fact the raiser already held.",[807,2583,1587,2584,2590,2591,2594,2595,2597,2598,2601,2602,2604,2605,2608],{},[993,2585,2586,2587,1009],{},"The raise leaves the span block. It must not close the span ",[814,2588,2589],{},"error"," The\n",[1005,2592,2593],{"href":277},"span recorder","\ncloses ",[814,2596,2026],{}," for every exception that crosses its context manager, and it\npublishes ",[814,2599,2600],{},"span.failed",". An approval is not a failure. Every gated write\nwould then read as a fault in the tree, beside the denials that close ",[814,2603,2589],{},"\non purpose, and an auditor could not tell the two apart. Catch the raise inside the\nblock. Record the approval id as an attribute. Close the span ",[814,2606,2607],{},"ok",". Then raise again,\noutside the block.",[807,2610,2611,2619,2620,2623,2624,2626,2627,2629,2630,2632],{},[993,2612,2613,2616,2617,1009],{},[814,2614,2615],{},"external_execution"," is what keeps the call in ",[814,2618,816],{}," Agno's other pause,\n",[814,2621,2622],{},"requires_confirmation",", runs the callable itself once confirmed, which would move every\nwrite off the invoke path. With ",[814,2625,2615],{}," the platform executes the call and\nhands back a result, so ",[814,2628,816],{}," keeps the idempotency claim, the connection, the\nmetering and the result boundary. Reserve ",[814,2631,2622],{}," for a callable that\ngenuinely belongs to Agno.",[807,2634,2635,2638,2639,2643],{},[993,2636,2637],{},"A read tool is not declared at all."," It stays an ordinary Agno callable that the\nadapter runs inside the loop: there is no decision to make, so there is no reason to pay a\npause for it. That split is what keeps the ",[1005,2640,2642],{"href":2641},"#handler-failure","handler failure"," rules alive.",[807,2645,2646,2649,2650,2656,2657,2660],{},[993,2647,2648],{},"Measured on Agno 2.5.10"," (",[1005,2651,2655],{"href":2652,"rel":2653},"https:\u002F\u002Flinear.app\u002Fagencycore\u002Fissue\u002FENG-2093",[2654],"nofollow","ENG-2093","),\nagainst ",[814,2658,2659],{},"ac-python-api\u002Fscripts\u002Fspikes\u002Feng2093_agno_native_pause.py",":",[839,2662,2663,2672],{},[842,2664,2665],{},[845,2666,2667,2669],{},[848,2668,910],{},[848,2670,2671],{},"Answer",[855,2673,2674,2690,2701,2709,2717],{},[845,2675,2676,2679],{},[860,2677,2678],{},"Does the pause reach the caller?",[860,2680,2681,2682,2685,2686,2689],{},"Yes. ",[814,2683,2684],{},"status=PAUSED",", and every proposal from the turn arrives as a ",[814,2687,2688],{},"RunRequirement"," naming the tool and its exact arguments",[845,2691,2692,2695],{},[860,2693,2694],{},"Does the allow path cost a model turn?",[860,2696,2697,2700],{},[993,2698,2699],{},"No."," 3 provider requests paused, 3 unpaused. The continue makes the one model call the unpaused loop would have made anyway",[845,2702,2703,2706],{},[860,2704,2705],{},"Does Agno ever run the callable?",[860,2707,2708],{},"No. The tool body carried a sentinel Agno never reached. One effect, recorded by the invoker",[845,2710,2711,2714],{},[860,2712,2713],{},"Does a denial come back as a value?",[860,2715,2716],{},"Yes. The refusal is delivered as that call's result, zero effects, and the model adapts and says so",[845,2718,2719,2722],{},[860,2720,2721],{},"Does it survive a process boundary?",[860,2723,2681,2724,2727],{},[814,2725,2726],{},"acontinue_run(run_id=, session_id=, requirements=)"," finished the loop in a process that never saw the pause",[807,2729,1587,2730,1259,2733,2736,2737,2740,2741,1009],{},[814,2731,2732],{},"requirements",[993,2734,2735],{},"not"," optional on the ",[814,2738,2739],{},"run_id"," path. Omit it and Agno looks for a\nresolved admin approval row and raises ",[814,2742,2743],{},"ValueError",[2745,2746,2748],"h4",{"id":2747},"the-runtime-drains-its-own-pauses","The runtime drains its own pauses",[807,2750,2751,2752,2755],{},"Every write and send tool pauses, so a call policy allows pauses too. That pause never\nreaches Inngest. ",[814,2753,2754],{},"execute()"," loops while the result is paused, resolving each requirement\nthe invoker answers, and returns only when the loop finishes or a call needs a person.",[819,2757,2759],{"className":1034,"code":2758,"language":1036,"meta":825,"style":825},"result = await bridge.consume(agent.arun(\n    input=segment_input.text,\n    stream=True, stream_events=True, yield_run_output=True,\n))\nwhile result.is_paused:\n    for req in result.active_requirements:\n        stop.check_ceilings()              # before EVERY call, not once per pause\n        try:\n            outcome = await invoker.invoke(req.tool_execution, ...)\n        except ApprovalRequired as exc:\n            return AgentExecutionResult(stop='needs_approval', approval_id=exc.approval_id, ...)\n        except CancelRequested:\n            return AgentExecutionResult(stop='cancelled')\n        if adapter.record_stop(outcome):        # a marked refusal ends the pass\n            return _answer(result, stop)        # the siblings stay undecided\n        req.set_external_execution_result(outcome.as_result())\n    result = await bridge.consume(agent.acontinue_run(\n        run_id=result.run_id, session_id=str(run_id), requirements=result.requirements,\n        stream=True, stream_events=True, yield_run_output=True,\n    ))\n",[814,2760,2761,2766,2771,2776,2781,2786,2791,2796,2801,2806,2811,2816,2821,2826,2831,2836,2841,2846,2851,2856],{"__ignoreMap":825},[1040,2762,2763],{"class":1042,"line":22},[1040,2764,2765],{},"result = await bridge.consume(agent.arun(\n",[1040,2767,2768],{"class":1042,"line":32},[1040,2769,2770],{},"    input=segment_input.text,\n",[1040,2772,2773],{"class":1042,"line":233},[1040,2774,2775],{},"    stream=True, stream_events=True, yield_run_output=True,\n",[1040,2777,2778],{"class":1042,"line":244},[1040,2779,2780],{},"))\n",[1040,2782,2783],{"class":1042,"line":264},[1040,2784,2785],{},"while result.is_paused:\n",[1040,2787,2788],{"class":1042,"line":222},[1040,2789,2790],{},"    for req in result.active_requirements:\n",[1040,2792,2793],{"class":1042,"line":360},[1040,2794,2795],{},"        stop.check_ceilings()              # before EVERY call, not once per pause\n",[1040,2797,2798],{"class":1042,"line":368},[1040,2799,2800],{},"        try:\n",[1040,2802,2803],{"class":1042,"line":375},[1040,2804,2805],{},"            outcome = await invoker.invoke(req.tool_execution, ...)\n",[1040,2807,2808],{"class":1042,"line":157},[1040,2809,2810],{},"        except ApprovalRequired as exc:\n",[1040,2812,2813],{"class":1042,"line":182},[1040,2814,2815],{},"            return AgentExecutionResult(stop='needs_approval', approval_id=exc.approval_id, ...)\n",[1040,2817,2818],{"class":1042,"line":290},[1040,2819,2820],{},"        except CancelRequested:\n",[1040,2822,2823],{"class":1042,"line":280},[1040,2824,2825],{},"            return AgentExecutionResult(stop='cancelled')\n",[1040,2827,2828],{"class":1042,"line":272},[1040,2829,2830],{},"        if adapter.record_stop(outcome):        # a marked refusal ends the pass\n",[1040,2832,2833],{"class":1042,"line":1113},[1040,2834,2835],{},"            return _answer(result, stop)        # the siblings stay undecided\n",[1040,2837,2838],{"class":1042,"line":1119},[1040,2839,2840],{},"        req.set_external_execution_result(outcome.as_result())\n",[1040,2842,2843],{"class":1042,"line":1125},[1040,2844,2845],{},"    result = await bridge.consume(agent.acontinue_run(\n",[1040,2847,2848],{"class":1042,"line":1131},[1040,2849,2850],{},"        run_id=result.run_id, session_id=str(run_id), requirements=result.requirements,\n",[1040,2852,2853],{"class":1042,"line":1137},[1040,2854,2855],{},"        stream=True, stream_events=True, yield_run_output=True,\n",[1040,2857,2858],{"class":1042,"line":520},[1040,2859,2860],{},"    ))\n",[807,2862,2863,2870,2871,2874,2875,2878,2879,2882,2883,1009],{},[993,2864,2865,2866,2869],{},"The continue streams like the first call, and ",[814,2867,2868],{},"bridge.consume"," is what makes\nboth one shape."," The bridge drives the event iterator, opens and closes the\n",[814,2872,2873],{},"llm"," span, writes the usage row, feeds ",[814,2876,2877],{},"text_delta",", and answers the final\n",[814,2880,2881],{},"RunOutput",". An unstreamed continue writes no heartbeat while a model turn runs,\nso the reaper fails a healthy segment at the first write tool. See\n",[1005,2884,2886],{"href":2885},"\u002Fengineering\u002Fsystem-design\u002Fagentic-platform\u002Fruntime#what-ends-the-loop-early-and-how","the agent runtime",[807,2888,2889,2892],{},[993,2890,2891],{},"The drain loop is the second place a ceiling is checked."," A paused write call\nnever reaches the adapter, so a run with nothing left would continue for ever on\nwrite tools alone.",[807,2894,1587,2895,2898],{},[993,2896,2897],{},"The check sits before every call, not at the top of the pause."," Agno holds\nevery proposal of the turn, so one pause can carry ten sends. Checked once, a\nremainder of one sends all ten and the ceiling stops only the next pass.",[807,2900,2901],{},"A second write proposed after a continue pauses again, and the loop drains that one too.\nMeasured: two sequential sends, two pauses, two effects, and 3 provider requests against 3\nunpaused. Agno marks a requirement it has handled, so a resolved one is skipped on every\nlater pass.",[2745,2903,2905],{"id":2904},"one-turn-may-hold-several-proposals-and-each-gets-its-own-decision","One turn may hold several proposals, and each gets its own decision",[807,2907,2908,2909,2912,2913,2916],{},"Agno holds ",[993,2910,2911],{},"every"," proposal from the turn, not the first. So the decisions are\nindependent, and a ",[993,2914,2915],{},"denial"," does not cost the calls beside it.",[819,2918,2921],{"className":2919,"code":2920,"language":824,"meta":825},[822],"requirement 1  email.send(to='jo@acme.test')    -> allow  -> sent\nrequirement 2  email.send(to='sam@other.test')  -> deny   -> rejected result, model adapts\n",[814,2922,2920],{"__ignoreMap":825},[807,2924,2925],{},"Measured: one effect, the allowed one. The model reported both outcomes truthfully.",[807,2927,2928,2929,2932],{},"A refusal the invoker ",[993,2930,2931],{},"marked"," is the one exception. It ends the pass at the call that met\nit, and the proposals beside it stay undecided. A spent ceiling ends the pass the same way,\nand for the same reason: the run is over, so a decision on the next proposal buys nothing.",[807,2934,2935,2941],{},[993,2936,2937,2938,2940],{},"A ",[814,2939,1779],{}," in a turn ends the segment."," Calls already executed stay\njournalled, and the remaining proposals are simply not invoked. They return as\nrequirements when the segment resumes, and the invoker decides them again with a fresh\nreading.",[1431,2943,2945],{"id":2944},"the-resume-executes-the-approved-call","The resume executes the approved call",[807,2947,2948],{},[993,2949,2950],{},"The runtime executes the approved call. It does not wait for the model to ask again.",[807,2952,2953,2954,2957],{},"This is the one place where relying on the model would be a correctness bug. The model is\nnot deterministic. It may propose the same call, and it may not. A person who pressed\nApprove twenty hours ago would then see an approval marked ",[814,2955,2956],{},"approved"," and no email, with\nnothing in the tree to explain it.",[807,2959,2960],{},"The approval row already holds everything the call needs.",[819,2962,2965],{"className":2963,"code":2964,"language":824,"meta":825},[822],"tool name · exact arguments · arguments hash · idempotency key · run · principal\n",[814,2966,2964],{"__ignoreMap":825},[807,2968,2969,2970,2973],{},"So the resume path is deterministic, and it is a ",[993,2971,2972],{},"continue of the paused run"," rather\nthan a fresh segment.",[819,2975,2978],{"className":2976,"code":2977,"language":824,"meta":825},[822],"approval resolved = approved\n  -> ApprovalService rechecks TTL, authority, arguments hash and target state\n  -> ToolInvoker.invoke(..., authorized_by=approval_id)\n       the action checkpoint is already decided; every other step still runs\n  -> set_external_execution_result(...), then acontinue_run(...)\n\napproval resolved = rejected\n  -> set_external_execution_result(\u003Ca rejected ToolResult>), then acontinue_run(...)\n  -> the loop continues, and the agent adapts\n",[814,2979,2977],{"__ignoreMap":825},[807,2981,2982,2985,2986,2989],{},[993,2983,2984],{},"This is the drain loop with one extra argument, and not a second code path.","\nThe segment after approval rehydrates the paused run, meets the same unresolved\nrequirements, and walks the same loop. ",[814,2987,2988],{},"authorized_by"," is passed for the one\nrequirement the approval names, and every other requirement is decided live as\nbefore. Written as its own path it would carry its own copy of the ceiling check,\nthe cancel rule and the requirement bookkeeping, and the two copies would drift.",[807,2991,2992,2993,2995,2996,1009],{},"A requirement the segment already executed before it stopped is met again on the\nresume, because the mutation never reached the stored session. The idempotency\nclaim answers ",[814,2994,2050],{}," and returns the stored result, so the effect happens\none time and the span carries ",[814,2997,2998],{},"replayed = true",[807,3000,3001,3003,3004,3006,3007,3010,3011,3014],{},[814,3002,2988],{}," answers the ",[814,3005,1779],{}," the person already decided. ⚠️ ",[993,3008,3009],{},"It skips\nno part of the decision."," The engine still runs, so stage 1 still reads the grant live and\na ",[814,3012,3013],{},"deny"," still refuses: a person offboarded while the run waited cannot execute the call\nthey approved, and a rule an admin added while they deliberated still stops the effect. It\nskips nothing else either — the live tool state, the input schema, the journal read, the\nidempotency claim, the connection and the result boundary all still apply.",[807,3016,1587,3017,3019],{},[993,3018,2121],{}," The journal is\nread first, so a segment after approval that meets a call it already made returns the stored\nresult. Policy is never asked to decide an effect that already happened. Only a write or a\nsend journals, and the key names one organization, one run, one step and one arguments\nhash, so a replay is always one run reading back its own effect.",[807,3021,1587,3022,3025],{},[993,3023,3024],{},"The invoker checks that the approval covers the call, and the runtime cannot."," One\nturn can hold several write proposals and a person answers one approval row, which names a\ntool and an arguments hash. The runtime holds neither, so it hands the same id to every\nproposal of that pause. An invoker that read the flag as pure trust would send a message a\nperson never saw, on the strength of an approval for a different call.",[807,3027,3028,3034,3035,3038,3039,3041],{},[993,3029,3030,3031,3033],{},"An ",[814,3032,2988],{}," that does not cover the call is ignored, and never denied."," The flag\nskips one policy decision, so a flag that names another call skips nothing and the call is\ndecided live. That call then raises ",[814,3036,3037],{},"ApprovalRequired"," again, and the run parks a second\ntime on its own approval row. A ",[814,3040,1473],{}," here would drop that call permanently. The\nid names a different proposal of the same pause, and no later pass carries an id that\nnames this one.",[807,3043,1587,3044,3050,3051,3053,3054,3057],{},[993,3045,3046,3047,3049],{},"A row that covers the call and reads ",[814,3048,2011],{}," is the one exception."," A person\nanswered this exact proposal, so the invoker returns a ",[814,3052,2011],{}," result before the engine\nis asked. Deciding it live would file a second proposal and show that person the same card\nagain. \"Covers\" means the same four fields ",[814,3055,3056],{},"authorizes"," reads — organization, run, action,\narguments hash — so a row naming another proposal of one pause is still ignored.",[807,3059,3060,3063,3064,1727,3066,3069,3070,3072,3073,3076],{},[993,3061,3062],{},"The claim carries that answer past the segment that received it."," A later segment holds\nno ",[814,3065,2988],{},[814,3067,3068],{},"uq_approvals_run_id_idempotency_key"," releases its key on a\nterminal row since ENG-2156, so the insert would succeed and ask again. ",[814,3071,816],{},"\ntherefore reads the rejection of ",[814,3074,3075],{},"\u003Crun_id>:\u003Cstep_path>:\u003Cargs_hash>"," before it files.",[807,3078,1587,3079,3082,3083,3086,3087,1399,3090,3093],{},[993,3080,3081],{},"That key is a claim, and never a proof."," It names no tool, and ",[814,3084,3085],{},"step_path"," is one\nconstant for an agent run, so two write tools whose validated arguments hash alike share\none key. The invoker compares ",[814,3088,3089],{},"action",[814,3091,3092],{},"arguments_hash"," on the row it reads back. A\nguard that trusted the key alone would drop every later call of the second tool with no\ncard shown, on the strength of a refusal of the first.",[807,3095,3096,3099,3100,3103,3104,3106,3107,3110],{},[993,3097,3098],{},"A read tool needs no such record."," It takes no claim, and ",[814,3101,3102],{},"AgnoRuntime"," ends the Run on\nits first ",[814,3105,3037],{}," with ",[814,3108,3109],{},"read_tool_needs_approval",": a read runs inside the agent\nloop, so there is no pause to carry a decision. A rejected read approval therefore cannot\nrecur.",[807,3112,1587,3113,3116,3117,3120,3121,3123,3124,3126],{},[993,3114,3115],{},"A later segment is not bound by the rejection the way the handed segment is."," The\nrefusal above runs before the engine, so a rule an admin relaxes to ",[814,3118,3119],{},"allow"," does not\nrelease the call in the segment holding the id. In a later segment ",[814,3122,2988],{}," is None,\n",[814,3125,3119],{}," returns before the approval is filed, and the claim is never read. One call can end\ntwo ways in one Run, by the segment that holds the id. ENG-2156 took the person's answer\nover the rule on the path where both are known.",[807,3128,3129],{},"The second pause terminates. The segment after approval meets the call that already ran. The\njournal answers before the policy checkpoint, so that call returns its stored result and\nthe pass reaches the next proposal. Each pass decides one more call, and no effect happens\ntwice.",[807,3131,3132,3133,3135],{},"If the model does propose the same call again in that segment, the journal answers\n",[814,3134,2050],{}," and returns the stored result. So the journal makes the deterministic path and the model path agree,\nand the effect happens once either way.",[807,3137,3138,3141,3142,3145,3146,3149,3150,3153,3154,3157],{},[993,3139,3140],{},"The paused call's result slot is empty, so nothing is edited."," This is what the pause\nbuys over a halted loop. A halt had already written a tool result for that call id\ncarrying ",[814,3143,3144],{},"tool_call_error",", so delivering the approved outcome meant editing Agno's message\nlist from our code — exactly the knowledge the ",[814,3147,3148],{},"AgentRuntime"," boundary exists to contain.\nA paused call carries ",[814,3151,3152],{},"result=None",", and the continue fills it. Measured: the invoker's\nresult arrived on the call with ",[814,3155,3156],{},"error=None",", on both the allowed and the refused path.",[1431,3159,3161],{"id":3160},"handler-failure","Handler failure",[839,3163,3164,3174],{},[842,3165,3166],{},[845,3167,3168,3171],{},[848,3169,3170],{},"Failure",[848,3172,3173],{},"Handling",[855,3175,3176,3192,3213,3231,3247,3255],{},[845,3177,3178,3184],{},[860,3179,3180,3181],{},"The output fails its own ",[814,3182,3183],{},"output_schema",[860,3185,3186,3189,3190],{},[814,3187,3188],{},"ToolResult(ok=False, code='invalid_output')",". It is the remote's defect, not ours, and never ",[814,3191,2205],{},[845,3193,3194,3197],{},[860,3195,3196],{},"Expected business failure",[860,3198,3199,3200,3203,3204,1184,3207,1184,3209,2418,3211],{},"The handler raises the domain exception. ",[814,3201,3202],{},"BUSINESS_FAILURES"," maps it to ",[814,3205,3206],{},"not_found",[814,3208,1440],{},[814,3210,2019],{},[814,3212,1473],{},[845,3214,3215,3218],{},[860,3216,3217],{},"A 429, transport fault or provider timeout after the bounded retry",[860,3219,3220,3221,3224,3225,3106,3228],{},"The handler raises ",[814,3222,3223],{},"RetryableUpstreamError",". The invoker answers ",[814,3226,3227],{},"retryable_upstream",[814,3229,3230],{},"retryable=True",[845,3232,3233,3236],{},[860,3234,3235],{},"An external resource is absent or unsupported",[860,3237,3220,3238,3224,3241,3106,3244],{},[814,3239,3240],{},"UpstreamUnavailableError",[814,3242,3243],{},"unavailable",[814,3245,3246],{},"retryable=False",[845,3248,3249,3252],{},[860,3250,3251],{},"Platform fault, such as a credential, the database or the meter",[860,3253,3254],{},"The handler raises. The segment fails, and Inngest retries it",[845,3256,3257,3260],{},[860,3258,3259],{},"Any other exception",[860,3261,3262,3263,3265],{},"The handler raises, exactly as the row above. Nothing on this path answers ",[814,3264,2205],{}," as a value",[807,3267,3268,3269,3272,3273,3276,3277,3280,3281,3284,3285,3287,3288,3290],{},"A handler under ",[814,3270,3271],{},"services\u002Ftools\u002Fhandlers\u002F"," constructs no ",[814,3274,3275],{},"ToolError",". It returns ",[814,3278,3279],{},"ok=True",", or it raises. ",[814,3282,3283],{},"test_no_handler_constructs_a_tool_error"," holds that rule, because ",[814,3286,2205],{}," is a ",[814,3289,2301],{}," member and a handler that answered it would fail the whole run.",[807,3292,3293,1399,3295,3297,3298,3300,3301,3303,3304,3306,3307,3309,3310,3312],{},[814,3294,3223],{},[814,3296,3240],{}," are neutral handler exceptions. They carry no vendor SDK type and no ",[814,3299,2256],{},". A vendor handler catches the provider class and raises one neutral class. ",[814,3302,816],{}," owns the stable error code and the retry flag. ",[814,3305,3240],{}," maps to ",[814,3308,3243],{},"; ",[814,3311,1428],{}," remains the code for a tool that is absent from the live registry.",[807,3314,3315,3316,3318,3319,3322],{},"Provider authentication, payment, request-shape and response-decode failures are not neutral upstream outcomes. A platform credential or a client contract is wrong, so the handler lets the error raise. A segment retry can then reach the normal retry limit and the span keeps the real fault. A decoded handler result that fails ",[814,3317,3183],{}," still follows the ",[814,3320,3321],{},"invalid_output"," row above.",[807,3324,3325,3330,3331,3334,3335,3337],{},[993,3326,3327,3328,1009],{},"The vendor retry budget fits inside ",[814,3329,1794],{}," The client timeout, every retry and every backoff must finish before the tool timeout. If the invoker cancels the handler first, it answers ",[814,3332,3333],{},"timeout"," and the handler never reaches ",[814,3336,3223],{},". A declaration review checks the full budget, not one attempt.",[807,3339,3340],{},"An exception never crosses into the Agno loop except for a platform fault, where a segment retry is the correct answer and the journal bounds its cost.",[807,3342,1587,3343,3346,3347,3349,3350,3352],{},[993,3344,3345],{},"The last two rows are one decision, and an internal tool is where it is hard."," A CRM read reaches Postgres, so most of what it can raise ",[993,3348,1502],{}," the platform fault of row four. Turned into a value, a database outage becomes one failed call among several and the segment ends ",[814,3351,2050],{}," with a confident answer built on nothing — the failure the read path's catch-everything rule exists to stop.",[807,3354,3355],{},"So the invoker splits on the exception, and the list is explicit rather than a judgement:",[819,3357,3360],{"className":3358,"code":3359,"language":824,"meta":825},[822],"a business exception the domain declares    -> ToolResult(ok=False) with a stable code\n                                               NotFoundError, ValidationError, ConflictError\nanything else                               -> raise; the segment ends and Inngest decides\n",[814,3361,3359],{"__ignoreMap":825},[807,3363,1587,3364,1312,3367,3370,3371,3374,3375,3378,3379,2086,3382,3385,3386,3389],{},[993,3365,3366],{},"The test is the exact class, never the base class.",[814,3368,3369],{},"ACError"," is the root of the AgencyCore error hierarchy ",[993,3372,3373],{},"and"," the wrapper every CRM service raises from its own ",[814,3376,3377],{},"except Exception"," — ",[814,3380,3381],{},"list_people",[814,3383,3384],{},"ACError('Failed to list people')"," for a Postgres outage. So ",[814,3387,3388],{},"isinstance(error, ACError)"," reads a database fault as a business failure, which is the whole defect written as one convenient line. Match the leaf classes and let the base raise.",[807,3391,3392],{},"A handler that catches a database error and answers a value has moved a platform fault into row five by hand. Reviewing for that is the one thing a reader of a new handler must do.",[807,3394,3395,3398,3399,3401,3402,3404],{},[993,3396,3397],{},"Where the fault is raised decides how it leaves."," A write or send tool pauses, so its\n",[814,3400,816],{}," call happens in ",[814,3403,2754],{},"'s drain loop, outside anything Agno wraps. Those\nraises are ordinary raises, and the runtime classifies each one. Nothing is swallowed,\nbecause nothing crosses the loop.",[819,3406,3409],{"className":3407,"code":3408,"language":824,"meta":825},[822],"ApprovalRequired    -> stop = needs_approval, with the approval id and its expiry\nCancelRequested     -> stop = cancelled\nany other exception -> propagates; Inngest replays the step\n",[814,3410,3408],{"__ignoreMap":825},[807,3412,3413,3416],{},[814,3414,3415],{},"CancelRequested"," needs its own row. A person pressed stop, and a raise that reached\nInngest would land as a failure and overwrite that.",[807,3418,2937,3419,3421,3422,3424],{},[993,3420,2384],{}," tool still runs inside the loop, and inside it the old measurement holds. Agno\nturns every exception raised in a tool into a tool result and continues. A database outage\nwould become one failed call among several, and the segment would end ",[814,3423,2050],{}," with a\nconfident answer built on nothing. So the adapter catches every exception a read tool's\ninvoker raises, and it classifies rather than re-raises. It re-raises one, and the table\nsays which.",[819,3426,3429],{"className":3427,"code":3428,"language":824,"meta":825},[822],"a result carrying stop  -> record_stop() -> SegmentStop(ceiling|failed) -> StopAgentRun\nCancelRequested         -> SegmentStop(cancelled)                       -> StopAgentRun\nStopAgentRun            -> re-raised; the framework honours this one\nany other exception     -> SegmentStop(failed, error=exc)               -> StopAgentRun\n",[814,3430,3428],{"__ignoreMap":825},[807,3432,1587,3433,1312,3439,1929,3441,3444],{},[993,3434,3435,3436,3438],{},"The ",[814,3437,2407],{}," row is load-bearing.",[814,3440,2407],{},[814,3442,3443],{},"Exception",", so a guard\nthat caught every exception would catch the stop the adapter itself raised. It would then\nrecord that stop a second time and re-raise it under the fault message.",[807,3446,3447,3450,3451,3454,3455,3458,3459,3462],{},[814,3448,3449],{},"SegmentStop"," carries ",[814,3452,3453],{},"ceiling | cancelled | failed",". ",[814,3456,3457],{},"needs_approval"," left it with the\npause: an approval is decided where the loop is already stopped, so there is no signal to\nsmuggle out. ",[814,3460,3461],{},"AgnoAgentRuntime"," re-raises the stored exception after the loop returns, so\nInngest still sees a real failure and still decides the retry.",[807,3464,1587,3465,1312,3470,3472,3473,3475,3476,3478],{},[993,3466,3467,3468,1009],{},"Read the stored stop before ",[814,3469,2564],{},[814,3471,2407],{}," ends the loop and\nstill answers a normal ",[814,3474,2881],{},", and a write tool proposed in the same turn makes that\nstatus read ",[814,3477,2567],{},". A runtime that checks the pause first drains a segment that already\nstopped.",[807,3480,3481,1312,3484,3487,3488,3491],{},[993,3482,3483],{},"Agno's own cancellation manager is not used.",[814,3485,3486],{},"agno.run.cancel"," holds one manager in a\nmodule level global, so two segments of two organizations in one worker share it. The\nplatform reads ",[814,3489,3490],{},"agent.run_control"," at the adapter instead, which is per segment and\ndurable.",[834,3493,3495],{"id":3494},"agnotooladapter",[814,3496,997],{},[807,3498,3499],{},"The adapter holds no business logic.",[819,3501,3504],{"className":3502,"code":3503,"language":824,"meta":825},[822],"read tool\n  ToolSpec + ToolInvocation -> Agno callable\n                            -> model proposes arguments\n                            -> ToolInvoker.invoke(...) inside the loop\n\nwrite or send tool\n  ToolSpec + ToolInvocation -> Agno callable, external_execution=True\n                            -> model proposes arguments\n                            -> Agno pauses; the callable never runs\n                            -> ToolInvoker.invoke(...) in the drain loop\n",[814,3505,3503],{"__ignoreMap":825},[807,3507,3508,3511,3512,3518,3519,3522,3523,3525,3526,3529,3530,3533,3534,3537,3538,3541,3542,3544,3545,3548],{},[993,3509,3510],{},"The adapter declares the name a vendor accepts."," The ",[1005,3513,3515,3517],{"href":3514},"#toolspec",[814,3516,1169],{}," bullet"," states the pattern. ",[814,3520,3521],{},"model_tool_name()"," replaces every character a vendor refuses with an underscore, so ",[814,3524,868],{}," reaches the model as ",[814,3527,3528],{},"email_send",". It lives in ",[814,3531,3532],{},"models.py",", beside ",[814,3535,3536],{},"NAME_PATTERN",", because registration reads it too and ",[814,3539,3540],{},"services"," may not import ",[814,3543,149],{},". The adapter holds the reverse map, which covers one segment because ",[814,3546,3547],{},"build()"," clears it.",[807,3550,3551,3554,3555,3557,3558,1399,3561,3564,3565,3568,3569,3572,3573,3576,3577,3579],{},[993,3552,3553],{},"The derivation is lossy, so registration owns the refusal."," It replaces a character and never removes one, so two platform names can answer one model facing name. ",[814,3556,3536],{}," allows one dot, so the pair reachable today is ",[814,3559,3560],{},"crm.search_people",[814,3562,3563],{},"crm_search.people",", which both derive ",[814,3566,3567],{},"crm_search_people",". The MCP phase widens the pattern and adds ",[814,3570,3571],{},"mcp.slack.send_message"," against ",[814,3574,3575],{},"mcp.slack_send.message",". An ",[814,3578,1421],{}," can also exceed 64 characters.",[807,3581,3582,3585,3586,3589],{},[814,3583,3584],{},"build_registry"," therefore refuses a derived name that collides with a registered one, and a derived name over ",[814,3587,3588],{},"MAX_MODEL_TOOL_NAME_LENGTH"," characters. Truncation is not the answer, because it maps two names onto one again. The adapter refuses a collision again when it builds a segment, because it reads the frozen run snapshot and not the registry. An overwritten contract sends the right arguments to the wrong handler.",[807,3591,3592],{},"If the agent framework changes, the adapter is replaced. The registry, the invoker, the policy and the handlers do not move.",[834,3594,3596],{"id":3595},"result-contract","Result contract",[819,3598,3600],{"className":1034,"code":3599,"language":1036,"meta":825,"style":825},"class ToolError(BaseModel):\n    code: str\n    message: str\n    retryable: bool = False\n\nclass ToolResultMeta(BaseModel):\n    count: int | None = None\n    truncated: bool = False\n    untrusted: bool = False\n    usage_id: UUID | None = None\n\n# What the run does about a refusal the invoker made. Only the invoker writes\n# it, and no caller renders it to a model.\nToolStop = Literal['budget_exhausted', 'fault']\n\nclass ToolResult(BaseModel):\n    ok: bool\n    data: Any | None = None\n    error: ToolError | None = None\n    meta: ToolResultMeta = ToolResultMeta()\n    stop: ToolStop | None = None\n",[814,3601,3602,3607,3612,3617,3622,3626,3631,3636,3641,3646,3651,3655,3660,3665,3669,3673,3678,3683,3688,3693,3698],{"__ignoreMap":825},[1040,3603,3604],{"class":1042,"line":22},[1040,3605,3606],{},"class ToolError(BaseModel):\n",[1040,3608,3609],{"class":1042,"line":32},[1040,3610,3611],{},"    code: str\n",[1040,3613,3614],{"class":1042,"line":233},[1040,3615,3616],{},"    message: str\n",[1040,3618,3619],{"class":1042,"line":244},[1040,3620,3621],{},"    retryable: bool = False\n",[1040,3623,3624],{"class":1042,"line":264},[1040,3625,1225],{"emptyLinePlaceholder":1224},[1040,3627,3628],{"class":1042,"line":222},[1040,3629,3630],{},"class ToolResultMeta(BaseModel):\n",[1040,3632,3633],{"class":1042,"line":360},[1040,3634,3635],{},"    count: int | None = None\n",[1040,3637,3638],{"class":1042,"line":368},[1040,3639,3640],{},"    truncated: bool = False\n",[1040,3642,3643],{"class":1042,"line":375},[1040,3644,3645],{},"    untrusted: bool = False\n",[1040,3647,3648],{"class":1042,"line":157},[1040,3649,3650],{},"    usage_id: UUID | None = None\n",[1040,3652,3653],{"class":1042,"line":182},[1040,3654,1225],{"emptyLinePlaceholder":1224},[1040,3656,3657],{"class":1042,"line":290},[1040,3658,3659],{},"# What the run does about a refusal the invoker made. Only the invoker writes\n",[1040,3661,3662],{"class":1042,"line":280},[1040,3663,3664],{},"# it, and no caller renders it to a model.\n",[1040,3666,3667],{"class":1042,"line":272},[1040,3668,2276],{},[1040,3670,3671],{"class":1042,"line":1113},[1040,3672,1225],{"emptyLinePlaceholder":1224},[1040,3674,3675],{"class":1042,"line":1119},[1040,3676,3677],{},"class ToolResult(BaseModel):\n",[1040,3679,3680],{"class":1042,"line":1125},[1040,3681,3682],{},"    ok: bool\n",[1040,3684,3685],{"class":1042,"line":1131},[1040,3686,3687],{},"    data: Any | None = None\n",[1040,3689,3690],{"class":1042,"line":1137},[1040,3691,3692],{},"    error: ToolError | None = None\n",[1040,3694,3695],{"class":1042,"line":520},[1040,3696,3697],{},"    meta: ToolResultMeta = ToolResultMeta()\n",[1040,3699,3700],{"class":1042,"line":318},[1040,3701,3702],{},"    stop: ToolStop | None = None\n",[807,3704,3705],{},"Stable error codes:",[819,3707,3710],{"className":3708,"code":3709,"language":824,"meta":825},[822],"denied                invalid_input          connection_required\nrejected              tool_unavailable       connection_expired\nnot_found             tool_changed           connection_revoked\ninvalid_output        unavailable            connection_ambiguous\nconflict              retryable_upstream     timeout\ninternal_error        batch_too_large\nbudget_exhausted\n",[814,3711,3709],{"__ignoreMap":825},[807,3713,3714,1259,3717,3719,3720,3723,3724,1009],{},[814,3715,3716],{},"parent_cancelled",[993,3718,2735],{}," in this list. A cancelled parent is refused by ",[814,3721,3722],{},"RunManager.start()"," before a child run exists, so it fails a workflow node and never reaches a model as a tool result. See ",[1005,3725,1609],{"href":372},[807,3727,3728],{},"The model never sees a provider exception, a credential or an unbounded payload.",[834,3730,3732],{"id":3731},"output-boundary","Output boundary",[819,3734,3737],{"className":3735,"code":3736,"language":824,"meta":825},[822],"handler result\n  -> validate the output schema\n  -> remove platform credentials and tokens\n  -> keep at most max_results items, when declared\n  -> bound the size to max_output_bytes or the platform limit\n  -> tag external content as untrusted\n  -> set meta.count and meta.truncated\n  -> span\n  -> agent\n",[814,3738,3736],{"__ignoreMap":825},[807,3740,3741,3748,3749,3753,3754,3757,3758,3760,3761,3764,3765,3768],{},[993,3742,3743,3744,3747],{},"Bounding is ",[814,3745,3746],{},"bound(output, 32 KB)",", and it never cuts a structure."," The algorithm is defined once in ",[1005,3750,3752],{"href":3751},"\u002Fengineering\u002Fsystem-design\u002Fagentic-platform\u002Fcontract#the-payload-boundary","the platform contract",". This layer owns two things around it: ",[814,3755,3756],{},"meta.truncated"," comes from what ",[814,3759,1315],{}," returns, ",[814,3762,3763],{},"meta.count"," stays honest, and ",[993,3766,3767],{},"redaction runs first",", so a credential is removed by rule rather than by which item happened to be dropped.",[807,3770,3771,1312,3774,3777,3778,3781,3782,1399,3785,3788,3789,3791,3792,3794],{},[993,3772,3773],{},"The item bound runs before the byte bound.",[814,3775,3776],{},"max_results"," applies only to an array output. It keeps the first items in order. ",[814,3779,3780],{},"max_output_bytes"," is at least 2, because ",[814,3783,3784],{},"[]",[814,3786,3787],{},"{}"," each need 2 bytes. It can lower the 32 KB platform limit and can never raise it. ",[814,3790,3756],{}," is true when either bound changes the payload. ",[814,3793,3763],{}," names the items that remain after both bounds.",[807,3796,3797,3798,3800,3801,3804],{},"An oversized scalar uses a ",[814,3799,1307],{}," marker when the marker fits. If a per-tool byte limit is too small for the marker, the boundary returns an empty JSON string and sets ",[814,3802,3803],{},"meta.truncated=true",". The empty string is the smallest JSON scalar and fits the two-byte minimum.",[807,3806,3807,3812,3813,3815,3816,3819,3820,3822,3823,3825,3826,1009],{},[993,3808,3809,3811],{},[814,3810,3183],{}," validates the handler answer before the boundary."," The bounded transport form can contain a ",[814,3814,1307],{}," marker or fewer items than the schema's ",[814,3817,3818],{},"minItems",". The marker is one reserved platform envelope, and it is not copied into every node of every tool schema. A caller must use ",[814,3821,3756],{}," and handle the marker. Registration checks that ",[814,3824,3776],{}," applies to an array, but it does not compare the limit with ",[814,3827,3818],{},[807,3829,3830,1312,3833,3835,3836,3840,3841,3843,3844,3847,3848,1009],{},[993,3831,3832],{},"The substitution reads the item it replaced, at the same position.",[814,3834,1315],{}," replaces every oversized item before it drops anything, and it replaces in place, so index ",[3837,3838,3839],"em",{},"i"," of the answer holds the marker for index ",[3837,3842,3839],{}," of the input, and a dict keeps its key. The invoker walks the two together and names the row from ",[814,3845,3846],{},"spec.item_kind"," and the item's own ",[814,3849,1327],{},[807,3851,3852],{},[993,3853,3854],{},"The ref goes inside the marker, and does not replace it.",[819,3856,3860],{"className":3857,"code":3858,"language":3859,"meta":825,"style":825},"language-json shiki shiki-themes github-dark","{\"__dropped__\": {\"reason\": \"too_large\", \"bytes\": 40960, \"ref\": {\"kind\": \"crm.person\", \"id\": \"p-2\"}}}\n","json",[814,3861,3862],{"__ignoreMap":825},[1040,3863,3864,3868,3872,3875,3878,3881,3885,3887,3890,3892,3895,3897,3900,3902,3905,3907,3910,3912,3915,3917,3920],{"class":1042,"line":22},[1040,3865,3867],{"class":3866},"s95oV","{",[1040,3869,3871],{"class":3870},"sDLfK","\"__dropped__\"",[1040,3873,3874],{"class":3866},": {",[1040,3876,3877],{"class":3870},"\"reason\"",[1040,3879,3880],{"class":3866},": ",[1040,3882,3884],{"class":3883},"sU2Wk","\"too_large\"",[1040,3886,1184],{"class":3866},[1040,3888,3889],{"class":3870},"\"bytes\"",[1040,3891,3880],{"class":3866},[1040,3893,3894],{"class":3870},"40960",[1040,3896,1184],{"class":3866},[1040,3898,3899],{"class":3870},"\"ref\"",[1040,3901,3874],{"class":3866},[1040,3903,3904],{"class":3870},"\"kind\"",[1040,3906,3880],{"class":3866},[1040,3908,3909],{"class":3883},"\"crm.person\"",[1040,3911,1184],{"class":3866},[1040,3913,3914],{"class":3870},"\"id\"",[1040,3916,3880],{"class":3866},[1040,3918,3919],{"class":3883},"\"p-2\"",[1040,3921,3922],{"class":3866},"}}}\n",[807,3924,3925,3926,3928,3929,3932,3933,3936],{},"A bare ",[814,3927,1311],{}," sitting among real rows reads as a row: the model receives ",[814,3930,3931],{},"{\"kind\": …, \"id\": …}"," in a list of people and has no signal that an item is missing, or why. One sentinel key keeps the reason, the size and the pointer together, and ",[814,3934,3935],{},"__dropped__"," is still the one lookup that finds it.",[807,3938,3939,3940,3942,3943,3945],{},"Two cases keep the marker with no ref. A spec that declares no ",[814,3941,1303],{}," names no kind, and an item that carries no ",[814,3944,1327],{}," names no row. A ref built from a guess resolves to nothing, and a reader cannot tell that from a row a principal may not see.",[807,3947,1587,3948,3951,3952,3954],{},[993,3949,3950],{},"The naming can push the payload back over the limit, so the boundary bounds twice."," A ref adds about sixty bytes per marker, and ",[814,3953,1315],{}," ran before the naming. Measured on four hundred oversized rows: 42,691 bytes against a 32,768 limit, so the caller would store what it promised not to. The second call is the same function, not a second rule, and it drops nothing for a payload that already fits.",[807,3956,3957,3960,3961,1184,3964,1184,3967,1184,3970,1184,3973,3976],{},[993,3958,3959],{},"Redaction is a key name rule, not a value scan."," A field whose key is a credential name — ",[814,3962,3963],{},"token",[814,3965,3966],{},"api_key",[814,3968,3969],{},"secret",[814,3971,3972],{},"password",[814,3974,3975],{},"authorization",", and the two OAuth pair members — is replaced wherever it sits in the payload. Value matching would need a catalogue of every credential shape a vendor mints, and it removes a real body the first time a person writes about a password.",[807,3978,3979,3982,3983,3985],{},[993,3980,3981],{},"The span input is redacted too, and it is the arguments."," The model wrote them. The ",[1005,3984,2593],{"href":277}," bounds a span payload and relies on this layer having redacted it, so the invoker redacts the arguments before it opens the span, not after.",[807,3987,1587,3988,3991,3992,3995,3996,3999,4000,4003,4004,4007,4008,4011,4012,4015],{},[993,3989,3990],{},"One encoding measures the payload and renders it."," Two encoders sat on this path and disagreed. The boundary measures with the database body encoder, which writes an ",[814,3993,3994],{},"Enum"," as its value, a ",[814,3997,3998],{},"datetime"," as ISO-8601 and compact separators. ",[814,4001,4002],{},"ToolResult.as_result()"," rendered with ",[814,4005,4006],{},"json.dumps"," defaults and ",[814,4009,4010],{},"default=str",", which writes ",[814,4013,4014],{},"\"Stage.ACTIVE\"",", a space separated date, and two extra bytes per separator. The model read the second of each pair, and on 5,000 bounded rows it read 37,312 bytes against a 32,768 ceiling. So the boundary normalizes every value to plain JSON in its own walk, and the rendering uses the separators the measure used.",[807,4017,4018,4021,4022,4025,4026,4029,4030,4033,4034,4037,4038,4040,4041,4044],{},[993,4019,4020],{},"Every value the boundary answers is renderable, and the rule is total."," The measure raises ",[814,4023,4024],{},"TypeError"," on a value it cannot encode, and that raise ends the segment — a ",[814,4027,4028],{},"set"," field and a ",[814,4031,4032],{},"bytes"," field each did it. Anything that is not a JSON scalar is therefore named rather than carried: ",[814,4035,4036],{},"\"\u003Cunrenderable bytes>\"",". A list of allowed types grows with every handler; this rule does not. A ",[814,4039,4028],{}," becomes a sorted list, because the order of a string set moves with ",[814,4042,4043],{},"PYTHONHASHSEED"," and two workers would otherwise render two strings for one row.",[807,4046,4047,4050,4051,4054],{},[993,4048,4049],{},"Untrusted content is labelled, not scanned."," V1 ships no prompt injection detector, and a detector we do not have must not appear in a design. The boundary does three things: it strips control characters, it bounds the size, and it sets ",[814,4052,4053],{},"meta.untrusted"," so the adapter wraps the body in a labelled envelope. The real defence is structural. External text can propose an action, and every action still needs a tool call that policy decides. A tool result never carries an instruction to the runtime.",[807,4056,4057,4060,4061,1009],{},[993,4058,4059],{},"Sensitive fields stay out of the output schema."," V1 has no per tool redaction language. A field the model must not see is absent from ",[814,4062,3183],{},[834,4064,4066],{"id":4065},"batch-limits","Batch limits",[807,4068,4069,4071,4072,4074,4075,4078,4079,4082],{},[814,4070,1291],{}," is per call. ",[814,4073,1287],{}," names the top-level list it measures. An oversized call is rejected ",[993,4076,4077],{},"after input validation and before the journal read",", and it is never silently split. It answers ",[814,4080,4081],{},"batch_too_large",". It reaches no policy read, claim, handler, preview, or meter.",[819,4084,4087],{"className":4085,"code":4086,"language":824,"meta":825},[822],"ToolSpec.max_batch_size   what one call supports\nPolicy count and cost     what a run or an account may do\nVendor wrapper            vendor pacing and bounded 429 retry\n",[814,4088,4086],{"__ignoreMap":825},[807,4090,4091,4092,4094,4095,4097],{},"Splitting is agent or workflow coordination. It is not tool infrastructure. A vendor rate limit is absorbed by a bounded retry inside ",[814,4093,1794],{},", and then returned as ",[814,4096,3227],{},". The platform adds no second limiter.",[1431,4099,4101],{"id":4100},"a-vendor-ceiling-is-arithmetic-not-a-limiter","A vendor ceiling is arithmetic, not a limiter",[807,4103,4104,4106,4107,4110],{},[814,4105,1291],{}," bounds one call, and Inngest concurrency bounds the runs. Neither bounds ",[993,4108,4109],{},"one vendor across every live run",". Two hundred runs each making a legal call can still cross an Exa or a FullEnrich account limit at the same second.",[807,4112,4113],{},"V1 does not answer this with a global semaphore. A semaphore is a second limiter, it needs Redis on the correctness path, and it fails in the one case it exists for, a worker dying while holding a slot.",[807,4115,4116],{},"V1 answers it with a size, chosen once and written down.",[819,4118,4121],{"className":4119,"code":4120,"language":824,"meta":825},[822],"sum for each lane(\n  active Run limit in that lane\n  x maximum simultaneous calls to this vendor in one active Run in that lane\n) \u003C= the vendor account limit\n",[814,4122,4120],{"__ignoreMap":825},[807,4124,4125,4126,4129,4130,1450,4133,4136,4137,4140],{},"Both terms are ours. Inngest sets the active Run limit per organization and lane. Within one Run, one wide node contributes its ",[814,4127,4128],{},"fanout_concurrency",". A ",[814,4131,4132],{},"sequence",[814,4134,4135],{},"branch"," take the largest reachable value. A ",[814,4138,4139],{},"parallel"," adds the values of its branches.",[807,4142,4143],{},"A child Run counts in its own lane. A suspended parent holds no slot, so the arithmetic never counts a child under its parent Run or its parent's lane. Add the interactive and batch lane products to get the account maximum.",[807,4145,4146],{},"This is a configuration review beside the vendor credential, not a run-time mechanism. Reading one node's concurrency would undercount two wide nodes in parallel. Validation still checks each node against the step budget. The bounded 429 retry handles short bursts that pass the account arithmetic.",[807,4148,4149],{},"Record the arithmetic beside the vendor credential, and check it when either number changes.",[807,4151,4152,4155,4156,4159],{},[993,4153,4154],{},"The escalation, when a vendor gets tight enough to need one:"," move that vendor call into its own Inngest function, keyed on the vendor name, and call it with ",[814,4157,4158],{},"step.invoke",". Inngest then owns the ceiling, exactly as it owns run concurrency, and the platform still holds no limiter of its own. Do not build it before a vendor forces it.",[834,4161,4163],{"id":4162},"connections","Connections",[819,4165,4168],{"className":4166,"code":4167,"language":824,"meta":825},[822],"ToolInvoker -> ConnectionResolver -> vault credential -> vendor or MCP server\n",[814,4169,4167],{"__ignoreMap":825},[807,4171,4172],{},"Durable lifecycle:",[819,4174,4177],{"className":4175,"code":4176,"language":824,"meta":825},[822],"connected -- auth or revocation failure --> needs_reauth\n    ^                                         |\n    +--------------- reconnect ---------------+\n\nconnected \u002F needs_reauth -- uninstall --> revoked\n",[814,4178,4176],{"__ignoreMap":825},[807,4180,4181,1184,4184,4187],{},[814,4182,4183],{},"connecting",[814,4185,4186],{},"refreshing",", health checks and retries are operational states. They are not durable product states.",[807,4189,4190,4193,4194,4197],{},[993,4191,4192],{},"The model chooses the mailbox, never the credential."," An organization may hold several connections of one provider, such as three Nylas grants. A tool that needs the choice declares a public ",[814,4195,4196],{},"connection_id"," argument. The resolver checks that the ID belongs to the organization and matches the provider, then injects the credential the model never sees.",[839,4199,4200,4210],{},[842,4201,4202],{},[845,4203,4204,4207],{},[848,4205,4206],{},"Case",[848,4208,4209],{},"Behaviour",[855,4211,4212,4220,4228,4240],{},[845,4213,4214,4217],{},[860,4215,4216],{},"One connection for the provider",[860,4218,4219],{},"Use it. The argument is optional",[845,4221,4222,4225],{},[860,4223,4224],{},"Several, and the model named one",[860,4226,4227],{},"Validate the ID, then use it",[845,4229,4230,4233],{},[860,4231,4232],{},"Several, and the model named none",[860,4234,4235,4236,4239],{},"Return ",[814,4237,4238],{},"connection_ambiguous",", so the agent asks",[845,4241,4242,4245],{},[860,4243,4244],{},"None",[860,4246,4235,4247],{},[814,4248,4249],{},"connection_required",[834,4251,4253],{"id":4252},"idempotency-and-the-replay-journal","Idempotency and the replay journal",[807,4255,4256,4257,1009],{},"Every write and every send reads the journal, then claims an idempotency key. The claim\nis never taken without the read before it, and both steps use one key, derived once. See ",[1005,4258,4260],{"href":4259},"#the-journal-is-read-before-the-decision-and-claimed-after-it","the journal is read before the decision",[819,4262,4265],{"className":4263,"code":4264,"language":824,"meta":825},[822],"scope = tool.\u003Ctool_name>\nkey   = \u003Crun_id>:\u003Cstep_path>:\u003Cargs_hash>\n",[814,4266,4264],{"__ignoreMap":825},[807,4268,4269,4271,4272,3454,4275,4278],{},[814,4270,3085],{}," is the workflow node ID, or the literal ",[814,4273,4274],{},"agent",[814,4276,4277],{},"args_hash"," hashes the canonical arguments.",[807,4280,4281,4284],{},[993,4282,4283],{},"Do not key on the model tool call ID."," An agent segment can restart after a crash, and the model mints a new tool call ID on every attempt. A key built from that ID never matches the earlier claim, so an approved send happens twice.",[807,4286,4287,1312,4290,4293,4294,1009],{},[993,4288,4289],{},"The claim record is the journal.",[814,4291,4292],{},"agent.idempotency_keys"," already stores the status, the request hash and the response. A completed claim returns the stored result, which is exactly what a restarted segment needs. Do not add a second table beside it. See the note in ",[1005,4295,4297],{"href":4296},"#what-changes-elsewhere","what changes elsewhere",[807,4299,4300],{},"Two identical write calls inside one run are one effect, on purpose. When a run must genuinely repeat one action, give the calls different arguments, or give the workflow two nodes, because the node ID separates them.",[807,4302,4303,4306],{},[993,4304,4305],{},"This rule has a sharp edge, so state it to a tool author."," The journal cannot tell a replayed call from a deliberate repeat. Both look like the same arguments at the same step path. A replay must return the stored result, so a deliberate repeat receives it too, and the model reads a success for work that never happened a second time.",[807,4308,4309],{},"It is the right trade, because a duplicated send costs more than a skipped duplicate. It is only safe while tool authors know about it.",[839,4311,4312,4322],{},[842,4313,4314],{},[845,4315,4316,4319],{},[848,4317,4318],{},"Tool shape",[848,4320,4321],{},"Safe?",[855,4323,4324,4334,4344],{},[845,4325,4326,4331],{},[860,4327,4328],{},[814,4329,4330],{},"crm.update(company=X, stage=qualified)",[860,4332,4333],{},"Yes. The second call is a no-op anyway",[845,4335,4336,4341],{},[860,4337,4338],{},[814,4339,4340],{},"email.send(to=X, subject=S, body=B)",[860,4342,4343],{},"Yes. Two identical mails is the failure we want to stop",[845,4345,4346,4351],{},[860,4347,4348],{},[814,4349,4350],{},"crm.append_note(company=X, text=\"called\")",[860,4352,4353,4355],{},[993,4354,2699],{}," Two identical notes can be legitimate",[807,4357,4358,4359,4361],{},"A tool whose repeat is meaningful cannot register in V1. ",[814,4360,2115],{}," is reserved for a later invoker that can carry a per-segment ordinal. This deployment refuses it rather than claim that it can separate a replay from a deliberate repeat.",[819,4363,4366],{"className":4364,"code":4365,"language":824,"meta":825},[822],"read                 no journal key\nrepeatable = False   key = \u003Crun_id>:\u003Cstep_path>:\u003Cargs_hash>\nrepeatable = True    registration refused in V1\n",[814,4367,4365],{"__ignoreMap":825},[807,4369,4370,4371,1399,4373,4375,4376,4379,4380,4383],{},"Registry validation cannot decide whether identical calls are one effect, so every ",[814,4372,2417],{},[814,4374,2421],{}," tool must declare ",[814,4377,4378],{},"repeatable = False",". A read declares no value because it takes no claim. A tool such as ",[814,4381,4382],{},"crm.append_note"," stays unavailable until the runtime has a crash-safe repeat rule.",[807,4385,4386],{},"When a vendor supports idempotency, pass the same key downstream. That closes the crash after vendor success window.",[834,4388,4390],{"id":4389},"cost-and-spans","Cost and spans",[807,4392,4393,4394,1009],{},"Every tool call opens one span. A metered call also writes one usage row through the canonical meter, and the span links it with ",[814,4395,4396],{},"usage_id",[807,4398,4399],{},"This is not decoration. Policy accrual reads that meter, so a Signals Search run that spends money at Exa and FullEnrich must stop at its ceiling like a run that spends money on tokens. A vendor call that is metered nowhere is a hole in the budget.",[807,4401,4402,4405],{},[993,4403,4404],{},"Only a successful billable response writes a usage row."," A policy or accrual refusal runs before the handler and writes none. An exhausted retryable failure also writes none. The vendor did not complete a billable operation that the handler can price.",[807,4407,4408,4411,4412,1009],{},[993,4409,4410],{},"An asynchronous provider job is the one exception, and it is not a second rule."," The billable operation still writes exactly one row, and the row is still written from what the provider reported. Only the frame moves: the provider answers through a callback, so a durable function writes the row instead of the handler. See ",[1005,4413,4415],{"href":4414},"#asynchronous-provider-jobs","asynchronous provider jobs",[807,4417,4418,4419,4422,4423,4426,4427,4430,4431,4434],{},"The handler calls a legacy vendor client with no usage logger. It then writes one ",[814,4420,4421],{},"UsageRecord"," through ",[814,4424,4425],{},"UsageMeter.record()",". The row carries the ambient ",[814,4428,4429],{},"root_run_id",", and the handler puts the returned row id in ",[814,4432,4433],{},"ToolResult.meta.usage_id",". A meter failure raises. The handler never returns vendor data that the run cannot count.",[807,4436,4437],{},"The metered checkpoint already proves that the call has an ambient root run id before the handler starts. The handler reads the same id for the row. This order prevents a paid call that cannot name its run tree.",[807,4439,4440],{},"When a provider returns usage, use it. Firecrawl returns the credit count for a scrape, so the row multiplies the per-credit price by that count. A fixed one-credit row can undercount an enhanced proxy or a multi-page document.",[1431,4442,4444],{"id":4443},"research-tools","Research tools",[839,4446,4447,4462],{},[842,4448,4449],{},[845,4450,4451,4453,4456,4459],{},[848,4452,862],{},[848,4454,4455],{},"Provider call",[848,4457,4458],{},"Public input",[848,4460,4461],{},"Result",[855,4463,4464,4480,4496,4512,4528,4544],{},[845,4465,4466,4471,4474,4477],{},[860,4467,4468],{},[814,4469,4470],{},"research.search_web",[860,4472,4473],{},"Exa Search",[860,4475,4476],{},"one bounded query, optional category and lookback",[860,4478,4479],{},"at most 10 pages with title, URL, date and highlights",[845,4481,4482,4487,4490,4493],{},[860,4483,4484],{},[814,4485,4486],{},"research.search_company",[860,4488,4489],{},"Parallel Search",[860,4491,4492],{},"one company name, optional website and one bounded research objective",[860,4494,4495],{},"at most 10 pages with title, URL, date and excerpts",[845,4497,4498,4503,4506,4509],{},[860,4499,4500],{},[814,4501,4502],{},"research.fetch_page",[860,4504,4505],{},"Firecrawl Scrape",[860,4507,4508],{},"one HTTP or HTTPS URL",[860,4510,4511],{},"the page URL, title and main-content Markdown",[845,4513,4514,4519,4522,4525],{},[860,4515,4516],{},[814,4517,4518],{},"research.discover_people",[860,4520,4521],{},"Exa People Search or Parallel Entity Search",[860,4523,4524],{},"one company, one bounded buyer persona, one provider and a limit",[860,4526,4527],{},"at most 10 projected candidates with provider provenance",[845,4529,4530,4535,4538,4541],{},[860,4531,4532],{},[814,4533,4534],{},"research.enrich_company",[860,4536,4537],{},"Hunter Company Enrichment",[860,4539,4540],{},"one exact company domain, and optionally the company name",[860,4542,4543],{},"both identity claims, their agreement state, and the canonical industry, location, employee, revenue and founding-year fields",[845,4545,4546,4551,4554,4557],{},[860,4547,4548],{},[814,4549,4550],{},"research.find_contact",[860,4552,4553],{},"Hunter Email Finder",[860,4555,4556],{},"one first and last name, and one company domain or one company name",[860,4558,4559],{},"the work address, its verification state, the provider status and confidence, both identity claims, a role-mailbox marker and the source count",[807,4561,4562,4563,4565,4566,1009],{},"These tools are reads. They take no idempotency claim. ",[814,4564,4486],{}," uses the low-latency Parallel Search endpoint that the current async client exposes. Parallel Task is the structured enrichment API, but it can run for hours. It is not an agent-loop tool. It is a submit tool and a collect tool over ",[1005,4567,4415],{"href":4414},[807,4569,4570,4571,4573],{},"An empty Exa result is a successful billable response. It returns an empty list and writes one usage row. An exhausted 429, transport fault or provider timeout raises ",[814,4572,3223],{}," and writes no row. The strict research path must keep these outcomes distinct even though legacy Exa callers can still fail open.",[807,4575,3435,4576,4578,4579,1009],{},[814,4577,4518],{}," tool takes one provider because one tool span links to one usage row. The workflow fans out company and provider pairs when it needs wider coverage. One call never hides two vendor charges behind one ",[814,4580,4396],{},[807,4582,4583],{},"The buyer persona can name a title, title family, seniority, function and location. The handler builds the vendor query from those bounded fields. It returns a common candidate projection and never returns the vendor payload.",[807,4585,4586,4588,4589,4591],{},[814,4587,4550],{}," answers one address the provider returned, and never one this platform built. A miss answers ",[814,4590,3206],{},", keeps the person and prices the usage row at zero. Hunter charges one credit for an answered address alone.",[807,4593,4594,4595,1184,4598,1184,4601,1184,4604,2418,4607,4609,4610,4613,4614,4617],{},"The answer carries one closed verification state: ",[814,4596,4597],{},"verified",[814,4599,4600],{},"unverifiable",[814,4602,4603],{},"accepts_all",[814,4605,4606],{},"unverified",[814,4608,3206],{},". The provider status outranks ",[814,4611,4612],{},"accept_all",", because a domain that takes every address still tells the truth when it names one address invalid. A role mailbox such as ",[814,4615,4616],{},"info@"," is flagged and kept, so the agent decides.",[807,4619,4620],{},"Two client behaviours make that rule necessary.",[807,4622,4623,4626,4627,4630,4631,4634,4635,4638],{},[814,4624,4625],{},"HUNTERIO_DRY_RUN"," makes the client answer synthetic data and reach no vendor. It builds ",[814,4628,4629],{},"first.last@domain"," for the email finder, and it builds the company name from the domain for the enrichment. The deploy passes ",[814,4632,4633],{},"dry_run=False",", and both Hunter handlers also refuse a dry run themselves. One guard reads the client before the call, so a dry run spends nothing. One guard reads the ",[814,4636,4637],{},"_dry_run"," marker on the row before the meter, for a client that carries no attribute. The rule is absolute, so it holds at the handler and not at one call site.",[807,4640,4641,4644,4645,4647,4648,4650,4651,4654],{},[814,4642,4643],{},"HunterIOClient.find_email"," folds a 400, a 401, an unreadable body and an unexpected fault into the ",[814,4646,4244],{}," a real miss returns. ",[814,4649,4550],{}," therefore passes ",[814,4652,4653],{},"strict=True",", and a bad request raises instead of reading as a person with no email.",[807,4656,4657,4659],{},[814,4658,3560],{}," is a different tool. It reads people that already exist in the organization's CRM. It does not call a research vendor.",[807,4661,4662,4663,1399,4665,4668,4669,4672,4673,4676],{},"Every research declaration sets ",[814,4664,1414],{},[814,4666,4667],{},"metered=True",". Each handler sets ",[814,4670,4671],{},"meta.untrusted=True",". The list tools set ",[814,4674,4675],{},"max_results=10",". The common byte boundary applies after output validation.",[834,4678,4680],{"id":4679},"asynchronous-provider-jobs","Asynchronous provider jobs",[807,4682,4683,4684,4686,4687,4690],{},"Some providers finish work after the call that started it returns. FullEnrich is\nthe first. Registry validation refuses a ",[814,4685,1794],{}," over ",[814,4688,4689],{},"STEP_BUDGET_S",", which\nis 120 seconds, so a job that runs for an hour cannot be one tool call. The\n300 second bound on the field is the outer limit of the type and no tool\nreaches it.",[807,4692,4693],{},"The platform answers this with four parts and one new table. A tool submits, a\nwebhook stores the outcome, a durable function settles the cost, and a second\ntool reads the result.",[819,4695,4698],{"className":4696,"code":4697,"language":824,"meta":825},[822],"tool  fullenrich.submit  claim -> reserve job -> one vendor call -> bind vendor id -> complete claim\n      (the run parks on the job, see the wait below)\nPOST  \u002Fapi\u002Fv1\u002Fagentic\u002Fwebhooks\u002F{provider}\u002F{job_id}\n                         verify -> read the reserved job -> bind vendor id -> store the transition\n                         -> claim webhook.\u003Cprovider> -> enqueue -> ACK\nfn    provider_job.settle  write one usage row, once\ntool  fullenrich.collect read one tenant job, return running | completed | failed | cancelled\n",[814,4699,4697],{"__ignoreMap":825},[1431,4701,4703],{"id":4702},"the-record","The record",[819,4705,4708],{"className":4706,"code":4707,"language":824,"meta":825},[822],"agent.provider_jobs\n  id                   the platform job id; the correlation key\n  organization_id\n  root_run_id          the tree the cost belongs to\n  run_id               the run that submitted\n  tool_call_id         the span of the submit call\n  provider             'fullenrich'\n  provider_job_id      the vendor's own id; null before confirmation\n  contact_refs         the ordered, unique refs in the submit request\n  state                submitted | running | completed | failed | cancelled\n  result jsonb         at most 32 KiB; larger results use provider_job.result refs\n  cost jsonb           what the provider reported\n  usage_id             the one usage row; null until settlement\n  submitted_at, updated_at, terminal_at\n",[814,4709,4707],{"__ignoreMap":825},[819,4711,4714],{"className":4712,"code":4713,"language":824,"meta":825},[822],"unique(provider, provider_job_id)\n",[814,4715,4713],{"__ignoreMap":825},[807,4717,4718,4727],{},[993,4719,4720,4721,4723,4724,1009],{},"The correlation key is ",[814,4722,1327],{}," and never ",[814,4725,4726],{},"provider_job_id"," The platform id is\nours, it is stable, and it is minted before the vendor answers. A workflow\ncorrelates on a value it can read from the submit step.",[1431,4729,4731],{"id":4730},"fullenrich-submit-and-collect","FullEnrich submit and collect",[807,4733,4734,4737,4738,4741,4742,4745,4746,4749],{},[814,4735,4736],{},"fullenrich.submit"," is a metered write. ",[814,4739,4740],{},"fullenrich.collect"," is an unmetered read.\nBoth names require explicit rights. Neither calls ",[814,4743,4744],{},"enrich_and_wait"," or polls.\nThe submit journal stores the small answer, including ",[814,4747,4748],{},"job_id","; it stores no separate resource pair.",[807,4751,4752,4753,4756,4757,4760],{},"Submit accepts 1 to 25 contacts. Each contact needs a unique ",[814,4754,4755],{},"ref"," of 1 to 64 characters,\nfirst and last names of 1 to 120 characters, and a company domain, company name or LinkedIn URL.\nCompany names have at most 200 characters. Domains are normalized. LinkedIn URLs have at most 512 characters.\nThe tool accepts no callback URL, organization, run ID, credential or arbitrary custom fields.\nThe provider receives work-email fields and ",[814,4758,4759],{},"custom.ref"," only. The deployment supplies the batch callback URL.",[807,4762,4763,4764,4766,4767,4769,4770,4773],{},"The platform derives ",[814,4765,4748],{}," from organization, run, step path and the validated arguments hash.\nIt inserts the job with ordered contact refs before calling the provider.\nThe insert winner alone sends one POST. Replays and concurrent losers return the same reserved job.\nA null ",[814,4768,4726],{}," means acceptance is unknown, not that the provider refused the job.\nThe submit response or a verified callback can bind this ID once. Neither can change an existing ID.\nThe batch name is ",[814,4771,4772],{},"agentic:\u003Cjob_id>",". Before callback binding, require this exact name in the signed body.\nThe signature alone does not bind the URL path. A callback for another reserved job must fail this name check.",[807,4775,4776,4777,4780],{},"There is no automatic POST retry. FullEnrich documentation does not establish support for ",[814,4778,4779],{},"Idempotency-Key",".\nA timeout, lost response or worker failure therefore leaves the reservation for callback recovery.\nA replay must not submit it again, even after the tool journal expires.\nA worker failure before the POST can leave an unsubmitted reservation. The workflow wait then times out.\nThis contract prevents duplicate batches; it does not promise a successful submission after every process failure.\nA definite HTTP rejection raises the provider error and leaves the same reservation. It does not infer terminal provider cost.",[807,4782,4783,4784,4786,4787,4789,4790,4792],{},"Submit returns ",[814,4785,4748],{},". Collect accepts only ",[814,4788,4748],{}," and filters by organization and provider in the database.\nAn unknown job and another tenant's job both return ",[814,4791,3206],{},".\nCollect returns the job ID, mapped state, acceptance confirmation, and one compact result per requested ref.\nA live job returns no contact results. A terminal job preserves partial success, including failed or cancelled batches.\nA missing email does not make a completed batch fail.",[807,4794,4795],{},"Collect maps only exact echoed refs. It never uses position, name or domain as a substitute.\nDuplicate known refs produce no email for that ref. Missing refs and malformed rows produce explicit diagnostics.\nUnknown refs are counted and ignored. Diagnostics never copy arbitrary provider text.\nEach result carries the ref, email, provider status, confidence and a result status.\nEmail and provider status have fixed length limits. Verification is derived only from an explicit provider grade.\nThe regular tool boundary applies after projection. The full 25-contact projection must fit its 32 KiB limit without loss.",[1431,4797,4799],{"id":4798},"large-provider-results","Large provider results",[807,4801,4802,4805,4806,4809],{},[814,4803,4804],{},"agent.provider_job_results"," owns the complete provider result as private data.\nEach row has the job ID, result digest, organization and result. A composite foreign key prevents a tenant mismatch.\nRows cascade with their job. RLS is enabled; only the service role has table access.\nThe key is ",[814,4807,4808],{},"(job_id, sha256(canonical result))",". Insert-on-conflict-do-nothing preserves each immutable body.\nThe conditional job transition selects one state, cost and result reference together.\nA competing callback cannot pair its cost with another callback's body. Events use the stored terminal state.",[807,4811,4812,4813,4815,4816,4819],{},"The callback body has a 2 MiB limit. The shared ",[814,4814,1315],{}," helper checks the 32 KiB job payload limit.\nA result that fits stays inline. A larger result is stored before the terminal transition.\nThe job then stores ",[814,4817,4818],{},"{\"kind\": \"provider_job.result\", \"id\": \"\u003Cjob_id>\u002F\u003Cresult_digest>\"}",".\nA failed result write prevents the transition and ACK, so a callback retry can recover.\nCollect resolves only this kind, the same job ID and that digest, with an organization filter.\nA missing referenced result is a storage fault, never an empty success.\nExisting inline jobs remain readable. A job with no stored contact refs cannot invent a mapping.",[1431,4821,4823],{"id":4822},"the-callback-url-carries-the-job-id","The callback URL carries the job id",[807,4825,4826],{},"A vendor callback carries the vendor's job id and no organization. The claim is\norganization scoped, so the tenant must be known first.",[807,4828,4829,4835,4836,4839],{},[993,4830,4831,4832,4834],{},"The platform mints ",[814,4833,1327],{}," before it calls the vendor, so it builds the callback\nURL from it."," FullEnrich takes a ",[814,4837,4838],{},"webhook_url"," for each batch. The route is\ntherefore per job, and the tenant comes from the path:",[819,4841,4844],{"className":4842,"code":4843,"language":824,"meta":825},[822],"POST \u002Fapi\u002Fv1\u002Fagentic\u002Fwebhooks\u002F{provider}\u002F{job_id}\n",[814,4845,4843],{"__ignoreMap":825},[819,4847,4850],{"className":4848,"code":4849,"language":824,"meta":825},[822],"1. resolve the adapter from the path          unknown provider -> 404\n2. rate limit by client IP\n3. verify the signature over the raw body     bad signature -> 401\n4. read agent.provider_jobs by id             the tenant, from the reserved row\n5. unknown job id  -> drop reason `unknown_job`, then ACK\n6. bind an absent vendor id, store a large result, then conditionally transition\n7. a progress update ends here -> drop reason `not_terminal`, then ACK\n8. IdempotencyService.claim(org, 'webhook.\u003Cprovider>', delivery_id, sha256(body))\n9. durable enqueue                            -> Inngest\n10. ACK                                       -> under 3 seconds\n",[814,4851,4849],{"__ignoreMap":825},[807,4853,4854],{},"Step 6 runs before step 9. The durable truth is the row, and the event is a\nwake-up signal alone.",[807,4856,1587,4857,4860,4861,1009],{},[993,4858,4859],{},"Steps 8 and 9 run for a terminal update alone."," A progress callback\nrecords what it says and stops. It takes no claim, so it cannot make the\nterminal delivery a hash conflict. The rule and its reason are under\n",[1005,4862,4864],{"href":4863},"#completion-emits-one-versioned-event","completion emits one versioned event",[807,4866,1587,4867,4870],{},[993,4868,4869],{},"Nothing unverified reaches the database."," The adapter comes from the path\nand the signing key is one platform-wide secret per provider, so verification\nneeds no tenant. Reading the job first gives an unauthenticated caller one\nindexed read for each request, and it answers a different status for a job id\nthat exists. That difference is an oracle over the id space.",[807,4872,4873,4876],{},[993,4874,4875],{},"The job id in the path is not a secret, and nothing here treats it as one.","\nStep 3 gates step 4, so an attacker who learns a job id still changes no state\nwithout the signing key. The path answers which tenant, and the signature\nanswers whether to believe the body.",[807,4878,4879,4882,4883,4886,4887,4890,4891,1184,4893,1184,4895,1184,4898,1399,4901,4903,4904,4907,4908,4911,4912,4915],{},[993,4880,4881],{},"FullEnrich posts once, on the terminal state."," ENG-2300 captured one live\nbatch. The vendor sent no callback at ",[814,4884,4885],{},"CREATED"," and none at ",[814,4888,4889],{},"IN_PROGRESS",". The\nbody carries ",[814,4892,1327],{},[814,4894,1169],{},[814,4896,4897],{},"status",[814,4899,4900],{},"cost",[814,4902,1319],{},", the status is uppercase,\nand ",[814,4905,4906],{},"cost.credits"," counts hits and not rows. It signs with ",[814,4909,4910],{},"X-Signature-SHA1","\nover the raw body and carries no delivery id header, no timestamp and no retry\ncounter. FullEnrich also exposes a second callback URL for each contact\n(",[814,4913,4914],{},"webhook_events.contact_finished","), and that URL does report progress. No tool\nsets it.",[807,4917,4918,4925],{},[993,4919,4920,4921,4924],{},"A provider that takes no per-request callback URL reads the job by ",[814,4922,4923],{},"(provider, provider_job_id)"," instead."," That pair is unique across every organization, so it\nresolves one tenant. It is the fallback and not the shape to copy, because it\nneeds one cross-tenant read.",[807,4927,1587,4928,1312,4931,4934],{},[993,4929,4930],{},"Every provider signs with one platform-wide key in V1.",[814,4932,4933],{},"verify()"," takes\nthe raw body and the headers, and nothing else, so it cannot check a secret held\nper tenant. A provider that needs one is a change to this Protocol and not a\nconfiguration choice, and no provider needs one today.",[807,4936,1587,4937,4940,4941,4944],{},[993,4938,4939],{},"An unknown job id is not an error."," A retry for a job this platform already\ndeleted reaches no operator, and a 5xx would tell the vendor that a delivery it\nmade successfully failed. The handler counts ",[814,4942,4943],{},"unknown_job"," and answers its ACK,\nexactly as a duplicate delivery does.",[807,4946,4947,4950,4951,4953],{},[993,4948,4949],{},"Duplicate and out-of-order callbacks settle to one transition."," Step 6 is\nconditional on the legal from-states, so a terminal job stays terminal. This is\nthe rule ",[1005,4952,1609],{"href":372},"\napplies to the Run row, applied to a second row.",[1431,4955,4957],{"id":4956},"completion-emits-one-versioned-event","Completion emits one versioned event",[819,4959,4962],{"className":4960,"code":4961,"language":824,"meta":825},[822],"event type    agentic.provider_job.completed.v1\nInngest name  platform\u002Fagentic.provider_job.completed.v1\nevent id      event:\u003Cprovider delivery id>\ndata.data     job_id     the platform id, and the correlation key\n              provider, state, terminal\n",[814,4963,4961],{"__ignoreMap":825},[807,4965,1587,4966,4973,4974,4976,4977,4979,4980,1879,4983,1009],{},[993,4967,4968,4969,4723,4971,1009],{},"The correlation field is ",[814,4970,4748],{},[814,4972,4726],{}," The row\nalready has a column of the second name, and it holds the vendor's id. One name\nfor two values is the defect that reaches production as a wait that matches\nnothing. The submit tool returns ",[814,4975,4748],{},", the event carries ",[814,4978,4748],{},", and a\n",[814,4981,4982],{},"match",[814,4984,4748],{},[807,4986,1587,4987,4990,4991,4994],{},[993,4988,4989],{},"A terminal update emits, and a progress update does not."," A progress\ncallback stores its transition and ends there. It takes no claim either. A wait\nreturns on the first matching event and reads no row again, so a ",[814,4992,4993],{},"running"," event\nwakes a run on work the provider has not finished. The claim compounds it: the\nkey is the delivery id, so a progress delivery that claimed it makes the\nterminal delivery a hash conflict, and the run then parks for its whole timeout\nwith the result already stored. ENG-2300 measured this.",[807,4996,4997,4998,5000],{},"The version is in the event type. See\n",[1005,4999,266],{"href":261}," for\nthe rule that governs it.",[807,5002,5003,5004,5007],{},"The Inngest event id gives short-term deduplication over a 24-hour window. It is\nnot the durable guard. The ",[814,5005,5006],{},"webhook.\u003Cprovider>"," claim and the conditional\ntransition are.",[1431,5009,5011],{"id":5010},"the-run-parks-on-the-job-and-two-reads-close-the-gap","The run parks on the job, and two reads close the gap",[807,5013,2937,5014,5017,5018,1009],{},[814,5015,5016],{},"wait"," node whose correlation names a provider job reads that row once before\nit registers the wait, and once again on the timeout. Those two reads are the\nmechanism the approval wait already uses. See\n",[1005,5019,5020],{"href":365},"the wait node",[807,5022,1587,5023,5026],{},[993,5024,5025],{},"Without the first read the run parks for the whole timeout."," The submit\nstep commits, the callback lands, and the wait registers after the event is\ngone. The job row survives that gap, and the read finds it terminal.",[807,5028,5029],{},"ENG-2300 measured one live FullEnrich batch. The submit call answered in 0.83\nseconds and the one callback arrived 307 seconds later, so the gap is wide for\na batch that does real work. A batch that ends at once closes it: a drained\naccount reports its ending immediately, so the submit tool reserves the row before it sends the request.",[807,5031,2937,5032,5034,5035,5038],{},[814,5033,4139],{}," node holds at most one parking subtree, so it holds at most one\nprovider-job wait. ",[814,5036,5037],{},"RunManager.resume()"," counts pending approval rows and does\nnot count provider jobs. Lifting that rule is not V1 work.",[1431,5040,5042],{"id":5041},"the-cost-settles-once-off-the-run-frame","The cost settles once, off the run frame",[807,5044,5045,5048,5049,4422,5051,5053,5054,5056],{},[814,5046,5047],{},"provider_job.settle"," is an Inngest function on the completion event. It writes\none ",[814,5050,4421],{},[814,5052,4425],{},", stamped with the job's\n",[814,5055,4429],{},", and then stamps the row:",[819,5058,5062],{"className":5059,"code":5060,"language":5061,"meta":825,"style":825},"language-sql shiki shiki-themes github-dark","UPDATE agent.provider_jobs SET usage_id = :usage_id\n WHERE id = :job_id AND usage_id IS NULL\n","sql",[814,5063,5064,5069],{"__ignoreMap":825},[1040,5065,5066],{"class":1042,"line":22},[1040,5067,5068],{},"UPDATE agent.provider_jobs SET usage_id = :usage_id\n",[1040,5070,5071],{"class":1042,"line":32},[1040,5072,5073],{}," WHERE id = :job_id AND usage_id IS NULL\n",[807,5075,5076],{},"The meter uses a deterministic usage ID derived from the platform job ID.\nIts existing upsert prevents a second charge if the process fails before step acknowledgement.\nZero rows from the stamp means another attempt settled it. Cancellation, timeout, a replayed callback and a repeated\ncollect all read the same stamped row and change no charge.",[807,5078,5079,5082,5083,5086],{},[993,5080,5081],{},"Settlement is not on a run frame, and this is deliberate."," A cancelled run and\na timed-out wait both end before the vendor answers. The money is still spent, so\nthe row still names ",[814,5084,5085],{},"agent_root_run_id"," and the tree total grows after the Run is\nterminal. A total that hides it understates a tenant's bill.",[807,5088,1587,5089,5092,5095,5096,5098,5099,5101,5102,5104],{},[993,5090,5091],{},"This is the one metered path that does not end a Run on a meter failure.",[1005,5093,5094],{"href":277},"Observability","\nstates that ",[814,5097,4425],{}," raises and the caller ends the Run. There is no\nRun to end here, and the Run that submitted is often already terminal. So the\nfunction raises, Inngest retries it, and an exhausted retry reports to Sentry and\nleaves ",[814,5100,4396],{}," null. A null ",[814,5103,4396],{}," on a terminal job is the query that\nfinds an unsettled charge.",[807,5106,5107,5110],{},[814,5108,5109],{},"provider.collect"," writes no usage row. It reads the job row. A read takes no\nclaim, so repeating it is free.",[819,5112,5115],{"className":5113,"code":5114,"language":824,"meta":825},[822],"row state              collect answers\nsubmitted, running     running\ncompleted              completed\nfailed                 failed\ncancelled              cancelled\n",[814,5116,5114],{"__ignoreMap":825},[807,5118,5119,1399,5122,5124,5125,5128,5129,5132],{},[814,5120,5121],{},"submitted",[814,5123,4993],{}," collapse, because a caller does nothing differently\nbetween them: the work is not finished either way. ",[814,5126,5127],{},"cancelled"," does not collapse\ninto ",[814,5130,5131],{},"failed",". The provider stopped the job, and a workflow that retries a\nfailure must not retry a job somebody cancelled.",[1431,5134,5136],{"id":5135},"fullenrich-design-stress-cases","FullEnrich design stress cases",[807,5138,5139],{},"ENG-2210 owns these cases.",[839,5141,5142,5151],{},[842,5143,5144],{},[845,5145,5146,5148],{},[848,5147,4206],{},[848,5149,5150],{},"Required outcome",[855,5152,5153,5161,5169,5177,5185,5193,5201,5209,5217,5225,5233,5241,5249,5257,5265,5273,5281,5289,5297,5305,5313,5321,5329,5337],{},[845,5154,5155,5158],{},[860,5156,5157],{},"Empty batch; 26 contacts; duplicate or blank ref",[860,5159,5160],{},"Refuse before any provider or storage write",[845,5162,5163,5166],{},[860,5164,5165],{},"Missing identity; invalid domain or LinkedIn URL; unknown input field",[860,5167,5168],{},"Refuse the input",[845,5170,5171,5174],{},[860,5172,5173],{},"Ordinary submit replay; concurrent submit; expired journal",[860,5175,5176],{},"Same job; at most one provider POST",[845,5178,5179,5182],{},[860,5180,5181],{},"Worker stops before POST",[860,5183,5184],{},"Reservation remains; no automatic resubmit; wait can time out",[845,5186,5187,5190],{},[860,5188,5189],{},"Provider accepts; response is lost",[860,5191,5192],{},"Callback binds the reserved job; replay makes no POST",[845,5194,5195,5198],{},[860,5196,5197],{},"Provider rejects; credential absent",[860,5199,5200],{},"Surface the fault; do not invent provider completion or cost",[845,5202,5203,5206],{},[860,5204,5205],{},"Callback precedes submit response",[860,5207,5208],{},"Bind once; preserve terminal state when the response arrives",[845,5210,5211,5214],{},[860,5212,5213],{},"Callback carries another vendor ID or another reserved job name",[860,5215,5216],{},"Refuse the mismatch; change no terminal data",[845,5218,5219,5222],{},[860,5220,5221],{},"Bad signature; unknown provider; unknown job",[860,5223,5224],{},"Reject signature\u002Fprovider or ACK unknown job, with no mutation",[845,5226,5227,5230],{},[860,5228,5229],{},"Progress before terminal; duplicate terminal; conflicting terminal",[860,5231,5232],{},"Progress emits nothing; first terminal result stays fixed",[845,5234,5235,5238],{},[860,5236,5237],{},"Result-store write fails; transition fails; enqueue fails",[860,5239,5240],{},"Retry recovers each boundary; no result loss or duplicate charge",[845,5242,5243,5246],{},[860,5244,5245],{},"32 KiB boundary; large Unicode result",[860,5247,5248],{},"Inline when it fits; otherwise preserve the full result behind a ref",[845,5250,5251,5254],{},[860,5252,5253],{},"Callback exceeds 2 MiB",[860,5255,5256],{},"Refuse the body before parsing or storing it",[845,5258,5259,5262],{},[860,5260,5261],{},"Forged ref; wrong tenant; wrong provider; missing result row",[860,5263,5264],{},"Refuse access or raise storage fault; never return another job's data",[845,5266,5267,5270],{},[860,5268,5269],{},"Submitted; running; completed; failed; cancelled",[860,5271,5272],{},"Return the documented collect state without provider calls",[845,5274,5275,5278],{},[860,5276,5277],{},"Reordered contacts; missing rows",[860,5279,5280],{},"Exact-ref mapping; explicit missing-ref result",[845,5282,5283,5286],{},[860,5284,5285],{},"Duplicate known ref; unknown ref; malformed row",[860,5287,5288],{},"No guessed mapping; bounded diagnostics; unaffected contacts survive",[845,5290,5291,5294],{},[860,5292,5293],{},"No email; invalid email; unknown grade; catch-all",[860,5295,5296],{},"Preserve truthful absence or grade; never invent verification",[845,5298,5299,5302],{},[860,5300,5301],{},"Large provider profile; long status; long email",[860,5303,5304],{},"Compact bounded projection; no raw profile in the tool result",[845,5306,5307,5310],{},[860,5308,5309],{},"Callback before wait; inside registration gap; after timeout",[860,5311,5312],{},"Durable read finds completion; gap can wait until timeout recheck",[845,5314,5315,5318],{},[860,5316,5317],{},"Run cancelled; late callback; repeated collection",[860,5319,5320],{},"No resumed cancelled lane; cost can settle once after the run ends",[845,5322,5323,5326],{},[860,5324,5325],{},"Meter commit before step acknowledgement; parallel settlement",[860,5327,5328],{},"Stable usage ID; one durable usage row",[845,5330,5331,5334],{},[860,5332,5333],{},"Role lacks submit or collect; exhausted budget",[860,5335,5336],{},"Policy refuses before the provider effect",[845,5338,5339,5342],{},[860,5340,5341],{},"25 maximum-size contact results",[860,5343,5344],{},"Complete projection fits 32 KiB; no silent contact loss",[1431,5346,5348],{"id":5347},"the-provider-adapter","The provider adapter",[807,5350,5351],{},"One Protocol and one implementation per provider. The deployment maps provider names to adapters.",[819,5353,5355],{"className":1034,"code":5354,"language":1036,"meta":825,"style":825},"class ProviderWebhookAdapter(Protocol):\n    provider: str\n    def verify(self, raw_body: bytes, headers: Mapping[str, str]) -> bool: ...\n    def delivery_id(self, payload: dict) -> str: ...\n    def read(self, payload: dict) -> ProviderJobUpdate: ...\n",[814,5356,5357,5362,5367,5372,5377],{"__ignoreMap":825},[1040,5358,5359],{"class":1042,"line":22},[1040,5360,5361],{},"class ProviderWebhookAdapter(Protocol):\n",[1040,5363,5364],{"class":1042,"line":32},[1040,5365,5366],{},"    provider: str\n",[1040,5368,5369],{"class":1042,"line":233},[1040,5370,5371],{},"    def verify(self, raw_body: bytes, headers: Mapping[str, str]) -> bool: ...\n",[1040,5373,5374],{"class":1042,"line":244},[1040,5375,5376],{},"    def delivery_id(self, payload: dict) -> str: ...\n",[1040,5378,5379],{"class":1042,"line":264},[1040,5380,5381],{},"    def read(self, payload: dict) -> ProviderJobUpdate: ...\n",[807,5383,5384,5387],{},[814,5385,5386],{},"ProviderJobUpdate"," carries the vendor job id, the next state and the reported\ncost. The vendor job id is the vendor's own, and the route compares it against\nthe row the path named. The adapter verifies authenticity and maps the vendor's vocabulary. It\ntouches no Run, no meter and no span.",[807,5389,5390,5401,5402,5404],{},[993,5391,5392,5394,5395,1399,5398,5400],{},[814,5393,4933],{}," runs before ",[814,5396,5397],{},"delivery_id()",[814,5399,2085],{},", and the route holds that\norder."," The path names the job, so no method reads an unverified body to find\nthe tenant. ",[814,5403,2085],{}," answers trusted fields alone, and it runs after the\nsignature passes.",[807,5406,5407,5413,5414,5417],{},[993,5408,5409,5410,5412],{},"The vendor job id ",[814,5411,2085],{}," returns must equal the one on the row."," A signed\ndelivery for job A that names job B in its body is a provider defect or a replay\nacross jobs. The handler counts ",[814,5415,5416],{},"job_mismatch"," and answers its ACK.",[807,5419,5420,5421,5424],{},"The platform refuses a registry of adapters for the same reason it refuses a\ngeneral plugin framework. One ",[814,5422,5423],{},"dict"," keyed on the provider name resolves the\nroute parameter, and a route parameter that names no adapter answers 404.",[807,5426,5427,5429,5430,5432],{},[814,5428,5047],{}," retries three times, which is the count every metered path\nuses. An exhausted retry reports to Sentry one time and leaves ",[814,5431,4396],{}," null.",[1431,5434,5436],{"id":5435},"retention-and-what-a-vendor-still-owes","Retention, and what a vendor still owes",[807,5438,5439,5442,5443,5446,5447,5450],{},[814,5440,5441],{},"agent.provider_jobs"," is deleted with its Run. The composite run key cascades, so\nthe 13 month ",[814,5444,5445],{},"agent.runs"," window is this table's window and ",[814,5448,5449],{},"retention.sweeper","\nneeds no fourth sweep. A job outlives the 90 day span window on purpose: the span\nthat submitted it is gone long before the row is.",[807,5452,5453,5459,5460,5463],{},[993,5454,5455,5456,5458],{},"Pick ",[814,5457,1794],{}," from the provider's own stated completion time, not from the\n30 day ceiling."," A wait that outlives the Run's ",[814,5461,5462],{},"max_run_duration_s"," is clamped\nto the Run deadline, so a generous timeout does not park a Run for ever. A\ntimeout under the provider's real latency does: the wait ends, the Run moves on,\nand the callback settles a cost for work the Run never read.",[807,5465,5466,5467,5470],{},"A vendor with no callback needs a poll instead. V1 builds no poll backstop: the\nwait timeout ends the Run, and a job whose callback never arrives keeps ",[814,5468,5469],{},"state","\nat its last transition. A provider that drops deliveries needs a reconcile sweep\nbefore it ships, and it is not built before a provider forces it.",[834,5472,5473],{"id":377},"Cancellation",[807,5475,5476,5477,5479],{},"A tool call is a safe boundary, so the executor reads ",[814,5478,3490],{}," before it and after it. The invoker never interrupts a call in flight. If the effect is already sent, let it settle, record it, then stop.",[834,5481,5483],{"id":5482},"registry-validation","Registry validation",[807,5485,5486,5488],{},[814,5487,3584],{}," runs when the process builds its object graph, and the MCP resync will run the same checks when a connection syncs. It does not run per call.",[807,5490,1587,5491,5494,5495,5498],{},[993,5492,5493],{},"It does not fail a deploy."," The graph builds on the first function run and on the first request, so a broken declaration fails those two. Every check below therefore raises ",[814,5496,5497],{},"RegistryError",", whose message a person reads.",[807,5500,5501],{},"It checks:",[1160,5503,5504,5507,5514,5517,5520,5529,5538,5553,5559,5566,5574,5589],{},[1163,5505,5506],{},"the name is unique, valid and not a platform collision;",[1163,5508,5509,5510,5513],{},"the derived ",[1005,5511,5512],{"href":1177},"model facing name"," is unique, and at most 64 characters;",[1163,5515,5516],{},"the input and output schemas are valid JSON Schema;",[1163,5518,5519],{},"the handler or the connection binding resolves;",[1163,5521,5522,5525,5526,5528],{},[814,5523,5524],{},"approval_preview=True"," resolves to a handler with a callable ",[814,5527,1760],{},";",[1163,5530,5531,5534,5535,5537],{},[814,5532,5533],{},"side_effects"," is known, and a ",[814,5536,2421],{}," tool declares a connection;",[1163,5539,5540,5541,1399,5543,5545,5546,5548,5549,5552],{},"every ",[814,5542,2417],{},[814,5544,2421],{}," tool declares ",[814,5547,4378],{},", a read leaves it unset, and ",[814,5550,5551],{},"True"," is refused;",[1163,5554,5555,5556,5558],{},"every count limit is positive, and ",[814,5557,3780],{}," is at least 2;",[1163,5560,5561,1399,5563,5565],{},[814,5562,1287],{},[814,5564,1291],{}," appear together, and the argument names a top-level array;",[1163,5567,5568,5570,5571,5573],{},[814,5569,3776],{}," appears only on an array output, and ",[814,5572,3780],{}," does not exceed the platform limit;",[1163,5575,5576,5577,5579,5580,1184,5583,2418,5586,5528],{},"every name in ",[814,5578,1271],{}," has a registered resolver, and none of them is ",[814,5581,5582],{},"principal",[814,5584,5585],{},"run",[814,5587,5588],{},"arguments",[1163,5590,5591,5593],{},[814,5592,1794],{}," is inside the step budget.",[807,5595,5596],{},"A broken tool never enters the runnable registry.",[834,5598,5600],{"id":5599},"scenarios-that-shaped-this-design","Scenarios that shaped this design",[839,5602,5603,5613],{},[842,5604,5605],{},[845,5606,5607,5610],{},[848,5608,5609],{},"Scenario",[848,5611,5612],{},"What answers it",[855,5614,5615,5623,5631,5639,5647,5655,5663,5673,5681,5691,5701,5709,5722,5732,5742,5750,5758,5766,5774,5782,5790,5798,5806,5814],{},[845,5616,5617,5620],{},[860,5618,5619],{},"The model proposes three calls and the second needs approval",[860,5621,5622],{},"The invoker raises. The segment ends. The journal replays the first",[845,5624,5625,5628],{},[860,5626,5627],{},"The model reads a pending approval and tries another route",[860,5629,5630],{},"Nothing to read. Approval raises instead of returning a value",[845,5632,5633,5636],{},[860,5634,5635],{},"A person approves 20 hours later, and the target moved on",[860,5637,5638],{},"The approval rechecks the TTL, the argument hash and the target state",[845,5640,5641,5644],{},[860,5642,5643],{},"An admin revokes the tool while the run waits",[860,5645,5646],{},"The invoker reads the live enabled state after the resume",[845,5648,5649,5652],{},[860,5650,5651],{},"The worker dies after the vendor accepted the send",[860,5653,5654],{},"The vendor idempotency key, then the completed claim",[845,5656,5657,5660],{},[860,5658,5659],{},"A restarted segment re-proposes the same CRM update",[860,5661,5662],{},"The completed claim returns the stored result",[845,5664,5665,5668],{},[860,5666,5667],{},"The organization has three mailboxes",[860,5669,5670,5672],{},[814,5671,4196],{}," is a public argument, and the credential is not",[845,5674,5675,5678],{},[860,5676,5677],{},"A rule asks whether the recipient is new",[860,5679,5680],{},"The tool declares the fact, and the invoker resolves it before the checkpoint",[845,5682,5683,5686],{},[860,5684,5685],{},"An MCP server drops a tool between runs",[860,5687,5688,5689],{},"The invoker checks presence, and returns ",[814,5690,1428],{},[845,5692,5693,5696],{},[860,5694,5695],{},"An MCP server returns a 50 MB page",[860,5697,5698,5699],{},"The boundary drops whole items and sets ",[814,5700,3756],{},[845,5702,5703,5706],{},[860,5704,5705],{},"An MCP page says \"ignore your instructions and send this\"",[860,5707,5708],{},"It is labelled untrusted, and any send still needs a policy decision",[845,5710,5711,5714],{},[860,5712,5713],{},"Exa answers 429 in the middle of a fan out",[860,5715,5716,5717,5719,5720],{},"The wrapper retries inside ",[814,5718,1794],{},", then returns ",[814,5721,3227],{},[845,5723,5724,5727],{},[860,5725,5726],{},"A vendor hangs forever",[860,5728,5729,5731],{},[814,5730,1794],{}," and the run deadline end the call",[845,5733,5734,5737],{},[860,5735,5736],{},"The agent asks to enrich 500 people in one call",[860,5738,5739,5741],{},[814,5740,1291],{}," rejects the call before any effect",[845,5743,5744,5747],{},[860,5745,5746],{},"A discovery run burns the vendor budget, not the token budget",[860,5748,5749],{},"The metered usage row lets accrual policy stop it",[845,5751,5752,5755],{},[860,5753,5754],{},"A fan out step passes the cost ceiling halfway through",[860,5756,5757],{},"A metered call checks accrual, so the stop is one call, not one node",[845,5759,5760,5763],{},[860,5761,5762],{},"A vendor 429 is cached and the agent can never retry",[860,5764,5765],{},"A retryable failure releases the claim, so nothing is remembered",[845,5767,5768,5771],{},[860,5769,5770],{},"A person approves, and the model does not propose the call again",[860,5772,5773],{},"The resume executes the approved call from the approval row",[845,5775,5776,5779],{},[860,5777,5778],{},"200 live runs reach one vendor at the same second",[860,5780,5781],{},"The concurrency arithmetic, then the bounded 429 retry",[845,5783,5784,5787],{},[860,5785,5786],{},"Our database falls over inside a handler",[860,5788,5789],{},"The handler raises, the claim is released, and the segment retries",[845,5791,5792,5795],{},[860,5793,5794],{},"A metered research call has no root run id",[860,5796,5797],{},"The accrual checkpoint refuses it before the vendor runs",[845,5799,5800,5803],{},[860,5801,5802],{},"Firecrawl uses five credits for one scrape",[860,5804,5805],{},"One usage row records five credits, not one request estimate",[845,5807,5808,5811],{},[860,5809,5810],{},"Exa returns a valid empty result",[860,5812,5813],{},"The tool returns an empty list and records the billable request",[845,5815,5816,5819],{},[860,5817,5818],{},"Firecrawl rejects an unsupported target",[860,5820,5821,5822,5824],{},"The invoker returns ",[814,5823,3243],{},", and the handler writes no usage row",[834,5826,5828],{"id":5827},"rules","Rules",[1160,5830,5831,5834,5837,5840,5843,5846,5849,5857,5860,5863,5869,5876,5885,5888,5891,5894,5897,5902,5910,5921,5924,5927,5936,5943,5946,5952,5955,5958,5961,5964,5967,5970,5973,5976,5979,5982,5985,5988,5994,5997,6002,6010,6015,6022,6025,6028,6031,6034,6037,6043,6046,6049,6055],{},[1163,5832,5833],{},"An agent reaches the world only through a tool.",[1163,5835,5836],{},"An agent never touches Postgres, a raw route or a credential.",[1163,5838,5839],{},"An agent sees only the tools its definition names.",[1163,5841,5842],{},"Visibility, policy, business validation and tenancy stay four checks.",[1163,5844,5845],{},"Visibility is the frozen snapshot. Authority is stage 1 of the policy engine. There is no factory between them, and the invoker holds no scope check of its own.",[1163,5847,5848],{},"Policy is outside the agent, and it is evaluated on every call.",[1163,5850,2557,5851,5853,5854,5856],{},[814,5852,2615],{},". It pauses, and ",[814,5855,816],{}," runs it at the pause.",[1163,5858,5859],{},"The runtime resolves in process every call the invoker answers. Only an approval reaches Inngest.",[1163,5861,5862],{},"One turn may hold several proposals, and each gets its own decision. A denial returns as a value.",[1163,5864,5865,5866,5868],{},"A read tool runs inside the loop, so the adapter catches every exception it raises and classifies it. It re-raises ",[814,5867,2407],{},", which is the stop it raised itself.",[1163,5870,5871,3450,5873,5875],{},[814,5872,3449],{},[814,5874,3453],{},", it belongs to the segment, and the first writer wins.",[1163,5877,5878,5881,5882,5884],{},[814,5879,5880],{},"ToolResult.stop"," is written by the invoker alone. ",[814,5883,2268],{}," drops a value a handler wrote, and no caller renders it to a model.",[1163,5886,5887],{},"A metered call refused at the run cost ceiling ends the run partial in all three callers. A fault ends the run failed under the result's own code.",[1163,5889,5890],{},"The drain loop is the third caller. A write or send tool never reaches the adapter body.",[1163,5892,5893],{},"The approved outcome fills the paused call's own empty result slot. No message is edited.",[1163,5895,5896],{},"The resume executes the approved call. It never waits for the model to propose it again.",[1163,5898,3030,5899,5901],{},[814,5900,2988],{}," the approval row does not cover is ignored. The call is decided live, never denied.",[1163,5903,5904,5905,2086,5907,5909],{},"A covering row that reads ",[814,5906,2011],{},[814,5908,2011],{},", and so does a claim a person already refused. Neither files a second proposal.",[1163,5911,5912,5914,5915,5917,5918,5920],{},[814,5913,3037],{}," closes the ",[814,5916,1733],{}," span ",[814,5919,2607],{},". A pause is not a failure.",[1163,5922,5923],{},"Every write and send claims an idempotency key, and the claim is the journal.",[1163,5925,5926],{},"The journal is read before the policy checkpoint, and the claim is taken after it.",[1163,5928,5929,5930,5933,5934,1009],{},"A live ",[814,5931,5932],{},"processing"," claim on the call's own key raises. It is never a ",[814,5935,2256],{},[1163,5937,5938,1730,5940,5942],{},[814,5939,816],{},[814,5941,1733],{}," span before the checkpoints, so a refused call is in the tree.",[1163,5944,5945],{},"Span identity is ambient. No invocation, request or handler carries a copy of it.",[1163,5947,5948,5949,5951],{},"An internal handler reaches organization data through ",[814,5950,1915],{},", never through the admin client.",[1163,5953,5954],{},"A global Intelligence handler calls dedicated Intelligence domain stores. Only those stores use the admin client.",[1163,5956,5957],{},"An internal handler answers a value for a business failure, and raises for everything else.",[1163,5959,5960],{},"A vendor handler raises a neutral upstream exception. The invoker owns the stable error and retry flag.",[1163,5962,5963],{},"A claim ends in complete or release. A retryable failure releases it.",[1163,5965,5966],{},"A metered tool checks accrual before it executes.",[1163,5968,5969],{},"A vendor ceiling is arithmetic over the run concurrency, not a limiter.",[1163,5971,5972],{},"A tool whose repeat inside one run is meaningful carries a distinguishing argument.",[1163,5974,5975],{},"Every call opens one span, and a metered call writes one usage row.",[1163,5977,5978],{},"A successful billable call writes one usage row. A refusal or retryable upstream failure writes none.",[1163,5980,5981],{},"The boundary bounds the result without breaking its structure.",[1163,5983,5984],{},"External content is labelled untrusted, and it is never treated as an instruction.",[1163,5986,5987],{},"Connections hold three durable states, and the model never sees a credential.",[1163,5989,5990,5991,5993],{},"Every stored name keeps its dot. ",[814,5992,3521],{}," derives the model facing name, and the adapter alone applies it to a segment.",[1163,5995,5996],{},"Registry validation catches declaration drift before run time.",[1163,5998,5999,6001],{},[814,6000,1760],{}," is read only, runs before authorization, and may fail without losing the approval.",[1163,6003,6004,6005,6007,6008,1009],{},"Output that fails its own schema is ",[814,6006,3321],{},", never ",[814,6009,2205],{},[1163,6011,6012,6013,1009],{},"A remote schema change bumps the spec version, and a run on the old contract stops with ",[814,6014,1469],{},[1163,6016,6017,6018,6021],{},"Every write and send tool declares ",[814,6019,6020],{},"repeatable",". Silence is not an answer.",[1163,6023,6024],{},"A metered call reads the run tree total. The organization day belongs to the node boundary.",[1163,6026,6027],{},"An asynchronous provider job is one row, and that row is the durable truth. The completion event is a wake-up signal, never the record.",[1163,6029,6030],{},"A provider callback resolves the tenant from the job row before it takes an organization scoped claim.",[1163,6032,6033],{},"A provider callback stores its state transition before the platform emits the completion event.",[1163,6035,6036],{},"A provider job state transition is conditional on the legal from-states. A terminal job stays terminal.",[1163,6038,6039,6040,6042],{},"The provider cost settles one time, and a conditional write on a null ",[814,6041,4396],{}," is the guard.",[1163,6044,6045],{},"A collect tool reads the job row. It calls no provider, writes no usage row and starts no second job.",[1163,6047,6048],{},"A wiring fault on the metered path reaches Sentry one time. The log receives every call.",[1163,6050,6051,6052,6054],{},"Accrual reads ",[814,6053,2176],{},", never the daily rollup.",[1163,6056,6057],{},"Organization authored executable tool code stays deferred. Use platform tools, approved MCP servers and organization skills first.",[834,6059,6061],{"id":6060},"open-decisions","Open decisions",[6063,6064,6065,6071,6080,6083],"ol",{},[1163,6066,6067,6068,6070],{},"Does a ",[814,6069,2421],{}," tool need a per organization daily ceiling of its own, or is the policy count limit enough?",[1163,6072,6073,6074,6076,6077,6079],{},"Which tools set ",[814,6075,1764],{}," in the first release, beyond ",[814,6078,868],{},"?",[1163,6081,6082],{},"Which vendor forces the per vendor Inngest function first, and at what account limit? FullEnrich forces the asynchronous job contract, which is a different mechanism: it moves the completion off the call, not the rate limit off the run.",[1163,6084,6085],{},"Is an hourly MCP resync the right exposure window, or does a busy server need a webhook?",[834,6087,6089],{"id":6088},"minimum-contract-tests","Minimum contract tests",[1160,6091,6092,6095,6098,6101,6104,6107,6110,6113,6116,6119,6122,6125,6128,6131,6134,6137,6145,6148,6151,6154,6157,6162,6165,6168,6173,6176,6179,6184,6190,6193,6196,6199,6202,6208,6213,6216,6222,6225,6228,6231,6234,6237,6243,6246,6252,6255,6258,6261,6264,6267,6270,6273,6276,6282,6285,6290,6296,6299,6307,6313,6319,6326,6329,6342,6345,6353,6356,6359,6362],{},[1163,6093,6094],{},"A replayed submit returns the same provider job and makes no second vendor call.",[1163,6096,6097],{},"A callback that lands before the wait registers still ends the wait, and the run does not park.",[1163,6099,6100],{},"Two deliveries of one callback produce one state transition and one usage row.",[1163,6102,6103],{},"A callback that reports an earlier state than the stored one changes nothing.",[1163,6105,6106],{},"A callback for an unknown provider job id counts a drop reason and answers an ACK.",[1163,6108,6109],{},"A callback whose signature fails changes no state.",[1163,6111,6112],{},"The cost settles once when the run was cancelled before the callback arrived.",[1163,6114,6115],{},"Repeated collection answers the same result and writes no second usage row.",[1163,6117,6118],{},"The Agno adapter cannot bypass a policy denial.",[1163,6120,6121],{},"A database fault inside a tool ends the segment, and the model claims nothing.",[1163,6123,6124],{},"Two segments in one process do not read each other's stop signal.",[1163,6126,6127],{},"A call policy allows pauses, is granted in process, and never reaches Inngest.",[1163,6129,6130],{},"A write tool's callable is never executed by Agno; every effect goes through the invoker.",[1163,6132,6133],{},"Two proposals in one turn get two decisions, and a denial does not stop the call beside it.",[1163,6135,6136],{},"A required approval ends the segment, and the resume executes the tool once into the paused call's slot.",[1163,6138,6139,6140,5917,6142,6144],{},"A required approval closes its ",[814,6141,1733],{},[814,6143,2607],{},", carrying the approval id.",[1163,6146,6147],{},"A resumed pause holding a second write call decides it live, and parks the run on its own approval.",[1163,6149,6150],{},"An oversized batch produces zero side effects.",[1163,6152,6153],{},"An oversized batch reaches no journal read, policy read, claim, handler, preview or meter.",[1163,6155,6156],{},"Batch argument and batch size must appear together, and the argument must name a top-level array.",[1163,6158,6159,6160,1009],{},"Result count and byte limits compose, and either limit sets ",[814,6161,3756],{},[1163,6163,6164],{},"A one-byte output limit does not build.",[1163,6166,6167],{},"A preview is one non-empty line of at most 512 UTF-8 bytes.",[1163,6169,6170,6171,1009],{},"A preview timeout or invalid result files the approval with ",[814,6172,1804],{},[1163,6174,6175],{},"A crash after a vendor effect does not repeat the effect.",[1163,6177,6178],{},"A turn holding two gated write calls finishes in three passes, with two effects.",[1163,6180,6181,6183],{},[814,6182,2085],{}," writes no row, so a call that stops for a person leaves no claim behind.",[1163,6185,6186,6187,6189],{},"A crashed worker's claim is read as ",[814,6188,2075],{},", and the next attempt reclaims and executes it.",[1163,6191,6192],{},"A live claim on the call's own key raises, and the model reads no result for it.",[1163,6194,6195],{},"A restarted segment returns completed journal results. Interrupted and new\ncalls follow the claim, lease, and vendor recovery rules.",[1163,6197,6198],{},"An expired, revoked or ambiguous connection maps to a stable error code.",[1163,6200,6201],{},"A revoked tool is refused after an approval resume.",[1163,6203,6204,6205,6207],{},"A handler result is valid against ",[814,6206,3183],{}," before the boundary changes its transport form.",[1163,6209,6210,6211,1009],{},"One item larger than the whole limit keeps its shape, with its big fields replaced by a ",[814,6212,1311],{},[1163,6214,6215],{},"Redaction runs before bounding, so a credential in a dropped item is still removed.",[1163,6217,6218,6219,6221],{},"An external secret never appears in a ",[814,6220,2256],{}," or a span payload.",[1163,6223,6224],{},"A metered vendor call reaches the usage meter and the accrual checkpoint.",[1163,6226,6227],{},"A metered call is refused when the run is already over its cost ceiling.",[1163,6229,6230],{},"A metered call with no root run id reaches no handler and writes no usage row.",[1163,6232,6233],{},"One successful vendor response writes one row, and its id reaches the tool span.",[1163,6235,6236],{},"A retryable upstream failure writes no usage row.",[1163,6238,6239,6240,6242],{},"A vendor handler constructs no ",[814,6241,3275],{},"; the invoker maps its neutral exception.",[1163,6244,6245],{},"A valid empty Exa response is distinct from an exhausted retryable failure.",[1163,6247,6248,6249,6251],{},"An unsupported Firecrawl target maps to ",[814,6250,3243],{}," and writes no usage row.",[1163,6253,6254],{},"A provider credential fault or a meter fault raises and returns no vendor data.",[1163,6256,6257],{},"Vendor arithmetic adds two wide nodes that can run in parallel against one account.",[1163,6259,6260],{},"The full vendor retry budget fits inside the declared tool timeout.",[1163,6262,6263],{},"A Firecrawl row uses the response credit count when it is available.",[1163,6265,6266],{},"Many metered calls that meet the same wiring fault produce one Sentry report, and one log line each.",[1163,6268,6269],{},"A retryable failure leaves no claim, and the next identical call executes.",[1163,6271,6272],{},"A business failure leaves a completed claim, and the next identical call returns it.",[1163,6274,6275],{},"An approved call executes on resume even when the model proposes nothing.",[1163,6277,6278,6279,6281],{},"A rejected approval returns a ",[814,6280,2011],{}," result into the loop, and the run continues.",[1163,6283,6284],{},"Schema and handler drift fails validation before publication.",[1163,6286,6287,6288,1009],{},"A handler returning off schema data produces ",[814,6289,3321],{},[1163,6291,6292,6293,6295],{},"A failing ",[814,6294,1760],{}," still creates the approval, with the generic render.",[1163,6297,6298],{},"A preview performs no write, no send and no metered call.",[1163,6300,6301,6302,6304,6305,1009],{},"A remote input schema change makes the next call return ",[814,6303,1469],{},", not ",[814,6306,1440],{},[1163,6308,6309,6310,6312],{},"A tool that declares ",[814,6311,2115],{}," cannot register in V1.",[1163,6314,6315,6316,6318],{},"A non ",[814,6317,6020],{}," tool called twice with identical arguments writes one.",[1163,6320,2937,6321,6323,6324,1009],{},[814,6322,2421],{}," tool cannot register with ",[814,6325,2115],{},[1163,6327,6328],{},"A run principal carries the tool names its definition names, and the invoker refuses a name it does not hold.",[1163,6330,2937,6331,6333,6334,6336,6337,6339,6340,1009],{},[814,6332,1307],{}," marker becomes a ",[814,6335,1311],{}," only when the spec names an ",[814,6338,1303],{}," and the item carries an ",[814,6341,1327],{},[1163,6343,6344],{},"A domain business exception is a value, and every other exception raises and ends the segment.",[1163,6346,6347,6348,6350,6351,1009],{},"A spec whose remote schema changed carries a new ",[814,6349,1449],{}," and a higher ",[814,6352,1453],{},[1163,6354,6355],{},"A dotted platform name reaches the model derived, and a paused call maps back to the dotted name.",[1163,6357,6358],{},"Two contracts that derive one model facing name are refused before the model sees either.",[1163,6360,6361],{},"A derived name over 64 characters is refused at registration.",[1163,6363,6364,6365,1009],{},"A tool result never carries ",[814,6366,3716],{},[6368,6369,6370],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}",{"title":825,"searchDepth":32,"depth":233,"links":6372},[6373,6374,6375,6376,6377,6380,6384,6386,6399,6400,6401,6402,6405,6406,6407,6410,6422,6423,6424,6425,6426,6427],{"id":836,"depth":32,"text":837},{"id":894,"depth":32,"text":895},{"id":975,"depth":32,"text":976},{"id":1012,"depth":32,"text":1015},{"id":1350,"depth":32,"text":1351,"children":6378},[6379],{"id":1433,"depth":233,"text":1434},{"id":1494,"depth":32,"text":922,"children":6381},[6382],{"id":1536,"depth":233,"text":6383},"There is no ToolFactory, and there was one",{"id":1612,"depth":32,"text":6385},"ToolInvocation and handlers",{"id":1969,"depth":32,"text":816,"children":6387},[6388,6389,6390,6391,6392,6393,6397,6398],{"id":2030,"depth":233,"text":2031},{"id":2125,"depth":233,"text":2126},{"id":2198,"depth":233,"text":2199},{"id":2237,"depth":233,"text":2238},{"id":2480,"depth":233,"text":2481},{"id":2553,"depth":233,"text":2554,"children":6394},[6395,6396],{"id":2747,"depth":244,"text":2748},{"id":2904,"depth":244,"text":2905},{"id":2944,"depth":233,"text":2945},{"id":3160,"depth":233,"text":3161},{"id":3494,"depth":32,"text":997},{"id":3595,"depth":32,"text":3596},{"id":3731,"depth":32,"text":3732},{"id":4065,"depth":32,"text":4066,"children":6403},[6404],{"id":4100,"depth":233,"text":4101},{"id":4162,"depth":32,"text":4163},{"id":4252,"depth":32,"text":4253},{"id":4389,"depth":32,"text":4390,"children":6408},[6409],{"id":4443,"depth":233,"text":4444},{"id":4679,"depth":32,"text":4680,"children":6411},[6412,6413,6414,6415,6416,6417,6418,6419,6420,6421],{"id":4702,"depth":233,"text":4703},{"id":4730,"depth":233,"text":4731},{"id":4798,"depth":233,"text":4799},{"id":4822,"depth":233,"text":4823},{"id":4956,"depth":233,"text":4957},{"id":5010,"depth":233,"text":5011},{"id":5041,"depth":233,"text":5042},{"id":5135,"depth":233,"text":5136},{"id":5347,"depth":233,"text":5348},{"id":5435,"depth":233,"text":5436},{"id":377,"depth":32,"text":5473},{"id":5482,"depth":32,"text":5483},{"id":5599,"depth":32,"text":5600},{"id":5827,"depth":32,"text":5828},{"id":6060,"depth":32,"text":6061},{"id":6088,"depth":32,"text":6089},"md",{},[6431,6432,6433,6434,6435],"engineering\u002Fsystem-design\u002Fagentic-platform\u002Fcontract","engineering\u002Fsystem-design\u002Fagentic-platform\u002Fruntime\u002Fexecution","engineering\u002Fsystem-design\u002Fagentic-platform\u002Fplanes\u002Fpolicy-and-governance","engineering\u002Fsystem-design\u002Fagentic-platform\u002Fplanes\u002Fidempotency","engineering\u002Fsystem-design\u002Fagentic-platform\u002Fcapabilities\u002Fstate-and-knowledge",{"title":191,"description":192},"engineering\u002Fsystem-design\u002Fagentic-platform\u002Fcapabilities\u002Ftools-and-integrations",[194,195,196,197,198,199,200],"Wop7fzlMMwX7k7-Vhdi1H_PWDfFV6jKmfSXs5yIcO6w",1788650174420]