Skip to content

feat(prompt): introduce PromptExecutorEvent and HookablePromptExecutor#1970

Open
Amaneusz wants to merge 8 commits intodevelopfrom
amanowicz/develop/prompt-executor-events
Open

feat(prompt): introduce PromptExecutorEvent and HookablePromptExecutor#1970
Amaneusz wants to merge 8 commits intodevelopfrom
amanowicz/develop/prompt-executor-events

Conversation

@Amaneusz
Copy link
Copy Markdown
Collaborator

@Amaneusz Amaneusz commented May 8, 2026


Add PromptExecutorEvents and a hookable executor API

Summary

This PR introduces a lifecycle-event surface on PromptExecutor so callers (and the agent pipeline) can observe what happens inside an executor — not just at the agent level. It adds:

  • A sealed PromptExecutorEvent hierarchy covering all four executor entry points (execute, executeStreaming, executeMultipleChoices, moderate), each with Requested, Dispatched, Completed, Failed (and FrameReceived for streaming) variants.
  • A new HookablePromptExecutor abstract base class. Each call accepts a PromptExecutionContext carrying a stable promptExecutionId and a per-call PromptExecutorHook. Implementations emit events through the hook at the appropriate execution points.
  • Two new pipeline events — LLMCallDispatched and LLMStreamingDispatched — emitted after the executor has resolved the effective model/prompt/tools but before the underlying call goes out. The existing LLMCallStarting / LLMStreamingStarting events still fire at the original location.
  • EventHandler config gains onLLMCallDispatched and onLLMStreamingDispatched (Kotlin and Java APIs).

Why

PromptExecutor implementations are dynamic by nature: the initial execution intent might not reflect what will finally be executed on LLMClient level. MultiLLMPromptExecutor falls back to a different provider/model when the requested one isn't configured, RoutingLLMPromptExecutor picks a client based on the model, and we expect more dynamic routing in the future (user-defined predicates, cost- or latency-based model selection, etc.).

Without *Dispatched events, observers only see the requested prompt/model/tools and have no way to know what was actually executed — which makes tracing, evaluation, and cost accounting unreliable. The new events expose the effective execution parameters at the moment they're dispatched, while Requested keeps the caller-visible view.

Pipeline integration: hookable vs. plain executors

The pipeline now dynamically picks how to bridge a user-supplied executor based on its type:

  • If the executor extends HookablePromptExecutor, it's wrapped in ContextualPromptExecutor, which subscribes to the executor's per-call hook and forwards every PromptExecutorEvent (including *Dispatched / *FrameReceived) into the pipeline. This is the full-fidelity integration.
  • If the executor only extends plain PromptExecutor, it's wrapped in LegacyContextualPromptExecutor — what ContextualPromptExecutor used to be before this PR. It intercepts calls from the outside and emits *Starting / *Completed / *Failed events, but *Dispatched and streaming *FrameReceived are not emitted because there is no way to observe the executor's internal dispatch boundary from outside.

Important

Custom PromptExecutor implementations continue to work unchanged, but they will silently miss the *Dispatched and per-frame events. To get full pipeline integration, migrate them to extend HookablePromptExecutor and emit events through PromptExecutionContext.handle(...) at the appropriate points — see the built-in executors as reference.

Migration / compatibility

This change is purely additive — no breaking changes for existing callers.

  • All built-in executors (SingleLLMPromptExecutor, MultiLLMPromptExecutor, RoutingLLMPromptExecutor) are migrated to extend HookablePromptExecutor; their existing PromptExecutor overrides delegate to the context-taking versions with a default PromptExecutionContext.
  • MockPromptExecutor and the testing DSL extend HookablePromptExecutor so tests get the same events.

Tests

  • New PromptExecutorEventTest covers event ordering and promptExecutionId correlation across the sealed hierarchy.
  • New HookablePromptExecutorTest exercises the hook contract for all four operations.
  • MultiLLMPromptExecutorTest, RoutingLLMPromptExecutorTest, SingleLLMPromptExecutorTest add event-emission cases — including the fallback path, which is exactly where requested ≠ dispatched.
  • EventHandlerTest covers the new onLLMCallDispatched / onLLMStreamingDispatched handlers.

Amaneusz added 8 commits May 8, 2026 13:13
# Conflicts:
#	agents/agents-core/src/jvmCommonMain/kotlin/ai/koog/agents/core/agent/feature/pipeline/AIAgentPipeline.kt
#	agents/agents-features/agents-features-event-handler/src/jvmCommonMain/kotlin/ai/koog/agents/features/eventHandler/feature/EventHandlerConfig.kt
#	agents/agents-features/agents-features-longterm-memory/src/commonMain/kotlin/ai/koog/agents/longtermmemory/feature/LongTermMemory.kt
@Amaneusz Amaneusz requested review from EugeneTheDev and sdubov and removed request for sdubov May 8, 2026 12:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant