Skip to content

symfonic.capabilities.memory.prospective_trigger

prospective_trigger

Opt-in turn-time evaluation of stored prospective triggers (CORE-97 candidate).

Legacy stores condition and task on a prospective node and never evaluates the condition on a turn; the native memory capability recalls the node as generic text. This capability is the smallest composition that does evaluate one, and it decides almost nothing on the deployment's behalf:

  • What a condition means is the caller's. evaluator is a required synchronous (condition, cue) -> bool; no default matcher is shipped, because a substring, token-overlap or model-judged reading would each be a product decision this repository has not made.
  • Selection is not acknowledgement. By contract, a selected trigger stays pending and can be selected again on every later matching turn until the deployment explicitly resolves it. "Selected" means the stage offered its task to prompt assembly; it does not mean the prompt budget admitted it or that the provider received it. This capability does not expire or rate-limit triggers.
  • Only the task is offered, and only when satisfied. An unsatisfied, erroring, non-boolean or awaitable evaluation, or a trigger with no task, contributes nothing.
  • Generic recall is the caller's choice. Legacy ProspectiveLayer.retrieve answers [], but the default native RetrievalPolicy searches every layer, so the same reminder is also recalled as bare [prospective] content whatever its condition says. :func:recall_policy_for opts a composition out of that by dropping the layer from generic recall when a trigger capability is composed. Nothing applies it by default, and a custom activation is not bounded by it.

Not wired into memory_capabilities or any default: a deployment composes it beside PromptingCapability with :func:prospective_trigger_capabilities.

GraphPendingTriggers

GraphPendingTriggers(graph: Any)

Pending triggers from the graph ProspectiveLayer writes to.

The same store ProspectiveCreateService and the legacy set_trigger write, so a trigger stored by either is evaluable here. The tenant filter is repeated on the way out: isolation is the adapter's obligation, and the consumer is the wrong place to discover a backend that forgot it. Rows an unpublished write or a retraction hides from every other reader are hidden here too; ProspectiveLayer.get_pending drops those markers, so the nodes are read raw.

Source code in src/symfonic/capabilities/memory/prospective_trigger.py
def __init__(self, graph: Any) -> None:
    from symfonic.memory.graph.store import GraphMemoryStore

    if graph is None:
        raise ValueError("pending triggers need a graph backend")
    self._store = GraphMemoryStore(graph)

PendingTrigger dataclass

PendingTrigger(trigger_id: str, condition: str, task: str)

One unresolved trigger, as a source reports it.

PendingTriggerSource

Bases: Protocol

Reads the unresolved triggers visible to one scope.

ProspectiveTriggerCapability dataclass

ProspectiveTriggerCapability(source: PendingTriggerSource, evaluator: ConditionEvaluator, scope: MemoryScope, sink: TriggerSelectionSink | None = None, cue_from: Callable[[Any], str] = lambda request: str(getattr(request, 'prompt', '') or ''))

Evaluate stored conditions against the turn; offer satisfied tasks.

TriggerSelectionSink

Bases: Protocol

Observes a selection. The capability itself writes nothing.

Called after this stage builds a prompt contribution, before downstream prompt assembly. The prompt budget may still drop it: selection is not evidence of provider delivery and does not resolve the trigger.

prospective_trigger_capabilities

prospective_trigger_capabilities(source: PendingTriggerSource, evaluator: ConditionEvaluator, scope: MemoryScope, *, sink: TriggerSelectionSink | None = None) -> list[Any]

The capability and the grant it needs, as one list to compose.

Source code in src/symfonic/capabilities/memory/prospective_trigger.py
def prospective_trigger_capabilities(
    source: PendingTriggerSource,
    evaluator: ConditionEvaluator,
    scope: MemoryScope,
    *,
    sink: TriggerSelectionSink | None = None,
) -> list[Any]:
    """The capability and the grant it needs, as one list to compose."""
    from symfonic.kernel.contracts.effects import GrantEffects

    return [
        GrantEffects("memory-read"),
        ProspectiveTriggerCapability(source, evaluator, scope, sink),
    ]

recall_policy_for

recall_policy_for(capabilities: Iterable[object], policy: RetrievalPolicy | None = None) -> RetrievalPolicy | None

Opt-in generic recall policy for a composition that includes capabilities.

Drops the prospective layer when a :class:ProspectiveTriggerCapability is composed, so the trigger stage alone decides what of that layer reaches the prompt. Without one, policy is returned as is.

Source code in src/symfonic/capabilities/memory/prospective_trigger.py
def recall_policy_for(
    capabilities: Iterable[object], policy: RetrievalPolicy | None = None
) -> RetrievalPolicy | None:
    """Opt-in generic recall policy for a composition that includes ``capabilities``.

    Drops the prospective layer when a :class:`ProspectiveTriggerCapability` is
    composed, so the trigger stage alone decides what of that layer reaches the
    prompt. Without one, ``policy`` is returned as is.
    """
    if any(isinstance(item, ProspectiveTriggerCapability) for item in capabilities):
        return without_prospective_recall(policy)
    return policy

without_prospective_recall

without_prospective_recall(policy: RetrievalPolicy | None = None) -> RetrievalPolicy

policy (default: RetrievalPolicy()) with the prospective layer disabled.

Source code in src/symfonic/capabilities/memory/prospective_trigger.py
def without_prospective_recall(policy: RetrievalPolicy | None = None) -> RetrievalPolicy:
    """``policy`` (default: ``RetrievalPolicy()``) with the prospective layer disabled."""
    from symfonic.capabilities.memory.coordinator import RetrievalPolicy

    policy = RetrievalPolicy() if policy is None else policy
    layers = policy.enabled_layers - {MemoryLayer.PROSPECTIVE}
    if not layers:
        raise MemoryContractError(
            "the recall policy enables only the prospective layer; a trigger capability "
            "owns that layer, so generic recall would retrieve nothing"
        )
    return replace(policy, enabled_layers=layers)