Skip to content

Record what a trigger's verdict actually led to #5408

Description

@yh928

Summary

Every external trigger is classified and, for react / escalate, hands off to a sub-agent — and from that moment nothing is recorded. A progress surface should hold what happened after the verdict: which run picked it up, what it touched, whether it finished, and why it stopped.

Problem

Trace a Gmail webhook through agent::triage today and three things are stored, none of which is progress:

stage what is kept where lifetime
receipt the raw webhook payload trigger_history daily JSONL forever (no pruning)
verdict source, external_id, display_label, decision, used_local, latency_ms DomainEvent::TriggerEvaluated none — the event has no subscriber
after the verdict nothing

So "what came in" is durable, "what we decided" is published to no one, and "what we then did" is not expressed at all. Concretely:

  • A react / escalate dispatch spawns trigger_reactor or orchestrator (triage::escalation::dispatch_target_agent). Whether that run succeeded, what it changed, and how long it took is knowable only by reading logs.
  • TriggerEscalationFailed exists but, like TriggerEvaluated, is published and dropped.
  • A user who asks "did you do anything about that mail?" cannot be answered from state. Neither can the agent itself on its next turn.

Two constraints that shape the design, both learned the hard way in this codebase:

  1. Do not store the input again. The connector sync already ingests the same mail into memory_docs / mem_tree_chunks / vector_chunks. A second copy competes with extracted memories for the same recall slots — that is Internal agents store their own prompts as the user's conversation memories #5312, and fix(memory): stop copying the conversation into memory; let recall answer with memories #5315 is the fix. A progress record must reference the input (metadata_uuid, or the synced document id), never restate it.
  2. Do not hang it on a thread. The existing task board is keyed graph.todos/<hex(thread_id)>, and a webhook has no thread. Today that falls back to a process-global scratch board on InMemoryStore, which does not survive a restart.

Solution (optional)

Sketch, not a decision — the storage question below is the one worth settling first.

A per-trigger record, keyed by the envelope's external_id (already stable and de-duplicated across Composio retries), holding: the verdict and its reason, the dispatch target, the run id, terminal status, and an ordered list of what the run touched. Written by apply_decision for every action, not only the dispatching ones, so the drop / acknowledge verdicts are visible as decisions rather than as absence.

Open questions:

  • Who reads it? If it is the agent's next-turn context, it wants to be recallable and bounded. If it is a human audit surface, it wants to be complete and queryable. These pull the storage in different directions and should not be answered by building both.
  • Where does it live? The tinyagents_store KV under its own namespace is the closest fit to existing patterns and is thread-free if keyed by external_id. trigger_history is already the durable trigger record and could carry a second file per day — but it is an append-only archive with no update path, and progress needs updates.
  • Retention. trigger_history currently has none. A progress record that also never prunes inherits the same problem.

Acceptance criteria

  • A verdict is recorded for every actiondrop / acknowledge / react / escalate all leave a record; none is inferred from silence.
  • Dispatch outcome is recorded — the run id, terminal status, and failure reason for a react / escalate handoff.
  • The input is referenced, not copied — the record carries an id into trigger_history and/or the synced document, and stores no message body.
  • Thread-free and durable — a webhook with no thread context still gets a record that survives a restart.
  • Retention is decided — either a documented cap/prune or an explicit, reasoned decision to keep everything.
  • Diff coverage ≥ 80% — the implementing PR meets the changed-lines coverage gate.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions