promptlayer 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +22 -0
  2. package/dist/claude-agents.d.mts +20 -0
  3. package/dist/claude-agents.d.ts +20 -0
  4. package/dist/claude-agents.js +2 -0
  5. package/dist/claude-agents.js.map +1 -0
  6. package/dist/esm/{chunk-SWBNW72U.js → chunk-DFBRFJOL.js} +2 -2
  7. package/dist/esm/{chunk-SWBNW72U.js.map → chunk-DFBRFJOL.js.map} +1 -1
  8. package/dist/esm/claude-agents.js +2 -0
  9. package/dist/esm/claude-agents.js.map +1 -0
  10. package/dist/esm/index.js +1 -1
  11. package/dist/esm/openai-agents.js +2 -2
  12. package/dist/esm/openai-agents.js.map +1 -1
  13. package/dist/index.js +2 -2
  14. package/dist/index.js.map +1 -1
  15. package/dist/openai-agents.js +2 -2
  16. package/dist/openai-agents.js.map +1 -1
  17. package/package.json +18 -1
  18. package/vendor/claude-agents/trace/.claude-plugin/plugin.json +8 -0
  19. package/vendor/claude-agents/trace/hooks/hook_utils.py +38 -0
  20. package/vendor/claude-agents/trace/hooks/hooks.json +60 -0
  21. package/vendor/claude-agents/trace/hooks/lib.sh +577 -0
  22. package/vendor/claude-agents/trace/hooks/parse_stop_transcript.py +375 -0
  23. package/vendor/claude-agents/trace/hooks/post_tool_use.sh +41 -0
  24. package/vendor/claude-agents/trace/hooks/session_end.sh +37 -0
  25. package/vendor/claude-agents/trace/hooks/session_start.sh +57 -0
  26. package/vendor/claude-agents/trace/hooks/stop_hook.sh +123 -0
  27. package/vendor/claude-agents/trace/hooks/user_prompt_submit.sh +25 -0
  28. package/vendor/claude-agents/vendor_metadata.json +5 -0
  29. package/.github/CODEOWNERS +0 -1
  30. package/.github/workflows/node.js.yml +0 -30
  31. package/.github/workflows/npm-publish.yml +0 -35
  32. package/src/groups.ts +0 -16
  33. package/src/index.ts +0 -383
  34. package/src/integrations/openai-agents/helpers.test.ts +0 -254
  35. package/src/integrations/openai-agents/ids.ts +0 -27
  36. package/src/integrations/openai-agents/index.ts +0 -8
  37. package/src/integrations/openai-agents/instrumentation.test.ts +0 -46
  38. package/src/integrations/openai-agents/instrumentation.ts +0 -47
  39. package/src/integrations/openai-agents/mapping.ts +0 -714
  40. package/src/integrations/openai-agents/otlp-json.ts +0 -120
  41. package/src/integrations/openai-agents/processor.test.ts +0 -509
  42. package/src/integrations/openai-agents/processor.ts +0 -388
  43. package/src/integrations/openai-agents/time.ts +0 -56
  44. package/src/integrations/openai-agents/types.ts +0 -49
  45. package/src/integrations/openai-agents/url.ts +0 -9
  46. package/src/openai-agents.ts +0 -1
  47. package/src/promptlayer.ts +0 -125
  48. package/src/run-error-tracking.test.ts +0 -146
  49. package/src/span-exporter.ts +0 -120
  50. package/src/span-wrapper.ts +0 -51
  51. package/src/templates.ts +0 -37
  52. package/src/tracing.ts +0 -20
  53. package/src/track.ts +0 -84
  54. package/src/types.ts +0 -689
  55. package/src/utils/blueprint-builder.test.ts +0 -727
  56. package/src/utils/blueprint-builder.ts +0 -1453
  57. package/src/utils/errors.test.ts +0 -68
  58. package/src/utils/errors.ts +0 -62
  59. package/src/utils/streaming.test.ts +0 -498
  60. package/src/utils/streaming.ts +0 -1402
  61. package/src/utils/utils.ts +0 -1228
  62. package/tsconfig.json +0 -115
  63. package/tsup.config.ts +0 -20
  64. package/vitest.config.ts +0 -9
