Skip to content

feat(agent): emit status notifications for orchestrator streaming#2732

Draft
VojtechBartos wants to merge 3 commits into
mainfrom
vojtab/agent-emit-status-events
Draft

feat(agent): emit status notifications for orchestrator streaming#2732
VojtechBartos wants to merge 3 commits into
mainfrom
vojtab/agent-emit-status-events

Conversation

@VojtechBartos

@VojtechBartos VojtechBartos commented Jun 17, 2026

Copy link
Copy Markdown
Member

Problem

When PostHog Code runs a task triggered from Slack, the user only sees a static "Working on task…" placeholder in the thread for the entire run. They have no visibility into what the agent is actually doing.

The orchestrator can render live progress, but it needs a semantic signal — inferring intent from raw session/update tool events is brittle (Bash ≠ "running tests"). Cleaner to have the agent describe its own intent as it works.

Changes

  • packages/agent/src/adapters/claude/conversion/sdk-to-acp.ts — emit POSTHOG_NOTIFICATIONS.STATUS (already declared) with the existing toolInfo.title as a human-readable text whenever a tool first starts. Fire-and-forget (void); a failed broadcast must never break tool execution.
  • Backwards-compatible: existing ACP clients already ignore unknown status values.

How did you test this code?

Agent-authored. The 15-line change reuses the existing toolInfo.title rendering (covered by the surrounding handleToolUseChunk tests) and the existing client.extNotification(STATUS, …) flow (already tested for compacting). End-to-end verification is paired with PostHog/posthog#64293 which consumes the new emit — once both deploy and the posthog-code-slack-agent-status flag is on, Slack threads will show live "Reading api.py" / "Running tests" status lines.

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

Model: Claude Opus 4.7. Paired with PostHog/posthog#64293 which consumes these events on the orchestrator side. Decision: chose to put status-string responsibility on the agent (knows intent) rather than the orchestrator (would have to infer from tool names) — sketched the inference path first, rejected for brittleness.

Emit a POSTHOG_NOTIFICATIONS.STATUS notification when a tool first starts.
The payload carries the existing toolInfo.title as a human-readable text so
downstream consumers can render a live "agent is doing X" status line
without inferring intent from raw tool names.

The orchestrator consumes this in a follow-up PR; existing ACP clients
already ignore unknown status values, so this is backwards-compatible.
@VojtechBartos VojtechBartos self-assigned this Jun 17, 2026
@github-actions

github-actions Bot commented Jun 17, 2026

Copy link
Copy Markdown

React Doctor found no issues in the changed files. 🎉

Reviewed by React Doctor for commit f5caa29.

@greptile-apps

greptile-apps Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
packages/agent/src/adapters/claude/conversion/sdk-to-acp.ts:255-264
**Generic title fired during streaming; specific title never sent**

In the streaming path, `handleToolUseChunk` is first called from `streamEventToAcpNotifications → content_block_start`. At that point `chunk.input` is always `{}` because the input arrives later via `input_json_delta` deltas (which go through `inputJsonDeltaToAcpNotifications`, never through this function again). So the notification fires immediately with the fallback title — `"Execute command"` for Bash, `"Read File"` for Read, `"Edit"` for Edit — not the specific `"Running tests"` / `"Reading api.py"` values the PR describes.

When the consolidated `SDKAssistantMessage` arrives with the full input, `alreadyCached` is already `true`, so the updated title is never sent. The only path that produces a specific title is a non-streaming scenario where `handleUserAssistantMessage` is the first call site for a given tool.

### Issue 2 of 2
packages/agent/src/adapters/claude/conversion/sdk-to-acp.ts:261-264
**Silent error suppression makes systematic failures invisible**

The `.catch(() => {})` swallows all errors without logging. If the `extNotification` call is systematically failing (e.g. due to a schema mismatch, a disconnected client, or a downstream bug), there will be no signal in any log output. A single `ctx.logger.warn(...)` call in the catch handler would make these failures observable without changing the fire-and-forget semantics.

