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:
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(