Fix streaming, plugin loading, and auth; exclude internal tools from generated-code
ober
4fd5ef8e7abc161623f5dadb2ac498cf2acdf1ef
--- a/build-binary.ss +++ b/build-binary.ss @@ -220,6 +220,14 @@ (generate-wpo-files #t)) (compile-program "main-binary.ss")) +;; Keep plugin-facing libraries visible at runtime by excluding from WPO. +(for-each (lambda (m) + (let ((wpo (format "~a.wpo" m))) + (when (file-exists? wpo) + (printf " Excluding ~a from WPO (plugin-visible)~n" m) + (delete-file wpo)))) + '("lib/jcode/tool/registry")) + ;; --- Step 2: Whole-program optimization --- (printf "[2/6] Running whole-program optimization...~n") (let ((missing (compile-whole-program "main-binary.wpo" "jcode-all.so"))) @@ -241,6 +249,7 @@ (map (lambda (m) (format "~a/~a.so" jerboa-lib-dir m)) '("jerboa/core" "jerboa/runtime" + "jerboa/prelude" "std/error" "std/format" "std/sort" --- a/lib/jcode/core/config.sls +++ b/lib/jcode/core/config.sls @@ -56,8 +56,13 @@ (hash-put! config "providers" providers) config)) (def (merge-opencode-keys! providers) - (let ([auth-file (path-join (getenv "HOME") ".local" "share" - "opencode" "auth.json")]) + (let ([auth-file (let ([jcode-auth (path-join + (jcode-home) + "auth.json")]) + (if (file-exists? jcode-auth) + jcode-auth + (path-join (getenv "HOME") ".local" "share" + "opencode" "auth.json")))]) (when (file-exists? auth-file) (try (let ([auth (call-with-input-file auth-file --- a/lib/jcode/core/models.sls +++ b/lib/jcode/core/models.sls @@ -60,15 +60,31 @@ ("gemini-2.5-flash" . "Gemini 2.5 Flash") ("gemini-2.0-flash" . "Gemini 2.0 Flash"))) (def openrouter-models - '(("anthropic/claude-sonnet-4" . "Claude Sonnet 4") ("anthropic/claude-opus-4" . "Claude Opus 4") - ("openai/gpt-4o" . "GPT-4o") - ("google/gemini-2.5-pro" . "Gemini 2.5 Pro") - ("google/gemini-2.5-flash" . "Gemini 2.5 Flash") - ("deepseek/deepseek-chat" . "DeepSeek V3") - ("deepseek/deepseek-r1" . "DeepSeek R1") - ("meta-llama/llama-4-maverick" . "Llama 4 Maverick") - ("meta-llama/llama-4-scout" . "Llama 4 Scout") - ("qwen/qwen3-235b-a22b" . "Qwen3 235B"))) + '(("anthropic/claude-sonnet-4" . "Claude Sonnet 4") + ("anthropic/claude-opus-4" . "Claude Opus 4") + ("anthropic/claude-haiku-3.5" . "Claude 3.5 Haiku") + ("openai/gpt-4o" . "GPT-4o") + ("openai/gpt-4o-mini" . "GPT-4o Mini") ("openai/o3" . "o3") + ("openai/o3-mini" . "o3 Mini") + ("openai/o4-mini" . "o4 Mini") + ("google/gemini-2.5-pro" . "Gemini 2.5 Pro") + ("google/gemini-2.5-flash" . "Gemini 2.5 Flash") + ("google/gemini-2.0-flash" . "Gemini 2.0 Flash") + ("meta-llama/llama-4-maverick" . "Llama 4 Maverick") + ("meta-llama/llama-4-scout" . "Llama 4 Scout") + ("meta-llama/llama-3.3-70b-instruct" . "Llama 3.3 70B") + ("deepseek/deepseek-chat-v3-0324" . "DeepSeek V3 0324") + ("deepseek/deepseek-r1" . "DeepSeek R1") + ("qwen/qwen3-235b-a22b" . "Qwen3 235B") + ("qwen/qwen3-30b-a3b" . "Qwen3 30B") + ("mistralai/mistral-large" . "Mistral Large") + ("mistralai/mistral-medium" . "Mistral Medium") + ("mistralai/codestral" . "Codestral") + ("cohere/command-r-plus" . "Command R+") + ("x-ai/grok-3" . "Grok 3") + ("x-ai/grok-3-mini" . "Grok 3 Mini") + ("microsoft/phi-4" . "Phi-4") + ("nousresearch/hermes-3-llama-3.1-405b" . "Hermes 3 405B"))) (def ollama-models '(("llama3.2" . "Llama 3.2") ("llama3.1" . "Llama 3.1") ("qwen2.5-coder" . "Qwen 2.5 Coder") --- a/lib/jcode/core/plugin.sls +++ b/lib/jcode/core/plugin.sls @@ -37,9 +37,20 @@ (let ([entries (directory-list dir)]) (if (list? entries) entries '()))))) dirs)))) - (def (load-plugin path) - "Load a single plugin file." + (def (ensure-library-dirs!) + "Ensure library-directories includes Jerboa lib dir for plugin imports." + (let ([jerboa-lib (path-join + (or (getenv "JERBOA_HOME") + (path-join (getenv "HOME") "mine/jerboa")) + "lib")]) + (when (file-directory? jerboa-lib) + (let ([entry (cons jerboa-lib jerboa-lib)]) + (unless (member entry (library-directories)) + (library-directories + (cons entry (library-directories)))))))) + (def (load-plugin path) "Load a single plugin file." (log-info logger "loading" `((path . ,path))) + (ensure-library-dirs!) (try (load path) (set! *loaded-plugins* (cons path *loaded-plugins*)) (log-info logger "loaded" `((path . ,path))) #t --- a/lib/jcode/mcp/client.sls +++ b/lib/jcode/mcp/client.sls @@ -166,7 +166,7 @@ [schema (or (hash-get tool "inputSchema") (make-hash-table))]) (let ([jcode-name (string-append prefix name)]) - (register-tool! + (register-internal-tool! jcode-name desc schema @@ -181,6 +181,7 @@ '()))) (def (init-mcp-tools) "Initialize all configured MCP servers." + (mcp-stop-all!) (let ([configs (load-mcp-config)]) (for-each (lambda (pair) --- a/lib/jcode/provider/provider.sls +++ b/lib/jcode/provider/provider.sls @@ -371,66 +371,123 @@ [body (openai-stream-body provider messages tools)] [text-acc (open-output-string)] [tc-table (make-hash-table)]) - (http-post-stream - url - headers - (json-object->string body) - (lambda (event-str) - (when event-str - (let* ([data (if (string-prefix? "data: " event-str) - (substring - event-str - 6 - (string-length event-str)) - event-str)]) - (cond - [(equal? data "[DONE]") (void)] - [else - (let ([json (guard (e [list #t #f]) - (string->json-object data))]) - (when json - (let* ([choices (hash-get json "choices")] - [choice (and (pair? choices) (car choices))] - [delta (and choice - (hash-get choice "delta"))]) - (when delta - (let ([content (hash-get delta "content")]) - (when (and content - (not (eq? content (void)))) - (put-string text-acc content) - (token-cb content))) - (let ([tcs (hash-get delta "tool_calls")]) - (when (and tcs (list? tcs)) - (for-each - (lambda (tc) - (let* ([idx (or (hash-get tc "index") - 0)] - [acc (or (hash-get tc-table idx) - (let ([a (make-hash-table)]) - (hash-put! - tc-table - idx - a) - a))] - [id (hash-get tc "id")] - [fn (hash-get tc "function")]) - (when id (hash-put! acc "id" id)) - (when fn - (let ([name (hash-get fn "name")] - [args (hash-get - fn - "arguments")]) - (when name - (hash-put! acc "name" name)) - (when args - (hash-put! - acc - "args" - (string-append - (or (hash-get acc "args") - "") - args))))))) - tcs)))))))]))))) + (let* ([body-json (json-object->string body)] + [dummy (log-info + logger + "stream-request" + `((url . ,url) + (body-len . ,(string-length body-json))))] + [http-status (http-post-stream + url + headers + body-json + (lambda (event-str) + (when event-str + (log-info + logger + "sse-event" + `((data + . + ,(if (> (string-length event-str) + 120) + (substring event-str 0 120) + event-str)))) + (let* ([data (if (string-prefix? + "data: " + event-str) + (substring + event-str + 6 + (string-length + event-str)) + event-str)]) + (cond + [(equal? data "[DONE]") (void)] + [else + (let ([json (guard (e [list #t #f]) + (string->json-object + data))]) + (when json + (let* ([choices (hash-get + json + "choices")] + [choice (and (pair? + choices) + (car choices))] + [delta (and choice + (hash-get + choice + "delta"))]) + (when delta + (let ([content (hash-get + delta + "content")]) + (when (and content + (not (eq? content + (void)))) + (put-string + text-acc + content) + (token-cb content))) + (let ([tcs (hash-get + delta + "tool_calls")]) + (when (and tcs + (list? tcs)) + (for-each + (lambda (tc) + (let* ([idx (or (hash-get + tc + "index") + 0)] + [acc (or (hash-get + tc-table + idx) + (let ([a (make-hash-table)]) + (hash-put! + tc-table + idx + a) + a))] + [id (hash-get + tc + "id")] + [fn (hash-get + tc + "function")]) + (when id + (hash-put! + acc + "id" + id)) + (when fn + (let ([name (hash-get + fn + "name")] + [args (hash-get + fn + "arguments")]) + (when name + (hash-put! + acc + "name" + name)) + (when args + (hash-put! + acc + "args" + (string-append + (or (hash-get + acc + "args") + "") + args))))))) + tcs)))))))])))))]) + (unless (= http-status 200) + (log-error + logger + "stream-http-error" + `((status . ,http-status) (url . ,url))))) (let* ([content (get-output-string text-acc)] [indices (sort < (hash-keys tc-table))] [tool-calls (map (lambda (idx) @@ -441,6 +498,11 @@ (or (hash-get acc "name") "unknown") (or (hash-get acc "args") "{}")))) indices)]) + (log-info + logger + "stream-result" + `((content-len . ,(string-length content)) + (tool-calls . ,(length tool-calls)))) (values content tool-calls)))) (def (anthropic-stream-headers provider) `(("Content-Type" . "application/json") @@ -463,79 +525,128 @@ [text-acc (open-output-string)] [tu-table (make-hash-table)] [current-idx (make-parameter #f)]) - (http-post-stream - url - headers - (json-object->string body) - (lambda (event-str) - (when event-str - (let* ([lines (string-split event-str #\newline)] - [event-type #f] - [data-str #f]) - (for-each - (lambda (line) - (cond - [(string-prefix? "event: " line) - (set! event-type - (substring line 7 (string-length line)))] - [(string-prefix? "data: " line) - (set! data-str - (substring line 6 (string-length line)))])) - lines) - (when (and event-type data-str) - (let ([json (guard (e [list #t #f]) - (string->json-object data-str))]) - (when json - (cond - [(equal? event-type "content_block_delta") - (let ([delta (hash-get json "delta")]) - (when delta - (let ([dtype (hash-get delta "type")]) - (cond - [(equal? dtype "text_delta") - (let ([text (hash-get delta "text")]) - (when text - (put-string text-acc text) - (token-cb text)))] - [(equal? dtype "input_json_delta") - (let ([idx (current-idx)] - [partial (hash-get - delta - "partial_json")]) - (when (and idx partial) - (let ([acc (hash-ref - tu-table - idx - #f)]) - (when acc - (hash-put! - acc - "args" - (string-append - (or (hash-get acc "args") - "") - partial))))))])))) - ((equal? event-type "content_block_start") - (let ([block (hash-get json "content_block")]) - (when (and block - (equal? - (hash-get block "type") - "tool_use")) - (let ([idx (hash-get json "index")]) - (current-idx idx) - (let ([acc (make-hash-table)]) - (hash-put! - acc - "id" - (hash-get block "id")) - (hash-put! - acc - "name" - (hash-get block "name")) - (hash-put! tu-table idx acc)))))) - ((equal? event-type "content_block_stop") - (current-idx #f)) - (#t (void))])))))))) + (let ([http-status (http-post-stream + url + headers + (json-object->string body) + (lambda (event-str) + (when event-str + (let* ([lines (string-split + event-str + #\newline)] + [event-type #f] + [data-str #f]) + (for-each + (lambda (line) + (cond + [(string-prefix? "event: " line) + (set! event-type + (substring + line + 7 + (string-length line)))] + [(string-prefix? "data: " line) + (set! data-str + (substring + line + 6 + (string-length line)))])) + lines) + (when (and event-type data-str) + (let ([json (guard (e [list #t #f]) + (string->json-object + data-str))]) + (when json + (cond + [(equal? + event-type + "content_block_delta") + (let ([delta (hash-get + json + "delta")]) + (when delta + (let ([dtype (hash-get + delta + "type")]) + (cond + [(equal? + dtype + "text_delta") + (let ([text (hash-get + delta + "text")]) + (when text + (put-string + text-acc + text) + (token-cb + text)))] + [(equal? + dtype + "input_json_delta") + (let ([idx (current-idx)] + [partial (hash-get + delta + "partial_json")]) + (when (and idx + partial) + (let ([acc (hash-ref + tu-table + idx + #f)]) + (when acc + (hash-put! + acc + "args" + (string-append + (or (hash-get + acc + "args") + "") + partial))))))])))) + ((equal? + event-type + "content_block_start") + (let ([block (hash-get + json + "content_block")]) + (when (and block + (equal? + (hash-get + block + "type") + "tool_use")) + (let ([idx (hash-get + json + "index")]) + (current-idx idx) + (let ([acc (make-hash-table)]) + (hash-put! + acc + "id" + (hash-get + block + "id")) + (hash-put! + acc + "name" + (hash-get + block + "name")) + (hash-put! + tu-table + idx + acc)))))) + ((equal? + event-type + "content_block_stop") + (current-idx #f)) + (#t (void))]))))))))]) + (unless (= http-status 200) + (log-error + logger + "stream-http-error" + `((status . ,http-status) (url . ,url))))) (let* ([content (get-output-string text-acc)] [indices (sort < (hash-keys tu-table))] [tool-calls (map (lambda (idx) @@ -548,14 +659,30 @@ indices)]) (values content tool-calls)))) (def (provider-stream-chat provider messages tools token-cb) + (unless (provider-api-key provider) + (error 'provider-stream-chat + (format + "No API key configured for provider '~a'. Set the appropriate env var or add it to jcode.json." + (provider-name provider)))) + (log-info + logger + "stream-chat" + `((provider . ,(provider-name provider)) + (model . ,(provider-model provider)) + (messages . ,(length messages)))) (case (string->symbol (provider-name provider)) [(openai openrouter deepseek ollama) (openai-stream-chat provider messages tools token-cb)] [(anthropic) (anthropic-stream-chat provider messages tools token-cb)] - [else + [(google) (let* ([response (provider-chat provider messages tools)] [content (or (message-content response) "")] [tcs (or (message-tool-calls response) '())]) (when (> (string-length content) 0) (token-cb content)) - (values content tcs))]))) + (values content tcs))] + [else + (error 'provider-stream-chat + (format + "Unknown provider '~a'" + (provider-name provider)))]))) --- a/lib/jcode/tool/lsp.sls +++ b/lib/jcode/tool/lsp.sls @@ -256,17 +256,17 @@ root)]) (lsp-initialize conn) (let ([schema (make-lsp-schema)]) - (register-tool! + (register-internal-tool! "lsp_definition" "Go to definition of symbol at file:line:character" schema handle-lsp-definition) - (register-tool! + (register-internal-tool! "lsp_hover" "Get hover information (type, docs) for symbol at file:line:character" schema handle-lsp-hover) - (register-tool! + (register-internal-tool! "lsp_references" "Find all references to symbol at file:line:character" schema --- a/lib/jcode/tool/registry.sls +++ b/lib/jcode/tool/registry.sls @@ -3,8 +3,8 @@ ;;; Source: src/jcode/tool/registry.ss (library (jcode tool registry) - (export register-tool! tool-execute get-tool-schemas - list-tools tool->openai-schema) + (export register-tool! register-internal-tool! tool-execute + get-tool-schemas list-tools tool->openai-schema) (import (except (chezscheme) make-hash-table hash-table? iota \x31;+ \x31;- getenv path-extension path-absolute? thread? make-mutex @@ -26,6 +26,15 @@ (let ([ht (make-hash-table)]) (for-each (lambda (p) (hash-put! ht (car p) (cdr p))) pairs) ht)) + (def (register-internal-tool! + name + description + schema + handler) + "Register a tool that won't be sent to the LLM (MCP, LSP, etc)." + (register-tool! name description schema handler) + (let ([t (hash-get *tools* name)]) + (when t (hash-put! t "internal" #t)))) (def (tool-execute name args) (log-info logger "execute" `((name . ,name))) (let ([tool (hash-get *tools* name)]) @@ -41,7 +50,7 @@ (def (get-tool-schemas) (map tool->openai-schema (filter - (lambda (t) t) + (lambda (t) (and t (not (hash-get t "internal")))) (map (lambda (name) (hash-get *tools* name)) (hash-keys *tools*))))) (def (list-tools) (hash-keys *tools*)) --- a/lib/jcode/ui/tui-status.sls +++ b/lib/jcode/ui/tui-status.sls @@ -11,7 +11,7 @@ (std misc string) (jcode ui tui-ffi) (jcode ui tui-theme) (jerboa core) (jerboa runtime)) (def (render-status-bar! x y width provider model tokens-in - tokens-out cost cwd mcp-ok? lsp-ok?) + tokens-out cost cwd) "Render the status bar at row y across width columns." (let ([bg (face-bg-attr 'status-bar)] [fg (face-fg-attr 'status-bar)]) @@ -50,11 +50,8 @@ (lloop (cdr parts) (+ next-col (if (pair? (cdr parts)) 3 0)))))))) - (let* ([mcp-ind (if mcp-ok? "● MCP" "○ MCP")] - [lsp-ind (if lsp-ok? "● LSP" "○ LSP")] - [cwd-short (shorten-path cwd 20)] - [right-str (string-append cwd-short " │ " mcp-ind " │ " - lsp-ind " ")] + (let* ([cwd-short (shorten-path cwd 30)] + [right-str (string-append " " cwd-short " ")] [right-col (max 0 (- (+ x width) (string-length right-str)))]) --- a/lib/jcode/ui/tui.sls +++ b/lib/jcode/ui/tui.sls @@ -471,41 +471,55 @@ (app-state-stream-buf-set! state "") (add-message! state (msg-block-assistant "")) (app-state-dirty?-set! state #t) - (tui-log - "run-agent: starting, session=~a" - (app-state-session-id state)) - (try (parameterize ([current-stream-cb - (lambda (token) - (tui-stream-token! state token))] - [current-tool-cb - (lambda (event name args) - (tui-tool-event! state event name args))]) - (agent-run (app-state-session-id state) text) - (tui-log - "run-agent: agent-run returned, buf=~s" - (app-state-stream-buf state)) - (let ([buf (app-state-stream-buf state)]) - (unless (string-empty? buf) - (update-last-assistant! state buf)))) - (catch - (e) - (tui-log "run-agent: ERROR ~a" (err->string e)) - (add-message! state (msg-block-error (err->string e))))) - (app-state-agent-busy?-set! state #f) - (app-state-scroll-offset-set! state 0) - (app-state-dirty?-set! state #t) (draw-all! state) - (tb-present!)) + (let ([p-name (or (current-provider-override) + (config-provider))] + [m-name (or (current-model-override) (config-model))] + [p-override (current-provider-override)] + [m-override (current-model-override)] + [s-id (app-state-session-id state)]) + (tui-log + "run-agent: spawning thread, session=~a provider=~a model=~a key?=~a" + s-id p-name m-name + (and (config-get-provider-key p-name) #t)) + (spawn + (lambda () + (try (parameterize ([current-provider-override p-override] + [current-model-override m-override] + [current-stream-cb + (lambda (token) + (tui-stream-token! state token))] + [current-tool-cb + (lambda (event name args) + (tui-tool-event! + state + event + name + args))]) + (agent-run s-id text) + (tui-log + "run-agent: agent-run returned, buf-len=~a" + (string-length (app-state-stream-buf state))) + (let ([buf (app-state-stream-buf state)]) + (unless (string-empty? buf) + (update-last-assistant! state buf)))) + (catch + (e) + (tui-log "run-agent: ERROR ~a" (err->string e)) + (add-message! + state + (msg-block-error (err->string e))))) + (app-state-agent-busy?-set! state #f) + (app-state-scroll-offset-set! state 0) + (app-state-dirty?-set! state #t))))) (def (tui-stream-token! state token) - "Handle a streaming token from the LLM." + "Handle a streaming token from the LLM — called from agent thread." (let ([buf (string-append (app-state-stream-buf state) token)]) (app-state-stream-buf-set! state buf) (update-last-assistant! state buf) (app-state-scroll-offset-set! state 0) - (app-state-dirty?-set! state #t) - (draw-all! state) - (tb-present!))) + (app-state-dirty?-set! state #t))) (def (tui-tool-event! state event name args) (case event [(start) @@ -514,9 +528,7 @@ (let ([tc (app-state-tool-counts state)]) (hash-put! tc name (+ 1 (or (hash-get tc name) 0)))) (update-sidebar-tools! state) - (app-state-dirty?-set! state #t) - (draw-all! state) - (tb-present!)] + (app-state-dirty?-set! state #t)] [(end) (let ([msgs (app-state-messages state)]) (let loop ([msgs (reverse msgs)]) @@ -596,9 +608,7 @@ (or (current-provider-override) (config-provider)) (or (current-model-override) (config-model)) (app-state-tokens-in state) (app-state-tokens-out state) - (app-state-cost state) (current-directory) - (not (null? (sidebar-state-mcp (app-state-sidebar state)))) - (and (sidebar-state-lsp (app-state-sidebar state)) #t)) + (app-state-cost state) (current-directory)) (when (app-state-sidebar-visible? state) (render-sidebar! (app-state-sidebar state) (sidebar-x state) 0 (app-state-sidebar-width state) --- a/src/jcode/core/config.ss +++ b/src/jcode/core/config.ss @@ -64,8 +64,11 @@ config)) (def (merge-opencode-keys! providers) - ;; Read opencode auth.json for provider keys - (let ((auth-file (path-join (getenv "HOME") ".local" "share" "opencode" "auth.json"))) + ;; Read auth.json for provider keys (check ~/.jcode first, then opencode) + (let ((auth-file (let ((jcode-auth (path-join (jcode-home) "auth.json"))) + (if (file-exists? jcode-auth) + jcode-auth + (path-join (getenv "HOME") ".local" "share" "opencode" "auth.json"))))) (when (file-exists? auth-file) (try (let ((auth (call-with-input-file auth-file read-json))) --- a/src/jcode/core/models.ss +++ b/src/jcode/core/models.ss @@ -67,16 +67,42 @@ ("gemini-2.0-flash" . "Gemini 2.0 Flash"))) (def openrouter-models - '(("anthropic/claude-sonnet-4" . "Claude Sonnet 4") - ("anthropic/claude-opus-4" . "Claude Opus 4") - ("openai/gpt-4o" . "GPT-4o") - ("google/gemini-2.5-pro" . "Gemini 2.5 Pro") - ("google/gemini-2.5-flash" . "Gemini 2.5 Flash") - ("deepseek/deepseek-chat" . "DeepSeek V3") - ("deepseek/deepseek-r1" . "DeepSeek R1") - ("meta-llama/llama-4-maverick" . "Llama 4 Maverick") - ("meta-llama/llama-4-scout" . "Llama 4 Scout") - ("qwen/qwen3-235b-a22b" . "Qwen3 235B"))) + '(;; Anthropic + ("anthropic/claude-sonnet-4" . "Claude Sonnet 4") + ("anthropic/claude-opus-4" . "Claude Opus 4") + ("anthropic/claude-haiku-3.5" . "Claude 3.5 Haiku") + ;; OpenAI + ("openai/gpt-4o" . "GPT-4o") + ("openai/gpt-4o-mini" . "GPT-4o Mini") + ("openai/o3" . "o3") + ("openai/o3-mini" . "o3 Mini") + ("openai/o4-mini" . "o4 Mini") + ;; Google + ("google/gemini-2.5-pro" . "Gemini 2.5 Pro") + ("google/gemini-2.5-flash" . "Gemini 2.5 Flash") + ("google/gemini-2.0-flash" . "Gemini 2.0 Flash") + ;; Meta + ("meta-llama/llama-4-maverick" . "Llama 4 Maverick") + ("meta-llama/llama-4-scout" . "Llama 4 Scout") + ("meta-llama/llama-3.3-70b-instruct" . "Llama 3.3 70B") + ;; DeepSeek + ("deepseek/deepseek-chat-v3-0324" . "DeepSeek V3 0324") + ("deepseek/deepseek-r1" . "DeepSeek R1") + ;; Qwen + ("qwen/qwen3-235b-a22b" . "Qwen3 235B") + ("qwen/qwen3-30b-a3b" . "Qwen3 30B") + ;; Mistral + ("mistralai/mistral-large" . "Mistral Large") + ("mistralai/mistral-medium" . "Mistral Medium") + ("mistralai/codestral" . "Codestral") + ;; Cohere + ("cohere/command-r-plus" . "Command R+") + ;; xAI + ("x-ai/grok-3" . "Grok 3") + ("x-ai/grok-3-mini" . "Grok 3 Mini") + ;; Other + ("microsoft/phi-4" . "Phi-4") + ("nousresearch/hermes-3-llama-3.1-405b" . "Hermes 3 405B"))) (def ollama-models '(("llama3.2" . "Llama 3.2") --- a/src/jcode/core/plugin.ss +++ b/src/jcode/core/plugin.ss @@ -37,9 +37,20 @@ (if (list? entries) entries '()))))) dirs)))) +(def (ensure-library-dirs!) + "Ensure library-directories includes Jerboa lib dir for plugin imports." + (let ((jerboa-lib (path-join (or (getenv "JERBOA_HOME") + (path-join (getenv "HOME") "mine/jerboa")) + "lib"))) + (when (file-directory? jerboa-lib) + (let ((entry (cons jerboa-lib jerboa-lib))) + (unless (member entry (library-directories)) + (library-directories (cons entry (library-directories)))))))) + (def (load-plugin path) "Load a single plugin file." (log-info logger "loading" `((path . ,path))) + (ensure-library-dirs!) (try (load path) (set! *loaded-plugins* (cons path *loaded-plugins*)) --- a/src/jcode/mcp/client.ss +++ b/src/jcode/mcp/client.ss @@ -182,7 +182,7 @@ (desc (or (hash-get tool "description") "MCP tool")) (schema (or (hash-get tool "inputSchema") (make-hash-table)))) (let ((jcode-name (string-append prefix name))) - (register-tool! jcode-name desc schema + (register-internal-tool! jcode-name desc schema (lambda (args) (mcp-call-tool conn name args)))))) tools) @@ -199,6 +199,7 @@ (def (init-mcp-tools) "Initialize all configured MCP servers." + (mcp-stop-all!) ;; clear any prior connections (let ((configs (load-mcp-config))) (for-each (lambda (pair) --- a/src/jcode/provider/provider.ss +++ b/src/jcode/provider/provider.ss @@ -343,9 +343,16 @@ (text-acc (open-output-string)) ;; tool-call accumulators: index -> alist with id/name/args-so-far (tc-table (make-hash-table))) - (http-post-stream url headers (json-object->string body) + (let* ((body-json (json-object->string body)) + (dummy (log-info logger "stream-request" + `((url . ,url) (body-len . ,(string-length body-json))))) + (http-status + (http-post-stream url headers body-json (lambda (event-str) (when event-str + (log-info logger "sse-event" `((data . ,(if (> (string-length event-str) 120) + (substring event-str 0 120) + event-str)))) ;; Strip "data: " prefix (let* ((data (if (string-prefix? "data: " event-str) (substring event-str 6 (string-length event-str)) @@ -387,7 +394,10 @@ (string-append (or (hash-get acc "args") "") args))))))) - tcs))))))))))))) + tcs))))))))))))))) + (unless (= http-status 200) + (log-error logger "stream-http-error" + `((status . ,http-status) (url . ,url))))) ;; Build result (let* ((content (get-output-string text-acc)) (indices (sort < (hash-keys tc-table))) @@ -399,6 +409,9 @@ (or (hash-get acc "name") "unknown") (or (hash-get acc "args") "{}")))) indices))) + (log-info logger "stream-result" + `((content-len . ,(string-length content)) + (tool-calls . ,(length tool-calls)))) (values content tool-calls)))) ;;; Anthropic Streaming ;;; @@ -422,7 +435,8 @@ ;; tool use accumulators: id -> alist (tu-table (make-hash-table)) (current-idx (make-parameter #f))) - (http-post-stream url headers (json-object->string body) + (let ((http-status + (http-post-stream url headers (json-object->string body) (lambda (event-str) (when event-str ;; Anthropic SSE uses multiple lines per event: "event: ...\ndata: ..." @@ -476,7 +490,10 @@ ;; Block ended ((equal? event-type "content_block_stop") (current-idx #f)) - (#t (void))))))))))) + (#t (void))))))))))))) + (unless (= http-status 200) + (log-error logger "stream-http-error" + `((status . ,http-status) (url . ,url))))) ;; Build result (let* ((content (get-output-string text-acc)) (indices (sort < (hash-keys tu-table))) @@ -493,13 +510,24 @@ ;; provider-stream-chat: stream text tokens to token-cb, accumulate tool calls. ;; Returns (values content-str tool-call-list). (def (provider-stream-chat provider messages tools token-cb) + (unless (provider-api-key provider) + (error 'provider-stream-chat + (format "No API key configured for provider '~a'. Set the appropriate env var or add it to jcode.json." + (provider-name provider)))) + (log-info logger "stream-chat" + `((provider . ,(provider-name provider)) + (model . ,(provider-model provider)) + (messages . ,(length messages)))) (case (string->symbol (provider-name provider)) ((openai openrouter deepseek ollama) (openai-stream-chat provider messages tools token-cb)) ((anthropic) (anthropic-stream-chat provider messages tools token-cb)) - ;; Fallback: non-streaming for other providers - (else + ((google) + ;; Google: non-streaming fallback (let* ((response (provider-chat provider messages tools)) (content (or (message-content response) "")) (tcs (or (message-tool-calls response) '()))) (when (> (string-length content) 0) (token-cb content)) - (values content tcs))))) + (values content tcs))) + (else + (error 'provider-stream-chat + (format "Unknown provider '~a'" (provider-name provider)))))) --- a/src/jcode/tool/lsp.ss +++ b/src/jcode/tool/lsp.ss @@ -253,13 +253,13 @@ (let ((conn (lsp-start command (if (list? args) args '()) root))) (lsp-initialize conn) (let ((schema (make-lsp-schema))) - (register-tool! "lsp_definition" + (register-internal-tool! "lsp_definition" "Go to definition of symbol at file:line:character" schema handle-lsp-definition) - (register-tool! "lsp_hover" + (register-internal-tool! "lsp_hover" "Get hover information (type, docs) for symbol at file:line:character" schema handle-lsp-hover) - (register-tool! "lsp_references" + (register-internal-tool! "lsp_references" "Find all references to symbol at file:line:character" schema handle-lsp-references)) (log-info logger "ready" `((tools . 3)))) --- a/src/jcode/tool/registry.ss +++ b/src/jcode/tool/registry.ss @@ -1,6 +1,7 @@ ;;; jcode tool registry (export register-tool! + register-internal-tool! tool-execute get-tool-schemas list-tools @@ -26,6 +27,12 @@ (for-each (lambda (p) (hash-put! ht (car p) (cdr p))) pairs) ht)) +(def (register-internal-tool! name description schema handler) + "Register a tool that won't be sent to the LLM (MCP, LSP, etc)." + (register-tool! name description schema handler) + (let ((t (hash-get *tools* name))) + (when t (hash-put! t "internal" #t)))) + (def (tool-execute name args) (log-info logger "execute" `((name . ,name))) (let ((tool (hash-get *tools* name))) @@ -38,7 +45,8 @@ (def (get-tool-schemas) (map tool->openai-schema - (filter (lambda (t) t) + (filter (lambda (t) + (and t (not (hash-get t "internal")))) (map (lambda (name) (hash-get *tools* name)) (hash-keys *tools*))))) --- a/src/jcode/ui/tui-status.ss +++ b/src/jcode/ui/tui-status.ss @@ -7,7 +7,7 @@