flintai-cli 1.0.0__py3-none-any.whl
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.
- flintai/__init__.py +0 -0
- flintai/cli/__init__.py +0 -0
- flintai/cli/__main__.py +3 -0
- flintai/cli/builtin_config.json +911 -0
- flintai/cli/console.py +116 -0
- flintai/cli/eval_cli.py +891 -0
- flintai/cli/init_cli.py +144 -0
- flintai/cli/main.py +262 -0
- flintai/cli/rich_observer.py +89 -0
- flintai/cli/runner.py +305 -0
- flintai/cli/scan_cli.py +238 -0
- flintai/cli/utils.py +37 -0
- flintai/cli/version.py +1 -0
- flintai/data/detector_prompts/llm01_adversarial.txt +55 -0
- flintai/data/detector_prompts/llm01_fixed.txt +8 -0
- flintai/data/detector_prompts/llm02_adversarial.txt +75 -0
- flintai/data/detector_prompts/llm02_fixed.txt +8 -0
- flintai/data/detector_prompts/llm05_adversarial.txt +63 -0
- flintai/data/detector_prompts/llm05_fixed.txt +16 -0
- flintai/data/detector_prompts/llm06_adversarial.txt +135 -0
- flintai/data/detector_prompts/llm06_fixed.txt +15 -0
- flintai/data/detector_prompts/llm07_adversarial.txt +185 -0
- flintai/data/detector_prompts/llm07_fixed.txt +14 -0
- flintai/data/detector_prompts/llm09_adversarial.txt +77 -0
- flintai/data/detector_prompts/llm09_fixed.txt +15 -0
- flintai/data/llm01_prompt_injection.csv +6708 -0
- flintai/data/llm02_sensitive_information.csv +1069 -0
- flintai/data/llm05_unsafe_output.csv +3882 -0
- flintai/data/llm06_excessive_agency.csv +778 -0
- flintai/data/llm07_system_prompt_leakage.csv +971 -0
- flintai/data/llm09_hallucination-goals.csv +32419 -0
- flintai/data/llm09_hallucination.csv +599 -0
- flintai/data/pii_leakage.csv +706 -0
- flintai/data/secret_leakage.csv +1380 -0
- flintai/eval/__init__.py +0 -0
- flintai/eval/common/converter_anthropic.py +151 -0
- flintai/eval/common/converter_genai.py +169 -0
- flintai/eval/common/converter_openai.py +165 -0
- flintai/eval/common/log.py +96 -0
- flintai/eval/common/reference.py +25 -0
- flintai/eval/common/schema.py +192 -0
- flintai/eval/common/utils.py +74 -0
- flintai/eval/core/__init__.py +0 -0
- flintai/eval/core/detectors/__init__.py +1 -0
- flintai/eval/core/detectors/detector.py +25 -0
- flintai/eval/core/detectors/detector_garak.py +80 -0
- flintai/eval/core/detectors/detector_model.py +76 -0
- flintai/eval/core/detectors/detector_pii.py +64 -0
- flintai/eval/core/detectors/detector_secret.py +82 -0
- flintai/eval/core/detectors/detector_topic_guard.py +70 -0
- flintai/eval/core/detectors/detector_toxicity.py +70 -0
- flintai/eval/core/eval/__init__.py +0 -0
- flintai/eval/core/eval/eval_creator.py +197 -0
- flintai/eval/core/eval/evaluation.py +100 -0
- flintai/eval/core/eval/evaluation_adversarial.py +523 -0
- flintai/eval/core/eval/evaluation_garak_module.py +52 -0
- flintai/eval/core/eval/evaluation_garak_probe.py +274 -0
- flintai/eval/core/eval/evaluation_message_list.py +44 -0
- flintai/eval/core/eval/evaluation_multi.py +207 -0
- flintai/eval/core/eval/evaluation_single.py +79 -0
- flintai/eval/core/eval/evaluation_single_prompt.py +58 -0
- flintai/eval/core/eval/evaluation_topic_guard.py +318 -0
- flintai/eval/core/eval/metric_conciseness.py +181 -0
- flintai/eval/core/eval/metric_factual_accuracy.py +294 -0
- flintai/eval/core/eval/metric_instruction_adherence.py +180 -0
- flintai/eval/core/eval/metric_tone.py +183 -0
- flintai/eval/core/eval/metric_toxicity.py +147 -0
- flintai/eval/core/eval/observer.py +136 -0
- flintai/eval/core/eval/probe_adversarial.py +14 -0
- flintai/eval/core/message/__init__.py +0 -0
- flintai/eval/core/message/message_collection.py +37 -0
- flintai/eval/core/message/message_collection_csv.py +46 -0
- flintai/eval/core/message/message_collection_garak.py +52 -0
- flintai/eval/core/message/message_collection_memory.py +36 -0
- flintai/eval/core/models/__init__.py +0 -0
- flintai/eval/core/models/generator_model.py +88 -0
- flintai/eval/core/models/model.py +111 -0
- flintai/eval/core/models/model_adk.py +158 -0
- flintai/eval/core/models/model_anthropic.py +51 -0
- flintai/eval/core/models/model_anthropic_agent.py +92 -0
- flintai/eval/core/models/model_gemini.py +107 -0
- flintai/eval/core/models/model_generic_http.py +117 -0
- flintai/eval/core/models/model_huggingface.py +61 -0
- flintai/eval/core/models/model_langserve.py +94 -0
- flintai/eval/core/models/model_litellm.py +38 -0
- flintai/eval/core/models/model_ollama.py +50 -0
- flintai/eval/core/models/model_openai.py +35 -0
- flintai/eval/core/models/model_openai_agent.py +80 -0
- flintai/eval/core/models/model_openai_compatible.py +57 -0
- flintai/eval/core/models/model_retry.py +134 -0
- flintai/eval/core/models/model_sync_wrapper.py +35 -0
- flintai/eval/db/__init__.py +0 -0
- flintai/eval/db/base/__init__.py +0 -0
- flintai/eval/db/base/detectors/__init__.py +1 -0
- flintai/eval/db/base/detectors/detector_helpers.py +78 -0
- flintai/eval/db/base/detectors/detector_repository.py +91 -0
- flintai/eval/db/base/detectors/detector_types.py +77 -0
- flintai/eval/db/base/eval/__init__.py +1 -0
- flintai/eval/db/base/eval/eval_helpers.py +227 -0
- flintai/eval/db/base/eval/eval_repository.py +115 -0
- flintai/eval/db/base/eval/eval_run.py +139 -0
- flintai/eval/db/base/eval/eval_types.py +129 -0
- flintai/eval/db/base/eval/model_eval_repository.py +78 -0
- flintai/eval/db/base/eval/model_eval_run_repository.py +70 -0
- flintai/eval/db/base/eval/model_eval_run_result_repository.py +40 -0
- flintai/eval/db/base/eval/model_eval_run_result_types.py +21 -0
- flintai/eval/db/base/eval/model_eval_run_types.py +84 -0
- flintai/eval/db/base/eval/model_eval_types.py +71 -0
- flintai/eval/db/base/message/__init__.py +0 -0
- flintai/eval/db/base/message/message_collection_helpers.py +61 -0
- flintai/eval/db/base/message/message_collection_repository.py +92 -0
- flintai/eval/db/base/message/message_collection_types.py +89 -0
- flintai/eval/db/base/models/__init__.py +0 -0
- flintai/eval/db/base/models/model_helpers.py +172 -0
- flintai/eval/db/base/models/model_repository.py +95 -0
- flintai/eval/db/base/models/model_types.py +156 -0
- flintai/eval/db/json/__init__.py +0 -0
- flintai/eval/db/json/repository_json.py +603 -0
- flintai/scan/__init__.py +0 -0
- flintai/scan/agent_scanner.py +797 -0
- flintai/scan/config/agent_opengrep_rules.yaml +533 -0
- flintai/scan/config/agent_taxonomy.json +481 -0
- flintai/scan/config/agentic_cvss_mapping.yaml +136 -0
- flintai/scan/config/compliance_mappings.json +110 -0
- flintai/scan/config/triage_prompt.txt +335 -0
- flintai/scan/constants.py +15 -0
- flintai/scan/file_filter.py +156 -0
- flintai/scan/llm_provider.py +197 -0
- flintai/scan/opengrep_resolver.py +12 -0
- flintai/scan/reasoner.py +552 -0
- flintai/scan/schema.py +337 -0
- flintai/scan/scorer.py +371 -0
- flintai/scan/secret_anonymizer.py +72 -0
- flintai/scan/static_scanner.py +542 -0
- flintai/scan/taxonomy.py +75 -0
- flintai/scan/tool_dispatcher.py +754 -0
- flintai/scan/trace_logger.py +118 -0
- flintai/scan/trace_logger_file.py +143 -0
- flintai/scan/trace_logger_log.py +100 -0
- flintai/scan/triage.py +496 -0
- flintai/schema.py +58 -0
- flintai_cli-1.0.0.dist-info/METADATA +442 -0
- flintai_cli-1.0.0.dist-info/RECORD +147 -0
- flintai_cli-1.0.0.dist-info/WHEEL +5 -0
- flintai_cli-1.0.0.dist-info/entry_points.txt +2 -0
- flintai_cli-1.0.0.dist-info/licenses/LICENSE +210 -0
- flintai_cli-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""
|
|
2
|
+
trace_logger.py — Structured Observability for the Agentic Reasoning Loop
|
|
3
|
+
=========================================================================
|
|
4
|
+
Writes a JSONL trace log of every tool call made during an agentic scan session.
|
|
5
|
+
|
|
6
|
+
WHY THIS EXISTS:
|
|
7
|
+
The agentic scanner itself exhibits the behaviours it scans for in customer agents:
|
|
8
|
+
autonomous tool-calling, adaptive control flow, incremental decision-making.
|
|
9
|
+
Per ASI10 (missing_agent_monitoring), any agent that takes consequential actions
|
|
10
|
+
without an audit trail is a security risk. This module is the scanner's own
|
|
11
|
+
compliance with that rule.
|
|
12
|
+
|
|
13
|
+
OUTPUT FORMAT:
|
|
14
|
+
One JSON object per line, written to <output_path>.trace.jsonl
|
|
15
|
+
Each line represents one tool call with timing, token estimates, and a result preview.
|
|
16
|
+
The final line is a session summary record.
|
|
17
|
+
|
|
18
|
+
USAGE:
|
|
19
|
+
logger = FileTraceLogger(session_id="AGT-SCAN-abc123", output_path="report.json")
|
|
20
|
+
logger.start(provider_model="claude-sonnet-4-6")
|
|
21
|
+
|
|
22
|
+
with logger.record_call("fetch_file", {"path": "tools/exec.py"}) as ctx:
|
|
23
|
+
result = do_fetch(...)
|
|
24
|
+
ctx.set_result(result)
|
|
25
|
+
|
|
26
|
+
logger.finish(findings_count=5, exit_reason="completed")
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import abc
|
|
32
|
+
from collections.abc import Iterator
|
|
33
|
+
from contextlib import contextmanager
|
|
34
|
+
from datetime import datetime, timezone
|
|
35
|
+
|
|
36
|
+
import tiktoken
|
|
37
|
+
|
|
38
|
+
# ── Token estimation ──────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
_TOKEN_REFERENCE_MODEL = "gpt-4o"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def estimate_tokens(text: str) -> int:
|
|
44
|
+
"""Estimate token count using tiktoken (accurate) or char heuristic (fallback).
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Estimated number of tokens (minimum 1).
|
|
48
|
+
"""
|
|
49
|
+
try:
|
|
50
|
+
return max(
|
|
51
|
+
1, len(tiktoken.encoding_for_model(_TOKEN_REFERENCE_MODEL).encode(text))
|
|
52
|
+
)
|
|
53
|
+
except Exception:
|
|
54
|
+
return max(1, len(text) // 4)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def now_iso() -> str:
|
|
58
|
+
"""Return the current UTC time as an ISO 8601 string."""
|
|
59
|
+
return datetime.now(timezone.utc).isoformat()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class CallContext:
|
|
63
|
+
"""Mutable context shared between the record_call context manager and the caller."""
|
|
64
|
+
|
|
65
|
+
def __init__(self):
|
|
66
|
+
self.result: str | None = None
|
|
67
|
+
self.success: bool = True
|
|
68
|
+
self.error: str | None = None
|
|
69
|
+
|
|
70
|
+
def set_result(self, result: str) -> None:
|
|
71
|
+
self.result = result
|
|
72
|
+
|
|
73
|
+
def set_error(self, error: str) -> None:
|
|
74
|
+
self.error = error
|
|
75
|
+
self.success = False
|
|
76
|
+
self.result = f"ERROR: {error}"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# ── Abstract interface ────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class TraceLogger(abc.ABC):
|
|
83
|
+
"""
|
|
84
|
+
Abstract interface for structured trace logging of tool calls
|
|
85
|
+
during an agentic scan session.
|
|
86
|
+
|
|
87
|
+
Thread-safety: single-threaded use only (scanner runs synchronously).
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
@abc.abstractmethod
|
|
91
|
+
def start(self, provider_model: str) -> None:
|
|
92
|
+
"""Begin a trace session. Call once before the reasoning loop starts."""
|
|
93
|
+
|
|
94
|
+
@abc.abstractmethod
|
|
95
|
+
@contextmanager
|
|
96
|
+
def record_call(
|
|
97
|
+
self, tool_name: str, tool_args: dict, iteration: int
|
|
98
|
+
) -> Iterator[CallContext]:
|
|
99
|
+
"""Context manager that times a tool call and writes the trace entry.
|
|
100
|
+
|
|
101
|
+
Usage:
|
|
102
|
+
with logger.record_call("fetch_file", {"path": "x.py"}, iteration=2) as ctx:
|
|
103
|
+
result = fetch_file(...)
|
|
104
|
+
ctx.set_result(result)
|
|
105
|
+
"""
|
|
106
|
+
yield CallContext() # pragma: no cover
|
|
107
|
+
|
|
108
|
+
@abc.abstractmethod
|
|
109
|
+
def set_iteration(self, n: int) -> None:
|
|
110
|
+
"""Update the current iteration count."""
|
|
111
|
+
|
|
112
|
+
@abc.abstractmethod
|
|
113
|
+
def finish(self, findings_count: int, exit_reason: str = "completed") -> None:
|
|
114
|
+
"""Write the session summary and close. Call once after the reasoning loop exits."""
|
|
115
|
+
|
|
116
|
+
@abc.abstractmethod
|
|
117
|
+
def as_dict(self) -> dict:
|
|
118
|
+
"""Return a summary dict suitable for embedding in ScanReport.scan_metadata."""
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""FileTraceLogger — writes a JSONL trace file to disk."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import time
|
|
8
|
+
import uuid
|
|
9
|
+
from contextlib import contextmanager
|
|
10
|
+
|
|
11
|
+
from flintai.scan.trace_logger import (
|
|
12
|
+
CallContext,
|
|
13
|
+
TraceLogger,
|
|
14
|
+
estimate_tokens,
|
|
15
|
+
now_iso,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class FileTraceLogger(TraceLogger):
|
|
22
|
+
"""
|
|
23
|
+
Writes a structured JSONL trace of all tool calls made during an agentic scan.
|
|
24
|
+
|
|
25
|
+
Output is written to ``<output_path>.trace.jsonl``.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, session_id: str | None = None, output_path: str | None = None):
|
|
29
|
+
self.session_id = session_id or f"AGT-SCAN-{uuid.uuid4().hex[:8].upper()}"
|
|
30
|
+
self.output_path = output_path
|
|
31
|
+
self._trace_path = f"{output_path}.trace.jsonl" if output_path else None
|
|
32
|
+
self._start_time: float | None = None
|
|
33
|
+
self._provider = "unknown"
|
|
34
|
+
self._iterations = 0
|
|
35
|
+
self._call_count = 0
|
|
36
|
+
self._total_tokens = 0
|
|
37
|
+
self._tool_calls: list[dict] = []
|
|
38
|
+
self._file = None
|
|
39
|
+
|
|
40
|
+
def start(self, provider_model: str) -> None:
|
|
41
|
+
self._start_time = time.monotonic()
|
|
42
|
+
self._provider = provider_model
|
|
43
|
+
self._write_line(
|
|
44
|
+
{
|
|
45
|
+
"event": "session_start",
|
|
46
|
+
"session_id": self.session_id,
|
|
47
|
+
"model": provider_model,
|
|
48
|
+
"timestamp": now_iso(),
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
logger.info(
|
|
52
|
+
"Trace session started: %s | model: %s", self.session_id, provider_model
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
@contextmanager
|
|
56
|
+
def record_call(self, tool_name: str, tool_args: dict, iteration: int):
|
|
57
|
+
call_start = time.monotonic()
|
|
58
|
+
ctx = CallContext()
|
|
59
|
+
try:
|
|
60
|
+
yield ctx
|
|
61
|
+
finally:
|
|
62
|
+
duration_ms = int((time.monotonic() - call_start) * 1000)
|
|
63
|
+
result_text = ctx.result or ""
|
|
64
|
+
tokens = estimate_tokens(result_text)
|
|
65
|
+
self._total_tokens += tokens
|
|
66
|
+
self._call_count += 1
|
|
67
|
+
|
|
68
|
+
entry = {
|
|
69
|
+
"event": "tool_call",
|
|
70
|
+
"session_id": self.session_id,
|
|
71
|
+
"iteration": iteration,
|
|
72
|
+
"call_number": self._call_count,
|
|
73
|
+
"tool": tool_name,
|
|
74
|
+
"args": tool_args,
|
|
75
|
+
"result_preview": result_text[:200],
|
|
76
|
+
"result_length": len(result_text),
|
|
77
|
+
"tokens_consumed": tokens,
|
|
78
|
+
"tokens_total": self._total_tokens,
|
|
79
|
+
"duration_ms": duration_ms,
|
|
80
|
+
"timestamp": now_iso(),
|
|
81
|
+
"success": ctx.success,
|
|
82
|
+
"error": ctx.error,
|
|
83
|
+
}
|
|
84
|
+
self._tool_calls.append(entry)
|
|
85
|
+
self._write_line(entry)
|
|
86
|
+
|
|
87
|
+
def set_iteration(self, n: int) -> None:
|
|
88
|
+
self._iterations = n
|
|
89
|
+
|
|
90
|
+
def finish(self, findings_count: int, exit_reason: str = "completed") -> None:
|
|
91
|
+
if self._start_time is None:
|
|
92
|
+
return
|
|
93
|
+
wall_ms = int((time.monotonic() - self._start_time) * 1000)
|
|
94
|
+
summary = {
|
|
95
|
+
"event": "session_end",
|
|
96
|
+
"session_id": self.session_id,
|
|
97
|
+
"model": self._provider,
|
|
98
|
+
"exit_reason": exit_reason,
|
|
99
|
+
"total_iterations": self._iterations,
|
|
100
|
+
"total_tool_calls": self._call_count,
|
|
101
|
+
"total_tokens": self._total_tokens,
|
|
102
|
+
"wall_clock_ms": wall_ms,
|
|
103
|
+
"findings_count": findings_count,
|
|
104
|
+
"timestamp": now_iso(),
|
|
105
|
+
}
|
|
106
|
+
self._write_line(summary)
|
|
107
|
+
if self._file:
|
|
108
|
+
self._file.close()
|
|
109
|
+
self._file = None
|
|
110
|
+
logger.info(
|
|
111
|
+
"Trace session complete: %s | iterations=%d | calls=%d | "
|
|
112
|
+
"tokens≈%d | %dms | exit=%s",
|
|
113
|
+
self.session_id,
|
|
114
|
+
self._iterations,
|
|
115
|
+
self._call_count,
|
|
116
|
+
self._total_tokens,
|
|
117
|
+
wall_ms,
|
|
118
|
+
exit_reason,
|
|
119
|
+
)
|
|
120
|
+
if self._trace_path:
|
|
121
|
+
logger.info("Trace written to: %s", self._trace_path)
|
|
122
|
+
|
|
123
|
+
def as_dict(self) -> dict:
|
|
124
|
+
return {
|
|
125
|
+
"session_id": self.session_id,
|
|
126
|
+
"provider_model": self._provider,
|
|
127
|
+
"total_iterations": self._iterations,
|
|
128
|
+
"total_tool_calls": self._call_count,
|
|
129
|
+
"total_tokens": self._total_tokens,
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
def _write_line(self, obj: dict) -> None:
|
|
133
|
+
"""Append a JSON line to the trace file. Opens on first write."""
|
|
134
|
+
if self._trace_path is None:
|
|
135
|
+
return
|
|
136
|
+
try:
|
|
137
|
+
if self._file is None:
|
|
138
|
+
self._file = open(self._trace_path, "w", encoding="utf-8")
|
|
139
|
+
self._file.write(json.dumps(obj) + "\n")
|
|
140
|
+
self._file.flush()
|
|
141
|
+
except Exception as e:
|
|
142
|
+
# Trace writes are best-effort — never crash the scanner
|
|
143
|
+
logger.warning("Could not write trace entry: %s", e)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""LogTraceLogger — emits trace events via Python logging."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import time
|
|
7
|
+
import uuid
|
|
8
|
+
from contextlib import contextmanager
|
|
9
|
+
|
|
10
|
+
from flintai.scan.trace_logger import (
|
|
11
|
+
CallContext,
|
|
12
|
+
TraceLogger,
|
|
13
|
+
estimate_tokens,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class LogTraceLogger(TraceLogger):
|
|
20
|
+
"""
|
|
21
|
+
Trace logger that emits structured information via Python's logging module
|
|
22
|
+
instead of writing to a file. Useful for environments where file output
|
|
23
|
+
is unavailable or when trace data should flow through the standard
|
|
24
|
+
log pipeline.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, session_id: str | None = None, log_level: int = logging.INFO):
|
|
28
|
+
self.session_id = session_id or f"AGT-SCAN-{uuid.uuid4().hex[:8].upper()}"
|
|
29
|
+
self._log_level = log_level
|
|
30
|
+
self._start_time: float | None = None
|
|
31
|
+
self._provider = "unknown"
|
|
32
|
+
self._iterations = 0
|
|
33
|
+
self._call_count = 0
|
|
34
|
+
self._total_tokens = 0
|
|
35
|
+
|
|
36
|
+
def start(self, provider_model: str) -> None:
|
|
37
|
+
self._start_time = time.monotonic()
|
|
38
|
+
self._provider = provider_model
|
|
39
|
+
logger.log(
|
|
40
|
+
self._log_level,
|
|
41
|
+
"[trace:%s] session_start | model=%s",
|
|
42
|
+
self.session_id,
|
|
43
|
+
provider_model,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
@contextmanager
|
|
47
|
+
def record_call(self, tool_name: str, tool_args: dict, iteration: int):
|
|
48
|
+
call_start = time.monotonic()
|
|
49
|
+
ctx = CallContext()
|
|
50
|
+
try:
|
|
51
|
+
yield ctx
|
|
52
|
+
finally:
|
|
53
|
+
duration_ms = int((time.monotonic() - call_start) * 1000)
|
|
54
|
+
result_text = ctx.result or ""
|
|
55
|
+
tokens = estimate_tokens(result_text)
|
|
56
|
+
self._total_tokens += tokens
|
|
57
|
+
self._call_count += 1
|
|
58
|
+
|
|
59
|
+
logger.log(
|
|
60
|
+
self._log_level,
|
|
61
|
+
"[trace:%s] tool_call #%d | iter=%d tool=%s "
|
|
62
|
+
"duration=%dms tokens=%d success=%s%s",
|
|
63
|
+
self.session_id,
|
|
64
|
+
self._call_count,
|
|
65
|
+
iteration,
|
|
66
|
+
tool_name,
|
|
67
|
+
duration_ms,
|
|
68
|
+
tokens,
|
|
69
|
+
ctx.success,
|
|
70
|
+
f" error={ctx.error}" if ctx.error else "",
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def set_iteration(self, n: int) -> None:
|
|
74
|
+
self._iterations = n
|
|
75
|
+
|
|
76
|
+
def finish(self, findings_count: int, exit_reason: str = "completed") -> None:
|
|
77
|
+
if self._start_time is None:
|
|
78
|
+
return
|
|
79
|
+
wall_ms = int((time.monotonic() - self._start_time) * 1000)
|
|
80
|
+
logger.log(
|
|
81
|
+
self._log_level,
|
|
82
|
+
"[trace:%s] session_end | exit=%s iterations=%d calls=%d "
|
|
83
|
+
"tokens≈%d wall=%dms findings=%d",
|
|
84
|
+
self.session_id,
|
|
85
|
+
exit_reason,
|
|
86
|
+
self._iterations,
|
|
87
|
+
self._call_count,
|
|
88
|
+
self._total_tokens,
|
|
89
|
+
wall_ms,
|
|
90
|
+
findings_count,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
def as_dict(self) -> dict:
|
|
94
|
+
return {
|
|
95
|
+
"session_id": self.session_id,
|
|
96
|
+
"provider_model": self._provider,
|
|
97
|
+
"total_iterations": self._iterations,
|
|
98
|
+
"total_tool_calls": self._call_count,
|
|
99
|
+
"total_tokens": self._total_tokens,
|
|
100
|
+
}
|