@@ -0,0 +1,375 @@
1
+ #!/usr/bin/env python3
2
+ """Parse a Claude transcript and return finalized turn/tool/llm spans as JSON."""
3
+
4
+ import json
5
+ import os
6
+ import sys
7
+ from datetime import datetime, timezone
8
+
9
+
10
+ def parse_iso_to_ns(raw):
11
+ if not raw:
12
+ return None
13
+ try:
14
+ dt = datetime.fromisoformat(raw.replace("Z", "+00:00"))
15
+ return int(dt.timestamp() * 1_000_000_000)
16
+ except Exception:
17
+ return None
18
+
19
+
20
+ def safe_int(value, default=0):
21
+ try:
22
+ return int(value)
23
+ except Exception:
24
+ return default
25
+
26
+
27
+ def stringify(value):
28
+ if isinstance(value, str):
29
+ return value
30
+ if value is None:
31
+ return ""
32
+ return json.dumps(value, ensure_ascii=False)
33
+
34
+
35
+ def content_to_text(content):
36
+ if isinstance(content, str):
37
+ return content
38
+ if isinstance(content, list):
39
+ parts = []
40
+ for block in content:
41
+ if isinstance(block, dict) and block.get("type") == "text":
42
+ text = block.get("text")
43
+ if isinstance(text, str) and text:
44
+ parts.append(text)
45
+ continue
46
+ serialized = stringify(block)
47
+ if serialized:
48
+ parts.append(serialized)
49
+ return "\n".join(parts).strip()
50
+ if isinstance(content, dict) and content.get("type") == "text":
51
+ text = content.get("text")
52
+ if isinstance(text, str):
53
+ return text
54
+ return stringify(content)
55
+
56
+
57
+ def message_text(content):
58
+ if isinstance(content, str):
59
+ return content
60
+ if isinstance(content, list):
61
+ parts = []
62
+ for block in content:
63
+ if isinstance(block, dict) and block.get("type") == "text":
64
+ text = block.get("text")
65
+ if isinstance(text, str) and text:
66
+ parts.append(text)
67
+ return "\n".join(parts).strip()
68
+ return ""
69
+
70
+
71
+ def flatten_indexed(prefix, items, out):
72
+ for i, item in enumerate(items):
73
+ for key, value in item.items():
74
+ attr_key = f"{prefix}.{i}.{key}"
75
+ if isinstance(value, (dict, list)):
76
+ out[attr_key] = json.dumps(value, ensure_ascii=False)
77
+ else:
78
+ out[attr_key] = value
79
+
80
+
81
+ def append_history_item(history, item):
82
+ if (
83
+ item.get("role") == "user"
84
+ and history
85
+ and history[-1].get("role") == "user"
86
+ and history[-1].get("content") == item.get("content")
87
+ ):
88
+ return
89
+ history.append(item)
90
+
91
+
92
+ def is_tool_result_user(rec):
93
+ if rec.get("type") != "user":
94
+ return False
95
+ content = rec.get("message", {}).get("content")
96
+ return (
97
+ isinstance(content, list)
98
+ and len(content) > 0
99
+ and isinstance(content[0], dict)
100
+ and content[0].get("type") == "tool_result"
101
+ )
102
+
103
+
104
+ def parse_transcript(transcript_path, turn_start_fallback, pending_payloads, expected_session_id=None):
105
+ records = []
106
+ with open(transcript_path, encoding="utf-8") as f:
107
+ for line in f:
108
+ line = line.strip()
109
+ if not line:
110
+ continue
111
+ try:
112
+ rec = json.loads(line)
113
+ if expected_session_id:
114
+ rec_session_id = rec.get("sessionId") if isinstance(rec, dict) else None
115
+ if rec_session_id and rec_session_id != expected_session_id:
116
+ continue
117
+ records.append(rec)
118
+ except Exception:
119
+ continue
120
+
121
+ if not records:
122
+ now_ns = int(datetime.now(timezone.utc).timestamp() * 1_000_000_000)
123
+ return {"turn": {"start_ns": now_ns, "end_ns": now_ns}, "tools": [], "llms": []}
124
+
125
+ turn_start_idx = 0
126
+ for i in range(len(records) - 1, -1, -1):
127
+ rec = records[i]
128
+ if rec.get("type") != "user":
129
+ continue
130
+ if is_tool_result_user(rec):
131
+ continue
132
+ turn_start_idx = i
133
+ break
134
+
135
+ history = []
136
+ tools = []
137
+ llms = []
138
+ pending_tool_uses = []
139
+ pending_payload_idx = 0
140
+ saw_human_input = False
141
+
142
+ turn_start_ns = turn_start_fallback
143
+ turn_end_ns = turn_start_fallback
144
+ last_input_ns = turn_start_fallback
145
+
146
+ for idx, rec in enumerate(records):
147
+ emit_for_turn = idx >= turn_start_idx
148
+ timestamp_ns = parse_iso_to_ns(rec.get("timestamp"))
149
+ if emit_for_turn and timestamp_ns is not None:
150
+ if turn_start_ns is None or timestamp_ns < turn_start_ns:
151
+ turn_start_ns = timestamp_ns
152
+ if turn_end_ns is None or timestamp_ns > turn_end_ns:
153
+ turn_end_ns = timestamp_ns
154
+
155
+ rec_type = rec.get("type")
156
+ if rec_type == "queue-operation":
157
+ operation = stringify(rec.get("operation"))
158
+ if operation == "enqueue":
159
+ content = content_to_text(rec.get("content"))
160
+ if content:
161
+ append_history_item(history, {"role": "user", "content": content})
162
+ last_input_ns = timestamp_ns or last_input_ns
163
+ saw_human_input = True
164
+ continue
165
+
166
+ if rec_type == "user":
167
+ content = rec.get("message", {}).get("content")
168
+ if is_tool_result_user(rec):
169
+ block = content[0]
170
+ tool_use_id = stringify(block.get("tool_use_id"))
171
+ tool_result_content = block.get("content")
172
+ is_error = bool(block.get("is_error", False))
173
+
174
+ match_idx = None
175
+ for idx, item in enumerate(pending_tool_uses):
176
+ if tool_use_id and item.get("id") == tool_use_id:
177
+ match_idx = idx
178
+ break
179
+ if match_idx is None and pending_tool_uses:
180
+ match_idx = 0
181
+ tool_use = pending_tool_uses.pop(match_idx) if match_idx is not None else {}
182
+
183
+ payload = {}
184
+ if emit_for_turn and pending_payload_idx < len(pending_payloads):
185
+ maybe_payload = pending_payloads[pending_payload_idx]
186
+ pending_payload_idx += 1
187
+ if isinstance(maybe_payload, dict):
188
+ payload = maybe_payload
189
+
190
+ tool_name = stringify(payload.get("tool_name")) or stringify(tool_use.get("name")) or "Tool"
191
+ function_input = payload.get("function_input", tool_use.get("input", {}))
192
+ function_output = payload.get(
193
+ "function_output",
194
+ {"content": tool_result_content, "is_error": is_error},
195
+ )
196
+
197
+ tool_start_ns = safe_int(tool_use.get("start_ns"), 0) or timestamp_ns or turn_start_ns
198
+ tool_end_ns = timestamp_ns or tool_start_ns
199
+ if tool_start_ns is None:
200
+ tool_start_ns = tool_end_ns
201
+ if tool_end_ns is None:
202
+ tool_end_ns = tool_start_ns
203
+
204
+ if emit_for_turn:
205
+ tools.append(
206
+ {
207
+ "name": f"Tool: {tool_name}",
208
+ "start_ns": int(tool_start_ns),
209
+ "end_ns": int(tool_end_ns),
210
+ "attributes": {
211
+ "source": "claude-code",
212
+ "hook": "PostToolUse",
213
+ "node_type": "CODE_EXECUTION",
214
+ "tool_name": tool_name,
215
+ "function_input": function_input,
216
+ "function_output": function_output,
217
+ },
218
+ }
219
+ )
220
+
221
+ history.append(
222
+ {
223
+ "role": "tool",
224
+ "tool_call_id": tool_use_id,
225
+ "content": stringify(tool_result_content),
226
+ }
227
+ )
228
+ last_input_ns = timestamp_ns or last_input_ns
229
+ continue
230
+
231
+ user_text = content_to_text(content)
232
+ append_history_item(history, {"role": "user", "content": user_text})
233
+ last_input_ns = timestamp_ns or last_input_ns
234
+ saw_human_input = True
235
+ continue
236
+
237
+ if rec_type != "assistant":
238
+ continue
239
+
240
+ msg = rec.get("message", {})
241
+ model = stringify(msg.get("model")) or "claude"
242
+ msg_id = stringify(msg.get("id"))
243
+ stop_reason = stringify(msg.get("stop_reason"))
244
+ usage = msg.get("usage", {})
245
+ if not isinstance(usage, dict):
246
+ usage = {}
247
+
248
+ prompt_tokens = safe_int(usage.get("input_tokens"), 0)
249
+ completion_tokens = safe_int(usage.get("output_tokens"), 0)
250
+ output_text = message_text(msg.get("content"))
251
+
252
+ tool_calls = []
253
+ content_blocks = msg.get("content")
254
+ if isinstance(content_blocks, list):
255
+ for block in content_blocks:
256
+ if not isinstance(block, dict) or block.get("type") != "tool_use":
257
+ continue
258
+ call_id = stringify(block.get("id"))
259
+ call_name = stringify(block.get("name")) or "tool"
260
+ call_input = block.get("input", {})
261
+ tool_calls.append(
262
+ {
263
+ "id": call_id,
264
+ "type": "function",
265
+ "function": {
266
+ "name": call_name,
267
+ "arguments": json.dumps(call_input, ensure_ascii=False),
268
+ },
269
+ }
270
+ )
271
+ pending_tool_uses.append(
272
+ {
273
+ "id": call_id,
274
+ "name": call_name,
275
+ "input": call_input,
276
+ "start_ns": timestamp_ns,
277
+ }
278
+ )
279
+
280
+ # Claude can emit intermediate assistant records that contain only
281
+ # empty thinking blocks. Those should not consume the user's prompt.
282
+ if not output_text and not tool_calls:
283
+ continue
284
+
285
+ llm_start_ns = last_input_ns or timestamp_ns or turn_start_ns
286
+ llm_end_ns = timestamp_ns or llm_start_ns
287
+ if llm_start_ns is None:
288
+ llm_start_ns = llm_end_ns
289
+ if llm_end_ns is None:
290
+ llm_end_ns = llm_start_ns
291
+
292
+ provider = "anthropic" if model.startswith("claude") else "unknown"
293
+
294
+ attrs = {
295
+ "source": "claude-code",
296
+ "hook": "Stop",
297
+ "node_type": "PROMPT_TEMPLATE",
298
+ "promptlayer.prompt_history_mode": "full_session",
299
+ "gen_ai.operation.name": "chat",
300
+ "gen_ai.provider.name": provider,
301
+ "gen_ai.request.model": model,
302
+ "gen_ai.response.model": model,
303
+ "gen_ai.usage.input_tokens": prompt_tokens,
304
+ "gen_ai.usage.output_tokens": completion_tokens,
305
+ }
306
+ if msg_id:
307
+ attrs["gen_ai.response.id"] = msg_id
308
+ if stop_reason:
309
+ attrs["gen_ai.completion.0.finish_reason"] = stop_reason
310
+
311
+ flatten_indexed("gen_ai.prompt", history, attrs)
312
+
313
+ completion_item = {"role": "assistant", "content": output_text}
314
+ if tool_calls:
315
+ completion_item["tool_calls"] = tool_calls
316
+ flatten_indexed("gen_ai.completion", [completion_item], attrs)
317
+
318
+ span_name = "LLM Call (User)" if saw_human_input else "LLM call"
319
+
320
+ if emit_for_turn:
321
+ llms.append(
322
+ {
323
+ "name": span_name,
324
+ "start_ns": int(llm_start_ns),
325
+ "end_ns": int(llm_end_ns),
326
+ "attributes": attrs,
327
+ }
328
+ )
329
+
330
+ assistant_history = {"role": "assistant", "content": output_text}
331
+ if tool_calls:
332
+ assistant_history["tool_calls"] = tool_calls
333
+ history.append(assistant_history)
334
+ saw_human_input = False
335
+
336
+ if turn_start_ns is None:
337
+ turn_start_ns = int(datetime.now(timezone.utc).timestamp() * 1_000_000_000)
338
+ if turn_end_ns is None:
339
+ turn_end_ns = turn_start_ns
340
+
341
+ return {
342
+ "turn": {"start_ns": int(turn_start_ns), "end_ns": int(turn_end_ns)},
343
+ "tools": tools,
344
+ "llms": llms,
345
+ }
346
+
347
+
348
+ def main():
349
+ if len(sys.argv) < 3:
350
+ print(
351
+ json.dumps(
352
+ {"error": "Usage: parse_stop_transcript.py <transcript_path> <turn_start_ns> [session_id]"}
353
+ )
354
+ )
355
+ return 1
356
+
357
+ transcript_path = sys.argv[1]
358
+ turn_start_fallback = safe_int(sys.argv[2], 0) or None
359
+ expected_session_id = sys.argv[3] if len(sys.argv) > 3 else None
360
+
361
+ pending_raw = os.environ.get("PL_PENDING_TOOL_CALLS", "[]")
362
+ try:
363
+ pending_payloads = json.loads(pending_raw)
364
+ except Exception:
365
+ pending_payloads = []
366
+ if not isinstance(pending_payloads, list):
367
+ pending_payloads = []
368
+
369
+ parsed = parse_transcript(transcript_path, turn_start_fallback, pending_payloads, expected_session_id)
370
+ print(json.dumps(parsed, ensure_ascii=False, separators=(",", ":")))
371
+ return 0
372
+
373
+
374
+ if __name__ == "__main__":
375
+ raise SystemExit(main())
@@ -0,0 +1,41 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ # shellcheck source=plugins/trace/hooks/lib.sh
6
+ source "$SCRIPT_DIR/lib.sh"
7
+
8
+ tracing_enabled || exit 0
9
+ check_requirements || exit 0
10
+
11
+ input="$(cat)"
12
+ session_id="$(echo "$input" | jq -r '.session_id // empty')"
13
+ tool_name="$(echo "$input" | jq -r '.tool_name // empty')"
14
+ tool_input="$(echo "$input" | jq -c '.tool_input // {}')"
15
+ tool_output="$(echo "$input" | jq -c '.tool_response // .output // {}')"
16
+ [[ -z "$session_id" || -z "$tool_name" ]] && exit 0
17
+
18
+ ensure_session_initialized "$session_id"
19
+
20
+ trace_id="$(get_session_state "$session_id" trace_id)"
21
+ [[ -z "$trace_id" ]] && exit 0
22
+ turn_start_ns="$(get_session_state "$session_id" current_turn_start_ns)"
23
+ if [[ -z "$turn_start_ns" ]]; then
24
+ set_session_state "$session_id" current_turn_start_ns "$(now_ns)"
25
+ fi
26
+
27
+ attrs="$(jq -nc \
28
+ --arg source claude-code \
29
+ --arg hook PostToolUse \
30
+ --arg tool_name "$tool_name" \
31
+ --argjson function_input "$tool_input" \
32
+ --argjson function_output "$tool_output" \
33
+ '{source:$source,hook:$hook,tool_name:$tool_name,node_type:"CODE_EXECUTION",function_input:$function_input,function_output:$function_output}')"
34
+
35
+ pending_tool_calls="$(get_session_state "$session_id" pending_tool_calls)"
36
+ [[ -z "$pending_tool_calls" ]] && pending_tool_calls='[]'
37
+
38
+ pending_tool_calls="$(echo "$pending_tool_calls" | jq -c --argjson attrs "$attrs" '. + [$attrs]')"
39
+ set_session_state "$session_id" pending_tool_calls "$pending_tool_calls"
40
+
41
+ log "INFO" "PostToolUse captured session_id=$session_id tool=$tool_name"
@@ -0,0 +1,37 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ # shellcheck source=plugins/trace/hooks/lib.sh
6
+ source "$SCRIPT_DIR/lib.sh"
7
+
8
+ tracing_enabled || exit 0
9
+ check_requirements || exit 0
10
+
11
+ input="$(cat)"
12
+ session_id="$(echo "$input" | jq -r '.session_id // empty')"
13
+ [[ -z "$session_id" ]] && exit 0
14
+
15
+ acquire_session_lock "$session_id" || exit 0
16
+ trap 'release_session_lock' EXIT
17
+
18
+ trace_id="$(get_session_state "$session_id" trace_id)"
19
+ session_span_id="$(get_session_state "$session_id" session_span_id)"
20
+ session_parent_span_id="$(get_session_state "$session_id" session_parent_span_id)"
21
+ session_start_ns="$(get_session_state "$session_id" session_start_ns)"
22
+ [[ -z "$trace_id" || -z "$session_span_id" ]] && exit 0
23
+ [[ -z "$session_start_ns" ]] && session_start_ns="$(now_ns)"
24
+
25
+ release_session_lock
26
+ trap - EXIT
27
+
28
+ # Always emit/re-emit root span with final end time. The server upserts on
29
+ # span_id conflict, so this safely updates the end time and lifecycle attribute.
30
+ end_ns="$(now_ns)"
31
+ attrs='{"source":"claude-code","hook":"SessionEnd","node_type":"WORKFLOW","session.lifecycle":"complete"}'
32
+ emit_span "$trace_id" "$session_span_id" "$session_parent_span_id" "Claude Code session" "1" "$session_start_ns" "$end_ns" "$attrs" || true
33
+
34
+ acquire_session_lock "$session_id" || exit 0
35
+ trap 'release_session_lock' EXIT
36
+ rm -f "$PL_SESSION_STATE_DIR/$session_id.json"
37
+ log "INFO" "SessionEnd finalized session_id=$session_id"
@@ -0,0 +1,57 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ # shellcheck source=plugins/trace/hooks/lib.sh
6
+ source "$SCRIPT_DIR/lib.sh"
7
+
8
+ tracing_enabled || exit 0
9
+ check_requirements || exit 0
10
+
11
+ input="$(cat)"
12
+ session_id="$(echo "$input" | jq -r '.session_id // empty')"
13
+ [[ -z "$session_id" ]] && session_id="$(uuidgen | tr '[:upper:]' '[:lower:]')"
14
+
15
+ existing_trace_id="$(get_session_state "$session_id" trace_id)"
16
+ existing_session_span_id="$(get_session_state "$session_id" session_span_id)"
17
+ if [[ -n "$existing_trace_id" && -n "$existing_session_span_id" ]]; then
18
+ if [[ -z "$(get_session_state "$session_id" session_start_ns)" ]]; then
19
+ set_session_state "$session_id" session_start_ns "$(now_ns)"
20
+ fi
21
+ if [[ -z "$(get_session_state "$session_id" pending_tool_calls)" ]]; then
22
+ set_session_state "$session_id" pending_tool_calls "[]"
23
+ fi
24
+ if [[ -z "$(get_session_state "$session_id" session_parent_span_id)" ]]; then
25
+ set_session_state "$session_id" session_parent_span_id ""
26
+ fi
27
+ if [[ -z "$(get_session_state "$session_id" session_traceparent_version)" ]]; then
28
+ set_session_state "$session_id" session_traceparent_version ""
29
+ fi
30
+ if [[ -z "$(get_session_state "$session_id" session_trace_flags)" ]]; then
31
+ set_session_state "$session_id" session_trace_flags ""
32
+ fi
33
+ if [[ -z "$(get_session_state "$session_id" trace_context_source)" ]]; then
34
+ set_session_state "$session_id" trace_context_source "generated"
35
+ fi
36
+ log "INFO" "SessionStart ignored existing state session_id=$session_id trace_id=$existing_trace_id"
37
+ exit 0
38
+ fi
39
+
40
+ load_initial_trace_context || true
41
+ trace_id="${PL_INITIAL_TRACE_ID:-}"
42
+ [[ -z "$trace_id" ]] && trace_id="$(generate_trace_id)"
43
+ span_id="$(generate_span_id)"
44
+ start_ns="$(now_ns)"
45
+
46
+ set_session_state "$session_id" trace_id "$trace_id"
47
+ set_session_state "$session_id" session_span_id "$span_id"
48
+ set_session_state "$session_id" session_parent_span_id "${PL_INITIAL_PARENT_SPAN_ID:-}"
49
+ set_session_state "$session_id" session_start_ns "$start_ns"
50
+ set_session_state "$session_id" current_turn_start_ns ""
51
+ set_session_state "$session_id" pending_tool_calls "[]"
52
+ set_session_state "$session_id" session_init_source "session_start_hook"
53
+ set_session_state "$session_id" session_traceparent_version "${PL_INITIAL_TRACEPARENT_VERSION:-}"
54
+ set_session_state "$session_id" session_trace_flags "${PL_INITIAL_TRACE_FLAGS:-}"
55
+ set_session_state "$session_id" trace_context_source "${PL_INITIAL_TRACE_CONTEXT_SOURCE:-generated}"
56
+
57
+ log "INFO" "SessionStart captured session_id=$session_id trace_id=$trace_id"
@@ -0,0 +1,123 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ # shellcheck source=plugins/trace/hooks/lib.sh
6
+ source "$SCRIPT_DIR/lib.sh"
7
+
8
+ tracing_enabled || exit 0
9
+ check_requirements || exit 0
10
+
11
+ input="$(cat)"
12
+ session_id="$(echo "$input" | jq -r '.session_id // empty')"
13
+ transcript_path="$(echo "$input" | jq -r '.transcript_path // empty')"
14
+
15
+ if [[ -z "$session_id" && -n "$transcript_path" ]]; then
16
+ session_id="$(basename "$transcript_path" .jsonl)"
17
+ fi
18
+ [[ -z "$session_id" ]] && exit 0
19
+ spans_file="$(mktemp "${TMPDIR:-/tmp}/pl-stop-spans.XXXXXX")"
20
+ cleanup() {
21
+ rm -f "$spans_file"
22
+ release_session_lock
23
+ }
24
+ trap cleanup EXIT
25
+
26
+ add_span_to_batch() {
27
+ local trace="$1"
28
+ local span="$2"
29
+ local parent="$3"
30
+ local span_name="$4"
31
+ local span_kind="$5"
32
+ local start="$6"
33
+ local end="$7"
34
+ local attrs="$8"
35
+
36
+ local span_json
37
+ span_json="$(build_span_json "$trace" "$span" "$parent" "$span_name" "$span_kind" "$start" "$end" "$attrs")" || return 1
38
+ printf '%s\n' "$span_json" >>"$spans_file"
39
+ }
40
+
41
+ acquire_session_lock "$session_id" || exit 0
42
+ ensure_session_initialized "$session_id"
43
+
44
+ trace_id="$(get_session_state "$session_id" trace_id)"
45
+ session_span_id="$(get_session_state "$session_id" session_span_id)"
46
+ session_parent_span_id="$(get_session_state "$session_id" session_parent_span_id)"
47
+ turn_start_ns="$(get_session_state "$session_id" current_turn_start_ns)"
48
+ pending_tool_calls="$(get_session_state "$session_id" pending_tool_calls)"
49
+ session_init_source="$(get_session_state "$session_id" session_init_source)"
50
+ session_start_ns="$(get_session_state "$session_id" session_start_ns)"
51
+
52
+ [[ -z "$trace_id" || -z "$session_span_id" ]] && exit 0
53
+ [[ -z "$pending_tool_calls" ]] && pending_tool_calls='[]'
54
+ [[ -z "$session_start_ns" ]] && session_start_ns="$(now_ns)"
55
+
56
+ [[ -z "$turn_start_ns" ]] && turn_start_ns="$(now_ns)"
57
+
58
+ # Keep lock scope short: snapshot + clear turn-specific mutable state.
59
+ set_session_state "$session_id" current_turn_start_ns ""
60
+ set_session_state "$session_id" pending_tool_calls "[]"
61
+
62
+ release_session_lock
63
+
64
+ parse_transcript_with_retry() {
65
+ local attempts=0
66
+ local parsed llm_count
67
+ while true; do
68
+ parsed="$(PL_PENDING_TOOL_CALLS="$pending_tool_calls" python3 "$SCRIPT_DIR/parse_stop_transcript.py" "$transcript_path" "$turn_start_ns" "$session_id")"
69
+ llm_count="$(echo "$parsed" | jq -r '.llms | length')"
70
+ if [[ "$llm_count" -gt 0 || $attempts -ge 10 ]]; then
71
+ echo "$parsed"
72
+ return 0
73
+ fi
74
+ attempts=$((attempts + 1))
75
+ sleep 0.2
76
+ done
77
+ }
78
+
79
+ if [[ -z "$transcript_path" || ! -f "$transcript_path" ]]; then
80
+ log "WARN" "Stop missing transcript session_id=$session_id"
81
+ else
82
+ parsed="$(parse_transcript_with_retry)"
83
+
84
+ turn_start_ns="$(echo "$parsed" | jq -r '.turn.start_ns')"
85
+ turn_end_ns="$(echo "$parsed" | jq -r '.turn.end_ns')"
86
+
87
+ # Emit (or re-emit) the root session span eagerly so the trace is visible
88
+ # in the UI before the session ends. The server upserts on span_id conflict,
89
+ # so re-emitting with an updated end time is safe.
90
+ if [[ "$session_init_source" == "lazy_init" ]]; then
91
+ session_hook_attr="StopFallback"
92
+ session_lifecycle_attr="stop_fallback"
93
+ else
94
+ session_hook_attr="Stop"
95
+ session_lifecycle_attr="in_progress"
96
+ fi
97
+ session_attrs="{\"source\":\"claude-code\",\"hook\":\"$session_hook_attr\",\"node_type\":\"WORKFLOW\",\"session.lifecycle\":\"$session_lifecycle_attr\"}"
98
+ add_span_to_batch "$trace_id" "$session_span_id" "$session_parent_span_id" "Claude Code session" "1" "$session_start_ns" "$turn_end_ns" "$session_attrs" || true
99
+
100
+ while IFS= read -r tool; do
101
+ [[ -z "$tool" ]] && continue
102
+ span_id="$(generate_span_id)"
103
+ name="$(echo "$tool" | jq -r '.name')"
104
+ start_ns="$(echo "$tool" | jq -r '.start_ns')"
105
+ end_ns="$(echo "$tool" | jq -r '.end_ns')"
106
+ attrs="$(echo "$tool" | jq -c '.attributes')"
107
+ add_span_to_batch "$trace_id" "$span_id" "$session_span_id" "$name" "3" "$start_ns" "$end_ns" "$attrs" || true
108
+ done < <(echo "$parsed" | jq -c '.tools[]?')
109
+
110
+ while IFS= read -r llm; do
111
+ [[ -z "$llm" ]] && continue
112
+ span_id="$(generate_span_id)"
113
+ name="$(echo "$llm" | jq -r '.name')"
114
+ start_ns="$(echo "$llm" | jq -r '.start_ns')"
115
+ end_ns="$(echo "$llm" | jq -r '.end_ns')"
116
+ attrs="$(echo "$llm" | jq -c '.attributes')"
117
+ add_span_to_batch "$trace_id" "$span_id" "$session_span_id" "$name" "3" "$start_ns" "$end_ns" "$attrs" || true
118
+ done < <(echo "$parsed" | jq -c '.llms[]?')
119
+ fi
120
+
121
+ emit_spans_batch_file "$spans_file" || true
122
+
123
+ log "INFO" "Stop finalized session_id=$session_id"
@@ -0,0 +1,25 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ # shellcheck source=plugins/trace/hooks/lib.sh
6
+ source "$SCRIPT_DIR/lib.sh"
7
+
8
+ tracing_enabled || exit 0
9
+ check_requirements || exit 0
10
+
11
+ input="$(cat)"
12
+ session_id="$(echo "$input" | jq -r '.session_id // empty')"
13
+ [[ -z "$session_id" ]] && exit 0
14
+
15
+ ensure_session_initialized "$session_id"
16
+
17
+ trace_id="$(get_session_state "$session_id" trace_id)"
18
+ session_span_id="$(get_session_state "$session_id" session_span_id)"
19
+ [[ -z "$trace_id" || -z "$session_span_id" ]] && exit 0
20
+ start_ns="$(now_ns)"
21
+
22
+ set_session_state "$session_id" current_turn_start_ns "$start_ns"
23
+ set_session_state "$session_id" pending_tool_calls "[]"
24
+
25
+ log "INFO" "UserPromptSubmit captured session_id=$session_id"
@@ -0,0 +1,5 @@
1
+ {
2
+ "repository": "https://github.com/MagnivOrg/promptlayer-claude-plugins",
3
+ "commit_sha": "7dff044080588cffb9b690fcdf772ee0b4f336da",
4
+ "timestamp": "2026-03-18T19:12:56.156Z"
5
+ }
@@ -1 +0,0 @@
1
- * @jzone3