# Covers ADR-004, ADR-005, ADR-012 (function tools, hosted tools, structured outputs) Mix.Task.run("app.start") Code.require_file(Path.expand("support/example_helper.exs", __DIR__)) alias CodexExamples.Support Support.init!() alias Codex.{Agent, AgentRunner, FileSearch, RunConfig, ToolOutput, Tools} alias Codex.FunctionTool alias Codex.Items.AgentMessage defmodule CodexExamples.StructuredBundleTool do use FunctionTool, name: "structured_bundle", description: "Returns structured text, image, and file outputs for a topic", parameters: %{topic: :string}, handler: fn %{"topic" => topic}, _ctx -> {:ok, [ ToolOutput.text("Structured summary for #{topic}"), ToolOutput.image(%{ url: "https://example.com/#{topic}.png", detail: "low" }), ToolOutput.file(%{ data: Base.encode64("demo file payload for #{topic}"), filename: "#{topic}.txt", mime_type: "text/plain" }) ]} end end defmodule CodexExamples.LiveStructuredHostedTools do @moduledoc false def main(argv) do prompt = case argv do [] -> "Summarize this repository in one or two sentences." values -> Enum.join(values, " ") end Tools.reset!() {:ok, _} = Tools.register(CodexExamples.StructuredBundleTool) {:ok, _} = Codex.Tools.ShellTool |> Tools.register(Keyword.merge([name: "hosted_shell"], shell_options())) {:ok, _} = Codex.Tools.ApplyPatchTool |> Tools.register(Keyword.merge([name: "apply_patch"], apply_patch_options())) {:ok, _} = Codex.Tools.ComputerTool |> Tools.register(Keyword.merge([name: "computer"], computer_options())) {:ok, _} = Codex.Tools.VectorStoreSearchTool |> Tools.register(Keyword.merge([name: "file_search"], file_search_options())) {:ok, _} = Codex.Tools.ImageGenerationTool |> Tools.register(Keyword.merge([name: "image_generation"], image_options())) {:ok, file_search} = FileSearch.new(%{ vector_store_ids: ["demo-store"], filters: %{"source" => "docs"}, include_search_results: true }) {:ok, agent} = Agent.new(%{ name: "StructuredHostAgent", instructions: "Provide a short summary. Use hosted tools only if they are available.", tools: [ "structured_bundle", "hosted_shell", "apply_patch", "computer", "file_search", "image_generation" ], reset_tool_choice: true }) {:ok, run_config} = RunConfig.new(%{ max_turns: 3, file_search: file_search }) codex_opts = Support.codex_options!(%{}) {:ok, thread_opts} = Codex.Thread.Options.new(%{ file_search: file_search }) {:ok, thread} = Codex.start_thread(codex_opts, Support.thread_opts!(thread_opts)) IO.puts(""" Running live structured/hosted tools demo. Prompt: #{prompt} File search: #{inspect(file_search)} """) case AgentRunner.run(thread, prompt, %{agent: agent, run_config: run_config}) do {:ok, result} -> tool_outputs = result.raw[:tool_outputs] || [] print_tool_outputs(tool_outputs) if tool_outputs == [] do demo_tool_invocations() end IO.puts("Usage: #{inspect(result.thread.usage || %{})}") IO.puts("Final response:\n#{render_response(result.final_response)}") {:error, reason} -> Mix.raise("Run failed: #{inspect(reason)}") end end defp print_tool_outputs(outputs) do outputs |> List.wrap() |> Enum.each(fn output -> IO.puts("Tool output: #{inspect(output)}") end) end defp demo_tool_invocations do IO.puts("No tool calls observed; invoking hosted tools locally.") invoke_tool("structured_bundle", %{"topic" => "codex repo"}) invoke_tool("hosted_shell", %{"command" => "echo ok"}) invoke_tool("file_search", %{"query" => "docs"}) end defp invoke_tool(name, args) do case Tools.invoke(name, args, %{}) do {:ok, output} -> IO.puts("Tool #{name} output: #{inspect(output)}") {:error, reason} -> IO.puts("Tool #{name} error: #{inspect(reason)}") end end defp render_response(%AgentMessage{text: text}), do: text defp render_response(%{"text" => text}), do: text defp render_response(nil), do: "" defp render_response(other), do: inspect(other) defp shell_options do [ # Executor receives (args_map, context, metadata) or (args_map, context) executor: fn %{"command" => command}, _context, _metadata -> {:ok, %{"command" => command, "stdout" => "simulated shell: #{command}"}} end, # Approval receives (command_string, context, metadata) or (command_string, context) # Note: first arg is the command STRING, not the args map approval: fn command, _context, _metadata -> if String.contains?(command, "rm"), do: {:deny, "blocked dangerous command"}, else: :allow end, max_output_bytes: 400 ] end defp apply_patch_options do [ editor: fn %{"patch" => patch}, _context -> {:ok, %{"applied" => String.slice(patch, 0, 60)}} end ] end defp computer_options do [ safety: fn args, _context, _metadata -> action = Map.get(args, "action", "") if String.contains?(action, "destructive") do {:deny, "computer action blocked"} else :ok end end, executor: fn args, _context -> {:ok, %{"action" => Map.get(args, "action", "noop"), "status" => "simulated"}} end ] end defp file_search_options do [ searcher: fn args, _context, _metadata -> {:ok, %{ "query" => Map.get(args, "query"), "results" => [%{"title" => "demo result", "score" => 0.99}] }} end ] end defp image_options do [ generator: fn args, _context, _metadata -> {:ok, %{"prompt" => Map.get(args, "prompt"), "url" => "https://example.com/generated.png"}} end ] end end CodexExamples.LiveStructuredHostedTools.main(System.argv())