{"name":"claude_agent.nu","source":"// claude_agent.nu — minimal tool-using Claude agent.\n//\n// Drives a Claude conversation that has access to two tools:\n//\n//   run_shell {command}  — executes a /bin/sh command, returns stdout\n//   read_file {path}     — returns the contents of a UTF-8 text file\n//\n// The agent loop runs up to AGENT_MAX_TURNS = 8 round-trips:\n//\n//   1. Send messages + tools to Claude.\n//   2. If stop_reason != \"tool_use\": print the assistant's final text\n//      and stop.\n//   3. Otherwise, run every tool_use block, collect the results into a\n//      single user turn, and loop.\n//\n// Build + run (Linux/macOS):\n//\n//     ./build.sh\n//     export ANTHROPIC_API_KEY=sk-ant-...\n//     ./nurl.sh examples/claude_agent.nu \"list the .nu files in stdlib/core and tell me which is largest\"\n//\n// The model is hard-coded to claude-opus-4-7. Tweak `MODEL` below to\n// switch to a smaller / faster variant.\n//\n// SECURITY: `run_shell` executes ARBITRARY commands the model decides\n// on. Run this only inside a sandbox (Docker, a VM, a throwaway repo\n// clone) when you don't fully trust your prompt. The `system_prompt`\n// below asks the model to behave, but that is not a security control.\n\n$ `stdlib/ext/anthropic.nu`\n$ `stdlib/ext/env.nu`\n$ `stdlib/ext/json.nu`\n$ `stdlib/std/process.nu`\n$ `stdlib/std/fs.nu`\n$ `stdlib/core/io.nu`\n$ `stdlib/core/string.nu`\n$ `stdlib/core/errors.nu`\n$ `stdlib/core/vec.nu`\n\n// ── Tool descriptors ────────────────────────────────────────────────\n\n// Build {type:\"object\", properties:{<name>:{type:\"string\",description:<desc>}}, required:[<name>]}.\n// One required string field is enough for both tools we expose.\n@ one_string_schema s field_name s field_desc → Json {\n    : Json schema ( json_obj_new )\n    ( json_obj_set schema `type` ( json_str_lit `object` ) )\n\n    : Json prop ( json_obj_new )\n    ( json_obj_set prop `type` ( json_str_lit `string` ) )\n    ( json_obj_set prop `description` ( json_str_lit field_desc ) )\n\n    : Json props ( json_obj_new )\n    ( json_obj_set props field_name prop )\n    ( json_obj_set schema `properties` props )\n\n    : Json req ( json_arr_new )\n    ( json_arr_push req ( json_str_lit field_name ) )\n    ( json_obj_set schema `required` req )\n\n    ^ schema\n}\n\n@ build_tools → ( Vec Json ) {\n    : ( Vec Json ) tools ( vec_new [Json] )\n\n    : Json shell_schema ( one_string_schema `command` `Shell command to execute via /bin/sh -c` )\n    ( vec_push [Json] tools\n    ( claude_tool_def `run_shell`\n    `Run a shell command and return its stdout. Use sparingly; prefer read_file when you only need to inspect a file.`\n    shell_schema ) )\n\n    : Json file_schema ( one_string_schema `path` `Filesystem path of the file to read` )\n    ( vec_push [Json] tools\n    ( claude_tool_def `read_file`\n    `Read a UTF-8 text file and return its contents.`\n    file_schema ) )\n\n    ^ tools\n}\n\n// ── Tool dispatch ───────────────────────────────────────────────────\n\n// Helper: pull a string field out of a tool_use's input object. Returns\n// \"\" if the field is absent or the wrong shape — let Claude resubmit.\n@ input_str Json input s field → s {\n    : ?Json v ( json_obj_get input field )\n    ?? v {\n        T j → { ^ ( json_str_data j ) }\n        F → { ^ `` }\n    }\n    ^ ``\n}\n\n// Cap the tool result so a wild `find /` can't blow our context window.\n// Trims to N bytes at most and appends a marker if truncated.\n@ MAX_TOOL_BYTES → i { ^ 80000 }\n\n@ truncate_for_model String src → String {\n    : i n ( string_len src )\n    : i lim ( MAX_TOOL_BYTES )\n    ? <= n lim {\n        ^ src\n    } {}\n    : String head ( string_substr src 0 lim )\n    ( string_push_str head `\\n…[truncated, ` )\n    ( string_push_int head - n lim )\n    ( string_push_str head ` more bytes]` )\n    ( string_free src )\n    ^ head\n}\n\n// Run a single tool_use block, returning (text, is_error). Both arms\n// hand back an owned String so the caller can free uniformly.\n@ run_tool Json tu → String {\n    : s name ( claude_tool_use_name tu )\n    : ?Json input_o ( claude_tool_use_input tu )\n\n    : Json input ?? input_o {\n        T j → j\n        F → @ Json { JNull }\n    }\n\n    ? != ( nurl_str_eq name `run_shell` ) 0 {\n        : s cmd ( input_str input `command` )\n        ? == ( nurl_str_len cmd ) 0 {\n            ^ ( string_from `error: tool 'run_shell' requires non-empty 'command' field` )\n        } {}\n        : !Output ProcessErr r ( process_run_shell cmd )\n        ?? r {\n            T out → {\n                : i ec ( output_exit_code out )\n                : String body ( string_with_cap 256 )\n                ? != ec 0 {\n                    ( string_push_str body `[exit ` )\n                    ( string_push_int body ec )\n                    ( string_push_str body `]\\n` )\n                } {}\n                ( string_push_str body ( output_stdout out ) )\n                : i errlen ( output_stderr_len out )\n                ? > errlen 0 {\n                    ( string_push_str body `\\n[stderr]\\n` )\n                    ( string_push_str body ( output_stderr out ) )\n                } {}\n                ( output_free out )\n                ^ ( truncate_for_model body )\n            }\n            F e → {\n                : ProcessErr pe # ProcessErr e\n                : String msg ( string_from `error: process_run failed: ` )\n                ( string_push_str msg ( process_err_name pe ) )\n                ^ msg\n            }\n        }\n    } {}\n\n    ? != ( nurl_str_eq name `read_file` ) 0 {\n        : s path ( input_str input `path` )\n        ? == ( nurl_str_len path ) 0 {\n            ^ ( string_from `error: tool 'read_file' requires non-empty 'path' field` )\n        } {}\n        : !String IoErr r ( read_file path )\n        ?? r {\n            T contents → { ^ ( truncate_for_model contents ) }\n            F e → {\n                : IoErr ie # IoErr e\n                : String msg ( string_from `error: read_file: ` )\n                ( string_push_str msg ( io_err_msg ie ) )\n                ^ msg\n            }\n        }\n    } {}\n\n    : String unknown ( string_from `error: unknown tool '` )\n    ( string_push_str unknown name )\n    ( string_push_str unknown `'` )\n    ^ unknown\n}\n\n// True if name is one of our advertised tools.\n@ is_known_tool s name → b {\n    ? != ( nurl_str_eq name `run_shell` ) 0 { ^ T } {}\n    ? != ( nurl_str_eq name `read_file` ) 0 { ^ T } {}\n    ^ F\n}\n\n// ── Main loop ───────────────────────────────────────────────────────\n\n@ AGENT_MAX_TURNS → i { ^ 8 }\n\n@ AGENT_MAX_TOKENS → i { ^ 4096 }\n\n@ MODEL → s { ^ `claude-opus-4-7` }\n\n@ SYSTEM_PROMPT → s {\n    ^ `You are an assistant running inside a NURL agent host. You have two tools: run_shell (for shell commands) and read_file (for file inspection). Use them to answer the user's request, then provide a final concise text reply. Avoid destructive commands (rm, mv, etc.) unless explicitly asked.`\n}\n\n@ main → i {\n    // Build initial user prompt.\n    : i argc ( env_args_count )\n    : String prompt ( string_with_cap 64 )\n    ? > argc 1 {\n        : ~ i i 1\n        ~ < i argc {\n            ? > i 1 { ( string_push_str prompt ` ` ) } {}\n            ( string_push_str prompt ( nurl_argv_get i ) )\n            = i + i 1\n        }\n    } {\n        : String stdin_text ( read_all_stdin )\n        ( string_push_str prompt ( string_data stdin_text ) )\n        ( string_free stdin_text )\n    }\n\n    ? == ( string_len prompt ) 0 {\n        ( nurl_print `usage: claude_agent <prompt>\\n` )\n        ( nurl_print `       echo \"<prompt>\" | claude_agent\\n` )\n        ( nurl_print `       set ANTHROPIC_API_KEY in the environment first\\n` )\n        ( string_free prompt )\n        ^ 1\n    } {}\n\n    : ?String key ( env_get `ANTHROPIC_API_KEY` )\n    : s api_key ?? key {\n        T s → ( string_data s )\n        F → ``\n    }\n    ? == ( nurl_str_len api_key ) 0 {\n        ( nurl_print `error: ANTHROPIC_API_KEY not set\\n` )\n        ?? key { T s → ( string_free s ) F → {} }\n        ( string_free prompt )\n        ^ 1\n    } {}\n\n    // Build messages = [user_text(prompt)] and tools.\n    : ( Vec Json ) msgs ( vec_new [Json] )\n    ( vec_push [Json] msgs ( claude_msg_user_text ( string_data prompt ) ) )\n    ( string_free prompt )\n\n    : ( Vec Json ) tools ( build_tools )\n\n    // Drop closure for the Vec[Json] cleanups at end-of-program.\n    : ( @ v Json ) drop_json \\ Json e → v { ( json_free e ) }\n\n    // Loop.\n    : ~ i turn 0\n    : ~ b done F\n    : ~ i exit_code 1\n    : ~ i total_in 0\n    : ~ i total_out 0\n\n    ~ & ! done < turn ( AGENT_MAX_TURNS ) {\n        ( nurl_eprint `[agent] turn ` )\n        ( nurl_eprint ( nurl_str_int turn ) )\n        ( nurl_eprint `\\n` )\n        ( eflush )\n\n        : !Json ClaudeErr r\n        ( claude_messages_full api_key\n        ( MODEL )\n        ( SYSTEM_PROMPT )\n        msgs tools `auto`\n        ( AGENT_MAX_TOKENS ) )\n        ?? r {\n            T resp → {\n                = total_in + total_in ( claude_input_tokens resp )\n                = total_out + total_out ( claude_output_tokens resp )\n\n                // Append the assistant turn (full content array — text +\n                // tool_use blocks) to the conversation so the model sees its\n                // own moves on the next call.\n                ( vec_push [Json] msgs ( claude_msg_assistant_response resp ) )\n\n                ? ( claude_has_tool_use resp ) {\n                    // Run every tool_use block in order, collect results.\n                    : ( Vec Json ) tcs ( claude_tool_calls resp )\n                    : ( Vec Json ) results ( vec_new [Json] )\n\n                    : i tn ( vec_len [Json] tcs )\n                    : ~ i k 0\n                    ~ < k tn {\n                        : ?Json e ( vec_get [Json] tcs k )\n                        ?? e {\n                            T tu → {\n                                : s tu_id ( claude_tool_use_id tu )\n                                : s tu_name ( claude_tool_use_name tu )\n\n                                ( nurl_eprint `[agent]  tool ` )\n                                ( nurl_eprint tu_name )\n                                ( nurl_eprint ` (` )\n                                ( nurl_eprint tu_id )\n                                ( nurl_eprint `)\\n` )\n                                ( eflush )\n\n                                : ~ b is_err F\n                                ? ( is_known_tool tu_name ) {} {\n                                    = is_err T\n                                }\n                                : String out ( run_tool tu )\n                                ? is_err {\n                                    ( vec_push [Json] results\n                                    ( claude_tool_result_block tu_id ( string_data out ) T ) )\n                                } {\n                                    // Heuristic: the tool itself signals errors with a\n                                    // string that starts with \"error:\". Mark them so\n                                    // Claude knows to retry / adjust.\n                                    : b looks_err ( string_starts_with out `error:` )\n                                    ( vec_push [Json] results\n                                    ( claude_tool_result_block tu_id ( string_data out ) looks_err ) )\n                                }\n                                ( string_free out )\n                            }\n                            F → {}\n                        }\n                        = k + k 1\n                    }\n                    ( vec_free_with [Json] tcs drop_json )\n\n                    // Push the user-side tool_result turn.\n                    ( vec_push [Json] msgs ( claude_msg_user_blocks results ) )\n                    ( claude_response_free resp )\n                } {\n                    // No tool_use: print the final answer and stop.\n                    ( nurl_print ( claude_text resp ) )\n                    ( nurl_print `\\n` )\n                    ( claude_response_free resp )\n                    = done T\n                    = exit_code 0\n                }\n            }\n            F e → {\n                : ClaudeErr ce # ClaudeErr e\n                ( nurl_eprint `[agent] error: ` )\n                ( nurl_eprint ( claude_err_name ce ) )\n                ( nurl_eprint `\\n` )\n                = done T\n            }\n        }\n\n        = turn + turn 1\n    }\n\n    ? & ! done >= turn ( AGENT_MAX_TURNS ) {\n        ( nurl_eprint `[agent] hit AGENT_MAX_TURNS without final reply\\n` )\n    } {}\n\n    ( nurl_eprint `[agent] tokens in=` )\n    ( nurl_eprint ( nurl_str_int total_in ) )\n    ( nurl_eprint ` out=` )\n    ( nurl_eprint ( nurl_str_int total_out ) )\n    ( nurl_eprint ` turns=` )\n    ( nurl_eprint ( nurl_str_int turn ) )\n    ( nurl_eprint `\\n` )\n\n    ( vec_free_with [Json] msgs drop_json )\n    ( vec_free_with [Json] tools drop_json )\n    ?? key { T s → ( string_free s ) F → {} }\n\n    ^ exit_code\n}\n","bytes":13183}