Skip to content

Evolving Mode Support: Auditing Capabilities #1756

Description

@Stuckya

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.

  1. 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.
  2. 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.
  3. 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.

  1. Episodes from internal action-produced facts.
  2. External fact ingress.
  3. Observability.
  4. Cooperative interruption.
  5. Open Evolving, ObjectiveAuthor re-authoring.
  6. Process-local scope expansion.
  7. Native long-running pure GOAP. Recurring goal episodes.
  8. An example application.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions