Internal reference · backend/recruiter

Fifteen agents behind Mira & Atlas.

The Mira backend is a multi-agent system: a conversational voice, a tool-using operator, two end-of-turn planner lanes (brief + identity), a buyer-intent router that fires once when the client approves the brief, a deep-research crew, a personalization pass, a PDF template picker, and a concierge that scouts talent, negotiates with each one, scores their offers, and writes each finalist's pitch. This page maps who each agent is, what context it reads, what it produces, and — most of all — how they are wired to one another.

Engine · OpenAI gpt-5.6-terra (every agent) Runs in · the worker, never the web request Push · Pusher channels only Last updated · 2026-07-29

01The mental model

Read these five sentences and you have the whole system. Everything after is detail.

One model, fifteen hats.

Every agent here is the same model — OpenAI gpt-5.6-terra. They diverge only in their system prompt, their tool allow-list, the context injected per call, and decoding knobs (reasoning_effort, temperature, forced output schemas). The three settings keys — agent_model, voice_conversational_model, research_subagent_model — all resolve to the same default. So "which agent" is really "which prompt + which tools + which context."

The fifteen run across three theaters plus one on-demand job:

  • Theater I — the live turn. One user message fans out into five parallel lanes. MIRA is the only voice the user hears. ATLAS thinks and acts silently with 35 tools. At end-of-turn three planner lanes compose in parallel, each emitting one structured object: MASON the brief, IRIS the "About You" identity file, and PULSE a high/low verdict on the client's hiring intent (a casual client is routed to plain search instead of the concierge). (ATLAS authors none of them — his edit_identity tool is gone.)
  • Theater II — background research. ATLAS fires SAGE and forgets it. SAGE investigates the client, then a silent chain runs: IRIS dedups the findings into the dossier, LENS (vision) places harvested images — the client's homepage screenshot is now pinned straight into the brief's About-the-client section — and STYLO re-skins the workspace. Separately, when the client won't name a budget/timeline, ATLAS fires GAUGE — a background estimator that prices the work off the live Fiverr talent market and open-web rates, and proposes a figure for MIRA to relay.
  • Theater III — the concierge, between turns. After the client settles on a shortlist, SCOUT hunts a deep pool of talent and ranks the strongest, STERLING negotiates with each one, JUNO rules pass or fail on every proposal against the brief, PORTIA writes each finalist's human "why they fit" pitch at the reveal, and HERALD narrates the live dashboard — all behind a client-approval gate and an anonymity wall.
  • On demand — HUE picks the export template (editorial/modern/classic) when the client clicks "Export PDF."

Agents never call each other over HTTP.

They communicate through five quieter channels: two end-of-turn planner lanes (MASON + IRIS, each one structured object) plus PULSE's once-on-approve buyer-intent classification, a fire-and-forget task (ATLAS → SAGE), one tagged notes channel read a turn later (ATLAS · MASON · IRIS → MIRA), shared workspace state (MASON's checklist; PULSE's intent verdict; the activity board), and Pusher pushes to the browser. Section 03 lays these out.

02System map

Where the agents live relative to the runtime. The browser only ever does short REST; the web tier authenticates and enqueues; all agent work happens in the worker; results return to the browser as Pusher events and durable state lands in Postgres.

Browser client + talent SPAs short REST only Web (FastAPI) auth · validate enqueue · return fast job queue WORKER — every agent runs here THEATER I · the live turn — run_agent_dual MIRA ATLAS MASON IRIS PULSE end-of-turn planners: MASON + IRIS THEATER II · background research chain SAGE IRIS LENS STYLO THEATER III · concierge (between turns) SCOUT STERLING JUNO HERALD ON DEMAND · PDF export HUE Postgres workspace · brief · offers Pusher channels client · talent (push only) REST dequeue events pushed to the browser
in-process / sync queued / async agent or service datastore user-visible push
The browser does short REST; web enqueues a job and returns; the worker runs the agents; state persists to Postgres and surfaces to the browser via Pusher. The theaters are entered by different jobs: agent_turn (Theater I, which spawns Theater II), the concierge jobs (Theater III), and brief_export (HUE).

03How agents talk to each other

The most important thing to understand about this system is its communication. No agent makes a network call to another agent. Instead there are exactly these channels — each with different timing and durability. When you read an agent card below, the "Invoked by", "Produces", and "Triggers" rows always map onto one of these.

ChannelWho → whoTimingWhat actually moves
Planner lanes dual_lane → MASON · IRIS end-of-turn, un-gated Two lanes run un-gated, in parallel with ATLAS's tool loop, with nothing gating on them: MASON emits one BriefPlan, IRIS one IdentityPlan. IRIS may first run one optional web_search round before composing (planner_search.gather_web_search — the same OpenAI-hosted search ATLAS/SAGE use, via tools.web_search_raw), filling the dossier from real web facts instead of leaving a gap or waiting on a lagged SAGE note; best-effort, degrades to a no-op, and runs after the fast reply so it never blocks the user. (MASON's round was removed 2026-07-02 — it cost 14–20 s on every turn with ~zero usable results and pushed the brief past the live-update window; SAGE's research briefing feeds MASON instead.) Each then validates and persists itself (persist_architect_plan / persist_identity_plan). A post-complete drain (≤32s) keeps the stream open — and a final queue sweep forwards whatever a planner emitted in its last instants — so a late architect-applied / identity-applied diff still paints this turn. (PULSE is no longer a lane — it runs once on brief-approve via the classify_intent job.)
Fire-and-forget ATLAS → SAGE (→ chain) async, detached deep_buyer_research does asyncio.create_task(...) and returns "STARTED" immediately. The research chain outlives the turn and persists silently.
Tagged notes ATLAS · MASON · IRISMIRA verbatim, +1 turn ONE channel for the whole team: each lane's note is stored as a source-tagged event message (metadata.source = atlas|mason|iris). MIRA reads them next turn as [ATLAS NOTE] / [MASON NOTE] / [IRIS NOTE] lines; the client chat filters event messages out. No compression (the old compressor is gone).
Shared state MASON → MIRA · ATLAS · UI · gate same turn MASON writes per-item checklist verdicts + a note_for_mira to checklist_state. That single record drives the enable_search gate, the right-rail UI, and MIRA's next question. Both planners also attach a one-line client-facing rationale to their architect-applied / identity-applied events — the transient "Brief" / "Profile" chips under MIRA's reply.
Activity board SAGE/STYLO → MIRA async Background workers write started/done rows to agent_activity; MIRA reads the board so she can say "I'm still researching" on the next turn.
In-process chain SAGEIRISLENSSTYLO awaited, ordered One background coroutine (_run_deep_research_background) awaits each stage in turn, threading block-ids forward so LENS attaches images to the blocks IRIS just minted.
Job queue Web → Worker durable, retried Every theater is entered by an enqueued job (agent_turn, concierge_discover, concierge, seller_turn, concierge_answer_ingest, brief_export). Idempotent & keyed so a retry can't double-act.
Pusher Worker → Browser push only Client channel (agent stream, concierge.progress, personalization-applied, brief.export.ready) and talent channel (chat + thread nudges). Oversized payloads are slimmed with resync:true → the client re-reads GET /api/workspace.

04Agent index

All fifteen at a glance. "Tools" counts function-tools the agent itself may call; agents with a forced/structured output (MASON, IRIS, STYLO, JUNO, PULSE, PORTIA) emit one schema'd object instead — though IRIS may first take one optional web_search round before composing (MASON's round was removed 2026-07-02 for latency). Each card links to that agent's full system prompt in the repo.

