Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 16 additions & 6 deletions docs/user/agent-hooks-protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,10 @@ need client-defined routing and failure policies.
| Event | Where Kit emits it | Hook authority |
| --- | --- | --- |
| `session.start` | A session's model connection opens | Inject context |
| `turn.start` | A logical agent-loop turn starts, with native session and turn IDs | Observe |
| `turn.start` | A logical agent-loop turn admits its input, with native session and turn IDs | Deny, replace the admitted prompt |
| `turn.end` | That logical turn completes, fails, or is cancelled | Observe |
| `session.end` | A prompt run finishes or an ACP session closes | Observe |
| `user.message.inbound` | A new turn admits a user prompt | Deny, replace the prompt |
| `user.message.inbound` | A user prompt arrives, including an interjection during a turn | Deny, replace the prompt |
| `model.request.before` | Before each model call, with the transcript | Deny, replace messages, supply a response, inject context |
| `model.response.after` | A model call finishes, with its output | Replace messages, block response |
| `model.error` | A model call fails | Observe |
Expand All @@ -92,10 +92,20 @@ Model message edits accept text replacements and preserve unchanged native
items. Response interception withholds streaming output until hooks finish.
A compaction denial fails the turn without replacing the transcript.

A denied prompt is removed, together with resources attached to it, and the
turn ends without a model call. A denied
model request fails the turn. A denied tool call returns a permission error to
the model.
`user.message.inbound` applies arrival policy before `turn.start`. The latter's
`items` contain the effective admitted input, including attachments and
notifications submitted with it, not prior-turn history. A `modify` effect with
target `prompt` and operation `replace` on `turn.start` replaces this entire
canonical item list. Retain the supplied opaque part identities to preserve
attachments; edits cannot fabricate media. Both events support user-facing
`message` effects. An interjection during an active turn emits
`user.message.inbound` without another `turn.start`.

A denied prompt is removed together with its admitted resources and
notifications. Denial fails the current turn without a further model call,
including when the denied input is an interjection. Prior-turn history is
preserved. A denied model request fails the turn. A denied tool call returns
a permission error to the model.

Kit checks edited tool input against the tool's input schema before the tool
runs; an edit that does not match counts as a hook failure. An edit that the
Expand Down
10 changes: 8 additions & 2 deletions src/ahp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ use agentkit_core::{FinishReason, Item, ItemKind, Part, ToolOutput, TurnCancella
use agentkit_loop::LoopError;

pub(crate) use model::{HookedAdapter, HookedSession};
pub(crate) use prompt::{PromptHook, PromptRecord};
pub(crate) use prompt::{PromptHook, PromptHookBuilder, PromptRecord};
pub(crate) use tool::hooked_tools;
pub(crate) use turn::TurnHook;

Expand Down Expand Up @@ -281,7 +281,13 @@ fn options() -> Result<HooksOptions, String> {
.inject_context(capability::InjectContext::Now),
),
session_end: Some(capability::session_end().observe()),
turn_start: Some(capability::turn_start().observe()),
turn_start: Some(
capability::turn_start()
.message()
.observe()
.deny()
.modify_prompt(Modify::Replace),
),
Comment thread
danielkov marked this conversation as resolved.
Resolved
turn_end: Some(capability::turn_end().observe()),
user_message_inbound: Some(
capability::user_message_inbound()
Expand Down
12 changes: 11 additions & 1 deletion src/ahp/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -614,10 +614,20 @@ impl<'a> OriginalMessage<'a> {
fn canonical_items<'a>(
messages: impl IntoIterator<Item = &'a CanonicalMessage>,
originals: &[Item],
) -> Result<Vec<Item>, LoopError> {
canonical_items_at(messages, originals, 0)
}

/// Keep synthesized identities at their transcript offsets while limiting
/// opaque-part authority to the supplied original message scope.
pub(super) fn canonical_items_at<'a>(
messages: impl IntoIterator<Item = &'a CanonicalMessage>,
originals: &[Item],
offset: usize,
) -> Result<Vec<Item>, LoopError> {
let mut by_id = HashMap::with_capacity(originals.len());
for (index, item) in originals.iter().enumerate() {
let id = super::item_id(item, index);
let id = super::item_id(item, offset + index);
let original = OriginalMessage::new(item, &id);
if by_id.insert(id, original).is_some() {
return Err(invalid("duplicate original model message identity"));
Expand Down
Loading
Loading