promptlayer 1.2.0 → 1.2.2
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/dist/claude-agents.js +1 -1
- package/dist/claude-agents.js.map +1 -1
- package/dist/esm/{chunk-DFBRFJOL.js → chunk-7Y65WGSZ.js} +2 -2
- package/dist/esm/claude-agents.js +1 -1
- package/dist/esm/claude-agents.js.map +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/openai-agents.js +1 -1
- package/dist/index.js +1 -1
- package/dist/openai-agents.js +1 -1
- package/package.json +1 -1
- package/vendor/claude-agents/trace/hooks/lib.sh +4 -522
- package/vendor/claude-agents/trace/hooks/post_tool_use.sh +2 -27
- package/vendor/claude-agents/trace/hooks/py/__init__.py +1 -0
- package/vendor/claude-agents/trace/hooks/py/cli.py +81 -0
- package/vendor/claude-agents/trace/hooks/py/context.py +63 -0
- package/vendor/claude-agents/trace/hooks/py/handlers.py +244 -0
- package/vendor/claude-agents/trace/hooks/py/otlp.py +278 -0
- package/vendor/claude-agents/trace/hooks/py/settings.py +33 -0
- package/vendor/claude-agents/trace/hooks/py/state.py +135 -0
- package/vendor/claude-agents/trace/hooks/{parse_stop_transcript.py → py/stop_parser.py} +69 -31
- package/vendor/claude-agents/trace/hooks/py/traceparent.py +31 -0
- package/vendor/claude-agents/trace/hooks/session_end.sh +1 -23
- package/vendor/claude-agents/trace/hooks/session_start.sh +5 -41
- package/vendor/claude-agents/trace/hooks/stop_hook.sh +3 -106
- package/vendor/claude-agents/trace/hooks/user_prompt_submit.sh +1 -11
- package/vendor/claude-agents/trace/setup.sh +170 -0
- package/vendor/claude-agents/vendor_metadata.json +2 -2
- package/vendor/claude-agents/trace/hooks/hook_utils.py +0 -38
- /package/dist/esm/{chunk-DFBRFJOL.js.map → chunk-7Y65WGSZ.js.map} +0 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
from traceparent import parse_traceparent
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class SessionState:
|
|
12
|
+
trace_id: str = ""
|
|
13
|
+
session_span_id: str = ""
|
|
14
|
+
session_parent_span_id: str = ""
|
|
15
|
+
session_start_ns: str = ""
|
|
16
|
+
current_turn_start_ns: str = ""
|
|
17
|
+
pending_tool_calls: str = ""
|
|
18
|
+
session_init_source: str = ""
|
|
19
|
+
session_traceparent_version: str = ""
|
|
20
|
+
session_trace_flags: str = ""
|
|
21
|
+
trace_context_source: str = ""
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
def from_dict(cls, data):
|
|
25
|
+
if not isinstance(data, dict):
|
|
26
|
+
return cls()
|
|
27
|
+
return cls(**{field: str(data.get(field, "")) for field in cls.__dataclass_fields__})
|
|
28
|
+
|
|
29
|
+
def to_dict(self):
|
|
30
|
+
return {field: getattr(self, field, "") for field in self.__dataclass_fields__}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def compact_json(value) -> str:
|
|
34
|
+
return json.dumps(value, ensure_ascii=False, separators=(",", ":"))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def session_state_path(session_state_dir: str, session_id: str) -> str:
|
|
38
|
+
return os.path.join(session_state_dir, f"{session_id}.json")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def load_session_state(session_state_dir: str, session_id: str):
|
|
42
|
+
path = session_state_path(session_state_dir, session_id)
|
|
43
|
+
if not os.path.exists(path):
|
|
44
|
+
return SessionState(), path
|
|
45
|
+
try:
|
|
46
|
+
with open(path, encoding="utf-8") as f:
|
|
47
|
+
data = json.load(f)
|
|
48
|
+
except Exception:
|
|
49
|
+
return SessionState(), path
|
|
50
|
+
return SessionState.from_dict(data), path
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def save_session_state(path: str, state: SessionState) -> None:
|
|
54
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
55
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
56
|
+
json.dump(state.to_dict(), f, ensure_ascii=False, separators=(",", ":"))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def parse_pending_tool_calls(raw: str):
|
|
60
|
+
if not raw:
|
|
61
|
+
return []
|
|
62
|
+
try:
|
|
63
|
+
data = json.loads(raw)
|
|
64
|
+
except Exception:
|
|
65
|
+
return []
|
|
66
|
+
return data if isinstance(data, list) else []
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def session_lock_path(lock_dir: str, session_id: str) -> str:
|
|
70
|
+
return os.path.join(lock_dir, f"{session_id}.lock")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def queue_lock_path(lock_dir: str) -> str:
|
|
74
|
+
return os.path.join(lock_dir, "queue.lock")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def acquire_lock(path: str, attempts: int = 250, sleep_seconds: float = 0.02) -> bool:
|
|
78
|
+
for _ in range(attempts):
|
|
79
|
+
try:
|
|
80
|
+
os.mkdir(path)
|
|
81
|
+
return True
|
|
82
|
+
except FileExistsError:
|
|
83
|
+
time.sleep(sleep_seconds)
|
|
84
|
+
except Exception:
|
|
85
|
+
return False
|
|
86
|
+
return False
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def release_lock(path: str) -> None:
|
|
90
|
+
try:
|
|
91
|
+
os.rmdir(path)
|
|
92
|
+
except Exception:
|
|
93
|
+
shutil.rmtree(path, ignore_errors=True)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def ensure_session_initialized(
|
|
97
|
+
state: SessionState,
|
|
98
|
+
*,
|
|
99
|
+
traceparent_raw: str,
|
|
100
|
+
generate_trace_id,
|
|
101
|
+
generate_span_id,
|
|
102
|
+
requested_start_ns=None,
|
|
103
|
+
):
|
|
104
|
+
if state.trace_id and state.session_span_id:
|
|
105
|
+
if not state.session_start_ns:
|
|
106
|
+
state.session_start_ns = str(requested_start_ns or time.time_ns())
|
|
107
|
+
if not state.session_init_source:
|
|
108
|
+
state.session_init_source = "unknown"
|
|
109
|
+
if not state.pending_tool_calls:
|
|
110
|
+
state.pending_tool_calls = "[]"
|
|
111
|
+
if not state.session_parent_span_id:
|
|
112
|
+
state.session_parent_span_id = ""
|
|
113
|
+
if not state.session_traceparent_version:
|
|
114
|
+
state.session_traceparent_version = ""
|
|
115
|
+
if not state.session_trace_flags:
|
|
116
|
+
state.session_trace_flags = ""
|
|
117
|
+
if not state.trace_context_source:
|
|
118
|
+
state.trace_context_source = "generated"
|
|
119
|
+
return state, False
|
|
120
|
+
|
|
121
|
+
trace_context = parse_traceparent(traceparent_raw)
|
|
122
|
+
if requested_start_ns is None:
|
|
123
|
+
requested_start_ns = time.time_ns()
|
|
124
|
+
|
|
125
|
+
state.trace_id = trace_context["trace_id"] if trace_context else generate_trace_id()
|
|
126
|
+
state.session_span_id = generate_span_id()
|
|
127
|
+
state.session_parent_span_id = trace_context["parent_span_id"] if trace_context else ""
|
|
128
|
+
state.session_start_ns = str(requested_start_ns)
|
|
129
|
+
state.current_turn_start_ns = ""
|
|
130
|
+
state.pending_tool_calls = "[]"
|
|
131
|
+
state.session_init_source = "lazy_init"
|
|
132
|
+
state.session_traceparent_version = trace_context["version"] if trace_context else ""
|
|
133
|
+
state.session_trace_flags = trace_context["trace_flags"] if trace_context else ""
|
|
134
|
+
state.trace_context_source = trace_context["source"] if trace_context else "generated"
|
|
135
|
+
return state, True
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
2
|
+
"""Pure transcript parsing and stop-hook span-spec derivation."""
|
|
3
3
|
|
|
4
4
|
import json
|
|
5
|
-
import os
|
|
6
|
-
import sys
|
|
7
5
|
from datetime import datetime, timezone
|
|
8
6
|
|
|
7
|
+
from otlp import SpanSpec
|
|
8
|
+
|
|
9
9
|
|
|
10
10
|
def parse_iso_to_ns(raw):
|
|
11
11
|
if not raw:
|
|
@@ -172,9 +172,9 @@ def parse_transcript(transcript_path, turn_start_fallback, pending_payloads, exp
|
|
|
172
172
|
is_error = bool(block.get("is_error", False))
|
|
173
173
|
|
|
174
174
|
match_idx = None
|
|
175
|
-
for
|
|
175
|
+
for candidate_idx, item in enumerate(pending_tool_uses):
|
|
176
176
|
if tool_use_id and item.get("id") == tool_use_id:
|
|
177
|
-
match_idx =
|
|
177
|
+
match_idx = candidate_idx
|
|
178
178
|
break
|
|
179
179
|
if match_idx is None and pending_tool_uses:
|
|
180
180
|
match_idx = 0
|
|
@@ -277,8 +277,6 @@ def parse_transcript(transcript_path, turn_start_fallback, pending_payloads, exp
|
|
|
277
277
|
}
|
|
278
278
|
)
|
|
279
279
|
|
|
280
|
-
# Claude can emit intermediate assistant records that contain only
|
|
281
|
-
# empty thinking blocks. Those should not consume the user's prompt.
|
|
282
280
|
if not output_text and not tool_calls:
|
|
283
281
|
continue
|
|
284
282
|
|
|
@@ -345,31 +343,71 @@ def parse_transcript(transcript_path, turn_start_fallback, pending_payloads, exp
|
|
|
345
343
|
}
|
|
346
344
|
|
|
347
345
|
|
|
348
|
-
def
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
346
|
+
def build_stop_hook_span_specs(
|
|
347
|
+
*,
|
|
348
|
+
parsed,
|
|
349
|
+
trace_id,
|
|
350
|
+
session_span_id,
|
|
351
|
+
session_parent_span_id,
|
|
352
|
+
session_start_ns,
|
|
353
|
+
session_init_source,
|
|
354
|
+
generate_span_id,
|
|
355
|
+
):
|
|
356
|
+
turn = parsed.get("turn", {})
|
|
357
|
+
turn_start_ns = str(turn.get("start_ns", session_start_ns))
|
|
358
|
+
turn_end_ns = str(turn.get("end_ns", turn_start_ns))
|
|
359
|
+
|
|
360
|
+
if session_init_source == "lazy_init":
|
|
361
|
+
session_hook_attr = "StopFallback"
|
|
362
|
+
session_lifecycle_attr = "stop_fallback"
|
|
363
|
+
else:
|
|
364
|
+
session_hook_attr = "Stop"
|
|
365
|
+
session_lifecycle_attr = "in_progress"
|
|
366
|
+
|
|
367
|
+
span_specs = [
|
|
368
|
+
SpanSpec(
|
|
369
|
+
trace_id=trace_id,
|
|
370
|
+
span_id=session_span_id,
|
|
371
|
+
parent_span_id=session_parent_span_id,
|
|
372
|
+
name="Claude Code session",
|
|
373
|
+
kind="1",
|
|
374
|
+
start_ns=str(session_start_ns),
|
|
375
|
+
end_ns=turn_end_ns,
|
|
376
|
+
attrs={
|
|
377
|
+
"source": "claude-code",
|
|
378
|
+
"hook": session_hook_attr,
|
|
379
|
+
"node_type": "WORKFLOW",
|
|
380
|
+
"session.lifecycle": session_lifecycle_attr,
|
|
381
|
+
},
|
|
382
|
+
)
|
|
383
|
+
]
|
|
384
|
+
|
|
385
|
+
for tool in parsed.get("tools", []):
|
|
386
|
+
span_specs.append(
|
|
387
|
+
SpanSpec(
|
|
388
|
+
trace_id=trace_id,
|
|
389
|
+
span_id=generate_span_id(),
|
|
390
|
+
parent_span_id=session_span_id,
|
|
391
|
+
name=tool.get("name", ""),
|
|
392
|
+
kind="3",
|
|
393
|
+
start_ns=str(tool.get("start_ns", turn_start_ns)),
|
|
394
|
+
end_ns=str(tool.get("end_ns", turn_end_ns)),
|
|
395
|
+
attrs=tool.get("attributes", {}),
|
|
353
396
|
)
|
|
354
397
|
)
|
|
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
398
|
|
|
399
|
+
for llm in parsed.get("llms", []):
|
|
400
|
+
span_specs.append(
|
|
401
|
+
SpanSpec(
|
|
402
|
+
trace_id=trace_id,
|
|
403
|
+
span_id=generate_span_id(),
|
|
404
|
+
parent_span_id=session_span_id,
|
|
405
|
+
name=llm.get("name", ""),
|
|
406
|
+
kind="3",
|
|
407
|
+
start_ns=str(llm.get("start_ns", turn_start_ns)),
|
|
408
|
+
end_ns=str(llm.get("end_ns", turn_end_ns)),
|
|
409
|
+
attrs=llm.get("attributes", {}),
|
|
410
|
+
)
|
|
411
|
+
)
|
|
373
412
|
|
|
374
|
-
|
|
375
|
-
raise SystemExit(main())
|
|
413
|
+
return span_specs
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
def parse_traceparent(raw: str):
|
|
2
|
+
if not raw:
|
|
3
|
+
return None
|
|
4
|
+
|
|
5
|
+
parts = raw.lower().split("-")
|
|
6
|
+
if len(parts) < 4:
|
|
7
|
+
return None
|
|
8
|
+
|
|
9
|
+
version, trace_id, parent_span_id, trace_flags = parts[:4]
|
|
10
|
+
suffix = parts[4:]
|
|
11
|
+
|
|
12
|
+
if len(version) != 2 or len(trace_id) != 32 or len(parent_span_id) != 16 or len(trace_flags) != 2:
|
|
13
|
+
return None
|
|
14
|
+
|
|
15
|
+
hexdigits = set("0123456789abcdef")
|
|
16
|
+
if any(ch not in hexdigits for ch in version + trace_id + parent_span_id + trace_flags):
|
|
17
|
+
return None
|
|
18
|
+
if version == "ff":
|
|
19
|
+
return None
|
|
20
|
+
if version == "00" and suffix:
|
|
21
|
+
return None
|
|
22
|
+
if trace_id == "0" * 32 or parent_span_id == "0" * 16:
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
"version": version,
|
|
27
|
+
"trace_id": trace_id,
|
|
28
|
+
"parent_span_id": parent_span_id,
|
|
29
|
+
"trace_flags": trace_flags,
|
|
30
|
+
"source": "external_traceparent",
|
|
31
|
+
}
|
|
@@ -9,29 +9,7 @@ tracing_enabled || exit 0
|
|
|
9
9
|
check_requirements || exit 0
|
|
10
10
|
|
|
11
11
|
input="$(cat)"
|
|
12
|
-
session_id="$(
|
|
12
|
+
session_id="$(printf '%s' "$input" | python3 "$SCRIPT_DIR/py/cli.py" session-end)"
|
|
13
13
|
[[ -z "$session_id" ]] && exit 0
|
|
14
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
15
|
log "INFO" "SessionEnd finalized session_id=$session_id"
|
|
@@ -9,49 +9,13 @@ tracing_enabled || exit 0
|
|
|
9
9
|
check_requirements || exit 0
|
|
10
10
|
|
|
11
11
|
input="$(cat)"
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
result="$(printf '%s' "$input" | python3 "$SCRIPT_DIR/py/cli.py" session-start)"
|
|
13
|
+
IFS=$'\t' read -r session_id trace_id status <<<"$result"
|
|
14
|
+
[[ -z "$session_id" ]] && exit 0
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
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"
|
|
16
|
+
if [[ "$status" == "existing" ]]; then
|
|
17
|
+
log "INFO" "SessionStart ignored existing state session_id=$session_id trace_id=$trace_id"
|
|
37
18
|
exit 0
|
|
38
19
|
fi
|
|
39
20
|
|
|
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
21
|
log "INFO" "SessionStart captured session_id=$session_id trace_id=$trace_id"
|
|
@@ -9,115 +9,12 @@ tracing_enabled || exit 0
|
|
|
9
9
|
check_requirements || exit 0
|
|
10
10
|
|
|
11
11
|
input="$(cat)"
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
if [[ -z "$session_id" && -n "$transcript_path" ]]; then
|
|
16
|
-
session_id="$(basename "$transcript_path" .jsonl)"
|
|
17
|
-
fi
|
|
12
|
+
result="$(printf '%s' "$input" | python3 "$SCRIPT_DIR/py/cli.py" stop-hook)"
|
|
13
|
+
IFS=$'\t' read -r session_id status <<<"$result"
|
|
18
14
|
[[ -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
15
|
|
|
58
|
-
|
|
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
|
|
16
|
+
if [[ "$status" == "missing_transcript" ]]; then
|
|
80
17
|
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
18
|
fi
|
|
120
19
|
|
|
121
|
-
emit_spans_batch_file "$spans_file" || true
|
|
122
|
-
|
|
123
20
|
log "INFO" "Stop finalized session_id=$session_id"
|
|
@@ -9,17 +9,7 @@ tracing_enabled || exit 0
|
|
|
9
9
|
check_requirements || exit 0
|
|
10
10
|
|
|
11
11
|
input="$(cat)"
|
|
12
|
-
session_id="$(
|
|
12
|
+
session_id="$(printf '%s' "$input" | python3 "$SCRIPT_DIR/py/cli.py" user-prompt-submit)"
|
|
13
13
|
[[ -z "$session_id" ]] && exit 0
|
|
14
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
15
|
log "INFO" "UserPromptSubmit captured session_id=$session_id"
|