Skip to content

[Sub-Task] Extend Feedback type with messageId for ACP session/inject (RFD #1261) support #154

Description

@Leoyzen

Context

Parent: #135 (AgentWolf v1 Foundation Architecture — Layer 4 RunLoop)

ACP RFD #1261 proposes session/inject with mode: "queue" | "steer", agent-owned messageId, user_message echo notification, and mandatory session/revoke_inject. This is the upstream protocol standardization of the steer/queue pattern AgentPool already implements internally via CommChannel + Feedback.

Related Bug Fix (Completed)

The steer() bug (PR #144 comment) was fixed in this session: steer() now checks enqueue("asap") before the CommChannel feedback path, ensuring ProtocolChannel sessions (ACP/OpenCode/AG-UI) get mid-turn injection. TDD tests added in tests/lifecycle/test_run_loop.py.

What's Missing

AgentPool's Feedback dataclass (src/agentpool/lifecycle/types.py L61-72) currently has only:

@dataclass
class Feedback:
    content: str
    is_steer: bool

No messageId, no revoke, no replace, no steer_in_stream capability. The RFD requires all of these for session/inject compliance.

Tasks

P1: Extend Feedback type

  • Add message_id: str | None = None field to Feedback dataclass
  • Add content_blocks: list[ContentBlock] | None = None for structured content (ACP ContentBlock[] shape)
  • Generate message_id in ProtocolChannel.deliver_feedback() when not provided (UUID-based)
  • Update steer() and followup() to accept and propagate message_id

P1: Define cancel() interaction with pending steer/followup

  • RFD #1261 specifies: pending injects survive session/cancel, deliver at next idle
  • Audit current RunHandle.cancel() — does it clear CommChannel feedback queue?
  • Add test: cancel() during pending steer → steer survives and delivers next turn
  • Document the interaction in steer() docstring

P2: Implement session/revoke_inject semantics

  • Add RevokedFeedback tracking (tombstone by message_id)
  • ProtocolChannel.recv() skips revoked feedback
  • Add RunHandle.revoke_steer(message_id: str) -> bool method
  • Add test: revoke before delivery → no user_message emitted
  • Add test: revoke after delivery → returns already_delivered error

P2: Add steer_in_stream capability declaration

  • Define capability: ["interrupt"] (truncate stream) vs ["finish"] (let stream complete)
  • Expose via agentCapabilities in ACP initialize response
  • Native agents (PydanticAI) → ["interrupt"] (PendingMessageDrainCapability supports this)
  • ACP agents → ["finish"] (must wait for tool boundary, can't interrupt mid-stream)

P3: Evaluate session/replace_inject (opt-in)

  • Add pending.replace: bool capability flag
  • ProtocolChannel.replace_feedback(message_id, new_content) -> bool
  • Only if underlying runtime supports it (content not yet serialized into prompt envelope)

P3: ACP protocol mapping

  • Implement _session/inject extension method on ACP v1 (underscore prefix, per ACP extensibility spec)
  • Map RunState.RUNNING → ACP v2 state_change(state="running")
  • Map RunState.IDLE → ACP v2 state_change(state="idle")
  • Evaluate upgrade to v2 prompt lifecycle (non-blocking session/prompt)

RFD #1261 Key Design Points

Aspect RFD Design AgentPool Current
Method session/inject with mode: "queue" | "steer" steer() + followup() (separate methods)
Response Immediate messageId (agent-owned) Fire-and-forget (no ID)
Delivery signal user_message notification with messageId CommChannel feedback queue drain
Revoke session/revoke_inject (mandatory) Not implemented
Replace session/replace_inject (opt-in via pending.replace) Not implemented
steer_in_stream ["interrupt"] / ["finish"] capability Not declared
cancel interaction Pending injects survive cancel Undefined (independent paths)
Version v2-only (depends on v2 prompt lifecycle) v1 (v2 not yet implemented)

References

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions