Skip to content

Agent prepare call#229

Open
fwang2002 wants to merge 3 commits into
charmbracelet:mainfrom
fwang2002:agent-prepare-call
Open

Agent prepare call#229
fwang2002 wants to merge 3 commits into
charmbracelet:mainfrom
fwang2002:agent-prepare-call

Conversation

@fwang2002
Copy link
Copy Markdown
Contributor

@fwang2002 fwang2002 commented May 10, 2026

Summary

Lets applications wire runtime-sourced configuration into an agent call (Langfuse, feature flags, etc.)
without rebuilding the agent or constructing a fresh closure per call. Two pieces working together:

  1. CallOptions + PrepareCall hook (3925f8b): a typed any field on AgentCall / AgentStreamCall carries
    application data from the call site to a hook that runs inside (*agent).prepareCall. The hook can read
    CallOptions and mutate the call fields before the first model invocation. Per-call PrepareCall overrides
    the agent-level default registered via WithPrepareCall. Covers both Generate and Stream.
  2. Per-call SystemPrompt (fe7678e): a *string field on both call structs that overrides WithSystemPrompt
    when non-nil. Combined with (1), the hook can fetch a system prompt from a remote source — typical Langfuse
    flow:
  agent := fantasy.NewAgent(model,
      fantasy.WithSystemPrompt("fallback default"),
      fantasy.WithPrepareCall(func(ctx context.Context, c *fantasy.AgentCall) (context.Context, error) {
          ref, ok := c.CallOptions.(LangfusePromptRef)
          if !ok { return ctx, nil }
          text, err := langfuse.Get(ctx, ref.Name, ref.Version, ref.Variables)
          if err != nil { return ctx, err }
          c.SystemPrompt = &text
          return ctx, nil
      }),
  )

  agent.Generate(ctx, fantasy.AgentCall{
      Prompt:      userInput,
      CallOptions: LangfusePromptRef{Name: "support-bot", Version: ptr(3)},
  })

Design notes

  • prepareCall resolves SystemPrompt against the agent default both before and after the hook. Before, so
    the hook sees the effective value (consistent with how MaxOutputTokens, Temperature, etc. are merged via
    cmp.Or ahead of the hook). After, so a hook that clears the field (c.SystemPrompt = nil) cleanly opts back
    into the agent default instead of leaving a nil pointer for downstream to deref.
  • Downstream contract: after prepareCall returns, call.SystemPrompt is guaranteed non-nil. Generate and
    Stream deref it once into a local and use that as the per-call baseline; the per-step comparison is now
    against the call baseline rather than agentSettings.systemPrompt, so dynamic prompts and PrepareStep
    overrides interact correctly.
  • No agent-level Extension/Options field added. Closures cover the agent-level case (the hook can capture
    any per-agent state at construction time); only per-call data needs the typed channel that CallOptions
    provides.
  • Non-breaking: both fields are zero-value-safe additions; existing call sites continue to work unchanged.

Test plan

  • TestPrepareCall/hook_can_override_system_prompt_via_CallOptions — Langfuse-style flow: hook reads
    CallOptions, sees the pre-resolved SystemPrompt (= agent default), overrides it, model receives the new
    system message.
  • TestPrepareCall/hook_clearing_SystemPrompt_falls_back_to_agent_default — hook sets c.SystemPrompt = nil;
    downstream uses the agent default rather than panicking.
  • TestPrepareCall/explicit_SystemPrompt_without_hook_bypasses_agent_default — direct per-call override via
    the field, no hook in play.
  • TestPrepareCall/PrepareStep_can_still_override_the_prepared_system — PrepareStep still wins over
    PrepareCall, and the step-level recreation guard compares against the call baseline (not the static agent
    setting).
  • TestPrepareCall/Stream_forwards_explicit_SystemPrompt_through_AgentCall_conversion — covers the Stream →
    AgentCall field copy specifically (a hook-based test would mask a missing copy).
  • go vet ./..., go test ./... — full workspace green.

Resolves #224

  • I have read CONTRIBUTING.md.
  • I have created a discussion that was approved by a maintainer (for new features).

wangfeng01 and others added 2 commits May 10, 2026 10:30
Lets applications attach arbitrary data to a single call via `CallOptions`
and translate it into concrete call fields before the first model
invocation. The hook fires inside `(*agent).prepareCall` so it covers
both `Generate` and `Stream`; per-call `PrepareCall` overrides the
agent-level default set via `WithPrepareCall`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds `SystemPrompt *string` to `AgentCall` / `AgentStreamCall`. When
non-nil it overrides the agent-level system prompt for this call, so
applications can drive system content from runtime sources (e.g. fetch
from Langfuse inside `PrepareCall` based on `CallOptions`).

`prepareCall` resolves the field against the agent default both before
and after the hook: hooks see the effective value and may clear it back
to nil to opt into the default, while downstream code can rely on the
pointer being non-nil.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@fwang2002 fwang2002 requested a review from andreynering as a code owner May 10, 2026 03:49
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

Add application-level extension field and PrepareCall hook to AgentCall / AgentStreamCall

1 participant