Reviews (1): Last reviewed commit: "feat(agent): broadcast tool-use status n..." | Re-trigger Greptile

Comment on lines +255 to +264
if (!alreadyCached && toolInfo.title) {
void ctx.client
.extNotification(POSTHOG_NOTIFICATIONS.STATUS, {
sessionId: ctx.sessionId,
status: "tool_use",
text: toolInfo.title,
})
.catch(() => {
// Best-effort — a failed status broadcast must not break tool execution.
});

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.

P1 Generic title fired during streaming; specific title never sent

In the streaming path, handleToolUseChunk is first called from streamEventToAcpNotifications → content_block_start. At that point chunk.input is always {} because the input arrives later via input_json_delta deltas (which go through inputJsonDeltaToAcpNotifications, never through this function again). So the notification fires immediately with the fallback title — "Execute command" for Bash, "Read File" for Read, "Edit" for Edit — not the specific "Running tests" / "Reading api.py" values the PR describes.

When the consolidated SDKAssistantMessage arrives with the full input, alreadyCached is already true, so the updated title is never sent. The only path that produces a specific title is a non-streaming scenario where handleUserAssistantMessage is the first call site for a given tool.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/agent/src/adapters/claude/conversion/sdk-to-acp.ts
Line: 255-264

Comment:
**Generic title fired during streaming; specific title never sent**

In the streaming path, `handleToolUseChunk` is first called from `streamEventToAcpNotifications → content_block_start`. At that point `chunk.input` is always `{}` because the input arrives later via `input_json_delta` deltas (which go through `inputJsonDeltaToAcpNotifications`, never through this function again). So the notification fires immediately with the fallback title — `"Execute command"` for Bash, `"Read File"` for Read, `"Edit"` for Edit — not the specific `"Running tests"` / `"Reading api.py"` values the PR describes.

When the consolidated `SDKAssistantMessage` arrives with the full input, `alreadyCached` is already `true`, so the updated title is never sent. The only path that produces a specific title is a non-streaming scenario where `handleUserAssistantMessage` is the first call site for a given tool.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +261 to +264
})
.catch(() => {
// Best-effort — a failed status broadcast must not break tool execution.
});

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.

P2 Silent error suppression makes systematic failures invisible

The .catch(() => {}) swallows all errors without logging. If the extNotification call is systematically failing (e.g. due to a schema mismatch, a disconnected client, or a downstream bug), there will be no signal in any log output. A single ctx.logger.warn(...) call in the catch handler would make these failures observable without changing the fire-and-forget semantics.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/agent/src/adapters/claude/conversion/sdk-to-acp.ts
Line: 261-264

Comment:
**Silent error suppression makes systematic failures invisible**

The `.catch(() => {})` swallows all errors without logging. If the `extNotification` call is systematically failing (e.g. due to a schema mismatch, a disconnected client, or a downstream bug), there will be no signal in any log output. A single `ctx.logger.warn(...)` call in the catch handler would make these failures observable without changing the fire-and-forget semantics.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

…eview

The downstream Slack orchestrator now renders each agent action as a
plan-block step (task_update chunk) with the bare tool name as the
title and a short preview of the args as the details line. Match Slack's
task_update field shape so the orchestrator can pass these through with
no further transformation.

tool_args_preview is derived from the first matching arg field
(bash command / file_path / path / query / pattern / url / description),
collapsed to a single line and trimmed to 240 chars (well under the
256-char Slack limit).
The Slack orchestrator suppresses the post-turn 'reply' relay when the
streaming plan-block path is active (the streamed message already
carries the agent's text). Questions still need to render as a
standalone thread message so the user can see and respond, so they're
passed through with kind="question".

Default stays "reply" — pre-existing callers continue to behave the
same; only the question relay opts into the new kind.
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