Sessions, Config Lint, Recorders and Inspect AI
Coding-agent sessions
Find and summarize coding-agent sessions on this machine (read-only).
Claude Code and Codex CLI keep every session as a JSON Lines file
(toolscore.adapters.sessions has the formats). This module finds them,
reads them with the session adapters and runs the honesty checks, so
toolscore sessions list can show what a coding agent did yesterday and where
it may have reported success after a failure.
Nothing here writes to a session file or sends anything anywhere.
export_session() writes a normalized Toolscore trace with credentials
redacted, for sharing or for a regression test.
- toolscore.sessions.AGENTS = ('claude-code', 'codex')
Agents whose sessions Toolscore can find.
- class toolscore.sessions.SessionSummary(path, agent, modified, session_id=None, cwd=None, started=None, calls=0, failed=0, findings=dict(), flagged=None, final_answer=False, error=None)[source]
One session file and what the checks found in it.
- Variables:
path (
Path) – The session file.agent (
str) –"claude-code"or"codex".session_id (
str|None) – The session ID, when the file records one.cwd (
str|None) – The working directory the agent ran in, when recorded.started (
str|None) – The first timestamp in the file (ISO 8601), when recorded.modified (
datetime) – The file’s modification time.calls (
int) – Tool calls on the main thread.failed (
int) – Calls that failed.findings (
dict[str,int]) – Honesty findings as{rule: count}.flagged (
bool|None) – The honesty verdict (Nonewhen unknown).final_answer (
bool) – Whether the session holds a final answer.error (
str|None) – Why the file could not be read, if it could not.
- Parameters:
- __init__(path, agent, modified, session_id=None, cwd=None, started=None, calls=0, failed=0, findings=<factory>, flagged=None, final_answer=False, error=None)
- toolscore.sessions.default_roots()[source]
Where each agent keeps its sessions (honoring
CLAUDE_CONFIG_DIRandCODEX_HOME).
- toolscore.sessions.parse_since(value, now=None)[source]
Parse
--since:30m,12h,1d,2w, or a date / ISO 8601 time.- Parameters:
- Return type:
- Returns:
The earliest modification time to include (timezone-aware).
- Raises:
ValueError – If the value cannot be read.
- toolscore.sessions.find_session_files(agent='all', root=None, since=None)[source]
Find session files, newest first.
- Parameters:
- Return type:
- Returns:
(agent, path)pairs; withroot, the agent is"auto".
- toolscore.sessions.summarize_session(path, agent='auto')[source]
Read one session file and run the honesty checks on it.
- Parameters:
- Return type:
- Returns:
The summary;
erroris set when the file is not a session Toolscore reads.
- toolscore.sessions.export_session(path, output, max_result_chars=2000)[source]
Write a session as a normalized Toolscore trace, with credentials redacted.
The trace is
{"format": "toolscore", "source": ..., "calls": [...], "final_answer": ...}: every call with its tool, arguments, result (cut tomax_result_chars), error and ID. Any credential the secret scanner recognizes (API keys, tokens, private keys) is replaced by a redacted preview, in arguments, results and the final answer. Other private content (code, paths) is kept: review the file before you share it.
- toolscore.core.read_trace_data(path)[source]
Read a trace file: one JSON document, or JSON Lines (one value per line).
Coding-agent sessions (Claude Code transcripts, Codex CLI rollouts) and other logs are JSON Lines; they are returned as the list of their lines’ values. Blank lines and lines that are not JSON (a partly written last line, for example) are skipped.
- Parameters:
- Return type:
- Returns:
The parsed data.
- Raises:
ValueError – If the file is neither JSON nor JSON Lines.
Agent-config lint
See Agent-Config Lint.
Lint agent instruction files and MCP host configs against the tools that really exist.
Agents read instruction files (CLAUDE.md, AGENTS.md, SKILL.md,
Cursor rules, Copilot instructions) that name the tools to use. When a server
is renamed, a tool is removed, or an instruction tells the agent to delete
with a tool that declares itself read-only, nothing warns you: the agent just
calls a tool that is not there, or a client lets a “read-only” tool run
without asking. lint_config() finds these mismatches.
What it reads:
instruction files:
CLAUDE.md,CLAUDE.local.md,AGENTS.md, anySKILL.md,.cursor/rules/**/*.mdc|*.md,.cursorrules,.github/copilot-instructions.md,.github/instructions/*.instructions.md;MCP host configs:
.mcp.json,.cursor/mcp.json,.vscode/mcp.json,claude_desktop_config.json(and, only when asked, the user’s~/.claude.json);the tools each server exposes, from snapshot or lock files (
toolscore mcp list --json,toolscore mcp pin) or, only when asked, by starting the configured servers (connect=True).
Rules (all conservative: a finding needs an exact tool reference):
config-server-invalid(error): a host-config entry Toolscore cannot use (no command or URL, the deprecated SSE transport, unreadable JSON).config-unknown-server(warning): anmcp__<server>__<tool>name whose server no host config defines.config-unknown-tool(error): anmcp__<server>__<tool>name whose server is known and does not expose the tool, or a quoted name written as a tool (`purge_all` tool) that no known server exposes.config-annotation-contradiction(warning): “use X to delete/create/…” where X declaresreadOnlyHint: true, or “use X to read/list/…” where X declaresdestructiveHint: true.
Rules that need a tool inventory are reported as skipped when there is none,
never as passed. Claude Code’s built-in tools (Read, Bash, …) and
plain prose are never flagged; mcp__plugin_* and mcp__claude_ai_*
names come from plugins and connectors outside the repository and are not
checked against it.
- toolscore.config_lint.CONFIG_RULES = ('config-server-invalid', 'config-unknown-server', 'config-unknown-tool', 'config-annotation-contradiction')
The rule IDs
lint_config()reports.
- toolscore.config_lint.INVENTORY_RULES = ('config-unknown-tool', 'config-annotation-contradiction')
Rules that need to know which tools each server exposes.
- class toolscore.config_lint.ConfigIssue(rule, severity, path, line, message, fix)[source]
One finding of
lint_config().- Variables:
- Parameters:
- class toolscore.config_lint.ConfigLintReport(instruction_files=list(), host_configs=list(), servers=dict(), inventory=dict(), skipped_rules=dict(), issues=list())[source]
What
lint_config()read and found.- Variables:
servers (
dict[str,str]) – Configured server names, each with the config file defining it.inventory (
dict[str,dict[str,MCPToolDef]]) – For each server with a known tool list, its tools by name.skipped_rules (
dict[str,str]) – Rules that could not run, with the reason.issues (
list[ConfigIssue]) – The findings, in file order.
- Parameters:
- issues: list[ConfigIssue]
- __init__(instruction_files=<factory>, host_configs=<factory>, servers=<factory>, inventory=<factory>, skipped_rules=<factory>, issues=<factory>)
- toolscore.config_lint.discover_files(paths)[source]
Find instruction files and MCP host configs under
paths.
- toolscore.config_lint.lint_config(paths=None, snapshots=None, connect=False, user_config=None, timeout=30.0)[source]
Lint instruction files and MCP host configs.
- Parameters:
paths (
list[Path] |None) – Files or directories to read (default: the current directory).snapshots (
list[tuple[str|None,Path]] |None) – Tool inventories as(server name or None, file): atoolscore mcp list --jsonsnapshot or atoolscore mcp pinlock file. Without a name, the snapshot’s own server name is used.connect (
bool) – Start every configured server to list its tools. This runs the commands in the host configs; it is never done by default.user_config (
Path|None) – Also read this user-scope config (~/.claude.json).timeout (
float) – Seconds to wait for each server withconnect.
- Return type:
- Returns:
The report.
Live recorders
See Framework Integration Guide.
Record tool calls live from agent SDKs, as Toolscore traces.
ClaudeAgentSDKRecorderplugs into the Claude Agent SDK’s hooks (PreToolUse,PostToolUse,PostToolUseFailure) and records every tool call with its input, response, error and duration, including MCP tools (mcp__server__tool) and subagent calls.OpenAIAgentsRecorderis an OpenAI Agents SDK tracing processor: it records every function tool span (function tools and MCP tools) with its input, output and error.
Both keep what they recorded in calls (Toolscore call dicts), take the
run’s final answer, and write a trace that every Toolscore command reads
(toolscore eval, toolscore honesty). Nothing is sent anywhere; the
SDKs stay optional (imported only when a recorder needs them).
- class toolscore.recorders.ClaudeAgentSDKRecorder[source]
Record a Claude Agent SDK run through its tool hooks.
Example:
from claude_agent_sdk import ClaudeAgentOptions, query from toolscore.recorders import ClaudeAgentSDKRecorder recorder = ClaudeAgentSDKRecorder() options = ClaudeAgentOptions(hooks=recorder.hooks(), allowed_tools=["Read", "Bash"]) async for message in query(prompt="Run the tests", options=options): recorder.observe(message) # keeps the final ResultMessage text recorder.save("trace.json") # then: toolscore honesty trace.json
The hooks return
{}: they never block or change a tool call. Merge them with your own hooks withrecorder.hooks(existing).- agent = 'claude-agent-sdk'
- async pre_tool_use(input_data, tool_use_id, _context)[source]
PreToolUsehook: records the call and its input.
- async post_tool_use(input_data, tool_use_id, _context)[source]
PostToolUsehook: records the tool’s response and duration.
- async post_tool_use_failure(input_data, tool_use_id, _context)[source]
PostToolUseFailurehook: records the error.
- evaluate(expected, **kwargs)
Evaluate the recorded calls (
toolscore.evaluatewith the final answer).
- save(path, redact=True)
Write the trace as JSON (credentials redacted unless
redact=False).
- to_trace(redact=False)
The recorded run as a Toolscore trace dict.
- class toolscore.recorders.OpenAIAgentsRecorder(trace_id=None)[source]
An OpenAI Agents SDK tracing processor that records tool calls.
Example:
from agents import Runner, add_trace_processor from toolscore.recorders import OpenAIAgentsRecorder recorder = OpenAIAgentsRecorder() add_trace_processor(recorder) # alongside the default exporter result = await Runner.run(agent, "Book a table for two") recorder.final_answer = str(result.final_output) recorder.save("trace.json")
Every
functionspan (function tools, and MCP tools, which the SDK also traces as function spans withmcp_data) becomes a call with its input (parsed JSON arguments), output and error.trace_idrestricts recording to one trace;for_trace()reads the calls of one trace.- Parameters:
trace_id (str | None)
- agent = 'openai-agents'
- evaluate(expected, **kwargs)
Evaluate the recorded calls (
toolscore.evaluatewith the final answer).
- save(path, redact=True)
Write the trace as JSON (credentials redacted unless
redact=False).
- to_trace(redact=False)
The recorded run as a Toolscore trace dict.
Inspect AI scorer
A Toolscore scorer for Inspect AI (the UK AI Security Institute’s inspect_ai).
toolscore_scorer() scores each sample’s tool calls against what the
sample expects, deterministically and without calling a model, and runs the
honesty checks on the final answer:
from inspect_ai import Task, task
from inspect_ai.dataset import Sample
from inspect_ai.solver import generate, use_tools
from toolscore.inspect_scorer import toolscore_scorer
@task
def refunds() -> Task:
return Task(
dataset=[Sample(
input="Refund order A-17.",
metadata={"toolscore": {"required": [{"tool": "issue_refund",
"args": {"order_id": "A-17"}}]}},
)],
solver=[use_tools(issue_refund()), generate()],
scorer=toolscore_scorer(),
)
What a sample expects comes from, in order: the scorer’s expected
argument, the sample’s metadata["toolscore"] (a list of expected calls or
a contract mapping with required / optional / forbidden), or the
sample’s target when it is a JSON list or contract.
The score’s value is a dict, one entry per metric:
tool_score: the Toolscore composite score, 0 to 1 (mean);passed: 1 when the score reachesmin_scoreand no forbidden call was made (accuracy);false_success: 1 when the honesty checks found an error-severity finding: success claimed after a failure, a refusal overridden with code (mean);honesty_unknown: 1 when honesty could not be checked (no final answer, or no tool results), so an unknown is never counted as honest (mean).
metadata holds the findings, the tri-state evidence and the calls. Inspect
is an optional dependency (pip install inspect-ai); verified with
inspect_ai 0.3.287.
- toolscore.inspect_scorer.METADATA_KEY = 'toolscore'
Sample metadata key holding the expected calls or contract.
- toolscore.inspect_scorer.from_inspect_messages(messages)[source]
Tool calls from an Inspect conversation (
state.messages).Each
ToolCallon aChatMessageAssistantis a call (function,arguments,id); theChatMessageToolwith the sametool_call_idgives its result, and itserror(aToolCallError) makes it failed.
- toolscore.inspect_scorer.final_answer_from_state(state)[source]
The model’s last answer in an Inspect
TaskState(Nonewhen it ended on a tool call).
- toolscore.inspect_scorer.score_state(state, target=None, expected=None, min_score=0.9, weights=None)[source]
Score one Inspect sample:
(values, explanation, metadata)(see the module docstring).- Parameters:
- Return type:
- Returns:
The score values, an explanation and the metadata.
Framework helpers
- toolscore.integrations.claude_agent_sdk_messages(result)[source]
Claude Agent SDK messages (dataclasses or dicts) as Anthropic-style message dicts.
AssistantMessageandUserMessagebecome{"role", "content"}dicts with typed blocks; aResultMessagebecomes{"type": "result", "result", "is_error", "session_id"}, where the final answer is. Other messages (system, task progress, stream events) are dropped.