Skip to content

feat(talon): add reaction approval routing - #4345

Merged
John Kennedy (jkennedyvz) merged 1 commit into
mainfrom
john/talon/reaction-approval-contract
Jun 28, 2026
Merged

feat(talon): add reaction approval routing#4345
John Kennedy (jkennedyvz) merged 1 commit into
mainfrom
john/talon/reaction-approval-contract

Conversation

@jkennedyvz

@jkennedyvz John Kennedy (jkennedyvz) commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Adds a provider-neutral ChannelReaction contract and optional reaction handler registration surface for Talon channels. The host now binds pending tool approvals to provider, channel conversation, agent conversation, prompt message id, and initiating sender before accepting thumbs-up or thumbs-down reactions. Reactions without prompt message ids fall back to the existing text approval flow, and existing text replies remain unchanged.

When this is useful

Chat platforms like Slack, Discord, or Teams let users react to a message with an emoji instead of typing a reply. Reaction approval lets an operator approve or deny a pending tool call with a single 👍/👎 on the approval prompt — no extra typing, and the reaction stays scoped to the exact message that asked for approval. This is especially handy in busy channels where typed approve/deny replies are easy to miss or get crossed with other conversation.

Example

An agent requests tool approval and the host posts the prompt (recording the returned message id):

🤖 Tool approval required.
   1. send_email(to="ops@example.com", subject="Deploy complete")
React 👍 to approve or 👎 to reject.

The operator reacts to that prompt with 👍. Skin tones and variation selectors are normalized, so 👍🏽 and 👎️ work too. The host resolves the reaction to the pending approval only when all of the following line up:

  • the emoji is 👍 (approve) or 👎 (reject)
  • the channel/provider matches the one that issued the prompt
  • the reaction is on the same conversation and the exact prompt message
  • the reactor is the same sender who triggered the run (when the sender is known)

