Skip to content

feat(kernel,runtime): async task tracker — non-blocking workflow/delegation results via kernel event injection #4983

Description

@DaBlitzStein

Summary

Agents need a way to launch async operations (workflows, agent_send delegations, external calls) and receive results without blocking their conversation loop or requiring a separate orchestrator agent burning tokens on continuous polling.

Motivation: real-world failure on production Rodela instance

Agent ltdata (Telegram assistant) launched a workflow via workflow_run. The workflow took several minutes. Result:

  1. workflow_run blocked ltdata's agent loop for the entire duration
  2. The user sent follow-up messages during this time — ltdata was unresponsive
  3. The workflow eventually timed out
  4. ltdata received no run_id, no partial status, no error context — just a timeout
  5. The agent was effectively "bricked" for the duration, unable to serve any user

The alternative workflow_start (async) returns a run_id immediately, but ltdata has no mechanism to check back on it. The run_id is returned to the LLM as a tool result, but by the next user message the agent has moved on and forgotten about it.

The current workaround is the orchestrator agent pattern: a separate agent on continuous mode (every 120s) that polls for pending work. This costs ~$2/day in LLM calls even when idle, and introduces coordination complexity between the orchestrator and the actual agents.

Problem statement

There are three async patterns in LibreFang, all with the same gap:

Pattern Launch Result delivery Problem
workflow_run Blocking tool call Returns when done Agent frozen, user ignored, timeout kills it
workflow_start Returns run_id Nothing — fire and forget Agent forgets, user never gets result
agent_send Sends to peer agent Peer response in sender's session Blocking — sender waits for full response

None of these allow: "launch → continue serving user → get notified when done → act on result."

Proposed: kernel-level async task tracker

Architecture

Agent launches async op → kernel registers (agent_id, task_type, task_id)
  → agent continues normally (responds to user, handles new messages)
  → async op completes → kernel injects result into agent's session as synthetic message
  → agent processes result on next turn (or wakes up if idle)

Kernel task registry

struct PendingTask {
    agent_id: AgentId,
    session_id: SessionId,
    task_type: TaskType,        // Workflow, AgentSend, Webhook, etc.
    task_id: String,            // run_id, message_id, etc.
    launched_at: Instant,
    timeout: Option<Duration>,
    callback_channel: Option<String>,  // channel to notify user through
}

enum TaskType {
    WorkflowRun { workflow_id: String },
    AgentDelegation { target_agent: AgentId },
    ExternalWebhook { url: String },
}

Event injection

When the async op completes, the kernel injects a synthetic message into the agent's session:

{
  "role": "system",
  "content": "[ASYNC_RESULT] workflow 'publish_article' (run abc-123) completed.\nOutput: ...\nArtifacts: [image_url, ...]",
  "metadata": {"task_type": "workflow", "task_id": "abc-123", "status": "completed"}
}

If the agent is idle (no active turn), this triggers a new turn — the agent wakes up, processes the result, and notifies the user through whatever channel the conversation is on.

If the agent is mid-turn, the result queues and is processed after the current turn completes (same as mid-turn message injection, #956).

New tool: async_run

Replace the blocking workflow_run with a unified async launcher:

workflow_start(workflow_id, input)  → returns run_id (existing, unchanged)
                                    → kernel auto-tracks, will inject result

The difference from today: workflow_start already exists, but the kernel doesn't track the pending task or inject the result. The change is in the kernel, not the tool.

Timeout handling

# agent.toml
[async_tasks]
default_timeout_secs = 300
notify_on_timeout = true

On timeout: inject a timeout event into the session so the agent can inform the user ("the workflow is taking longer than expected, I'll keep checking").

Before

  • workflow_run: agent blocks, user ignored, timeout = dead end
  • workflow_start: fire and forget, user never gets result
  • Workaround: $2/day orchestrator agent polling every 120s

After

  • workflow_start: returns immediately, agent continues chatting
  • Workflow completes → kernel injects result → agent notifies user
  • No orchestrator needed, no polling, no token waste
  • Works for workflows, agent_send, and any future async pattern

Files

  • crates/librefang-kernel/src/kernel/messaging.rs — event injection on async completion
  • crates/librefang-kernel/src/workflow.rs — emit completion event to task registry
  • crates/librefang-runtime/src/agent_loop.rs — process injected async results
  • crates/librefang-kernel/src/kernel/mod.rs — PendingTask registry

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/kernelCore kernel (scheduling, RBAC, workflows)area/runtimeAgent loop, LLM drivers, WASM sandboxenhancementNew feature or requesthas-prA pull request has been linked to this issue

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions