# Richer agent transcripts + lifecycle data for observability (Langfuse / stop hooks)

**URL:** <https://forum.cursor.com/t/richer-agent-transcripts-lifecycle-data-for-observability-langfuse-stop-hooks/166592>\
**Category:** Feature Requests\
**Tags:** cli, hooks, cloud-agents\
**Created:** [July 24, 2026, 11:43pm UTC](https://forum.cursor.com/t/richer-agent-transcripts-lifecycle-data-for-observability-langfuse-stop-hooks/166592 "2026-07-24T23:43:35Z")\
**Posts on this page:** 4\
**Page:** 1

<div class="post-metadata">

**Author:** ![Martin\_Kaiser](https://avatars.discourse-cdn.com/v4/letter/m/e8c25b/32.png) [@Martin\_Kaiser](https://forum.cursor.com/u/Martin_Kaiser)\
**Post date:** [July 24, 2026, 11:43pm UTC](https://forum.cursor.com/t/richer-agent-transcripts-lifecycle-data-for-observability-langfuse-stop-hooks/166592/1 "2026-07-24T23:43:35Z")

</div>

### Feature request for product/service

Cursor CLI

### Describe the request

## Feature request: richer agent transcripts + lifecycle data for observability

**Context.** We’re replaying Cursor agent sessions into Langfuse via the `stop` hook + on-disk  
transcript (`~/.cursor/projects/.../agent-transcripts/`). That pattern works (zero SDK in the  
request path), but Cursor’s transcript is much thinner than Claude Code’s JSONL, so traces are  
incomplete.  
**What already works well**

- `stop` fires in local CLI + IDE and includes `conversation_id`, `transcript_path`, `model` /  
`model_id` / `model_params`
- Transcript has user/assistant text and `tool_use` (name + input)
- Nested layouts for transcripts are discoverable  
**What we can’t get today (and why it matters)**  
| Gap | Impact on Langfuse / evals |  
|—|—|  
| No per-row timestamps | Waterfall timings are synthetic; can’t measure real tool latency or turn  
duration |  
| No token / cache usage | No cost dashboards, no model cost comparison |  
| No `tool_result` / tool outputs | Tool spans are input-only; can’t debug failures or build eval  
datasets from real tool I/O |  
| No message / tool-call IDs | Hard to join tool\_use ↔ tool\_result, hard to dedupe |  
| Model only on `stop`, not on transcript rows | If several turns flush in one `stop`, older turns  
get the _current_ model |  
| `afterAgentResponse` / `afterAgentThought` not in CLI | Can’t stream mid-turn text/thinking into  
observability without stdout hacks |  
| Cloud agents: lifecycle hooks historically incomplete | Same setup doesn’t cover Cloud the same  
way as local |  
| No project env injection into hooks | Secrets/config need sidecar files; Claude Code’s  
`settings.local.json` env is cleaner |

* * *

### Ask 1 — Enrich the conversation transcript (highest value)

Emit one JSONL object per event with a stable schema, e.g.:

```json
{
  "type": "user" | "assistant" | "tool_result" | "turn_ended",
  "id": "msg_…",
  "timestamp": "2026-07-24T22:00:00.123Z",
  "role": "user" | "assistant" | "tool",
  "model": "claude-opus-4-8-thinking-high",
  "model_id": "claude-opus-4-8",
  "message": {
    "id": "msg_…",
    "content": [
      { "type": "text", "text": "…" },
      { "type": "tool_use", "id": "toolu_…", "name": "Shell", "input": { } },
      { "type": "tool_result", "tool_use_id": "toolu_…", "content": "…", "is_error": false }
    ],
    "usage": {
      "input_tokens": 0,
      "output_tokens": 0,
      "cache_read_input_tokens": 0,
      "cache_creation_input_tokens": 0
    }
  }
}

```

**Minimum viable fields (in priority order):**

1. ISO timestamps on every row
2. `usage` on assistant rows (input/output + cache if available)
3. `tool_result` rows (or blocks) with `tool_use_id` + truncated output + error flag
4. Stable `id` on messages and `tool_use`
5. `model` / `model_id` on each assistant row (not only on `stop`)  
Optional but useful: thinking blocks (or a redacted length/hash), request/generation id matching  
the hook payload’s `generation_id`.

* * *

### Ask 2 — Flush transcript before `stop` (or document a ready signal)

Today `stop` can fire before the last turn’s lines are on disk, so hooks must poll. Prefer:

- Flush + fsync transcript before invoking `stop`, **or**
- Add `"transcript_ready": true` / `transcript_byte_size` on the `stop` payload so hooks don’t  
guess

* * *

### Ask 3 — CLI / Cloud hook parity

- Fire `afterAgentResponse` and `afterAgentThought` in headless CLI (`agent --print`) the same as  
IDE
- Fire lifecycle hooks (`sessionStart`, `stop`, `afterAgentResponse`) on Cloud agents with the  
same payload shape as local
- Keep `stop` as the reliable “turn complete” signal; don’t make observability depend only on tool  
hooks

* * *

### Ask 4 — Hook config / secrets ergonomics

- Allow project-scoped env for hooks (similar to Claude Code `settings.local.json` → `env`), or  
document a supported secrets path
- Keep `transcript_path` always absolute and non-null when transcripts are enabled; document  
nested vs flat transcript layouts as a stable contract

* * *

### Ask 5 — Privacy controls (so richer data is shippable)

If richer transcripts raise privacy concerns, ship toggles rather than omitting fields entirely:

- `transcript.include_tool_results`: off | truncated | full
- `transcript.include_usage`: on/off
- `transcript.max_tool_result_chars`: N  
Default can stay conservative; power users / teams can opt into observability fidelity.

* * *

### Why this matters

With the fields above, third-party hooks can produce Langfuse-quality traces **without  
instrumenting the agent or proxying model calls**: real waterfalls, real cost, joinable tool I/O,  
and accurate per-turn model tags — the same bar Claude Code already enables via its Stop-hook +  
JSONL transcript design.  
Happy to share a sample of current vs desired JSONL from local `agent-transcripts/` if useful.

---

<div class="post-metadata">

**Author:** ![deanrie](https://sea3.discourse-cdn.com/cursor1/user_avatar/forum.cursor.com/deanrie/32/1263_2.png) [@deanrie](https://forum.cursor.com/u/deanrie)\
**Post date:** [July 26, 2026, 5:49pm UTC](https://forum.cursor.com/t/richer-agent-transcripts-lifecycle-data-for-observability-langfuse-stop-hooks/166592/5 "2026-07-26T17:49:50Z")

</div>

Hey, thanks for such a well thought-out request. The Ask 1 to 5 breakdown with an example of the desired JSONL schema is exactly the level of detail we need. I’ve forwarded it as a feature request.

I also want to clarify the status on a few items so you don’t build a pipeline on the wrong expectations:

- `stop` payload: it fires in local CLI plus IDE and includes `conversation_id`, `transcript_path`, `model` or `model_id` or `model_params`. Token usage `input_tokens`, `output_tokens`, `cache_*` is not included in the `stop` payload right now. That’s one of the key points in your request and it’s tracked.
- Token usage, thinking, and assistant message are available today via headless `--print` with `--output-format stream-json`. The final `result` event includes token usage and the request ID, and along the way you also get thinking deltas and the full assistant message. For cost dashboards and trace reconstruction, this is a workable interim option without transcript enrichment.
- `afterAgentResponse` and `afterAgentThought`: these do not fire in CLI right now, neither headless nor interactive. This is a known gap, see [Hooks afterAgentResponse / afterAgentThought not firing in headless CLI](https://forum.cursor.com/t/hooks-afteragentresponse-afteragentthought-not-firing-in-headless-cli/156220). They work in the IDE.
- Env for hooks Ask 4: the `sessionStart` hook response can set env for later hook calls in the session, but there are important caveats. Subagents do not inherit that env, see [SubAgents do not inherit environment variables](https://forum.cursor.com/t/subagents-do-not-inherit-environment-variables/155212), and variables are lost when you close and reopen a chat or restart Cursor. `sessionStart` is not replayed on resume, see [Env exported in sessionStart hook does not persist across Cursor restarts](https://forum.cursor.com/t/env-exported-in-sessionstart-hook-does-not-persist-across-cursor-restarts/157196). So as a channel for persistent secrets it’s not reliable right now. Sidecar files are the more honest option for now.

On the main asks, Ask 1 transcript schema enrichment, per-row timestamps, `usage`, `tool_result` with `tool_use_id`, stable IDs, per-row model, and Ask 2 flush or ready signal before `stop`, there’s no implementation timeline yet. The request is logged in that form.

Also, a sample current vs desired JSONL from your local `agent-transcripts/` would be helpful. If you can share it here, it’ll help us pin down the minimum viable schema more precisely.

---

<div class="post-metadata">

**Author:** ![Martin\_Kaiser](https://avatars.discourse-cdn.com/v4/letter/m/e8c25b/32.png) [@Martin\_Kaiser](https://forum.cursor.com/u/Martin_Kaiser)\
**Post date:** [July 27, 2026, 1:07am UTC](https://forum.cursor.com/t/richer-agent-transcripts-lifecycle-data-for-observability-langfuse-stop-hooks/166592/7 "2026-07-27T01:07:45Z")

</div>

Thanks for the clarifications — really helpful so we don’t build on the wrong contract.  
Quick confirmations from our side:

- We’ll keep treating **sidecar files** (e.g. `.cursor/langfuse.env`) as the honest path for  
secrets/opt-in, given the `sessionStart` env caveats (subagents, restart/resume).
- For **cost / usage / thinking** , we’ll treat `agent --print --output-format stream-json` as the  
interim path (final `result` + thinking/assistant events), separate from the `stop` + transcript path.
- We’ll keep relying on **`stop`** for local CLI + IDE turn completion, and not on `afterAgentResponse`  
/ `afterAgentThought` in CLI.  
Below is a **current vs desired** sample from a real local transcript (paths redacted). Source layout:  
`~/.cursor/projects/<encoded-cwd>/agent-transcripts/<conversation_id>/<conversation_id>.jsonl`

### Current (actual rows from `agent-transcripts/`)

Full-file stats for one conversation (87 rows):

- top-level keys are only `role` + `message` (no `timestamp`, no `type` control rows in this file)
- `message` keys are only `content` (no `id`, no `usage`, no `model`)
- content blocks are only `text` and `tool_use`
- every `tool_use` has keys exactly: `type`, `name`, `input` — **no `id`**
- **0** `tool_result` blocks, **0** usage fields, **0** timestamps, **0** message ids  
Example (redacted / truncated):

```jsonl
{"role":"user","message":{"content":[{"type":"text","text":"<timestamp>Thursday, Jul 23, 2026, 11:20 PM
(UTC+2)</timestamp>\n<user_query>\n• For Cursor, do you want full automatic Langfuse tracing…"}]}}
{"role":"assistant","message":{"content":[{"type":"text","text":"I'll look at the current state of the
project…"},{"type":"tool_use","name":"Shell","input":{"command":"ls -la && echo \"---DOCS---\" && ls -la
docs","description":"List project
structure"}},{"type":"tool_use","name":"Glob","input":{"glob_pattern":"**/*cursor*"}}]}}
{"role":"assistant","message":{"content":[{"type":"tool_use","name":"Read","input":{"path":"/Users/<reda
cted>/.../README.md"}},{"type":"tool_use","name":"Shell","input":{"command":"ls -la
hooks","description":"Inspect hooks"}}]}}
{"role":"assistant","message":{"content":[{"type":"text","text":"I dug into the current state…"},{"type"
:"tool_use","name":"AskQuestion","input":{"questions":[{"id":"cursor_path","prompt":"Which do you want
me to do for Cursor?","options":[{"id":"docs_first","label":"docs/cursor.md
first…"},{"id":"full_auto","label":"Full automatic tracing now…"}]}]}}]}}

```

Note: some older sessions also had a control row like `{"type":"turn_ended","status":"success"}`; this  
newer file has none. Worth treating that as optional/unstable until the schema is formalized.

### Desired (minimum viable for Langfuse-quality stop-hook replay)

Same turn, enriched so a `stop` hook can emit real waterfalls + cost + joinable tools without pairing  
`stream-json`:

```jsonl
{"type":"user","id":"msg_u1","timestamp":"2026-07-23T21:20:00.120Z","role":"user","message":{"id":"msg_u
1","content":[{"type":"text","text":"<user_query>…</user_query>"}]}}
{"type":"assistant","id":"msg_a1","timestamp":"2026-07-23T21:20:01.450Z","role":"assistant","model":"cla
ude-opus-4-8-thinking-high","model_id":"claude-opus-4-8","message":{"id":"msg_a1","content":[{"type":"te
xt","text":"I'll look at the current state of the
project…"},{"type":"tool_use","id":"toolu_1","name":"Shell","input":{"command":"ls
-la","description":"List project structure"}},{"type":"tool_use","id":"toolu_2","name":"Glob","input":{"
glob_pattern":"**/*cursor*"}}],"usage":{"input_tokens":18240,"output_tokens":312,"cache_read_input_token
s":12000,"cache_creation_input_tokens":800}}}
{"type":"tool_result","id":"msg_tr1","timestamp":"2026-07-23T21:20:01.820Z","tool_use_id":"toolu_1","con
tent":"total 56\ndrwxr-xr-x …","is_error":false}
{"type":"tool_result","id":"msg_tr2","timestamp":"2026-07-23T21:20:01.910Z","tool_use_id":"toolu_2","con
tent":"[\"hooks/cursor_langfuse_hook.py\", …]","is_error":false}
{"type":"assistant","id":"msg_a2","timestamp":"2026-07-23T21:20:03.100Z","role":"assistant","model":"cla
ude-opus-4-8-thinking-high","model_id":"claude-opus-4-8","message":{"id":"msg_a2","content":[{"type":"te
xt","text":"I dug into the current state…"}],"usage":{"input_tokens":19102,"output_tokens":840,"cache_re
ad_input_tokens":15000,"cache_creation_input_tokens":0}}}
{"type":"turn_ended","timestamp":"2026-07-23T21:20:03.200Z","status":"success","generation_id":"gen_…"}

```

### Minimum fields that unblock us (priority order)

1. ISO `timestamp` on every row
2. `usage` on assistant rows (`input_tokens` / `output_tokens` / cache\_\*)
3. `tool_result` with `tool_use_id` (+ truncated `content`, `is_error`)
4. Stable `id` on messages and `tool_use`
5. `model` / `model_id` on each assistant row (not only on `stop`)  
Plus Ask 2: flush / `transcript_ready` (or `transcript_byte_size`) before `stop` so hooks don’t race the  
async write.  
Happy to share a longer redacted file privately if useful. Thanks again for logging Ask 1–5 and for the  
interim `stream-json` guidance.

────────────────────────────────────────

Implication for our hook: stay on stop + transcript; don’t rely on CLI afterAgentResponse or  
sessionStart env for secrets. Optional later: a CLI stream-json sidecar for usage/cost when people run  
headless.

---

<div class="post-metadata">

**Author:** ![deanrie](https://sea3.discourse-cdn.com/cursor1/user_avatar/forum.cursor.com/deanrie/32/1263_2.png) [@deanrie](https://forum.cursor.com/u/deanrie)\
**Post date:** [July 27, 2026, 6:15am UTC](https://forum.cursor.com/t/richer-agent-transcripts-lifecycle-data-for-observability-langfuse-stop-hooks/166592/8 "2026-07-27T06:15:40Z")

</div>

Thanks for the sample. Comparing current vs desired on a real file really helps lock down the minimum viable schema. I added it to the request along with the prioritized field list ISO `timestamp` on every row, `usage` on assistant rows, `tool_result` with `tool_use_id`, stable `id`, and per-row `model`, plus Ask 2 flush, `transcript_ready`, and `transcript_byte_size` before `stop`.

Your three working assumptions are correct at the moment:

- Sidecar files like `.cursor/langfuse.env` are a reliable way to handle secrets and opt-in, given the caveats with `sessionStart` env variables sub-agents, restart, and resume.
- `agent --print --output-format stream-json` is a valid interim solution for usage, cost, and thinking the final `result` event plus thinking and assistant events, separate from `stop` plus the transcript.
- `stop` is for turn completion in the local CLI and IDE, without relying on `afterAgentResponse` or `afterAgentThought` in the CLI.

On the main items Ask 1 schema enrichment and Ask 2 flush and ready signal there’s no implementation timeline yet. The request is recorded in this form. If needed, you can share a longer redacted file, it won’t hurt. If you hit any more contract mismatches while working on the hook, message me here.