CodenameTheaterRoleToolsFile
MIRA live turn The voice. The only agent the user hears; acks, asks one question, paraphrases ATLAS's note. none agent/conversational.py
ATLAS live turn The thinker. Tool-using operator — research, search prefs, facts, brief signal. Silent; hands a tagged note to MIRA. 35, stage-gated agent/runner.py
MASON live turn The brief composer. Designs the brief as a printable doc at end-of-turn; converges in place. BriefPlan JSON agent/architect_subagent.py
IRIS live turn + research The identity planner — MASON's twin for "About You". End-of-turn IdentityPlan; add/update only, never delete; also reconciles research findings. 1 opt. web_search · IdentityPlan JSON agent/identity_editor_subagent.py
PULSE live turn The buyer-intent router. One binary high/low read of the client's hiring intent each turn; a "low" verdict hard-routes them to plain search instead of the concierge. none · IntentVerdict JSON agent/pulse_subagent.py
SAGE research Deep client research. Fire-and-forget loop that builds the client file from real data + the web. 13 (SEO·web·browser) agent/research_subagent.py
GAUGE estimate Budget & timeline estimator. Fire-and-forget loop that prices the work from the live Fiverr talent market + open-web rates. 5 (market·web·finish) agent/estimation_subagent.py
LENS research Vision image placement. Per harvested image: skip / reference / body. Skips liberally. 1 · place_images forced agent/image_placement_subagent.py
STYLO research Personalization. One write that re-skins the workspace (accent, tone, density, chips, locale). none · StyloPayload JSON agent/personalization_subagent.py
SCOUT concierge Talent scout. An agentic hunt over the gateway — one tool per search engine — building a ≥20 pool, then ranking the top 12. 5 search engines + finish scout/loop.py · prompts.py · tools.py
STERLING concierge Negotiator. Deterministic state machine + LLM brain: writes outreach, judges replies, flags offers. 4 function tools · required concierge/runner.py · sterling.py
JUNO concierge Proposal judge. Rules PASS/FAIL on each talent proposal — scope credibility + an inside-deadline estimate's believability; budget and lateness are code gates. Verdict is BINARY: PASS → fit_score 100, FAIL → 0 (a validity gate, not a grade). Finalist order is JUNO's verdict in bands of 10, then SCOUT shortlist position, with seller standing demoted to a tiebreak. none · forced JSON concierge/tools.py
HERALD concierge Activity-log narrator. Turns each concierge action into the client's one personalized, anonymity-safe dashboard line. none · emits text concierge/narration.py
PORTIA concierge Finalist-pitch writer. At the reveal, writes each finalist's human "why they fit" paragraph + real skill chips — one call per finalist. none · forced JSON concierge/portia.py
HUE on demand PDF template selector. Picks the export template (editorial/modern/classic); the export is template-only + light-mode. none · emits a template choice agent/brief_design_subagent.py

Deterministic, non-LLM glue (not agents, but load-bearing — see §09): the intent classifier intent.py, the checklist/gate renderer brief_checklist.py, the dual-lane orchestrator + tagged-notes handoff dual_lane.py, the shared conversation projector conversation.py, the SSRF-guarded image harvester site_image.py, the concierge pure-function tools, and the thin LiveSellerAdapter. The lineage's voice-filler and partner-note compressor are gone; ATLAS's edit_identity tool is gone too (identity belongs to the IRIS lane); STYLO is not a per-turn lane (it runs in the research chain).

05Theater I — the live turn, lane by lane

One user message enters run_agent_dual, which spawns five asyncio lanes that share a single event queue, merged into one ordered stream. The fast lane (MIRA) is unlocked so she replies instantly; the slow lane (ATLAS) holds a per-project lock; the three planner lanes — MASON (brief), IRIS (identity), and PULSE (buyer intent) — run un-gated in parallel with ATLAS's tools and each emits one structured object at end-of-turn (MASON a BriefPlan, IRIS an IdentityPlan, PULSE a high/low IntentVerdict). Nothing downstream waits on the three; a post-complete drain keeps the stream open long enough for their writes to land. ATLAS spawns SAGE within his turn; the notes ATLAS, MASON, and IRIS leave become MIRA's tagged reading next turn. Each lane runs in its own agent <name> OTel span, so a turn's trace breaks out per agent.

ONE USER TURN — five parallel lanes over a shared event queue, merged into one stream User message dual_lane.run_agent_dual MIRA fast · unlocked · no tools streams the reply the user sees ATLAS slow · locked · 35 tools silent — runs the tool loop MASON brief planner · waits for ATLAS composes BriefPlan at end-of-turn IRIS identity planner · waits for ATLAS composes IdentityPlan at end-of-turn PULSE intent router · un-gated high/low verdict → routes the CTA classify_intent → ATLAS prompt checklist_state gate · UI · MIRA SAGE → chain Theater II (async) Postgres brief · identity · checklist Pusher client channel spawns (fire-and-forget) BriefPlan + checklist IdentityPlan client_intent (on change) tagged notes atlas·mason·iris verbatim · +1 turn assistant deltas (user-visible) architect-applied · identity-applied (+ "Brief"/"Profile" rationale chips)
user-visible async spawn tagged notes (+1 turn) MASON persists BriefPlan IRIS persists IdentityPlan PULSE persists IntentVerdict
MIRA and ATLAS each read their own workspace snapshot and run in parallel; ATLAS spawns SAGE (async) within his turn. The three planner lanes run un-gated and each persists itself at end-of-turn — MASON a BriefPlan (+ the checklist that gates search), IRIS an IdentityPlan over "About You", and PULSE a high/low IntentVerdict (it patches client_intent only when the verdict changes, routing a casual client to plain search instead of the concierge). MASON's and IRIS's tagged notes ([ATLAS NOTE] / [MASON NOTE] / [IRIS NOTE]) are what MIRA reads next turn; the 1-turn lag is bridged by that shared checklist + the notes, not by intra-turn messaging. PULSE leaves no note — its verdict drives the frontend's routing, not MIRA.

Two things enter this turn that are not a typed message (both landed 2026-08-03).

A hand edit to the brief runs an ORDINARY turn. A client editing a brief block directly used to fire a MIRA-only acknowledgment — she said "got it, budget's now $8,000" and no other lane moved, so ATLAS never re-read the plan against the change. brief_edit_notice now builds an AgentTurnJob from the notice and re-enters agent_turn.run_full_turn, inheriting the whole path (project resolution, the blocked/takeover gates, (turn_id, seq) events, Pusher publish + offload, latency SLOs, the research resync) instead of re-implementing it; run_brief_edit_ack is deleted and brief_edit.py keeps only the wording. Visibility is the one thing that must NOT become ordinary, so inbound_kind ("user" | "event") now says how a turn's inbound row is persisted: a brief edit rides "event", the raw before/after lands as a hidden system note filtered from the client's chat and rendered to every lane as [SYSTEM EVENT], and the abuse gate is skipped, since judging a notice we wrote ourselves could only produce a false flag. run_agent_dual also grew planner_lanes (default True, unchanged for a typed message): brief_edit_notice is the one caller passing False, which never creates the MASON/IRIS tasks at all, because those two exist to rewrite the brief spine and the About You — precisely the surface the client had just written by hand — so the turn would otherwise talk over the edit that triggered it. A hand edit is therefore MIRA + ATLAS: she acknowledges it in one beat, he re-reads the plan and may use tools, and the client's own words stay exactly as they left them.

The pre-agent triage can outrank the stage. The classifier that already reads every client message for frustration, human-asks and chat misuse now answers a fifth question: is this person a CLIENT here to buy, or a TALENT here to sell? The bar is deliberately high in one direction: describing what your own business sells, hiring for your own agency, and doing part of the work yourself all still read as a client, because the expensive failure is telling a real buyer they are on the wrong side. An attention flag and a Slack alert follow either branch below (the arrival rate is worth watching), and the admin console carries a durable "Talent, not a client" tag read over the whole flag history rather than the open set.

A seller_intent verdict ASKS rather than asserts (reworked 2026-08-04). Acting on the classifier's read directly put that expensive failure one false positive away, so the FIRST hit in a project short-circuits the turn before the agent runs: agent_turn emits build_seller_check_widget, a fixed two-option decision card ("are you here to hire someone for your business, or are you offering your own services?"), and returns. A wrong verdict costs a genuine buyer one tap — hiring writes seller_check_answered, stands the gate down, and carries on into an ordinary turn with nothing recorded. selling is the only answer that stops anything, and what it stops is the CONVERSATION, not the person: SELLER_CONFIRMED_NOTICE is persisted (fixed copy, not MIRA's, because it is the last message the chat will carry), then the project is closed (projects.closed_at, migration 0117) — the composer disappears, a POST into the turn route 409s, and the worker drops anything still in flight. Deliberately NOT a block or an account state: their other projects keep working, a new one starts clean, and the closed chat stays listed and readable. The card is the product's ONE unskippable widget (allowSkip: False strips the skip link, the escape row, the collapse chevron, the dismiss and the Escape binding; EntryStage derives composerLocked from it), and because a client flag is not enforcement the worker turns away any turn that is not one of the two typed option ids while the card is open (SELLER_CHECK_REMINDER, guarded on inbound_kind == "user" so our own system turns walk past it). The answer rides the turn as an option ID (CreateTurnRequest.widget_option_id), never as the label's words: a lock may not hinge on copy an editor is free to reword. That leaves exactly one route to the old SELLER_INTENT_NOTE — an URGENT private note persisted before run_agent_dual, so it lands in the turn-start snapshot every lane reads — and it is the person who answered "I want to hire talent" and kept pitching, where MIRA holds the line in prose because showing the same card twice reads as nobody having read the answer.

