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

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.:

{
  "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.jsonenv), 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.

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. 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, 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. 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.

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, inputno id
  • 0 tool_result blocks, 0 usage fields, 0 timestamps, 0 message ids
    Example (redacted / truncated):
{"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:

{"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.

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.