Intro
Embabel Agent already replans as actions add typed objects to the blackboard, and existing goals can become achievable during a running process. What's missing is an explicit goal lifecycle that lets a process evolve. These goals should complete without ending the process and become eligible again for a later occurrence. This matters most in long-running, event-driven processes.
I've coined 2 terms:
- Deterministic Evolving. Deterministic evolving is when a runtime fact drives a known goal to completion without ending the process, and a later fact can run it again. The fact-to-goal connection is typed and known ahead of time.
- Open Evolving. Open evolving aims to solve the case where not every fact-to-goal connection can be known and wired ahead of time. Instead of failing, open evolving attempts to solve the problem on-the-fly. Here, an
ObjectiveAuthor proposes a revised policy.
My read on Evolving Mode is that declared agent metadata should remain immutable, and we shouldn't introduce a new PlannerType. We should reuse as much of the machinery that already exists within Embabel today.
Motivating Example
A concrete event-driven process might have a very long-running objective:
As a robot, collect samples in Zone A until 500 samples are stored.
The process runs over scoped capabilities / agents:
- sample collection
- robot navigation
- sample storage
- hazard response
- sensor calibration
The traditional Embabel pieces:
- declared actions handle collection, navigation, and storage
- declared goals are satisfied by normal action outputs such as
SamplesStored
- conditions control availability and action selection
For example, sampleTrayFull is ongoing state. It stays true while the robot's sample tray is full and becomes false after the samples are stored. It should be modeled with @Condition and action preconditions, not as a runtime goal:
@Condition(name = "sampleTrayFull")
boolean sampleTrayFull(SampleTraySnapshot tray) {
return tray.count() >= tray.capacity();
}
@Action(pre = "sampleTrayFull")
AtStorage navigateToStorage(CurrentLocation current, StorageLocation storage) {
// navigate to the storage area
}
In this example, sampleTrayFull only gates whether storage/navigation actions are available. Evolving Mode is not involved merely because the tray is full.
Deterministic Evolving takes over when a fact should run a goal episode:
SensorCalibrationRequested
UrgentHazardDetected
Other facts can remain ordinary blackboard facts that completion policies, conditions, or actions read:
SampleCollected
SamplesStored
This is the sketched API:
var process = EvolvingInvocation.on(agentPlatform)
.withScope(AgentScopeBuilder.fromInstances(
new SampleCollection(),
new Navigation(),
new SampleStorage(),
new Calibration(),
new HazardResponse()))
.withProcessOptions(
ProcessOptions.DEFAULT.withPlannerType(PlannerType.HYBRID))
.withEpisodes(EpisodePolicy
.episode(GoalTarget.output(SensorCalibrationCompleted.class))
.consumeOnCompletion(SensorCalibrationRequested.class)
.episode(GoalTarget.output(HazardHandled.class))
.consumeOnCompletion(UrgentHazardDetected.class)
// Cooperative interruption of the running action,
// backed by the existing AgentProcess.terminateAction().
.terminateCurrentAction())
// A declared condition-gated terminal goal in the scope retains
// existing goal-completes-process behavior at samplesStored >= 500.
.createAgentProcess(new CollectSamples("Zone A", 500));
agentPlatform.start(process);
process.ingress().publish(new SensorCalibrationRequested("cal-123"));
EvolvingInvocation and its fluent chain are proposed syntactical sugar. The scope, planner, conditions, goals, and BB behavior around them exist on main.
Stripping away the syntactical sugar, we could make it even simpler:
// The core: one new field on ProcessOptions, following the withers that already exist
var options = ProcessOptions.DEFAULT
.withPlannerType(PlannerType.HYBRID)
.withEpisodes(
/* same EpisodePolicy as above */
);
// Any existing invocation path now supports episodes. No new invocation type required:
var agent = AgentScopeBuilder.fromInstances(
new SampleCollection(),
new Navigation(),
new SampleStorage(),
new Calibration(),
new HazardResponse())
.createAgentScope()
.createAgent(...);
var process = agentPlatform.createAgentProcessFrom(agent, options, new CollectSamples("Zone A", 500));
agentPlatform.start(process);
process.ingress().publish(new SensorCalibrationRequested("cal-123"));
Everything in this sketch except withEpisodes / EpisodePolicy, GoalTarget, and ingress() exists on main today.
Fact Lifecycle (runtime logic)
The framework recognizes satisfaction at a planning tick, the same point where a process completes today.
On completion of an episode's goal, the triggering fact and the satisfying output are hidden. Nothing is removed, the BB stays append-only. Hiding resets the goal to unsatisfied so the next occurrence plans work.
The lifecycle is per-occurrence. The goal is reusable. Completing an episode never completes the process.
Ordinary declared goals keep today's behavior. A condition-gated terminal goal still stops the process. withCompletionPolicy() can come later as ergonomics on top.
API should follow Embabel's existing patterns.
Episode selection uses existing conditions and plan values. No priority mechanism.
GoalTarget.output(T) matching several goals is one activation with candidate goals, and the first completion consumes it. GoalTarget.named() is the exact reference. A target no scoped goal can produce fails fast.
What's Missing in Infrastructure
The machinery exists. The contracts don't.
- Nonterminal goal completion. Today whether a process survives a completed goal depends on plan-value ordering, and it flips between planners. ==> Solved by episodes (phase 1).
EpisodePolicy marks selected goals as nonterminal.
- Fact lifecycle. Could be hand-rolled today with
hide(), a janitor action, and value tuning. The framework should own it. ==> Solved by episodes (phase 1). consumeOnCompletion hides the triggering fact and satisfying output so a later occurrence plans work.
- Platform-owned wake.
waitFor already parks a process WAITING for a promised fact. The resume is manual today. ==> Solved by ingress (phase 2). Publish enqueues the fact and the platform re-runs the process, reusing the existing Awaitable delivery path. Combined with episodes, this covers event-driven pure GOAP.
Baseline tests documenting current behavior under HYBRID and pure GOAP: https://github.com/Stuckya/embabel-agent/blob/goal-episode-baseline-tests/embabel-agent-api/src/test/kotlin/com/embabel/plan/GoalEpisodeBaselineTest.kt
Ordering of Work
My suggested ordering of the work.
- Episodes from internal action-produced facts.
- External fact ingress.
- Observability.
- Cooperative interruption.
- Open Evolving, ObjectiveAuthor re-authoring.
- Process-local scope expansion.
- Native long-running pure GOAP. Recurring goal episodes.
- An example application.
Intro
Embabel Agent already replans as actions add typed objects to the blackboard, and existing goals can become achievable during a running process. What's missing is an explicit goal lifecycle that lets a process evolve. These goals should complete without ending the process and become eligible again for a later occurrence. This matters most in long-running, event-driven processes.
I've coined 2 terms:
ObjectiveAuthorproposes a revised policy.My read on Evolving Mode is that declared agent metadata should remain immutable, and we shouldn't introduce a new
PlannerType. We should reuse as much of the machinery that already exists within Embabel today.Motivating Example
A concrete event-driven process might have a very long-running objective:
The process runs over scoped capabilities / agents:
The traditional Embabel pieces:
SamplesStoredFor example,
sampleTrayFullis ongoing state. It stays true while the robot's sample tray is full and becomes false after the samples are stored. It should be modeled with@Conditionand action preconditions, not as a runtime goal:In this example,
sampleTrayFullonly gates whether storage/navigation actions are available. Evolving Mode is not involved merely because the tray is full.Deterministic Evolving takes over when a fact should run a goal episode:
SensorCalibrationRequestedUrgentHazardDetectedOther facts can remain ordinary blackboard facts that completion policies, conditions, or actions read:
SampleCollectedSamplesStoredThis is the sketched API:
EvolvingInvocationand its fluent chain are proposed syntactical sugar. The scope, planner, conditions, goals, and BB behavior around them exist on main.Stripping away the syntactical sugar, we could make it even simpler:
Everything in this sketch except
withEpisodes/EpisodePolicy,GoalTarget, andingress()exists on main today.Fact Lifecycle (runtime logic)
The framework recognizes satisfaction at a planning tick, the same point where a process completes today.
On completion of an episode's goal, the triggering fact and the satisfying output are hidden. Nothing is removed, the BB stays append-only. Hiding resets the goal to unsatisfied so the next occurrence plans work.
The lifecycle is per-occurrence. The goal is reusable. Completing an episode never completes the process.
Ordinary declared goals keep today's behavior. A condition-gated terminal goal still stops the process.
withCompletionPolicy()can come later as ergonomics on top.API should follow Embabel's existing patterns.
Episode selection uses existing conditions and plan values. No priority mechanism.
GoalTarget.output(T)matching several goals is one activation with candidate goals, and the first completion consumes it.GoalTarget.named()is the exact reference. A target no scoped goal can produce fails fast.What's Missing in Infrastructure
The machinery exists. The contracts don't.
EpisodePolicymarks selected goals as nonterminal.hide(), a janitor action, and value tuning. The framework should own it. ==> Solved by episodes (phase 1).consumeOnCompletionhides the triggering fact and satisfying output so a later occurrence plans work.waitForalready parks a processWAITINGfor a promised fact. The resume is manual today. ==> Solved by ingress (phase 2). Publish enqueues the fact and the platform re-runs the process, reusing the existing Awaitable delivery path. Combined with episodes, this covers event-driven pure GOAP.Baseline tests documenting current behavior under HYBRID and pure GOAP: https://github.com/Stuckya/embabel-agent/blob/goal-episode-baseline-tests/embabel-agent-api/src/test/kotlin/com/embabel/plan/GoalEpisodeBaselineTest.kt
Ordering of Work
My suggested ordering of the work.