Sentinels¶
How a Step says where its input comes from. Without sentinels you'd
thread arguments manually through every step; with them, the data
flow is one declaration per step. All sentinel references are
validated at Plan construction time — typos become PlanCompileError
before any LLM call.
Signature¶
from lazybridge import (
from_prev, # singleton: previous step's output (default)
from_start, # singleton: original user task
from_step, # callable: from_step("name") — named prior step
from_parallel, # callable: from_parallel("name") — alias for from_step
from_parallel_all, # callable: from_parallel_all("name") — aggregate a parallel band
from_memory, # callable: from_memory("name") — agent's live conversation history
from_agent, # callable: from_agent("name") — agent's last output from Store
)
# Valid placements on a Step:
Step(target, task=<sentinel>)
Step(target, context=<sentinel>)
Step(target, context=[<sentinel>, <sentinel>, "literal string"])
The seven sentinels¶
| Sentinel | Reads | Resolved | Compile-time validation |
|---|---|---|---|
from_prev |
The previous step's output | Plan execution history | — (always available) |
from_start |
The Plan's original input | Plan execution history | — |
from_step("n") |
A named prior step's output | Plan execution history | "n" must name an earlier step |
from_parallel("n") |
Same as from_step("n") — different name signals intent |
Plan execution history | "n" must name an earlier step (validated identically to from_step). |
from_parallel_all("n") |
Every consecutive parallel sibling starting at "n", joined as labelled text |
Plan execution history | "n" must exist, come earlier, be parallel=True, AND be the FIRST member of its band (the step immediately before it must be non-parallel) |
from_memory("n") |
The live Memory of the agent registered under "n" |
Step execution time (live) | The named tool must be an agent with memory= attached |
from_agent("n") |
The last output of agent "n" from a shared Store |
Step execution time (live) | The tool must be an agent (returns_envelope=True) AND the source agent must have store= attached |
Synopsis¶
Sentinels split into two categories:
Plan-only — resolve against the Plan's execution history at
step dispatch time. They cannot reach outside the current Plan.run:
from_prev— the workhorse. The default fortask=. Each step reads the one before it.from_start— the original user task. Use it when a step needs the input regardless of intermediate processing (verification, re-routing, fresh framing).from_step("name")— name-keyed access to any earlier step's output. The compiler validates the name; a typo fails at construction.from_parallel("name")— alias forfrom_stepthat reads better at the call site when the referenced step ran concurrently with siblings.from_parallel_all("name")— aggregator. Folds every consecutiveparallel=Truestep starting atnameinto one envelope whose payload is a labelled-text join ("[step_a]\n<text>\n\n[step_b]\n<text>"). See Parallel plan steps (Phase 3b) for the full mechanics.
Universal — resolve at step execution time and work both
inside a Plan and standalone:
from_memory("name")— reads the liveMemoryof the agent registered undername. Always reflects the most recent conversation history; absent or empty memory contributes nothing (silent no-op).from_agent("name")— reads the last output of agentnamefrom a sharedStore. Every successful agent run writes to__agent_output__:{alias};from_agentreads it back. Works across runs and outside the currentPlan.
The store key is always the alias passed to as_tool("alias"),
not the agent's internal name=.
When to use which¶
- Inside the same Plan, prefer
from_step("name")overfrom_agent("name").from_stepreads from in-memory step history (no Store required), is validated more tightly at compile time, and is cheaper. - Use
from_agent("name")only when you need the agent's output independent of the current Plan's history: across Plan runs, in a standalone LLM orchestrator with no step history, or in a step that needs the output of an agent invoked outside this Plan. - Use
from_memory("name")for conversation continuity — when a downstream agent should see what an upstream agent has been talking about (memory), not just its last output (Store). - Use
from_startwhen the original user task is the right prompt for a step that's deep in the pipeline (verifier, apology branch, fresh-framing summariser).
Example¶
from pydantic import BaseModel
from lazybridge import (
Agent,
LLMEngine,
Memory,
Plan,
Step,
Store,
from_agent,
from_memory,
from_prev,
from_start,
from_step,
)
store = Store(db="pipeline.sqlite")
mem = Memory(strategy="summary")
# Researcher with memory + store — feeds both `from_memory` and
# `from_agent` references downstream.
researcher = Agent(
engine=LLMEngine("gemini-3-flash-preview"),
memory=mem,
store=store,
name="research",
)
fact_checker = Agent(
engine=LLMEngine("gemini-3-flash-preview"),
store=store,
name="fact_check",
)
writer = Agent(
engine=LLMEngine("gpt-5.4-mini"),
name="write",
)
# 1) Mixed sentinels in a single Plan.
plan = Agent(
engine=Plan(
Step("research"),
# fact_checker sees the researcher's output as task,
# the original user task as context.
Step("fact_check",
task=from_prev,
context=from_start),
# writer sees the researcher's live memory PLUS the fact_checker output.
Step("write",
context=[from_memory("research"), from_step("fact_check")]),
),
tools=[researcher, fact_checker, writer],
store=store,
)
plan("AI trends April 2026")
# 2) Multi-source synthesis via context=[...] — no combiner step needed.
class Brief(BaseModel):
title: str
body: str
policy_loader = Agent(
engine=LLMEngine("gemini-3-flash-preview"),
name="policy",
)
synthesiser = Agent(
engine=LLMEngine("gemini-3-flash-preview"),
name="synth",
output=Brief,
)
plan2 = Agent(
engine=Plan(
Step("research"),
Step("policy"),
Step("synth",
task="Draft a brief citing both sources.",
context=[
from_step("research"),
from_step("policy"),
"Style: neutral, third person, no superlatives.",
]),
),
tools=[researcher, policy_loader, synthesiser],
)
# 3) from_agent across runs — read what the researcher produced
# in a previous Plan execution.
standalone = Agent(
engine=LLMEngine("gemini-3-flash-preview"),
tools=[researcher],
store=store,
)
standalone("find AI trends")
# Later, in a different Plan / process, with the same store:
later_plan = Agent(
engine=Plan(
Step("write",
task="Write a follow-up brief based on prior research.",
context=from_agent("research")),
),
tools=[writer],
store=store,
)
Pitfalls¶
from_prevafter a parallel band returns the join step's output, not one of the branches. Usefrom_parallel("<branch-name>")for a specific branch orfrom_parallel_all("<first-branch-name>")for the aggregate.- Sentinels are module-level imports. Don't shadow them with
local variables of the same name (
from_prev = "literal"is a bug waiting to happen). - A
strpassed astask=is a LITERAL, not a sentinel reference.task="from_prev"sets the step's task to the string"from_prev". Use the importedfrom_prevsymbol. from_memory("n")is a silent no-op when the agent hasn't run. Empty memory contributes nothing, no error. If you want fail-fast behaviour, validate the memory yourself before the Plan dispatches the step.from_agent("n")requiresstore=on the source agent. PlanCompiler rejects it at construction time when the named agent has no store attached. The error message names the offending agent — passstore=...to that agent, not just to the Plan.from_agent("n")requires the tool to be an agent (returns_envelope=True), not a plain function. PlanCompiler rejects plain-function targets at construction time.- The Store key is the alias, not the agent's
name=. When the agent is wrapped viaagent.as_tool("alias"), the auto-write key is__agent_output__:alias.from_agent("alias")reads it back. Mixing these up is one of the most common sources of confusion — keep aliases stable across runs that share a Store. from_parallel_all("n")requiresnto be the FIRST step of its parallel band. Mid-band references fail at construction with a clear error.
See also¶
- Plan — the engine that interprets sentinels.
- Step — the surface that consumes sentinels via
task=andcontext=. - Routing — sentinels are about data flow; routing is about control flow. They don't overlap.
- Store — the backing store for
from_agentand the receiver ofStep(writes=...). - Memory — the conversation-history layer
that
from_memoryreads live. - Guides → Full → Parallel plan steps (Phase 3b) —
from_parallel_allaggregation in full.