MIRA conversational / voice · the fast lane

The only voice the user hears. Acknowledges intent, asks exactly one question, and paraphrases whatever the team did last turn — in one or two spoken sentences.

File agent/conversational.py Prompt full prompt ↗ Model voice_conversational_model · reasoning_effort:none Tools none
Invoked by
The fast lane of run_agent_dual, unlocked, in parallel with ATLAS. Two entry points: stream_conversational_reply (voice, token deltas) and generate_conversational_reply (text routes, may return one widget).
Consumes
Her own workspace read; a slim persona; the live stage + search_enabled gate; the industry tag; the brief checklist verdicts; the activity board; and chat history — where the team's notes arrive as source-tagged [ATLAS NOTE] / [MASON NOTE] / [IRIS NOTE] lines (hidden from the client) and her own past questions appear as assistant turns. Her prompt knows the full team and what each tag means.
Produces
Text deltas the orchestrator wraps as assistant-start/assistant-delta, or one ConversationalReply (text + tappable suggestion chips, or a single widget). Persisted early so a fast follow-up turn can't make her re-ask.
Triggers
Nothing — she is a terminal leaf. She only consumes the team's tagged notes and MASON's checklist.
Hard rules
  • Never claim a tool action. "I updated your brief," "here are five talents" — that's ATLAS's work, not hers.
  • An URGENT note outranks the stage she is in (added 2026-08-03). The pre-agent triage pass runs before she writes anything, and when it spots a conversation she must not answer the ordinary way it hands her a note persisted ahead of run_agent_dual, so it is in the turn-start snapshot rather than arriving a turn late like the team's tagged notes. Today that fires on seller_intent — a freelancer who walked into the client side trying to sell rather than hire — and she names the mix-up kindly in her own voice, that same turn, and points them at the seller side of Fiverr instead of collecting a brief from someone who was never a client. Reworked 2026-08-04: she no longer sees this on the FIRST hit. That one now short-circuits the turn before she runs and shows a fixed, unskippable two-option card (hire / offer my services); a real buyer's tap costs one turn and records nothing, and "I'm offering my services" closes that project with fixed copy while leaving the person and their other projects untouched. The note is what remains for the person who answered "I want to hire talent" and kept pitching anyway.
  • One question, ever. 1–2 short sentences, no markdown or lists (it's read aloud), always ending on a single open question.
  • The transcript is truth. Never re-ask something already answered, even if the (lagging) checklist or note shows it missing.
  • Paraphrase notes, never recite — and surface genuinely new findings, or an honest heads-up when a note begins FAILED:.
  • A name on file is not shared history (fixed 2026-07-31, Monday 3127188107) — the returning-client opening is now gated on the PAST PROJECTS block alone, not on identity_block or past_block. seed_buyer_identity_from_dossier fills the identity row from the client's Fiverr account at sign-in, before any project exists, so the old gate greeted a brand-new client "good to see you back" on their first ever message. Past projects are also the only evidence that makes the opening's own script ("how did the logo land?") answerable. The identity block still renders — she greets by name and never re-asks it — and now carries the explicit rule that account details are not history; step 1 of THE OPENING in the persona prompt states the same gate.

ATLAS tools / thinking · the slow lane

The tool-using operator. Runs research, manages search preferences, captures facts, shapes the brief signal — silently. His output is never spoken; it becomes his tagged note for MIRA.

File agent/runner.py + prompts/{base,overlays,context}.py Prompt full prompt ↗ Model agent_model Tools 35, stage-gated
Invoked by
The slow lane of run_agent_dual, under a per-project lock. (A legacy single-lane mode exists where ATLAS also speaks and asks; production always runs dual-lane.)
Consumes
His own workspace read; the deterministic classify_intent result; and a 4-part composed prompt — BASE_SYSTEM + per-stage overlay + a live context block (brief snapshot with block-ids, identity, search prefs, checklist) + TURN_RULES, plus a dual-mode overlay. For a signed-in buyer a BUYER PROFILE block (ADR-0005) is appended, rendered from the buyer's own Fiverr-account dossier prefetched at login: identity / company / market facts to personalize with, plus a never-display internal-signals zone (predicted LTV, spend, price affinity) the agent may reason with but must never surface — empty for guests. (The same dossier also seeds the visible "About You" profile at login, filling only fields the buyer hasn't set themselves.) Chat history and attached images are appended.
Produces
In dual-mode he suppresses his own assistant bubble and emits a voice_thinking_text sentinel → stored verbatim as his tagged note to MIRA. Streams tool and mid-turn block-*/stage-change events as tools run.
Triggers
SAGE via deep_buyer_research (fire-and-forget) — but only with the client's own confirmed website URL (added 2026-07-03): the tool refuses to run URL-less, so a name-only client first gets a web-searched candidate homepage that MIRA asks them to confirm (a "we have no website" answer skips the deep pass for good). He authors neither the brief (MASON's lane) nor the identity file (IRIS's lane — his edit_identity tool was removed), and enable_search only flips a gate; the user clicks Run.
Hard rules
  • Never invent talents, numbers, or ratings — every fact traces to a tool output or the snapshot.
  • Never trigger search — the gate opens itself once MASON marks every required checklist row done. Never claim "I ran the search."
  • Don't speak and don't ask (he has no ask tool): state what's needed and why in ≤150 words, lead with what he did — MIRA phrases the question.
  • Tools are stage-scoped. Each call is re-checked against the current stage's allow-list (entry · brief · search · results + the concierge-flow stages, concierge_dashboard through fiverr_handoff) and rejected if out of stage.

MASON brief structure · the composition lane (was ARCHITECT)

Composes the entire brief as a printable document — section spine, ordering, typed blocks, plus the per-item checklist — in one structured call. It replaced ATLAS as the brief-structure author.

File agent/architect_subagent.py Prompt full prompt ↗ Model agent_model · reasoning_effort:none Output forced BriefPlan JSON
Invoked by
One of the three end-of-turn planner lanes (architect_producer) — un-gated: it starts at turn-start in parallel with ATLAS's tool loop and reads the turn-START workspace (the old slow_tools_done_event gate is gone), so nothing downstream waits on it and a post-complete drain (≤32s) lets a late architect-applied diff still paint this turn. Also reused by the concierge's concierge_answer_ingest job, folding each answered clarification into the brief.
Consumes
One signal payload: recent conversation, industry, client name, identity + brief summaries, existing sections & editable blocks (so it can converge), the do-not-touch user-edited block-ids, and the checklist items.
Produces
A BriefPlan: section ops, block add/update/delete ops, one checklist verdict per item, a note_for_mira (delivered on the tagged-notes channel), a client-facing rationale (the "Brief" chip), confidence. The runner validates it, then persists. ai_image ops materialize via a server-side gpt-image-2 call. Emits architect-status/architect-applied.
Triggers
Converges in place (not a chain). Its checklist drives the enable_search gate, the right-rail UI, and MIRA's next question — persisted every turn, even when the plan is empty.
Hard rules
  • Rule zero — never fabricate. No stated facts → empty plans, confidence ≈ 0.
  • Converge, don't accumulate. Prefer update/noop over add; re-stating an existing idea as a new block is the #1 failure.
  • Mirror the client's voice, but make one editing pass (changed 2026-07-31, Monday 3127188079) — rule 13 used to say "mirror their cadence" while rule zero forbade adding anything, so nothing in the chain ever tidied rough client prose and a deliberately badly-phrased ask landed in the Overview almost verbatim (the persist-layer normalizers are cosmetic only). Rule 13 now asks for one pass over grammar, fragments, filler and noun piles while the client's vocabulary, tools and formality stay. It is editing, not rewriting: polish may never add a requirement, number, tool or qualifier, and a quote block is never touched.
  • Never touch user-edited blocks; ops only target real block UUIDs (also enforced in validation).
  • Silent. Never speaks to the user; never touches identity, search, or tasks. On failure, falls back to an industry-keyed section skeleton.

IRIS identity planner · the brief lane's twin for "About You"

The "About You" identity planner: reads the full conversation at end-of-turn and emits one structured IdentityPlan — add genuinely-new blocks, update stale ones in place, never delete. The one agent that works in all three theaters.

File agent/identity_editor_subagent.py Prompt full prompt ↗ Model agent_model Output forced IdentityPlan JSON
Invoked by
Three callers. (1) The identity planner lane of run_agent_dual at end-of-turn — MASON's twin, applied by repo.persist_identity_plan; emits identity-applied (+ the client-facing "Profile" chip rationale), and the frontend refetches the dossier. (2) The research chain, reconciling SAGE's findings as candidate_blocks. (3) The concierge's answer-ingest job, folding a client's clarification answer into the dossier (alongside MASON).
Consumes
The full-fidelity identity snapshot (every block's whole payload + section + id) plus the full conversation (the shared, uncapped conversation.py projector MASON also reads) or the researched batch.
Produces
One IdentityPlan of add/update ops + a note_for_mira (tagged-notes channel). The runner validates and persists it. In the research path it mints the block-ids that LENS later attaches images to; if IRIS can't run, a direct-persist fallback keeps the findings.
Output
Add / update ops only — the plan has no delete op; dedup-aware by design.
Hard rules
  • No duplicates — the single most important job; scan the whole file before adding.
  • Never delete or merge (no delete op exists); an empty plan is the correct answer when everything's captured.
  • File into the right section — professional / business / voice / notes.
  • Paraphrase, never paste raw user words.

PULSE buyer-intent classifier · the router

The lead-qualifier. In one shot it reads two independent axes. The PRIMARY one is binary: is this a SERIOUS hire we should hand to the concierge, or a CASUAL / exploratory client we should send to the plain self-serve search? It is deliberately CONSERVATIVE — only a clearly casual client is "low"; everything else stays "high" so a real buyer is never denied the concierge. A "low" verdict hard-routes the client to search even when they're signed in, and skips the concierge sign-in popup for guests. Alongside it PULSE reads the ENGAGEMENT KIND — a one-off project (a scoped deliverable) vs hiring (an ongoing relationship: retainer, recurring work, a long-term collaborator) — which, when "hiring", makes STERLING lead talent outreach with the longer-term opportunity.

File agent/pulse_subagent.py Prompt explainer ↗ · source ↗ Model agent_model · effort:none Output forced IntentVerdict JSON (high | low · project | hiring)
Invoked by
The classify_intent worker job — run ONCE when the client presses Approve on the brief-ready banner (NOT a per-turn lane). The frontend POSTs /api/projects/{id}/classify-intent; the handler (worker/handlers/classify_intent.py) runs PULSE off the web cycle. Run-once + replay-safe: it no-ops once client_intent is set, and skips entirely when there's no brief (intent is meaningless before there's something to hire for).
Consumes
A turn-start workspace read plus this turn's transcript: the live conversation as native chat turns ending on the client's latest message (the primary signal), plus a "workspace signal" block — the brief, SAGE's research report, the identity summary, and the client's cross-project history (repo.get_buyer_history_summary: past projects, searches, concierge runs, offers, hand-offs). Since 2026-07-07 it also receives the signed-in client's Fiverr account profile, FULL zone (ADR-0005) — including the internal signals (LTV, spend, price affinity, strategic flag) as lead-scoring input, under a raise-only ratchet: strong signals can upgrade a borderline client toward the concierge, but weak or missing signals are never grounds for "low". Guests (no user_id) are still classified, just without the history or profile enrichment. Since 2026-07-30 the signal also carries committed_budget — the FX-converted USD figure ATLAS committed to the project (repo.read_committed_budget) — as its own labelled field, so the $500 floor is judged from one authoritative number instead of a currency figure re-read out of the chat.
Produces
One IntentVerdictintent (high|low), confidence, an internal reason line (logs + the admin dashboard, never shown to the client), and an orthogonal engagement_type (project|hiring, defaulting to project). It patches client_intent to project metadata and emits a bodyless workspace-snapshot resync so the verdict is ready before the client clicks the continue FAB — a low verdict hard-routes the FAB to plain search (and skips the guest upsell); high/unknown keeps the concierge. The engagement_type is persisted to projects.metadata_json and, when hiring, frozen into the concierge brief_snapshot (via enrich_brief_with_constraints) so STERLING injects a HIRING-INTENT block and pitches the ongoing relationship rather than just this one piece of work.
Triggers
The frontend's brief-continue CTA (brief-continue.ts) reads the verdict as WorkspaceState.client_intent: a low client is HARD-ROUTED to the plain self-serve search regardless of sign-in state (and a guest is not shown the sign-in popup), while null/high — the safe default — keeps the concierge on offer. PULSE leaves no note for MIRA.
Hard rules
  • Default to 'high'; only 'low' on clear, converging evidence — wrongly denying a real buyer the concierge is a far worse miss than offering a casual one a path they'll ignore.
  • One missing fact is not low intent — early briefs are legitimately incomplete; look for a pattern of disengagement, not a single gap.
  • The $500 floor is a code gate, symmetric, judged from the committed budget — a committed ceiling under BUDGET_FLOOR_USD ($500) short-circuits to low before the model runs; at or above it the floor is settled, and a model low carrying its own budget_floor_applied flag is VETOED back to high. Added 2026-07-30 after PULSE re-read "ILS 3,000" (committed $978) as sub-$500 at 0.99 confidence and hard-routed a real buyer to self-serve — the prompt already carried the guard and lost; the seam is the guarantee.
  • Judge hiring intent, not brief polish — a short brief from someone clearly trying to hire is 'high'.
  • Read-only, fail-safe — never talks to the client or touches the brief/dossier; on any model failure the prior verdict simply stands.

06Theater II — the background research chain

When ATLAS calls deep_buyer_research — which it may do only once the client's own website URL is confirmed on file (the tool refuses otherwise; added 2026-07-03) — it spawns a detached task and returns instantly. That task — _run_deep_research_background — runs a strict, ordered chain inside the worker. It emits no Pusher events; results land in Postgres and surface on the user's next workspace read. The order matters: IRIS must persist blocks before LENS so the vision pass has real block-ids to attach to.

_run_deep_research_background — awaited, ordered; persists silently (no push) ATLAS create_task SAGE deep research loop 12 tools · ≤24 iters SEO · web · browser IRIS dedup reconcile candidate_blocks mints block-ids site_image harvest (SSRF-safe) og · icon · screenshot LENS vision placement place_images (forced) skip · ref · body STYLO re-skin workspace StyloPayload accent · tone · locale Postgres (silent) agent_activity board started/done → MIRA next turn persist
spawn (fire-and-forget) awaited chain step deterministic helper Postgres (no push)
SAGE returns validated blocks; IRIS reconciles them into "About You" and mints block-ids; site_image harvests the client's own-site imagery (behind an SSRF guard); LENS decides placement against those block-ids; STYLO re-skins. Every stage after SAGE is best-effort and swallows its own errors so the worker loop never crashes. SAGE and STYLO log to the activity board MIRA reads next turn.

SAGE deep client research · fire-and-forget

A second, self-contained tool loop that investigates the client step-by-step — DataForSEO, the web, and a headless browser — and returns a validated "client file" of identity blocks plus a personalization payload.

File agent/research_subagent.py Prompt full prompt ↗ Model research_subagent_modelagent_model Tools 12 · ≤24 iters
Invoked by
ATLAS's deep_buyer_research tool → asyncio.create_task → returns "STARTED IN BACKGROUND." Fire-and-forget; idempotent per (user, project) with an in-flight guard.
Consumes
The client handle, website URL, industry, optional focus hints, and injected callables for web search, fetch, and the four browser tools.
Produces
A report of validated blocks (text/finding/kpi/quote/bullet/callout, each tagged to an About-You section) + a personalization dict + a required site_status verdict on the confirmed website + tool traces + cost. It persists nothing itself; it raises on budget-exceeded or malformed finish (no partial success).
Triggers
Returns to the orchestrator, which runs IRIS → harvest → LENS → STYLO.
Hard rules
  • The confirmed website is ground truth (added 2026-07-03) — SAGE studies only the client's own site and discards same-named lookalike pages a bare-name web search turns up, so a namesake business never becomes the client's dossier.
  • Every number traces to a tool result — no estimating.
  • Every finding needs a real source_url seen this run; invented URLs fail validation.
  • Never researches, names, or references competitors (added 2026-07-08) — SAGE builds the client's own dossier, not a competitive-landscape analysis; a rival's site never enters the report or the reference panel. The dataforseo_local_competitors tool and the Competitors workflow step were removed.
  • Forbidden from emitting image URLs (it hallucinates them) — imagery is captured deterministically instead.
  • site_status answers "is there a business here", not "did I get in" (added 2026-07-30, split 2026-07-31 — Monday 3121486463, 3128941813). finish requires one of live · blocked · parked · unreachable. parked (a for-sale / placeholder domain) and unreachable (a dead address: DNS or connection failure) are the only two in NO_BUSINESS_SITE_STATUSES — they drop every panel reference, starve the image harvester, and send MIRA the "confirm the right address" script. blocked is a real business site that refused the read (bot wall, captcha, 403/429, login or region gate): turned away by a server that answered is blocked, never unreachable. It keeps its references, since they are ordinary off-site research about a real company, and MIRA is told the address is correct and not to re-ask it. Both seams test membership of that set — never != "live". The early-stop rule is scoped to the two no-business statuses, so a blocked site still runs the off-site web_search steps; before the split, one tester's fiverr.com hit Fiverr's own bot protection, was labelled unreachable, stopped the loop after a single finding, and shipped an About section of two generic sentences with no sources and no screenshot.
  • Our own key dying DEGRADES the run, it no longer kills the chain (added 2026-08-03 — Monday 3133975429). DataForSEO is one of SAGE's four sources and the only one needing an account of ours (web search, fetch and the browser need no key), and it used to be fatal: missing credentials raised at client construction, a rejected account (401/403) re-raised out of the tool dispatch, and _run_deep_research_background caught both and simply returned — costing the client the research report, About You, the harvested imagery, the STYLO re-skin and the references panel over an expired key, silently (correctly silent, since we must never blame their URL for our billing). Now the dataforseo_* tools come off the menu, SEO_DISABLED_NOTICE tells the model to skip workflow Steps A–C, an in-flight SEO call gets a disabled=true envelope (established prompt vocabulary), and SAGE finishes on the other three sources so the whole downstream chain still runs. A rejected account also stops the next SEO call at the door rather than spending four more round trips on a key that cannot heal mid-run. Because nothing fails loudly any more the degrade is metered — record_research_degraded → counter recruiter.research.degraded {reason=no_seo_creds|seo_auth_rejected} (root doctrine 1.12) — since a dead key means every client from that moment on quietly gets a thinner file, and the rate is the only signal that says so. Separately, the in-flight guard against duplicate deep research moved off a process-local task dict onto a Valkey lock (research_inflight:{user}:{norm_url}, 300s TTL, fails OPEN): in prod the turn fans out over many autoscaled workers, each saw an empty local dict, and one real client URL took 4 full duplicate runs in 90 seconds — 4× the research cost plus the browser-lease saturation that then dropped the read of the client's own site.

GAUGE budget & timeline estimator · fire-and-forget

A small, self-contained tool loop that prices a project from TWO sources. When the scope is clear but the client won't give a budget and/or a timeline, ATLAS hands it the job; GAUGE reads the LIVE Fiverr talent market (the real on-platform gig-package prices — its primary anchor, the same pool the matcher will search) and cross-checks the open web for the wider going rate, then proposes a grounded range and/or delivery window for the client to confirm.

File agent/estimation_subagent.py Prompt full prompt ↗ Model estimation_subagent_modelagent_model Tools 5 · ≤8 iters
Invoked by
ATLAS's request_budget_estimate tool → asyncio.create_task → returns immediately. Fire-and-forget; runs ONCE per project (an in-flight guard + an existing live proposal both make a re-trigger a no-op).
Consumes
A scope brief assembled off the live workspace: the project type, a compact dump of the brief sections, the client's own recent chat, the team's research on their business, the currency, and which dimension(s) are missing. Plus injected market callables — search_talent_market (live Fiverr pool summary) and list_talent_market (full talent cards) over the gateway — and web_search / fetch_url.
Produces
An EstimationReport (budget range and/or delivery days + a one-line market basis + a friendly rationale). The runner writes it as an internal BudgetTimelineProposal (chat-only, NOT the brief/prefs), logs GAUGE on the activity board, and leaves MIRA a [GAUGE NOTE] to relay. It persists no brief/pref itself.
Triggers
MIRA relays the recommendation; on the client's explicit yes/number, ATLAS commits it via set_search_preference (no auto-accept).
Hard rules
  • Ground the number in BOTH sources — one search_talent_market (live Fiverr prices) AND one web_search (open-web rates) before finish; reconcile the two, never invented.
  • Price the scope in front of it — a logo is not a full identity; match the deliverable, not the maximal category.
  • One realistic band, not the whole "$5–$50,000" market; honest currency; only the missing dimension(s).
  • Writes nothing to the brief/prefs — it's a proposal; commit is explicit-acceptance only.

LENS vision image placement

A single-shot vision call that looks at every harvested client image alongside the persisted blocks and decides, per image: skip, attach as a reference thumbnail, or insert as a body image — and to which block.

File agent/image_placement_subagent.py Prompt full prompt ↗ Model agent_model (vision · detail:low) Tools 1 · forced
Invoked by
The image pipeline inside the research chain, after IRIS persists blocks so it can target real block-ids. Synchronous within the background task; degrades to "no placements" on failure.
Consumes
The persisted blocks + a catalog of harvested images (id, kind, source page, alt, downscaled thumbnail). A "matches finding" hint is computed when an image's host matches a finding's source.
Produces
Validated placement decisions → reference ops (append a thumbnail to a block) or body ops (insert an ai_image block). Only images it actually places get stored as assets — skipped ones never hit the Files panel.
Tools
place_images, forced. It picks an integer image-id from the catalog, never a URL; unknown ids/blocks are dropped.
Hard rules
  • Skip liberally — default is skip; icons, glyphs, and tiny files are chrome.
  • The homepage screenshot is the best body candidate — the client's front door.
  • One image per idea; de-dup near-duplicates.
  • Only real ids — reference/body must target a listed block; no inventing.

STYLO workspace personalization · the chain's last step

One structured-JSON call that re-skins the workspace — accent colors, tone, density, corner radius, suggestion chips, copy overrides, currency/locale — from whatever signals exist. Touches nothing in the brief, search, or identity.

File agent/personalization_subagent.py Prompt full prompt ↗ Model agent_model · 12s Output forced StyloPayload
Invoked by
The research chain, as its final, durable step (it used to be a flaky per-turn lane; that lane has since been removed). Best-effort.
Consumes
The freshly-written workspace: industry, client name, identity summary, brief summary, and existing personalization (only honored when ≥3 non-default fields are set).
Produces
A personalization payload the caller persists. The worker path emits no event — the re-skin surfaces on the next workspace sync. A deterministic industry-keyed palette table backs it when the model returns weak output.
Triggers
Terminal — the last step of the chain. Nothing fires after it.
Hard rules
  • Never leave the profile neutral when there's any signal; derive ≥3 chips + tone/density.
  • Agent names & hero motif are off-limits — excluded from the schema entirely.
  • Don't undo user or prior confirmations; currency + locale must match the market.
  • Empty fields + confidence 0 is the correct answer for no-signal input.

07Theater III — the concierge, between turns

After the client settles on what they want, the concierge takes over between turns and talks to talents on their behalf. It is event-driven, not polled: every reactive step is a job enqueued from the web layer (a talent reply, a client answer, an approval). Two invariants dominate — a client-approval gate before anything reaches a talent, and an anonymity wall that keeps the client's view anonymized until the reveal. Two newer wires: a run auto-finalizes the moment its last thread resolves with ≥1 offer (finalize_if_complete — no manual reveal click), and every answered client clarification is also folded back into the brief + About You by MASON + IRIS (concierge_answer_ingest) — the concierge feeds Theater I's planners.

BETWEEN TURNS — discovery is optional; the approval gate is not Client shortlist / approve SCOUT Fiverr seed + LLM pick /discover only · default OFF runner.start drafts outreach threads: queued (unsent) approval gate approve_and_send STERLING deterministic state machine + LLM brain runner.py start·send·tick finalize (deterministic) sterling.py writes msg · judges reply (LLM) Talents talent SPA + DB inbox JUNO offer scorer · + rank_offers (det.) auto-finalize → reveal fires when the last thread lands (≥1 offer) · top-3 ranked · then client picks /discover /start (skips SCOUT) opening msgs reply → seller_turn job anonymity wall — client sees anonymized view until reveal →
concierge action approval gate reveal to client anonymity wall
Two entry points converge on runner.start, which only drafts. Nothing reaches a talent until the client approves. STERLING's runner is deterministic (start/send/tick/finalize); its brain (sterling.py) is the LLM that writes each message and judges each reply. Talent replies arrive as seller_turn jobs; offers are scored by a separate LLM call and ranked deterministically; finalize reveals the top three.

SCOUT concierge talent-scout

The relentless headhunter. Runs an agentic loop over the Recruiter Gateway — choosing engines, queries, and filters itself — to build a deep pool of at least 20 real candidates, then ranks the top 12 it hands to the client. SCOUT does BOTH discovery and ranking; the order it emits is exactly what the client sees.

File scout/loop.py · prompts.py · tools.py Prompt full prompt ↗ Model agent_model Tools 3 search engines + ask_client + finish
Invoked by
Only the no-shortlist path: POST /api/concierge/discoverConciergeDiscoverJob → worker → discover_and_start (and the self-serve search funnel). The explicit-shortlist /start path skips SCOUT entirely.
Consumes
The authoritative brief plus the client↔MIRA conversation beneath it (no pre-digested preferences block since 2026-07-28 — SCOUT reads the requirements itself, the brief winning any conflict, and derives the hard_filters, including a client-stated budget range whose low end becomes a soft matching floor), and the client's Files & References — the brand / style sources shown to MIRA, uploaded reference images included as MIRA-vision descriptions, used as style anchors that mine query terms and tie-break the ranking among capable talents. Over a bounded loop (gateway_scout_max_iterations) it fires four engines (all firable in parallel) — search_talent (the primary hybrid net that carries the shared hard_filters object + the one real must_have content screen) · search_semantic (a distinct semantic net that hard-filters location / language / delivery and hits a populated seller-location index, so a local or on-language brief surfaces in-market talent the blended net misses; added 2026-07-28) · search_lexical (literal keyword / Pro talent) · search_experts (Fiverr's curated experts) — every engine taking the SAME enforced hard_filters, always opening with search_talent and reworking queries and angles until the pool is deep enough, then reasons over each candidate's deep gateway enrichment (bio, tenure, credentials, real gig text, per-gig prices, and structured gig facets) to rank. Since 2026-07-16 that evidence is unclipped — the ranker sees each candidate in full, paid for by a cheap pre-pass (scout/gig_bullets.py, gig_bullets_model) that compresses each unique gig description ~5× into fixed-schema bullets, Valkey-cached by description hash for 30d and fail-open at every layer. If it needs one fact only the client can give, it pauses the loop with ask_client and resumes exactly where it left off once the answer arrives.
Produces
A ranked shortlist (top 12) — each pick {seller, brief-relative rationale, UI signal tags, core_profession, fit_category, best-matching package, concierge_fit_score} — emitted at finish and handed to runner.start (carrying the rationale so finalist cards can show why each talent was picked). From 2026-07-22 the order was gated first on core profession (the talent's one real craft, read off skill tags / portfolio / order mix — a profession that isn't the brief's discipline could never be EXACT or STRONG); since 2026-08-03 that gate is the CHOSEN PACKAGE instead (see the hard rules below), and core_profession is kept only as a card label and a spam check. The order is then driven by a coarse fit tier (fit_category — EXACT > STRONG > CORE_FIT > ADJACENT > OFF, the CORE_FIT rung added 2026-07-28 for a talent who passes the core discipline+medium gate and nothing else, and the ceiling an UNDERPRICED seller is capped at) as the primary sort key, with the talent's proven quality & track record breaking ties within a tier; concierge_fit_score (0–1) is now just an internal confidence signal, no longer the sort key. Since 2026-07-29 fit is graded on two separate axes: fit_category is CRAFT-ONLY, and a new agent-authored misses_rank (0 = meets every stated ask, higher = worse, a buyer-EXPLICIT miss weighing far heavier than an inferred one, a price out of band in either direction counting as a miss) orders picks within a craft tier BEFORE seller quality — so a pick that misses a stated must can never sit above a comparable-craft pick that meets it, while relaxed stays a human label the ranking never reads. EXACT additionally requires price fit (a seller topping out below the client's band is not EXACT). Each pick also names a chosen_package — the gig TIER that DELIVERS the brief's scope within budget, read off the gateway's per-tier features — and the card resolves to that tier, so the price shown is the price of the tier SCOUT graded. Since 2026-07-31 finish also requires ONE project-level field, fiverr_search_query — 2–4 plain words naming the search the CLIENT would type for the role they asked to hire, read off their ask in the brief and the conversation. It rides ScoutResult through both surfaces, persists per run (migration 0110, on concierge_runs and search_runs alike), and outranks the hunting terms wherever a back-to-Fiverr link is built; search_queries stays untouched telemetry and the fallback for legacy rows.
Triggers
STERLING's runner.start, which drafts outreach (and sends nothing yet).
Hard rules
  • Be relentless on the pool — at least 20 seriously-considerable candidates before ranking; never settle for the first search, change engine/query/angle and go again.
  • SCOUT is the ranker — there is no second ranking step; the order it emits is what the client sees. It grades fit into five coarse tiers (EXACT / STRONG / CORE_FIT / ADJACENT / OFF) and sorts on the tier first, proven quality within it — never a hard sort by a fine score. The gate moved on 2026-08-03, after a Roblox-game brief returned weak generalists over a Top-Rated studio whose reviewed, order-backed "Roblox full game" gig proved the discipline but whose headline persona read AR/VR. The tier had been gated on core_profession, a skill-tag persona, so a proven on-brief PACKAGE lost to a self-labelled specialist. The tier is now a property of the chosen package: SCOUT picks the closest gig+package whose service IS the brief's deliverable and grades how fully THAT package covers it (discipline, medium, scope, price-fit, timeline), across the whole ladder — so a reviewed on-brief gig grades EXACT even when the studio's headline is broader, while a bare keyword package with no orders or portfolio behind it stays ADJACENT/OFF. core_profession survives only as an honest card label and a spam check. The other half of the same move: a package delivering only a COMPONENT of the job (a "$50 script, one system" gig against a whole-custom-game brief), or a catalogue whose ceiling cannot reach the brief's scope, is not a low tier — it is not a fit, and DROPS (OFF) rather than surfacing near the bottom, which is why cheap component gigs had been appearing as CORE_FIT. With those removed at the gate the LOW-COST fit cap became redundant and was withdrawn: seller quality (standing, a bargain-floor catalogue) is now a strictly SEPARATE axis moving only the QUALITY-GRADE that orders talents WITHIN a tier — the same deterministic score_by_config points the loop already sorted by, now SURFACED on every evidence line so SCOUT sets the tier while seeing the exact grade the sort will apply. Capability comes before soft preferences; a required location or time-zone and any budget band are hard caps applied at rank, and a client-stated budget floor only ranks below-floor talent lower, it never drops them. What changed on 2026-08-02 is the SEARCH side, not this gate: the prompt used to say a mere preference does NOT go in hard_filters, so a stated "preferring Italy or Spain" never reached the pool at all. A stated preference now rides the FIRST waves as a filter too, is relaxed only once the filtered pool has proved thin (softest first), and a shipped pick that misses one is never free — it carries a light misses_rank weight, always beneath a stated-MUST miss, and is named in that pick's reason. Two supporting rules landed with it: a missing quality bucket now scores NEUTRAL rather than bottom (absence of an A–E grade is lane coverage, not a fact about the talent; real D/E still score 0), and must_have reaches the engine as ONE OR-facet rather than per-term AND facets, which had re-created the keyword starvation removed on 2026-07-28.
  • One in-loop client question, max. The single clarification SCOUT may need is asked mid-loop via ask_client (a resumable pause→resume), not a separate triage step; it never blocks the hunt on more than one.
  • No invented talents — a pick's username must match a real candidate exactly.
  • The back-to-Fiverr search is the CLIENT's words, not SCOUT's angle (added 2026-07-31, Monday 3128443289) — fiverr_search_query is the role the client asked to hire ("website developer"), never a niche angle tried while hunting ("landing page design"), never a badge or filter, never their brand or industry, and never a sentence lifted from the brief. The earlier guard was a badge-word blocklist, useless against a query that is a real service but the wrong one.
  • Anonymity holds — discovery exposes only progress phases over Pusher, never raw talent PII pre-run.

STERLING concierge negotiator · state machine + brain

Two parts. The runner is the deterministic state machine (start / approve-and-send / tick / finalize). The brain is the pure LLM that writes each talent-facing message, judges each reply, and flags offers — acting only through four function tools.

File concierge/runner.py + sterling.py Prompt full prompt ↗ Model agent_model (no reasoning_effort — 400s with tools=) Tools 4 · tool_choice:required
Invoked by
Runner entrypoints driven by worker jobs (start on-cycle; send/tick/finalize/extend enqueued by web routes). Conversational turns run in the separate seller_turn handler, enqueued when a talent replies or the client answers a clarification.
Consumes
A context: the frozen brief + client-card snapshots, the trigger (opening / talent message / client answer), the full thread history, the latest inbound — plus the client↔assistant chat + the full IRIS dossier (load_client_knowledge) and known_answers: every answer the client has given on the run, across all threads, so he never re-asks what another talent already got answered. Since 2026-07-07 the card snapshot also carries the client's Fiverr account profile, frozen at run start (buyer_card_snapshot.fiverr_profile, ADR-0005 — a run outlives the dossier cache's 24h TTL): the safe half personalizes the client-disclosing outreach, the internal signals calibrate the negotiation band and push-back strength behind an absolute never-hint-to-talent wall, and — like the private talent preferences — the section never enters the close-out prompt.
Produces
His tool calls — send_reply / escalate_to_client / register_offer / register_decline — fold into a SterlingOutput: reply text, escalation question, offer_detected, decline_reason, next thread state, guardrail flags. LLM: the prose, the judgment, offer detection (with an LLM fallback extraction when the $-price/timeline regex can't parse a flagged offer). Deterministic: all persistence, push, and offer scoring/ranking.
Triggers
Talent messaging (the adapter's send is a no-op — the talent SPA + DB are the inbox); escalations → a client clarification, whose answer relays to each asking thread and enqueues one concierge_answer_ingest job (MASON + IRIS fold it into brief + About You); finalize_if_complete auto-reveals ranked offers + a recommended winner when the last thread lands.
Hard rules
  • Client-approval gate. start only drafts; approve_and_send refuses without a recorded approval. No path bypasses it.
  • Pure brain, no I/O — and tools only. One LLM call, zero side effects, tool_choice:required; a turn with no usable tool call degrades (ok=False), never guesses. The handler owns all persistence and push.
  • Never invent an offer — if neither the regex nor the LLM fallback can extract one, the thread stays open; no fabricated scorecard.
  • Guardrails: stay on-platform, decline out-of-scope, sanity-check price/timeline vs budget. (Full client disclosure to talents is allowed — a deliberate decision, separate from the client-side anonymity wall.)
  • Two standing carve-outs from escalation, and one standing FAQ (added 2026-08-04). Samples / test runs / trials join credentials as a thing he explains rather than escalates: he cannot arrange one, so promising to check leaves the talent waiting on something that is never coming — instead he says the sample gets agreed directly once client and talent are connected, tells them their estimate may assume a pilot step, and is explicitly forbidden from reading a sample request as a narrower scope (a tester review found him escalating a test-email request the client then agreed to with nothing able to send it, and reading one offered sample page as a talent's whole coverage). A _PRODUCT_FAQ block now rides every chat turn including a closed thread — say plainly he is AI, what the service is, and an honest non-answer to "can I get one" — because a talent asked how to set up an assistant like this one and got an invented answer about posting projects and contacting Fiverr Support. And a bare acknowledgement ("ok", "thanks") is answered with ONE short line that repeats nothing: send_reply is required every turn, so the paragraph it used to earn was the tool policy showing through the voice.

JUNO concierge proposal judge · the verdict

Returns a PASS/FAIL verdict on every talent proposal — never a grade (spec 2026-07-16). It judges exactly TWO things: whether an inside-deadline estimate is credible, and whether the scope honors the brief. Budget and a late timeline are checked by exact arithmetic in code before JUNO sees the proposal, and are explicitly not its to judge. STERLING parses the terms; JUNO judges them.

File concierge/tools.py Prompt full prompt ↗ Model agent_model Output forced JSON verdict
Invoked by
The seller_turn handler the moment STERLING flags an offer — score_offer_terms_with_brief (terms STERLING parsed from the whole thread) or extract_custom_offer_with_brief (extraction path), both routing to the one canonical scorer _score_offer_llm. One verdict per offer, no re-parse.
Consumes
The parsed offer (price, timeline, revisions, inclusions/exclusions), the talent's latest message and any files the TALENT attached, the full concierge⇄talent negotiation thread (talent_conversation — the whole back-and-forth STERLING had with this talent, folded in since 2026-07-19 so a commitment made anywhere in it counts, not only in STERLING's structured terms or the latest message; capped generously at the last 40 messages × 1000 chars), plus a slim brief view — summary, budget_min/budget_max, delivery_max_days, must-haves, industry — the client's intake conversation, excerpts from the client's uploaded files, and the client's private client_preferences (languages, countries, notes). It judges the offer relative to that brief, not on generic heuristics.
Produces
timeline_ok + timeline_note, scope_ok + scope_note, and a one-sentence (18-word max) rationale that must reference the brief and never mention price (read only when the proposal passes everything). The verdict is BINARY (spec 2026-07-21): it maps onto the stored fit_score via _verdict_fit_score — FAIL → 0, PASS → 100. There is no scope_rank grade any more; fit_score is ONLY the validity gate, and order among passers is decided elsewhere (seller standing, then SCOUT position — see below). The five sub-scores and scorecard_breakdown were deleted with the grade model; the field stays empty. An LLM error degrades to the code-computed budget+timeline gate — it passes only if both arithmetic checks clear, otherwise it fails — never a mid-band grade.
Triggers
Feeds the matching engine: only offers clearing OFFER_VALID_FIT_MIN (60) — i.e. the ones that PASSED — count as "valid" and drive the shrinking collection window + the present/keep-waiting call. Ranking itself (rank_offers) is deterministic, and since 2026-08-02 it sorts on (fit_score // 10, scout_rank, standing, received_at): JUNO's offer verdict DOMINATES, in BANDS of 10 (a 2-point wiggle on a 0–100 score is judge noise, not a verdict, which is what keeps the 3100529163 protection — an 82-vs-80 must not beat a Pro's standing), then SCOUT's resolved shortlist position, then the standing ladder (Gili's — Pro/Top-Rated → level → orders → credibility-weighted rating), then arrival. The old key put STANDING first, which let a high-standing seller win Mira's Choice before fit was ever read: the German-designer incident (Monday 3123812501) crowned an EN-only Pro/TRS over the perfect-fit, SCOUT-#1 talent. SCOUT's rank already encodes craft fit, misses of stated musts, and quality (which itself includes standing), so rank outranking raw standing is the correct order. fit_score is still a binary pass/fail gate rather than a grade — banding it is how a binary verdict participates in the sort without becoming one.
Hard rules
  • PASS or FAIL, never a grade — the verdict is strictly binary (spec 2026-07-21), with no 0–100 scope_rank behind it. Order among the talents who passed is decided by rank_offers, not inside JUNO — but since 2026-08-02 that order LEADS with JUNO's verdict, banded in tens, then SCOUT's shortlist position, with seller standing demoted to a tiebreak beneath both.
  • The budget is not JUNO's to judge — code checks it first, symmetrically: BUDGET_NUMBER_TOLERANCE (±20%) around a single figure, BUDGET_RANGE_TOLERANCE (±5%) around each bound of a real range. A missing or non-positive budget passes; we never fail an offer over data we don't have.
  • A late estimate is arithmetic, not judgment — an estimate more than TIMELINE_LATE_TOLERANCE (10%, rounded up to whole days) past the deadline fails in code. JUNO only rules on whether an inside-deadline estimate is believable: beating a large scope by a wide margin is suspicious, not impressive.
  • Preferences weigh, they never fail — a mismatch on language, country or seniority makes a talent a weaker fit, but is NEVER grounds to fail a proposal and is never a gap named to the talent, who could not fix it anyway.
  • Do not over-reject — a capable talent who covers the scope passes. Empty inclusion/exclusion lists, no assumption register, a missing pitch, or an unstated revision count are NOT gaps; the concierge collects a staged proposal, not a terms sheet.
  • Two fences on the negotiation thread — reading the full talent_conversation lets a commitment the talent made in an earlier message count, but it must not become a new way to fail: chat is not a terms sheet (nothing merely absent from the conversation can fail a proposal), and the concierge's own words are never the talent's commitments. Both fences (2026-07-19) keep the wider context from tipping JUNO into over-rejection.
  • Notes must be actionable — each note is relayed to the talent as ONE concrete change anchored to the brief. "Weak scope" is useless; "omits the mobile layout the brief requires" is right. A scope_note ends the conversation, so fail scope only when no re-quote could fix it.

PORTIA concierge finalist-pitch writer

The card writer. At the finalist reveal, PORTIA composes each of the ≤3 finalists' client-facing pitch in one LLM call per finalist — the honest, human case for why this talent fits this brief. It replaces STERLING's inline card-field gathering: STERLING negotiates the deal; PORTIA, given the whole picture at reveal, writes the card.

File concierge/portia.py Prompt full prompt ↗ Model agent_model · reasoning_effort: low Output forced JSON card
Invoked by
runner.finalize at the reveal (in the worker) — write_finalist_card per finalist. The same agent + prompt also backs write_search_pitch for a guest/direct search result (minus the offer + negotiation transcript).
Consumes
The brief, the talent's real Fiverr track record from seller_threads.seller_profile_json (skills, gig titles, order/review counts, rating, seller level, Top-Rated standing), and — for a concierge run only — the offer, an internal fit review, and the negotiation transcript. A best-effort candidate_detail enrichment (recent-review themes, portfolio count) is folded in before the tool-free call. Each finalist gets a rotated opening_angle so the ≤3 cards don't read alike.
Produces
A bounded card_json: fit_reasoning (one warm ~40–70-word pitch, the card's single fit prose), 3–5 real skill_chips, a short expertise_title, a fit_badge (only when genuinely exceptional), and a personal_note in the talent's own words. Persisted post-reveal-only on custom_offers.card_json.
Triggers
Terminal — it only writes the card. Any failure returns the caller's fallback_card unchanged, so a reveal never blocks on PORTIA.
Hard rules
  • Never invent — skill chips are verbatim real skills; no fabricated stat, client, or personal note.
  • Numbers only when exceptional — never a bare order/review count; a count only paired with a strong rating (e.g. "300+ orders at 4.9★"), else left out.
  • The pitch is the match, not the terms — never restate price, budget, timeline, delivery, or start date shown elsewhere on the card.
  • Degrade, never block — any error (or an empty result) returns the STERLING-drafted fallback card unchanged.

HERALD concierge activity-log narrator

Turns each concierge action into the one personalized line the client reads on the live dashboard ticker — distilling the run, the talent, and what just happened into a de-attributed, anonymity-safe sentence. It used to be MIRA's voice; now it's its own agent.

File concierge/narration.py Prompt full prompt ↗ Model agent_model Output one feed line (text)
Invoked by
The concierge worker handlers (seller_turn, the gated-send + discovery flows) whenever a row that produces a ticker entry is written — a reply, an offer, a decline, a forwarded question. Best-effort, in the worker only.
Consumes
Run + talent summaries (distilled once and cached on the run) plus the specific action detail. Three labelled prompts: herald_run_summary / herald_seller_summary (the reusable summaries) and herald_narrate (each individual line).
Produces
One personalized activity line, persisted next to the row that produces the ticker entry. The dashboard renders it instead of the generic de-attributed template.
Triggers
Terminal — it only narrates. Always degrades to the default template on any failure (doctrine 1.5).
Hard rules
  • Anonymity-safe — never a talent's name or a verbatim quote; a discipline + the gist only.
  • Degrade to the template — any failure falls back to the generic ticker line; never crashes the feed.
  • One line per action — short, client-facing, present-tense.
  • Distill once — run/talent summaries are cached on the run so a later line doesn't re-distill.

08On demand — HUE, the PDF template picker

HUE is not part of any turn. It runs when the client clicks "Export PDF." The button only records a turn and enqueues a job (doctrine: no slow work on the web cycle); the worker then runs HUE — one cheap art-direction call — and stores the chosen base template for the export. The export renders template-only and light-mode; there is no bespoke CSS.

HUE PDF template selector

Looks at a digest of the brief and picks the single base template (editorial/modern/classic) the export renders on. One JSON answer + a one-line rationale — no CSS, so the model can never drop content or emit broken markup.

File agent/brief_design_subagent.py Prompt full prompt ↗ Model agent_model · 75s Output a base_template choice (no tools)
Invoked by
POST /api/workspace/brief.exportBriefExportJob → the brief_export worker handler. Never in a web request.
Consumes
A token-cheap digest of the brief: project + summary, industry, brand accent hex, tone, block-type counts, section titles.
Produces
A result (base template, rationale). The handler persists the chosen template when the pick succeeds, renders the PDF on it, then publishes one brief.export.ready Pusher event carrying the download URL.
Call
One art-direct LLM call returning JSON: base_template ∈ editorial/modern/classic + a one-line rationale. The old design → refine CSS phases were removed.
Hard rules
  • One JSON object, no prose — the reply is parsed for base_template + rationale; anything else is ignored.
  • Choice is constrained to three templates — an unrecognized value falls back to editorial.
  • Template-only + light-mode export — no CSS is generated or inlined, so there is no injection sink to sanitize.
  • Degrade, never fail the export — an LLM error returns ok=false and the export renders on the stored or default template.

09The deterministic glue (not agents, but load-bearing)

Several non-LLM components do the unglamorous work that lets the agents stay focused. They have no prompt and make no model call — but the system breaks without them.

intent.py — intent classifier

Pure keyword/stage rules that label the turn (run_search, analyze_file, small_talk…). Feeds a HINT: line into ATLAS's prompt; the model may override it.

brief_checklist.py — gate & render

Joins the checklist template with MASON's persisted verdicts into the rendered block read by both lanes. The enable_search gate refuses while required items are missing.

dual_lane.py — orchestrator + handoff

Spawns the five lanes over one queue, holds the per-project lock, persists the fast reply early, and writes the team's tagged notes (atlas|mason|iris) for MIRA — verbatim, no compressor.

conversation.py — shared projector

One uncapped conversation projector MASON, IRIS, and PULSE all read, so they compose / classify from the same full transcript — including Mira's widget-asked questions (lifted from metadata.widget.question), not just the buyer's answers.

site_image.py — image harvester

Fans out over the client's own pages for og/twitter/icon/inline imagery (and a homepage screenshot for JS SPAs), behind an SSRF guard, deduped by hash. Feeds LENS.

concierge tools.py — pure functions

The deterministic outreach template, the regex offer-extractor, and rank_offers. The LLM offer-scorer is a separate call layered on top; ranking itself has no model.

LiveSellerAdapter — thin send

The only messaging adapter; its send_message is intentionally a no-op. The talent test-platform SPA and the seller_messages table are the inbox; the mock adapter/scenarios are gone.

agent_identity — canonical names

Stdlib-only telemetry-label → display-agent map (runneratlas, …) — the single source the debug dashboard, the agent-trace store, and the per-lane OTel spans (gen_ai.agent.name) all name agents by.

Not in the active set

STYLO does not run as a per-turn lane; its durable trigger is the research chain (the old inert no-op lane was removed). The legacy lineage also carried a voice filler and a partner-note compressor; the compressor is gone (notes are verbatim) and the filler is not part of the active set. ATLAS's edit_identity tool is gone as well — identity authoring moved into the IRIS planner lane.

10Rules every agent obeys

Beyond each agent's own prompt, the platform doctrine binds all of them. These are why the design looks the way it does.

  • No slow work on the web request. Every agent runs in the worker; the web tier authenticates, enqueues, and returns in milliseconds.
  • No streaming HTTP, no polling. Server→client is Pusher channels only; the agent package itself just yields events, transport-agnostic, and the worker republishes them.
  • Async is durable and idempotent. Jobs carry stable ids; externally-visible effects are keyed (uuid5 + ON CONFLICT DO NOTHING) so a retry can't double-send or double-write.
  • Degrade, don't crash. Every background stage swallows its own errors; LLM failures fall back to deterministic skeletons, palettes, or templates rather than an empty result.
  • Never invent. Across ATLAS, MASON, SAGE, IRIS, and SCOUT the same line recurs: every fact, number, source, and talent must trace to real evidence.
  • Mind the ~10KB Pusher cap. A full workspace is never pushed; oversized payloads set resync:true and the client re-reads GET /api/workspace.
  • Every call is visible. Each LLM call logs tokens, latency, and dollar cost; each lane runs in its own agent <name> OTel span (gen_ai.agent.name, named via agent_identity) nested under the worker's job span — a turn's distributed trace breaks out per agent.