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.
- package/README.md +22 -0
- package/dist/claude-agents.d.mts +20 -0
- package/dist/claude-agents.d.ts +20 -0
- package/dist/claude-agents.js +2 -0
- package/dist/claude-agents.js.map +1 -0
- package/dist/esm/{chunk-SWBNW72U.js → chunk-DFBRFJOL.js} +2 -2
- package/dist/esm/{chunk-SWBNW72U.js.map → chunk-DFBRFJOL.js.map} +1 -1
- package/dist/esm/claude-agents.js +2 -0
- package/dist/esm/claude-agents.js.map +1 -0
- package/dist/esm/index.js +1 -1
- package/dist/esm/openai-agents.js +2 -2
- package/dist/esm/openai-agents.js.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/openai-agents.js +2 -2
- package/dist/openai-agents.js.map +1 -1
- package/package.json +18 -1
- package/vendor/claude-agents/trace/.claude-plugin/plugin.json +8 -0
- package/vendor/claude-agents/trace/hooks/hook_utils.py +38 -0
- package/vendor/claude-agents/trace/hooks/hooks.json +60 -0
- package/vendor/claude-agents/trace/hooks/lib.sh +577 -0
- package/vendor/claude-agents/trace/hooks/parse_stop_transcript.py +375 -0
- package/vendor/claude-agents/trace/hooks/post_tool_use.sh +41 -0
- package/vendor/claude-agents/trace/hooks/session_end.sh +37 -0
- package/vendor/claude-agents/trace/hooks/session_start.sh +57 -0
- package/vendor/claude-agents/trace/hooks/stop_hook.sh +123 -0
- package/vendor/claude-agents/trace/hooks/user_prompt_submit.sh +25 -0
- package/vendor/claude-agents/vendor_metadata.json +5 -0
- package/.github/CODEOWNERS +0 -1
- package/.github/workflows/node.js.yml +0 -30
- package/.github/workflows/npm-publish.yml +0 -35
- package/src/groups.ts +0 -16
- package/src/index.ts +0 -383
- package/src/integrations/openai-agents/helpers.test.ts +0 -254
- package/src/integrations/openai-agents/ids.ts +0 -27
- package/src/integrations/openai-agents/index.ts +0 -8
- package/src/integrations/openai-agents/instrumentation.test.ts +0 -46
- package/src/integrations/openai-agents/instrumentation.ts +0 -47
- package/src/integrations/openai-agents/mapping.ts +0 -714
- package/src/integrations/openai-agents/otlp-json.ts +0 -120
- package/src/integrations/openai-agents/processor.test.ts +0 -509
- package/src/integrations/openai-agents/processor.ts +0 -388
- package/src/integrations/openai-agents/time.ts +0 -56
- package/src/integrations/openai-agents/types.ts +0 -49
- package/src/integrations/openai-agents/url.ts +0 -9
- package/src/openai-agents.ts +0 -1
- package/src/promptlayer.ts +0 -125
- package/src/run-error-tracking.test.ts +0 -146
- package/src/span-exporter.ts +0 -120
- package/src/span-wrapper.ts +0 -51
- package/src/templates.ts +0 -37
- package/src/tracing.ts +0 -20
- package/src/track.ts +0 -84
- package/src/types.ts +0 -689
- package/src/utils/blueprint-builder.test.ts +0 -727
- package/src/utils/blueprint-builder.ts +0 -1453
- package/src/utils/errors.test.ts +0 -68
- package/src/utils/errors.ts +0 -62
- package/src/utils/streaming.test.ts +0 -498
- package/src/utils/streaming.ts +0 -1402
- package/src/utils/utils.ts +0 -1228
- package/tsconfig.json +0 -115
- package/tsup.config.ts +0 -20
- 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"
|
package/.github/CODEOWNERS
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
* @jzone3
|