vet

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

commit 0287887b9d6f7fffbbe0642cfd9c77ba096c2760
parent d7619686b535d69cae1f6deb0b2772edada65018
Author: andrewlaack-collab <andrew.laack@imbue.com>
Date:   Tue, 24 Feb 2026 19:06:47 +0000

Add tool blocks (#138)

* Add tool blocks

* Fixed found issues

* Formatter

* Better skill

---------

Co-authored-by: Andrew Laack <andrew@laack.co>
Diffstat:
Mskills/vet/SKILL.md | 8++++----
Mskills/vet/scripts/export_claude_code_session.py | 67+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mskills/vet/scripts/export_codex_session.py | 79++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Mskills/vet/scripts/export_opencode_session.py | 25+++++++++++++++++++++++++
4 files changed, 170 insertions(+), 9 deletions(-)

diff --git a/skills/vet/SKILL.md b/skills/vet/SKILL.md @@ -32,21 +32,21 @@ Before running vet, determine the correct Python binary: ```bash $(command -v python3 || command -v python) ``` -Use whichever resolves (prefer `python3`). The examples below use `python3` — substitute `python` if that is what your system provides. +Use whichever resolves (prefer `python3`). The examples below use `python3`, substitute `python` if that is what your system provides. **OpenCode:** ```bash -vet "goal" --history-loader "$(command -v python3 || command -v python) ~/.agents/skills/vet/scripts/export_opencode_session.py --session-id <ses_ID>" +vet "goal" --history-loader "python3 ~/.agents/skills/vet/scripts/export_opencode_session.py --session-id <ses_ID>" ``` **Codex:** ```bash -vet "goal" --history-loader "$(command -v python3 || command -v python) ~/.codex/skills/vet/scripts/export_codex_session.py --session-file <path-to-session.jsonl>" +vet "goal" --history-loader "python3 ~/.codex/skills/vet/scripts/export_codex_session.py --session-file <path-to-session.jsonl>" ``` **Claude Code:** ```bash -vet "goal" --history-loader "$(command -v python3 || command -v python) ~/.claude/skills/vet/scripts/export_claude_code_session.py --session-file <path-to-session.jsonl>" +vet "goal" --history-loader "python3 ~/.claude/skills/vet/scripts/export_claude_code_session.py --session-file <path-to-session.jsonl>" ``` **Without Conversation History** diff --git a/skills/vet/scripts/export_claude_code_session.py b/skills/vet/scripts/export_claude_code_session.py @@ -12,6 +12,10 @@ SESSION_FILE = Path(args.session_file) if not SESSION_FILE.exists(): sys.exit(0) +# Map tool_use_id -> (tool_name, tool_input) so ToolResultBlocks can reference the tool name +tool_use_info: dict[str, tuple[str, dict]] = {} +msg_counter = 0 + for line in SESSION_FILE.read_text().splitlines(): if not line.strip(): continue @@ -38,16 +42,75 @@ for line in SESSION_FILE.read_text().splitlines(): if isinstance(content, str) and content.strip(): print(json.dumps({"object_type": "ChatInputUserMessage", "text": content})) elif isinstance(content, list): - text = " ".join(c.get("text", "") for c in content if isinstance(c, dict) and c.get("type") == "text") + text_parts = [] + tool_result_blocks = [] + for c in content: + if not isinstance(c, dict): + continue + if c.get("type") == "text" and c.get("text"): + text_parts.append(c["text"]) + elif c.get("type") == "tool_result": + result_content = c.get("content", "") + if isinstance(result_content, list): + result_content = " ".join( + rc.get("text", "") + for rc in result_content + if isinstance(rc, dict) and rc.get("type") == "text" + ) + tool_use_id = c.get("tool_use_id", "") + tool_name, tool_input = tool_use_info.get(tool_use_id, ("unknown", {})) + tool_result_blocks.append( + { + "object_type": "ToolResultBlock", + "type": "tool_result", + "tool_use_id": tool_use_id, + "tool_name": tool_name, + "invocation_string": f"{tool_name}({json.dumps(tool_input)})", + "content": { + "content_type": "generic", + "text": result_content, + }, + } + ) + text = " ".join(text_parts) if text.strip(): print(json.dumps({"object_type": "ChatInputUserMessage", "text": text})) + if tool_result_blocks: + msg_counter += 1 + print( + json.dumps( + { + "object_type": "ResponseBlockAgentMessage", + "role": "user", + "assistant_message_id": f"claude_code_tool_result_{msg_counter}", + "content": tool_result_blocks, + } + ) + ) elif entry_type == "assistant": if not isinstance(content, list): continue blocks = [] for c in content: - if isinstance(c, dict) and c.get("type") == "text" and c.get("text"): + if not isinstance(c, dict): + continue + if c.get("type") == "text" and c.get("text"): blocks.append({"object_type": "TextBlock", "type": "text", "text": c["text"]}) + elif c.get("type") == "tool_use": + tool_use_id = c.get("id", "") + tool_name = c.get("name", "") + tool_input = c.get("input", {}) + # Record for later ToolResultBlock lookups + tool_use_info[tool_use_id] = (tool_name, tool_input) + blocks.append( + { + "object_type": "ToolUseBlock", + "type": "tool_use", + "id": tool_use_id, + "name": tool_name, + "input": tool_input, + } + ) if blocks: print( json.dumps( diff --git a/skills/vet/scripts/export_codex_session.py b/skills/vet/scripts/export_codex_session.py @@ -12,6 +12,32 @@ SESSION_FILE = args.session_file if not Path(SESSION_FILE).exists(): sys.exit(0) +# Map call_id -> (fn_name, fn_input) so ToolResultBlocks can reference the tool name +call_info: dict[str, tuple[str, dict]] = {} +# Buffer tool blocks so they can be wrapped in a ResponseBlockAgentMessage +tool_block_buffer: list[dict] = [] +msg_counter = 0 + + +def flush_tool_blocks() -> None: + """Emit any buffered tool blocks wrapped in a ResponseBlockAgentMessage.""" + global msg_counter + if not tool_block_buffer: + return + msg_counter += 1 + print( + json.dumps( + { + "object_type": "ResponseBlockAgentMessage", + "role": "assistant", + "assistant_message_id": f"codex_tool_msg_{msg_counter}", + "content": list(tool_block_buffer), + } + ) + ) + tool_block_buffer.clear() + + for line in Path(SESSION_FILE).read_text().splitlines(): if not line.strip(): continue @@ -28,29 +54,76 @@ for line in Path(SESSION_FILE).read_text().splitlines(): continue payload = entry.get("payload", {}) - if payload.get("type") != "message": + payload_type = payload.get("type") + + if payload_type == "function_call": + call_id = payload.get("call_id", payload.get("id", "")) + fn_name = payload.get("name", "") + fn_args = payload.get("arguments", "") + # arguments is a JSON string in the Responses API; try to parse it + try: + fn_input = json.loads(fn_args) if isinstance(fn_args, str) else fn_args + except (json.JSONDecodeError, TypeError): + fn_input = {"raw": fn_args} + call_info[call_id] = (fn_name, fn_input) + tool_block_buffer.append( + { + "object_type": "ToolUseBlock", + "type": "tool_use", + "id": call_id, + "name": fn_name, + "input": fn_input, + } + ) + continue + + if payload_type == "function_call_output": + call_id = payload.get("call_id", "") + output = payload.get("output", "") + fn_name, fn_input = call_info.get(call_id, ("unknown", {})) + tool_block_buffer.append( + { + "object_type": "ToolResultBlock", + "type": "tool_result", + "tool_use_id": call_id, + "tool_name": fn_name, + "invocation_string": f"{fn_name}({json.dumps(fn_input)})", + "content": {"content_type": "generic", "text": output}, + } + ) + continue + + if payload_type != "message": continue role = payload.get("role") content = payload.get("content", []) if role == "user": + # Flush any pending tool blocks before the user message + flush_tool_blocks() text = " ".join(c.get("text", "") for c in content if c.get("type") == "input_text") if text: print(json.dumps({"object_type": "ChatInputUserMessage", "text": text})) elif role == "assistant": - blocks = [] + # Merge buffered tool blocks into this assistant message + blocks = list(tool_block_buffer) + tool_block_buffer.clear() for c in content: if c.get("type") == "output_text" and c.get("text"): blocks.append({"object_type": "TextBlock", "type": "text", "text": c["text"]}) if blocks: + msg_counter += 1 print( json.dumps( { "object_type": "ResponseBlockAgentMessage", "role": "assistant", - "assistant_message_id": "codex_msg", + "assistant_message_id": f"codex_msg_{msg_counter}", "content": blocks, } ) ) + +# Flush any remaining tool blocks at end of file +flush_tool_blocks() diff --git a/skills/vet/scripts/export_opencode_session.py b/skills/vet/scripts/export_opencode_session.py @@ -66,6 +66,31 @@ for msg in data.get("messages", []): for p in parts: if p.get("type") == "text" and p.get("text"): content.append({"object_type": "TextBlock", "type": "text", "text": p["text"]}) + elif p.get("type") == "tool": + call_id = p.get("callID", "") + tool_name = p.get("tool", "") + state = p.get("state", {}) + tool_input = state.get("input", {}) + tool_output = state.get("output", "") + content.append( + { + "object_type": "ToolUseBlock", + "type": "tool_use", + "id": call_id, + "name": tool_name, + "input": tool_input, + } + ) + content.append( + { + "object_type": "ToolResultBlock", + "type": "tool_result", + "tool_use_id": call_id, + "tool_name": tool_name, + "invocation_string": f"{tool_name}({json.dumps(tool_input)})", + "content": {"content_type": "generic", "text": tool_output}, + } + ) if content: print( json.dumps(