Problem
Agent templates define multi-step workflows in their system prompts, but models routinely skip steps — especially reflection, quality checks, and state-update loops that happen after the main task is done.
Concrete example: the content-writer template.
The template (registry/agents/content-writer/agent.md) instructs the model to:
- Write or improve a piece based on the user's request
- After writing, reflect: does it match the user's voice in
voice.md?
- After reflecting, update
voice.md with any new patterns learned
- Save accepted pieces to
writing/ for future reference
In practice, the model does step 1 and stops. Steps 2-4 — the "evolving voice" loop that makes the agent get better over time — almost never fires. This isn't a prompting problem. It's an architectural one: we're asking a single inference pass to be both executor and orchestrator.
Every other serious software system separates execution from orchestration (CI pipelines, database triggers, git hooks). Agent runtimes are the exception — they dump instructions into a system prompt and hope for the best.
Proposal: Post-Turn Triggers
Add a declarative triggers field to oc-agent.yaml that lets template authors define conditions and follow-up prompts that the runtime enforces — not the model.
Example configuration
# in oc-agent.yaml for content-writer
triggers:
- name: voice-review
when:
file_written: "writing/*.md"
prompt: |
You just saved a new piece to writing/. Before moving on:
1. Re-read voice.md
2. Does the piece match the user's voice? Be specific about what matches and what doesn't.
3. If it doesn't match, revise the piece now.
4. Update voice.md if you learned any new patterns about how the user writes.
How it works
Intervention point: internal/runtime/native_runner.go, lines 661-662 — the moment the model decides it's "done" (returns no tool calls):
// CURRENT behavior:
if len(msg.ToolCalls) == 0 {
return nil // trust the model's decision to stop
}
// NEW behavior:
if len(msg.ToolCalls) == 0 {
if trigger := evaluateTriggers(state, toolCtx.Config.Triggers, firedTriggers); trigger != nil {
firedTriggers[trigger.Name] = true
triggerMsg := Message{Role: "user", Content: trigger.Prompt}
state.Messages = append(state.Messages, triggerMsg)
state.Transcript = append(state.Transcript, triggerMsg)
continue // re-enter the agent loop
}
return nil
}
Key detail: firedTriggers is a map[string]bool scoped to the current runNativePrompt call. Each trigger fires at most once per user prompt, preventing infinite loops (trigger fires → model writes again → trigger fires again).
Trigger conditions
The WorkingSet (in internal/runtime/context_view.go) already tracks everything we need:
| Condition |
Matches against |
Example |
file_written: "pattern" |
WorkingSet.FilesWritten |
"writing/*.md" |
file_edited: "pattern" |
WorkingSet.FilesEdited |
"voice.md" |
tool_used: "ToolName" |
Tool call history in turn |
"Bash" |
Patterns use the same glob matching we already use for context syncing.
Important: conditions are evaluated against changes since the last trigger evaluation, not the cumulative working set. This means a trigger only fires when a new matching file appears during the current turn, not because a file was written 10 turns ago.
What changes
| File |
Change |
internal/agent/agent.go |
Add Triggers []TriggerConfig to AgentConfig struct |
internal/runtime/session_config.go |
Add Triggers []TurnTrigger to SessionConfig, wire from agent config |
internal/runtime/native_runner.go |
Add trigger evaluation at turn boundary (~20 lines in runNativeLoop) |
internal/runtime/triggers.go |
New file: TurnTrigger struct, evaluateTriggers() function, glob matching against WorkingSet |
registry/agents/content-writer/oc-agent.yaml |
Add triggers: block as first consumer |
Estimated scope: ~150 lines of Go + tests.
Walk-through: content-writer with triggers
Without triggers (current):
User: "Turn this into a LinkedIn post: [rough draft]"
→ Model writes the post, saves to writing/2026-03-29-linkedin-ai-tooling.md
→ Model stops. voice.md never updated. No voice review happened.
→ Next session: voice.md is still the generic bootstrap version.
With triggers:
User: "Turn this into a LinkedIn post: [rough draft]"
→ Model writes the post, saves to writing/2026-03-29-linkedin-ai-tooling.md
→ Model stops (no more tool calls)
→ Runtime checks triggers: file_written "writing/*.md" matches!
→ Runtime injects: "You just saved a new piece. Review against voice.md..."
→ Model re-reads voice.md, reflects, revises the piece, updates voice.md
→ Model stops again. Trigger already fired (in firedTriggers set).
→ Runtime returns. Turn complete.
→ Next session: voice.md has been refined. Agent writes better.
Future evolution
This is the foundation for more advanced patterns:
- Chained triggers: trigger B fires only after trigger A completes (e.g., voice-evolve fires after voice-review)
- Obligation checking: verify the model actually did the work (e.g., check
WorkingSet.FilesRead includes voice.md after the trigger prompt) using heuristics or a lightweight small_model call
- Tool-level hooks: fire triggers immediately after specific tool calls, not just at turn boundaries — useful for inline feedback
- Trigger metrics: track how often triggers fire vs. how often the model would have skipped the step — measures model reliability and template quality
Why this matters
No existing agent runtime does this. Claude Code, Cursor, Windsurf, Devin — they all treat the model as a monolithic executor where the prompt is the only control mechanism. This is the difference between:
- Prompt engineering: "Please remember to always review your work"
- Runtime enforcement: The runtime won't let you finish until the review runs
This positions toc-native as the runtime that guarantees agent workflow quality, not just agent execution. Template authors can encode quality standards that the runtime enforces, regardless of model discipline.
Problem
Agent templates define multi-step workflows in their system prompts, but models routinely skip steps — especially reflection, quality checks, and state-update loops that happen after the main task is done.
Concrete example: the
content-writertemplate.The template (
registry/agents/content-writer/agent.md) instructs the model to:voice.md?voice.mdwith any new patterns learnedwriting/for future referenceIn practice, the model does step 1 and stops. Steps 2-4 — the "evolving voice" loop that makes the agent get better over time — almost never fires. This isn't a prompting problem. It's an architectural one: we're asking a single inference pass to be both executor and orchestrator.
Every other serious software system separates execution from orchestration (CI pipelines, database triggers, git hooks). Agent runtimes are the exception — they dump instructions into a system prompt and hope for the best.
Proposal: Post-Turn Triggers
Add a declarative
triggersfield tooc-agent.yamlthat lets template authors define conditions and follow-up prompts that the runtime enforces — not the model.Example configuration
How it works
Intervention point:
internal/runtime/native_runner.go, lines 661-662 — the moment the model decides it's "done" (returns no tool calls):Key detail:
firedTriggersis amap[string]boolscoped to the currentrunNativePromptcall. Each trigger fires at most once per user prompt, preventing infinite loops (trigger fires → model writes again → trigger fires again).Trigger conditions
The
WorkingSet(ininternal/runtime/context_view.go) already tracks everything we need:file_written: "pattern"WorkingSet.FilesWritten"writing/*.md"file_edited: "pattern"WorkingSet.FilesEdited"voice.md"tool_used: "ToolName""Bash"Patterns use the same glob matching we already use for
contextsyncing.Important: conditions are evaluated against changes since the last trigger evaluation, not the cumulative working set. This means a trigger only fires when a new matching file appears during the current turn, not because a file was written 10 turns ago.
What changes
internal/agent/agent.goTriggers []TriggerConfigtoAgentConfigstructinternal/runtime/session_config.goTriggers []TurnTriggertoSessionConfig, wire from agent configinternal/runtime/native_runner.gorunNativeLoop)internal/runtime/triggers.goTurnTriggerstruct,evaluateTriggers()function, glob matching againstWorkingSetregistry/agents/content-writer/oc-agent.yamltriggers:block as first consumerEstimated scope: ~150 lines of Go + tests.
Walk-through: content-writer with triggers
Without triggers (current):
With triggers:
Future evolution
This is the foundation for more advanced patterns:
WorkingSet.FilesReadincludesvoice.mdafter the trigger prompt) using heuristics or a lightweightsmall_modelcallWhy this matters
No existing agent runtime does this. Claude Code, Cursor, Windsurf, Devin — they all treat the model as a monolithic executor where the prompt is the only control mechanism. This is the difference between:
This positions toc-native as the runtime that guarantees agent workflow quality, not just agent execution. Template authors can encode quality standards that the runtime enforces, regardless of model discipline.