Skip to content

Latest commit

 

History

History
86 lines (70 loc) · 7.48 KB

File metadata and controls

86 lines (70 loc) · 7.48 KB
concept PropelLoop
status stable
tracked_sources
ampere-core/src/commonMain/kotlin/link/socket/ampere/agents/domain/reasoning/**
ampere-core/src/commonMain/kotlin/link/socket/ampere/agents/domain/cognition/sparks/PhaseSpark.kt
ampere-core/src/commonMain/kotlin/link/socket/ampere/agents/domain/cognition/sparks/PhaseSparkManager.kt
ampere-core/src/commonMain/kotlin/link/socket/ampere/trace/ArcRunTrace.kt
docs/AGENT_LIFECYCLE.md
related
CognitiveRelay
MemoryProvenance
SparkSystem
CognitionTrace
EventSerialBus
last_verified 2026-06-04

PROPEL Loop

What it is

PROPEL is AMPERE's autonomous cognitive cycle: Perceive → Recall → Observe → Plan → Execute → Learn. Every animated agent runs this loop continuously. Each phase is a discrete cognitive step that emits structured events, writes typed memory cells, and hands a refined context object to the next phase. The phases collectively form one Arc run (ArcRunId), which is the unit of observability the rest of the system reports against.

The loop is not a hot path through a single function — it is a contract between independent reasoning services (PerceptionEvaluator, PlanGenerator, PlanExecutor, OutcomeEvaluator, KnowledgeExtractor) composed by AgentReasoning. Each service owns one phase's transformation.

Why it exists

The loop is the load-bearing answer to "how does an animated agent decide what to do next?". Three forces shaped it:

  1. Recall before action. LLMs are notoriously bad at remembering what this very system has already tried. PROPEL forces a Recall step before any planning, so prior ExecutionOutcomes and Knowledge entries are surfaced into the prompt, not silently re-discovered.
  2. Observation is a distinct step. The leap from "I have ideas and memories" to "I have a chosen approach" requires reading the current state of the environment, including what changed since the last Arc run. Folding Observe into Plan made the planning prompt too crowded and recalled knowledge went unused against stale state.
  3. Loop closes via Knowledge. Without the explicit closing phase that stores knowledge, the loop is open-loop and the agent never improves. The Learn phase is where the autocatalytic property lives.

The phase boundaries exist because of cognitive load on the LLM, not on the runtime. Each phase has a focused prompt (optionally narrowed further by a PhaseSpark), a clear input/output contract, and a single point at which we emit telemetry.

Where it lives

  • ampere-core/src/commonMain/kotlin/link/socket/ampere/agents/domain/reasoning/AgentReasoning.kt — the facade composing all phase services.
  • agents/domain/reasoning/PerceptionEvaluator.kt — Perceive: distills AgentState into Ideas.
  • agents/domain/reasoning/PlanGenerator.kt — Observe + Plan: combines ideas, recalled Knowledge, and the ticket into a Plan.
  • agents/domain/reasoning/PlanExecutor.kt — Execute: dispatches each Task through ToolExecutionEngine.
  • agents/domain/reasoning/OutcomeEvaluator.kt — first half of Learn: turns raw tool returns into typed ExecutionOutcomes.
  • agents/domain/reasoning/KnowledgeExtractor.kt — second half of Learn: distils outcomes into Knowledge.
  • agents/domain/cognition/sparks/PhaseSpark.kt + PhaseSparkManager.kt — phase-aware prompt augmentation (PERCEIVE | RECALL | OBSERVE | PLAN | EXECUTE | LEARN).
  • trace/ArcRunTrace.ktPropelPhase is the telemetry record per phase.
  • docs/AGENT_LIFECYCLE.md — the human-readable narrative.

Invariants

  • Recall precedes Plan. No Plan may be generated without first calling AgentMemoryService.recallRelevantKnowledge and feeding the result into PlanGenerator. Skipping Recall when context "feels obvious" is the canonical failure mode.
  • Each phase emits its own boundary events. CognitivePhaseEvent.PhaseEntered / PhaseExited mark phase transitions when the phase manager has a bus; ProviderCallStartedEvent / ProviderCallCompletedEvent carry a cognitivePhase; memory writes carry the phase that produced them; tool calls are tagged via the active phase. ArcTraceProjection relies on this to bucket activity per phase. A phase that runs without emitting boundary events is invisible to the trace, which is equivalent to it not having run.
  • The loop closes through Knowledge. Every successful Arc run ends with KnowledgeExtractor writing at least one Knowledge entry tagged with the run_id. An Arc run that produced outcomes but no Knowledge entry has not closed the loop and will not contribute to future Recall.
  • Phase order is fixed. Perceive → Recall → Observe → Plan → Execute → Learn. The declaration order in CognitivePhase matches the PROPEL acronym; enumValues<CognitivePhase>() yields the cycle. New phases are added by extending the enum and updating every service that switches on it; phases are never reordered or skipped per call site.
  • Observe is not a side-effect of Plan. When recalled Knowledge contradicts an Idea or current state has drifted since the last run, that contradiction must be resolved in Observe and recorded in the Plan's rationale, not silently dropped during Plan generation.

Common operations

  • Add a phase — extend CognitivePhase (in PhaseSpark.kt), add a corresponding PhaseSpark data object with prompt contribution, update PhaseSpark.forPhase, add a service implementing the phase transform, wire it through AgentReasoning, and update ArcTraceProjection.phaseNameFor so the trace knows about the new phase.
  • Hook into a phase — prefer CognitivePhaseEvent.PhaseEntered / PhaseExited when an event bus is wired. For older spark-stack consumers, subscribe to SparkAppliedEvent filtering on phaseSparkName(). Do not assume PhaseSparks are always enabled — they're gated on AgentConfiguration.cognitiveConfig.phaseSparks.enabled or AMPERE_PHASE_SPARKS=true.
  • Read what one Arc run didArcTraceProjection.project(runId) returns an ArcRunTrace with one PropelPhase per phase. This is the "playback" of a cognitive cycle. Use this for debugging, not for orchestration.
  • Validate a change to the loop — run ./gradlew jvmTest (the primary gate) and verify the loop still produces a Knowledge entry tagged with the run id.

Anti-patterns

  • Short-circuiting Recall when the context "feels obvious" — the whole point of Recall is to override the agent's confidence with prior outcomes. Plans that look obvious are exactly the ones where past failures live.
  • Treating Learn as bookkeeping — the closing phase is where the system learns. If your change records outcomes but doesn't extract Knowledge, you've made the loop open-loop.
  • Inlining a "quick LLM call" outside a phase — every model invocation should be wrapped in a ProviderCallStartedEvent / ProviderCallCompletedEvent pair tagged with cognitivePhase. Calls outside this contract don't appear in ArcRunTrace and break the glass-brain guarantee.
  • Folding Observe into Plan — recalled Knowledge becomes a passive context dump rather than an explicit selection between alternatives. The Plan emerges with no record of why this approach over the others.
  • Holding state across runs in the agent object — agents are animated, not stateful in memory. Cross-run state belongs in OutcomeMemoryRepository / KnowledgeRepository, keyed by ids retrievable in Recall. Anything else is invisible to the trace and fragile across restarts.