If anything doesn't match — wrong message, different sender, unrelated emoji — the reaction is ignored and the pending approval keeps waiting. Channels that don't expose message ids (or don't implement ReactionChannelAdapter) keep using the existing text approve/deny flow unchanged.

Validation run:

  • uv run --group test pytest --disable-socket --allow-unix-socket tests/test_host.py --timeout 10
  • make lint_diff
  • make test

@github-actions github-actions Bot added feature New feature/enhancement or request for one internal User is a member of the `langchain-ai` GitHub organization size: M 200-499 LOC talon labels Jun 27, 2026
@jkennedyvz
John Kennedy (jkennedyvz) marked this pull request as ready for review June 27, 2026 22:47

@open-swe open-swe Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Open SWE Review: No issues found

Open SWE reviewed this PR and found no potential bugs to report.

Open in WebView Open SWE trace

@jkennedyvz
John Kennedy (jkennedyvz) enabled auto-merge (squash) June 27, 2026 22:56
@jkennedyvz
John Kennedy (jkennedyvz) merged commit 3fe8c0c into main Jun 28, 2026
64 checks passed
@jkennedyvz
John Kennedy (jkennedyvz) deleted the john/talon/reaction-approval-contract branch June 28, 2026 22:12
John Kennedy (jkennedyvz) added a commit that referenced this pull request Jun 28, 2026
Stacked on #4345.

Adds `DEEPAGENTS_TALON_INTERRUPT_ON_TOOLS` so Talon operators can force
channel approval for named tools at runtime without changing Fleet
exports. The overlay is additive with imported Fleet `interrupt_on`
config and is also applied on the non-Fleet MCP runtime path.

The env value is parsed as a comma-separated list, trims surrounding
whitespace, ignores empty entries, and maps each listed tool to the
default `True` interrupt configuration. Fleet reloads reapply the same
overlay.

## When this is useful

Some tools are safe to run autonomously in one deployment but risky in
another (e.g. `bash`, `execute`, `github_create_pr`). Rather than
re-exporting a Fleet agent or editing `interrupt_on` in code every time
the risk profile changes, an operator can flip a single env var at
deploy/runtime to force those tools through Talon's channel approval
flow. The override is additive, so Fleet-provided approvals keep working
and the env list just layers extra guarded tools on top. This also
covers the non-Fleet (local/MCP) runtime path, which previously had no
equivalent knob.

## Example

Force `bash` and `github_create_pr` to require channel approval:

```bash
DEEPAGENTS_TALON_INTERRUPT_ON_TOOLS=bash,execute,github_create_pr
```

Whitespace and empty entries are tolerated, so `" bash, execute, ,
github_create_pr "` parses to the same three tools. When the agent then
calls `bash`, Talon posts an approval prompt in the channel:

```
🤖 Tool approval required.
   1. bash(command="rm -rf /tmp/build")
React 👍 to approve or 👎 to reject.
```

Because this PR stacks on #4345, the operator can approve/deny by
reacting with 👍/👎 on that exact prompt (scoped to provider,
conversation, prompt message id, and sender) — or by typing
`approve`/`deny` as before. With `fleet_tool` already configured via
Fleet, the merged `interrupt_on` becomes `{"fleet_tool": True, "bash":
True, "execute": True, "github_create_pr": True}`, and Fleet reloads
reapply the same overlay so the gate survives credential refreshes.

Tests cover the parser, Fleet merge behavior, Fleet reload components,
non-Fleet runtime wiring, empty env behavior, and graph construction.

---------

Co-authored-by: Deep Agent <agent@deepagents.dev>
John Kennedy (jkennedyvz) added a commit that referenced this pull request Jun 28, 2026
Stacked on #4345.

Adds Telegram `message_reaction` ingestion for approval reactions. The
adapter now requests `message_reaction` updates, extracts concrete-user
emoji reactions into `ChannelReaction`, applies the existing Telegram
exposure policy before dispatch, ignores anonymous or senderless
reactions, and preserves offset advancement for processed or dropped
reaction updates. Normal message and channel-post parsing remain
unchanged.

## Approving by reaction

When a tool requires approval the host posts a prompt like:

```
Tool approval required.
1. `shell`
Args: `{"cmd": "make deploy production"}`
Reply `👍` / `approve` to run or `👎` / `deny` to skip.
```

The operator can now respond two ways:

- **Text reply** — `approve` / `deny` (unchanged).
- **Emoji reaction** — tap 👍 on the prompt message to approve, 👎 to
reject.

Reactions are gated the same way as messages: only operators in the
exposure policy's `operator_ids` or the channel's `allowed_user_ids` are
honored, and the reaction must land on the exact approval prompt in the
matching conversation. Anonymous or senderless reactions are dropped. 👍
maps to `approve`, 👎 to `reject`; everything else is ignored.

This is most useful on mobile, where a single tap on the prompt beats
typing a reply, and for high-volume approval queues where the operator
is triaging many tool calls quickly.

Validation run:
- `uv run --group test pytest --disable-socket --allow-unix-socket
tests/channels/test_telegram.py --timeout 10`
- `make lint_diff`
- `make test`

<img width="936" height="738" alt="Screenshot 2026-06-27 at 4 30 02 PM"
src="https://github.com/user-attachments/assets/58756f30-c8ff-4bd8-8057-84c8681b6687"
/>

---------

Co-authored-by: Deep Agent <agent@deepagents.dev>
John Kennedy (jkennedyvz) added a commit that referenced this pull request Jun 28, 2026
Stacked on #4345.

Adds host-side `tool_approval.reaction` audit events for reaction
approval attempts. The event records provider, stable hashed channel
conversation/prompt/sender references, emoji, decision, match status,
and resolution mode. Raw provider IDs are included only when
`DEEPAGENTS_TALON_APPROVAL_LOG_RAW_IDS=true`; raw provider metadata,
approval prompt text, arbitrary message text, and tool arguments are not
logged. Matched reaction approvals use `operator_reaction` as the
resolution so they can be distinguished from text approvals.

## When this is useful

Reaction approvals are a one-tap action that's easy to fire off and easy
to miss-context (wrong message, wrong channel, stale prompt). Without an
audit trail there's no way to answer "did someone try to approve that
tool call, and why didn't it take?" after the fact. These events let
operators and security reviewers trace every reaction attempt — matched
or ignored — back to a specific provider, conversation, prompt message,
and sender, without leaking raw IDs or prompt/message contents by
default. Distinguishing `operator_reaction` from text `approve`/`deny`
also makes it possible to monitor how often the reaction path is
actually used versus the text fallback.

## Example

An operator reacts 👍 to the approval prompt. The host emits a structured
`tool_approval.reaction` event:

```json
{
  "event": "tool_approval.reaction",
  "provider": "slack",
  "channel_conversation_ref": "9f1c…a4e2",
  "prompt_message_ref": "b7d3…0c91",
  "reacting_sender_ref": "e2a8…77f0",
  "emoji": "👍",
  "decision": "approve",
  "match_status": "matched",
  "resolution": "operator_reaction"
}
```

Conversation/prompt/sender references are stable hashes (via
`stable_log_ref`), so the same IDs correlate across events without
exposing the underlying Slack IDs. Ignored attempts are logged too, with
a precise `resolution` explaining why:

- `no_pending_approval` — reaction arrived with no pending tool approval
- `unsupported_emoji` — emoji wasn't 👍/👎
- `missing_prompt_message_id` — channel didn't supply a prompt message
id (falls back to text)
- `provider_mismatch` / `conversation_mismatch` / `message_mismatch` —
reaction didn't target the prompt that issued the request
- `sender_missing` / `sender_mismatch` — reactor wasn't the initiating
operator (when sender is known)

For debugging a specific incident, set
`DEEPAGENTS_TALON_APPROVAL_LOG_RAW_IDS=true` to additionally include
`raw_channel_conversation_id`, `raw_prompt_message_id`, and
`raw_reacting_sender_id`. Sensitive values — provider metadata, approval
prompt text, arbitrary message text, and tool arguments — are never
logged regardless of that flag.

Validation run:
- `uv run --group test pytest --disable-socket --allow-unix-socket
tests/test_host.py --timeout 10`
- `make lint_diff`
- `make test`

Co-authored-by: Deep Agent <agent@deepagents.dev>
John Kennedy (jkennedyvz) pushed a commit that referenced this pull request Jun 30, 2026
> [!CAUTION]
> Merging this PR will automatically publish to **PyPI** and create a
**GitHub release**.

For the full release process, see
[`.github/RELEASING.md`](https://github.com/langchain-ai/deepagents/blob/main/.github/RELEASING.md).

---

_Everything below this line will be the GitHub release body._

---


##
[0.0.2](deepagents-talon==0.0.1...deepagents-talon==0.0.2)
(2026-06-30)


### Features

* **talon:** `DEEPAGENTS_TALON_RECURSION_LIMIT` env var
([#4354](#4354))
([82d1eac](82d1eac))
* **talon:** add reaction approval routing
([#4345](#4345))
([3fe8c0c](3fe8c0c))
* **talon:** add Telegram channel adapter, CLI wiring, and offset
persistence
([#4097](#4097))
([7c87cec](7c87cec))
* **talon:** add tool approval env override
([#4349](#4349))
([d26481d](d26481d))
* **talon:** audit reaction approval attempts
([#4348](#4348))
([d7895c4](d7895c4))
* **talon:** ingest Telegram approval reactions
([#4346](#4346))
([437af0b](437af0b))


### Bug Fixes

* **talon:** default workspace to current directory
([#4099](#4099))
([5e337ae](5e337ae))

---

_Everything above this line will be the GitHub release body._

---

> [!NOTE]
> A **New Contributors** section is appended to the GitHub release notes
automatically at publish time (see [Release
Pipeline](https://github.com/langchain-ai/deepagents/blob/main/.github/RELEASING.md#release-pipeline),
step 2).

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Marcelo5444 pushed a commit to Marcelo5444/deepagents that referenced this pull request Jul 30, 2026
Adds a provider-neutral `ChannelReaction` contract and optional reaction
handler registration surface for Talon channels. The host now binds
pending tool approvals to provider, channel conversation, agent
conversation, prompt message id, and initiating sender before accepting
thumbs-up or thumbs-down reactions. Reactions without prompt message ids
fall back to the existing text approval flow, and existing text replies
remain unchanged.

## When this is useful

Chat platforms like Slack, Discord, or Teams let users react to a
message with an emoji instead of typing a reply. Reaction approval lets
an operator approve or deny a pending tool call with a single 👍/👎 on the
approval prompt — no extra typing, and the reaction stays scoped to the
exact message that asked for approval. This is especially handy in busy
channels where typed `approve`/`deny` replies are easy to miss or get
crossed with other conversation.

## Example

An agent requests tool approval and the host posts the prompt (recording
the returned message id):

```
🤖 Tool approval required.
   1. send_email(to="ops@example.com", subject="Deploy complete")
React 👍 to approve or 👎 to reject.
```

The operator reacts to that prompt with 👍. Skin tones and variation
selectors are normalized, so 👍🏽 and 👎️ work too. The host resolves the
reaction to the pending approval only when **all** of the following line
up:

- the emoji is 👍 (approve) or 👎 (reject)
- the channel/provider matches the one that issued the prompt
- the reaction is on the same conversation and the exact prompt message
- the reactor is the same sender who triggered the run (when the sender
is known)

If anything doesn't match — wrong message, different sender, unrelated
emoji — the reaction is ignored and the pending approval keeps waiting.
Channels that don't expose message ids (or don't implement
`ReactionChannelAdapter`) keep using the existing text `approve`/`deny`
flow unchanged.

Validation run:
- `uv run --group test pytest --disable-socket --allow-unix-socket
tests/test_host.py --timeout 10`
- `make lint_diff`
- `make test`

Co-authored-by: Deep Agent <agent@deepagents.dev>
Marcelo5444 pushed a commit to Marcelo5444/deepagents that referenced this pull request Jul 30, 2026
Stacked on langchain-ai#4345.

Adds `DEEPAGENTS_TALON_INTERRUPT_ON_TOOLS` so Talon operators can force
channel approval for named tools at runtime without changing Fleet
exports. The overlay is additive with imported Fleet `interrupt_on`
config and is also applied on the non-Fleet MCP runtime path.

The env value is parsed as a comma-separated list, trims surrounding
whitespace, ignores empty entries, and maps each listed tool to the
default `True` interrupt configuration. Fleet reloads reapply the same
overlay.

## When this is useful

Some tools are safe to run autonomously in one deployment but risky in
another (e.g. `bash`, `execute`, `github_create_pr`). Rather than
re-exporting a Fleet agent or editing `interrupt_on` in code every time
the risk profile changes, an operator can flip a single env var at
deploy/runtime to force those tools through Talon's channel approval
flow. The override is additive, so Fleet-provided approvals keep working
and the env list just layers extra guarded tools on top. This also
covers the non-Fleet (local/MCP) runtime path, which previously had no
equivalent knob.

## Example

Force `bash` and `github_create_pr` to require channel approval:

```bash
DEEPAGENTS_TALON_INTERRUPT_ON_TOOLS=bash,execute,github_create_pr
```

Whitespace and empty entries are tolerated, so `" bash, execute, ,
github_create_pr "` parses to the same three tools. When the agent then
calls `bash`, Talon posts an approval prompt in the channel:

```
🤖 Tool approval required.
   1. bash(command="rm -rf /tmp/build")
React 👍 to approve or 👎 to reject.
```

Because this PR stacks on langchain-ai#4345, the operator can approve/deny by
reacting with 👍/👎 on that exact prompt (scoped to provider,
conversation, prompt message id, and sender) — or by typing
`approve`/`deny` as before. With `fleet_tool` already configured via
Fleet, the merged `interrupt_on` becomes `{"fleet_tool": True, "bash":
True, "execute": True, "github_create_pr": True}`, and Fleet reloads
reapply the same overlay so the gate survives credential refreshes.

Tests cover the parser, Fleet merge behavior, Fleet reload components,
non-Fleet runtime wiring, empty env behavior, and graph construction.

---------

Co-authored-by: Deep Agent <agent@deepagents.dev>
Marcelo5444 pushed a commit to Marcelo5444/deepagents that referenced this pull request Jul 30, 2026
Stacked on langchain-ai#4345.

Adds Telegram `message_reaction` ingestion for approval reactions. The
adapter now requests `message_reaction` updates, extracts concrete-user
emoji reactions into `ChannelReaction`, applies the existing Telegram
exposure policy before dispatch, ignores anonymous or senderless
reactions, and preserves offset advancement for processed or dropped
reaction updates. Normal message and channel-post parsing remain
unchanged.

## Approving by reaction

When a tool requires approval the host posts a prompt like:

```
Tool approval required.
1. `shell`
Args: `{"cmd": "make deploy production"}`
Reply `👍` / `approve` to run or `👎` / `deny` to skip.
```

The operator can now respond two ways:

- **Text reply** — `approve` / `deny` (unchanged).
- **Emoji reaction** — tap 👍 on the prompt message to approve, 👎 to
reject.

Reactions are gated the same way as messages: only operators in the
exposure policy's `operator_ids` or the channel's `allowed_user_ids` are
honored, and the reaction must land on the exact approval prompt in the
matching conversation. Anonymous or senderless reactions are dropped. 👍
maps to `approve`, 👎 to `reject`; everything else is ignored.

This is most useful on mobile, where a single tap on the prompt beats
typing a reply, and for high-volume approval queues where the operator
is triaging many tool calls quickly.

Validation run:
- `uv run --group test pytest --disable-socket --allow-unix-socket
tests/channels/test_telegram.py --timeout 10`
- `make lint_diff`
- `make test`

<img width="936" height="738" alt="Screenshot 2026-06-27 at 4 30 02 PM"
src="https://github.com/user-attachments/assets/58756f30-c8ff-4bd8-8057-84c8681b6687"
/>

---------

Co-authored-by: Deep Agent <agent@deepagents.dev>
Marcelo5444 pushed a commit to Marcelo5444/deepagents that referenced this pull request Jul 30, 2026
Stacked on langchain-ai#4345.

Adds host-side `tool_approval.reaction` audit events for reaction
approval attempts. The event records provider, stable hashed channel
conversation/prompt/sender references, emoji, decision, match status,
and resolution mode. Raw provider IDs are included only when
`DEEPAGENTS_TALON_APPROVAL_LOG_RAW_IDS=true`; raw provider metadata,
approval prompt text, arbitrary message text, and tool arguments are not
logged. Matched reaction approvals use `operator_reaction` as the
resolution so they can be distinguished from text approvals.

## When this is useful

Reaction approvals are a one-tap action that's easy to fire off and easy
to miss-context (wrong message, wrong channel, stale prompt). Without an
audit trail there's no way to answer "did someone try to approve that
tool call, and why didn't it take?" after the fact. These events let
operators and security reviewers trace every reaction attempt — matched
or ignored — back to a specific provider, conversation, prompt message,
and sender, without leaking raw IDs or prompt/message contents by
default. Distinguishing `operator_reaction` from text `approve`/`deny`
also makes it possible to monitor how often the reaction path is
actually used versus the text fallback.

## Example

An operator reacts 👍 to the approval prompt. The host emits a structured
`tool_approval.reaction` event:

```json
{
  "event": "tool_approval.reaction",
  "provider": "slack",
  "channel_conversation_ref": "9f1c…a4e2",
  "prompt_message_ref": "b7d3…0c91",
  "reacting_sender_ref": "e2a8…77f0",
  "emoji": "👍",
  "decision": "approve",
  "match_status": "matched",
  "resolution": "operator_reaction"
}
```

Conversation/prompt/sender references are stable hashes (via
`stable_log_ref`), so the same IDs correlate across events without
exposing the underlying Slack IDs. Ignored attempts are logged too, with
a precise `resolution` explaining why:

- `no_pending_approval` — reaction arrived with no pending tool approval
- `unsupported_emoji` — emoji wasn't 👍/👎
- `missing_prompt_message_id` — channel didn't supply a prompt message
id (falls back to text)
- `provider_mismatch` / `conversation_mismatch` / `message_mismatch` —
reaction didn't target the prompt that issued the request
- `sender_missing` / `sender_mismatch` — reactor wasn't the initiating
operator (when sender is known)

For debugging a specific incident, set
`DEEPAGENTS_TALON_APPROVAL_LOG_RAW_IDS=true` to additionally include
`raw_channel_conversation_id`, `raw_prompt_message_id`, and
`raw_reacting_sender_id`. Sensitive values — provider metadata, approval
prompt text, arbitrary message text, and tool arguments — are never
logged regardless of that flag.

Validation run:
- `uv run --group test pytest --disable-socket --allow-unix-socket
tests/test_host.py --timeout 10`
- `make lint_diff`
- `make test`

Co-authored-by: Deep Agent <agent@deepagents.dev>
Marcelo5444 pushed a commit to Marcelo5444/deepagents that referenced this pull request Jul 30, 2026
> [!CAUTION]
> Merging this PR will automatically publish to **PyPI** and create a
**GitHub release**.

For the full release process, see
[`.github/RELEASING.md`](https://github.com/langchain-ai/deepagents/blob/main/.github/RELEASING.md).

---

_Everything below this line will be the GitHub release body._

---


##
[0.0.2](langchain-ai/deepagents@deepagents-talon==0.0.1...deepagents-talon==0.0.2)
(2026-06-30)


### Features

* **talon:** `DEEPAGENTS_TALON_RECURSION_LIMIT` env var
([langchain-ai#4354](langchain-ai#4354))
([82d1eac](langchain-ai@82d1eac))
* **talon:** add reaction approval routing
([langchain-ai#4345](langchain-ai#4345))
([3fe8c0c](langchain-ai@3fe8c0c))
* **talon:** add Telegram channel adapter, CLI wiring, and offset
persistence
([langchain-ai#4097](langchain-ai#4097))
([7c87cec](langchain-ai@7c87cec))
* **talon:** add tool approval env override
([langchain-ai#4349](langchain-ai#4349))
([d26481d](langchain-ai@d26481d))
* **talon:** audit reaction approval attempts
([langchain-ai#4348](langchain-ai#4348))
([d7895c4](langchain-ai@d7895c4))
* **talon:** ingest Telegram approval reactions
([langchain-ai#4346](langchain-ai#4346))
([437af0b](langchain-ai@437af0b))


### Bug Fixes

* **talon:** default workspace to current directory
([langchain-ai#4099](langchain-ai#4099))
([5e337ae](langchain-ai@5e337ae))

---

_Everything above this line will be the GitHub release body._

---

> [!NOTE]
> A **New Contributors** section is appended to the GitHub release notes
automatically at publish time (see [Release
Pipeline](https://github.com/langchain-ai/deepagents/blob/main/.github/RELEASING.md#release-pipeline),
step 2).

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New feature/enhancement or request for one internal User is a member of the `langchain-ai` GitHub organization size: M 200-499 LOC talon

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant