{"slug": "building-shoots-a-hybrid-event-driven-agent-system-for-photography", "title": "Building Shoots: a hybrid event-driven agent system for photography", "summary": "A developer built Shoots, a hybrid event-driven agent system for photography, as an entry for the All Things Agentic Hackathon. The system uses constrained model calls within an inspectable workflow, where agents communicate via Pub/Sub and domain rules are kept separate from I/O to ensure reproducibility. The developer deliberately avoided chains of agents passing prose, instead using structured schemas and measured facts to veto conflicting visual claims.", "body_md": "I created this article for the purposes of entering the All Things Agentic Hackathon.\n\nShoots is an Android and web photography Companion. A photographer takes ordinary\n\nShots. Shoots reviews them in the background, preserves the Evidence behind its\n\nreading, and can offer one optional Experiment when the record supports it.\n\nThis is the technical version of the project. It is about how a file becomes a\n\ndurable learning record, how the agents communicate, and where I deliberately\n\nrefused to let a model make the decision.\n\nThe model panel reads a single Shot. The system around it does the work: it\n\ncreates a durable Run, moves tiny events through independently retryable stages,\n\nre-reads state at every boundary, records every outcome, and only settles a Shoot\n\nafter every member Run is accounted for.\n\nThat distinction matters. I did not want a chain of agents passing prose to one\n\nanother until it sounded convincing. I wanted constrained model calls inside a\n\nworkflow whose state, retries, and outputs could be inspected later.\n\nThe codebase is deliberately split by responsibility rather than by screen or\n\nagent name:\n\n```\nandroid/\n  phone/         # approved Camera media\n  work/          # background upload and retry\n  data/          # cache, API, identity\n  ui/            # Android screens\n\nbackend/app/\n  api/           # FastAPI ingress and push endpoints\n  domain/        # pure rules and state transitions\n  imaging/       # EXIF, pixels, visual artifacts\n  agents/        # ADK agents and prompts\n  services/      # workflow orchestration\n  infra/         # storage, Pub/Sub, Drive, secrets\n\nfrontend/src/\n  stores/        # API and SSE state\n  pages/         # web audit desk\n  components/    # receipts and visual Evidence\n\ninfra/           # Google Cloud deployment and Scheduler\n```\n\nThe important boundary is `domain/`\n\n. It has no I/O. It owns the rules that must be\n\nreproducible: grid-cell conversion, taxonomy validation, panel consensus, Criteria\n\nchecks, Technique Map projections, and Run state transitions. `services/`\n\ncan call\n\nmodels and storage. `domain/`\n\ncannot.\n\nShoots has two coordination layers, because model orchestration and background\n\nwork have different failure modes.\n\nThe Analyst is an ADK `SequentialAgent`\n\n. Its first step is a `ParallelAgent`\n\nwith\n\nthree readers. Its second step is a Synthesizer.\n\n```\ngridded Shot + camera facts ──> Technician\ngridded Shot                ──> Composer\nclean Shot                  ──> Storyteller\n                                  │\n                                  ▼\n                    structured readings + measured facts\n                                  │\n                                  ▼\n                   Synthesizer, with no image input\n```\n\nThe readers do not all see the same prompt and image. The Technician gets EXIF and\n\nthe gridded Shot. The Composer gets the gridded Shot for composition and light. The\n\nStoryteller gets the clean Shot. The Synthesizer gets their structured readings and\n\nthe measurements, but no image.\n\nEach reader returns a Pydantic schema, not an unbounded paragraph. Before a model\n\nclaim reaches storage, code drops unknown Technique IDs, invalid cell references,\n\nand unsupported geometry. `domain/panel.py`\n\nthen requires agreement between\n\nreaders, or a higher-confidence read from the Technique's accountable reader.\n\nMeasured facts can veto a conflicting visual claim.\n\nFor example, a model can say where it sees a motion technique, but phase-correlation\n\ncode can rule out a locked-off camera. A model can locate a region with grid cells,\n\nbut it never emits pixels or a hand-drawn overlay.\n\n*ADK coordinates the small panel inside the Analyst. Pub/Sub coordinates the durable work outside it.*\n\nAgent stages do not call the next remote stage directly. They publish a small,\n\nID-only message, then the next handler re-reads the Shot and its current state.\n\n``` php\nsource accepted\n  -> create Shot + durable Run\n  -> media.new\n     -> Ingest\n  -> media.ingested\n     -> Analyst\n  -> media.analyzed\n     -> Cartographer\n     -> Judge\n  -> media.judged\n     -> Scribe\n```\n\nThe actual message is intentionally boring:\n\n```\nawait ctx.bus.publish(TOPICS[\"media.analyzed\"], {\"shot_id\": shot.id})\n```\n\nThat one choice avoids a lot of accidental complexity. Pub/Sub never carries a\n\nstale Analysis object or an agent's prose. A handler loads the authoritative\n\nrecords it needs, writes one idempotent outcome, and can safely see the same event\n\nagain.\n\n`media.analyzed`\n\nfans out to Cartographer and Judge. They get separate push\n\nsubscriptions, retry policies, and dead-letter paths even though they start from\n\nthe same event. Judge always emits `media.judged`\n\n, including when there is no\n\nExperiment Verdict, so Scribe can still write the reviewed output.\n\nLocal development uses an `InProcessBus`\n\n. Production uses Pub/Sub push requests to\n\n`/pubsub/<stage>`\n\non Cloud Run. The handlers are registered the same way in both\n\nmodes. Transport changes, but the stage code does not.\n\nEvery accepted Shot gets a durable `Run`\n\nbefore the first event is published. A Run\n\nhas a separate outcome for Ingest, Analyst, Cartographer, Judge, Scout, and Scribe.\n\nThe outcomes can be complete, skipped, retrying, or terminal.\n\n`ActivityEvent`\n\nrecords explain what happened. `Run`\n\nstate decides whether the\n\nwork is finished. This prevents a misleading result such as \"review complete\"\n\nbecause Scribe happened to finish while Cartographer was still retrying.\n\nOnce every member Run of a natural camera period has settled, Shoots creates a\n\nShoot Record. Only then does the Shoot-level Scout choose a typed route: explain\n\nsupported work, ask one consequential question, offer an evidence-backed\n\nExperiment, or stay silent. The model writes within a route that code already\n\nselected. It does not get to invent a new task for the photographer.\n\n```\nmany Shot Runs settle\n        │\n        ▼\nShoot barrier\n        │\n        ▼\nShoot synthesis -> typed Scout decision -> Shoot Record\n```\n\nThis is also why a Shoot is not just a time window. It is a revisioned record with\n\nexact member Shots. A later Camera Shot can create a newer revision without\n\nrewriting the earlier record.\n\nAt-least-once delivery is normal with Pub/Sub. I treated it as a design condition,\n\nnot an error case.\n\n`ANALYSING`\n\nlease, so a duplicate delivery does not buy another panel run.\nA genuinely abandoned lease can later be reclaimed.`StageGate`\n\nlimits expensive Analyst panels on one Cloud Run\ninstance. Pub/Sub owns the durable backlog, so the gate is not a fake global queue.The result is a repair path I can audit. A replay records which stage was retried\n\nand when. It does not pretend the failure never happened.\n\nI use Gemini 3.7 Flash for bounded visual interpretation and writing. Deterministic\n\ncode owns evidence rules, state changes, and anything that sounds like a fact.\n\nThe boundary is easiest to see in three places:\n\n*The cyan marks are measured contrast edges in the model-located area. They support a narrow claim about visible edge structure, not a verdict on the composition.*\n\nThis is why Shoots does not store an overall aesthetic score. It stores Evidence,\n\nFindings, a labelled visual reading, and the exact model and prompt version that\n\nproduced that reading. A Reproduce Verdict is reserved for Criteria that were\n\ndeclared before the photographer tried again. Explore and Compare do not get a\n\npass/fail label.\n\nFirestore holds the Photographer record, Shots, Analyses, Runs, ActivityEvents,\n\nTechnique Map projections, Experiments, and Shoot Records. Cloud Storage holds\n\noriginal files and display assets. Google Drive is an optional import and reviewed\n\noutput adapter.\n\nScribe can prepare a Deconstruction draft from stored Evidence. It cannot treat the\n\ncurrent image as permission to invent a new claim. The writer gets supported\n\nEvidence IDs; domain code validates those references and the photographer reviews\n\nthe result before sharing or downloading it.\n\n*The Deconstruction is a draft built from already-stored Evidence. It never posts itself.*\n\nI ran a recorded production Drive-import workflow with 75 test files in three\n\nbatches. All 75 completed. Shoots wrote 75 reviewed copies to Google Drive and\n\nproduced three settled Shoot Records. Five failed Shots recovered through six\n\nautomatic repair replays.\n\nThe median backend Run took 48.72 seconds. The complete session took 40 minutes and\n\n7 seconds from the first import request to the final Shoot Record. Because the\n\nbatches overlapped, multiplying the median by 75 would be misleading.\n\nI also ran a separate five-Shot metered sample with real Gemini calls. The model-only\n\nestimate was about $0.039 per Shot, or $2.95 projected to 75. That is not the\n\nhistorical bill. It excludes cloud infrastructure, Drive transfer, discounts,\n\ncredits, optional Experiments, and visual-story generation.\n\nThe 75 files included repeats and deterministic variations of real hobbyist Shots.\n\nThat proves the workflow, repair path, and delivery. It does not prove that Shoots\n\nmakes someone a better photographer. That needs people using it over time and\n\ntelling me whether the Experiment was useful.\n\nThe source is open at [github.com/fikri2992/shoots](https://github.com/fikri2992/shoots). The project entry is [Shoots on Devpost](https://devpost.com/software/shoots).", "url": "https://wpnews.pro/news/building-shoots-a-hybrid-event-driven-agent-system-for-photography", "canonical_source": "https://dev.to/fikuri/building-shoots-a-hybrid-event-driven-agent-system-for-photography-3a58", "published_at": "2026-08-31 09:23:06+00:00", "updated_at": "2026-08-31 09:51:56.562782+00:00", "lang": "en", "topics": ["ai-agents", "developer-tools", "machine-learning", "artificial-intelligence"], "entities": ["Shoots", "All Things Agentic Hackathon", "ADK", "FastAPI", "Google Cloud"], "alternates": {"html": "https://wpnews.pro/news/building-shoots-a-hybrid-event-driven-agent-system-for-photography", "markdown": "https://wpnews.pro/news/building-shoots-a-hybrid-event-driven-agent-system-for-photography.md", "text": "https://wpnews.pro/news/building-shoots-a-hybrid-event-driven-agent-system-for-photography.txt", "jsonld": "https://wpnews.pro/news/building-shoots-a-hybrid-event-driven-agent-system-for-photography.jsonld"}}