Skip to content

OpenAI-compatible provider: sub-agents, Skill tool, roles-tab defaults, capability-resolver fix - #173

Closed
spopescu wants to merge 42 commits into
mainfrom
fix/chat-model-selection-jcef-hang
Closed

OpenAI-compatible provider: sub-agents, Skill tool, roles-tab defaults, capability-resolver fix#173
spopescu wants to merge 42 commits into
mainfrom
fix/chat-model-selection-jcef-hang

Conversation

@spopescu

Copy link
Copy Markdown
Collaborator

Summary

Accumulated work on the OpenAI-compatible provider integration, plus a capability-resolution bugfix:

  • Capability-resolver fix (latest commit): AgentBackendFactory and the wiki-librarian/drift authoring paths resolved model capability from profile modelRules only, ignoring the per-model capability already verified and persisted by "Refresh Models". A model probed as agentic with no matching profile rule (e.g. most NVIDIA NIM models) wrongly fell through to COMPLETION_ONLY and the backend refused to start. Added ModelCapabilityResolver.catalogCapability to feed the verified verdict into resolve()'s endpointCapability slot (which already outranks profile rules per the documented precedence userOverride > endpointCapability > matchingRule > COMPLETION_ONLY).
  • Dispatchable Agent (sub-agent) tool for the OpenAI-compatible backend
  • Skill tool support for OpenAI-compatible models, with fail-soft arg parsing and compact skill-injection chips
  • Context-window management + tool-call loop-breaker for the OpenAI-compatible agent loop
  • Consolidated completions settings into the Advanced tab; moved tool-approval/auto-accept-edits into a persistent CLI Behavior panel
  • Cross-backend ask_wiki_librarian with a real Codex librarian path; /refresh-wiki now runs as an out-of-band wiki-author subprocess
  • Smart per-role model defaults + diagnosable wiki-librarian errors
  • Various fixes: new-chat model selection, JCEF module dependency, headless turn-test hang, Windows-specific path/argv issues

Test plan

  • ./gradlew compileKotlin compileTestKotlin clean
  • ModelCapabilityResolverTest (7 tests) and CliBridgeBackendSelectionTest (12 tests) green, 0 skipped/failed — includes new regression coverage for the capability-resolver fix (verified-agentic, unknown/absent, completion-only cases, and a factory-level NVIDIA NIM repro)
  • Manually verified against a live NVIDIA NIM profile — the previously-failing agentic model now starts correctly

🤖 Generated with Claude Code

spopescu and others added 30 commits July 20, 2026 13:26
OpenAI-compatible providers (Qwen, GPT, etc.) were truncating responses at 9-25s
because the openai-tooling-prompt.md resource was missing, causing a silent
fallback to incomplete tooling guidance. This incomplete guidance lacked critical
OpenAI-specific instructions for:
- Function-calling format specification
- Streaming behavior and completion semantics
- Tool approval workflows
- Best practices for safe tool execution

Without this guidance, non-Claude models emit malformed tool calls or misinterpret
stream boundaries, causing the CLI to exit prematurely mid-response.

Created src/main/resources/prompts/openai-tooling-prompt.md with comprehensive
OpenAI-specific tooling guidance mirroring the structure used for other backends.

Root cause: CliProcess.loadStaticPrompt() errors loudly when resources are missing,
but OpenAiInstructions.build() silently falls back to buildFallbackToolingGuidance().
The fallback was intended as a temporary measure but lacked the model-specific
instructions non-Claude backends require.

Fixes: Qwen 3.6-35B-A3B and other non-Claude OpenAI-compatible providers now
receive proper tooling instructions and no longer truncate mid-response.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
…turn-test hang

Model selection regression: a fresh chat showed the pinned model (e.g. Opus)
in the dropdown but launched the CLI on its own default (Haiku) until the user
manually re-picked. The dropdown reads the tab's AgentSelection.modelId while
the Claude/Codex backends re-derived --model from the selectedModels settings
map, which is empty when the default was set via the Roles tab. CliProcess now
honors the tab's pinned model id (threaded through AgentBackendFactory), falling
back to the settings-map read only when blank; the Codex branch mirrors this.

JCEF NoClassDefFoundError: JBCefJSQuery$Response lives in the separate
com.intellij.modules.jcef module, which ClawDEA never declared. Without it the
jcef module is absent from the plugin classloader and ChatPanel.<init> throws on
installs where JCEF is not already on the shared classpath. Add the dependency.

Headless turn-test hang: the three OpenAiCompatibleAgentBackend turn tests hung
when run in a fork without an IntelliJ Application, because a running turn read
ClawDEASettings.getInstance() and the dying coroutine never emitted a terminal
Result, blocking the queue reader forever. Add a settingsProvider test seam so
turns are self-sufficient headless, plus a bounded @test timeout as a backstop.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…vider

Applying settings (e.g. setting Roles → Chat default to Opus) rebuilt any open
chat tab onto the GLOBAL effective provider. ChatPanel.onSettingsChanged read
AuthManager.effectiveProviderId() (global = Qwen), compared its backend kind to
the tab's own backend, saw they differed, and called rebuildSessionForBackendChange(),
reseeding the tab from the global provider. So an open Opus chat flipped to Qwen
on any settings apply — the Roles change was incidental.

A settings apply must never change an already-open tab's provider/backend: the
tab keeps its own per-tab AgentSelection and only NEW chats adopt a changed
global default. onSettingsChanged now only restarts the SAME bridge in place to
pick up non-provider settings (tool-approval mode, toggles, CLI path/args, wiki
model); CliProcess re-reads the tab's pinned model on restart, so provider and
model are preserved. The global-provider-based rebuild coordinator is replaced
by a pure settingsApplyAction() decision (RESTART_IN_PLACE / NONE), and the
now-unused deferred-rebuild path (onTurnBecameIdle flush, no-arg
rebuildSessionForBackendChange) is removed.

Also includes an in-flight enhancement: a failed wiki-librarian subagent now
prepends its WIKI-role model to the error summary for at-a-glance diagnosis.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Completes the WIP begun with the EventStreamHandler librarian-error change: a
stale WIKI-role model (e.g. Bedrock Claude 3 Haiku, which rejects prompt caching
with an HTTP 400) silently broke the in-chat wiki librarian, and the surfaced
error named neither the model nor the cause.

Root-cause fix — smart per-role defaults (RoleDefaults/RoleDefaultsResolver):
compute Chat / Wiki / Completions from whichever provider is authenticated —
latest Opus for chat + latest Haiku for wiki/completions on Claude, Terra/Luna
on Codex, first agentic model on openai-compatible — so each role stays on a
caching-capable, task-appropriate model. RoleSelectionStore seeds these on fresh
install (falling back to the legacy clone-across-roles when nothing is
authenticated), and the Roles settings tab gains a "Reset defaults" button.

Diagnostics: AgenticLibrarian now names the failing model in its error
(mirroring the in-chat subagent path in EventStreamHandler), so an
unsupported-model 400 is legible at a glance instead of an opaque failure.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The skill-invocation probe was hardcoded true, so every backend forwarded
'/skill-name' as literal text. OpenAI-compatible and Codex backends have no
CLI-native slash resolver, so skills silently did nothing. Gate the native
branch on BackendKind.CLAUDE_CLI; other backends take the SKILL.md-injection
fallback.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Capture session skills in start(); advertise the Skill tool (gated on
preloadSkillCatalog + non-empty skills) and route Skill calls to SkillTool.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ture)

Regression guard: currentSkills must be captured on every start(), incl.
resume, so the Skill tool is advertised after a resumed session.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Remove the false 'IDE will intercept slash text' claim; instruct the model to
invoke skills via the Skill tool.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wrong-typed or null "name"/"args" now return a soft tool-error instead of
throwing out of the executor and ending the turn (matching the Bash branch).
Restore the dropped "proceed without skills if none listed" prompt caveat.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
propose_write / apply_patch could report a file as written when it never
landed on disk: EditDiffReviewer.applyContent returned Unit and silently
bailed when parent.mkdirs() failed, and reviewAndRespond / HostPatchTool
reported ACCEPTED regardless. Non-Claude backends (Qwen) also emit relative
file_path args, which File(path) resolved against the IDE JVM CWD (~ "/"),
so writes mislanded or apply_patch wrongly rejected them as "outside project".

- applyContent now returns Boolean and resolves relative paths against the
  project base (mirrors PsiUtils); callers surface a truthful error on false.
- HostPatchTool normalizes relative file_path against projectBasePath before
  the path-inside-project check.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On non-Claude backends a user-typed /skill fell back to injecting the full
SKILL.md as the bridge payload, which ChatPanel rendered verbatim as a user
bubble. Now the fallback renders a compact "Using skill /name" chip and
dispatches the markdown via CommandContext.dispatchToBridge (renderInChat=false,
mirroring BridgeExpandingHandler) so the model still receives the full skill
text. Falls back to the plain (rendering) send only when no hidden-dispatch
channel is available (headless/tests).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ackend

Agentic OpenAI-compatible models had no way to dispatch a sub-agent, so skills
like subagent-driven-development narrated "dispatching..." and stalled. Add an
Agent tool that runs a depth-1 nested tool loop and streams its steps into the
existing sub-agent card (SubAgentController already recognizes the "Agent" tool
and routes children by parentToolUseId).

- AgentLoopController gains an optional SubAgentRunner; a tool call named "Agent"
  is routed to it (suspend + emit) instead of the synchronous executor, so the
  executor interface and all implementors are untouched.
- SubAgentDispatcher runs a fresh nested turn, re-tags child events with the
  dispatching tool_use id, swallows the nested terminal Result, and returns the
  sub-agent's final report as the tool result. Fail-soft on missing prompt /
  malformed args. Sub-agents are NOT given the Agent tool (no recursion).
- OpenAiToolCatalog.agentToolDefinition() advertises it (subagent_type/description
  match the card fields; prompt is the task). Gated on a new enableOpenAiSubagents
  setting (default on).
- openai-tooling-prompt: document the Agent tool + require absolute file paths and
  actually writing spec/plan files instead of pasting them into chat.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the completions model dropdown from ProvidersTab to AdvancedTab
since the Roles tab already handles per-role model selection and the
behavioral settings (enabled/debounce/manual-only) are global, not
provider-scoped.
Since the Roles tab now handles per-role model selection and completions
settings are global (not provider-specific), move the model dropdown from
ProvidersTab to AdvancedTab alongside the existing behavioral toggles.
…ompatible

Weak/limited-context models (e.g. 128K Qwen) degraded into repetitive tool-call
loops mid-turn. Root cause: the model's context window was unknown, so the agent
loop used a ~1M-char compaction proxy that exceeded the real window — compaction
never fired and history overflowed. There was also no UI to set a window: the
models table had no such column, and ContextWindows read only profile JSON.

- Add a "Context window" column to the OpenAI-compatible models table
  (ModelEntry.contextWindow + OpenAiModelTableModel); preserved across /models
  refresh by both catalog mergers.
- ContextWindows.resolve(): catalog column → profile map → conservative 128K
  default (never null). The backend passes the resolved window so the loop always
  uses the token budget. When a provider reports no usage, the loop estimates
  tokens from chars against the same window so compaction still fires.
- ToolCallLoopGuard: detects a run of identical FAILING tool calls and escalates
  nudge → clean stop, breaking degeneration loops without erroring the turn.
  Sub-agent dispatches are exempt; a success resets the streak; distinct args are
  distinct signatures.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ModifiedFrom

Complete the wiring for the new completionsModelCombo so it persists
the selected model to ClawDEASettings.State.completionsModel on apply
and reads it back on load.
…Behavior panel, fix CLI path visibility for all providers
…hardcoded completions model, extract CLI settings, fix CLI path visibility
Makes ask_wiki_librarian work for every WIKI-role provider, fixing the
"OpenAI-compatible profile '' not configured" crash when a Codex or Claude
WIKI role invoked the tool, and adds a genuine Codex execution path.

chooseLibrarianExecution tiers the handler by WIKI provider:
- Claude   -> ClaudeSubprocessLibrarian (claude -p, allowlisted MCP tools)
- openai   -> AgenticLibrarian in-process loop
- Codex    -> CodexExecLibrarian (NEW): codex exec --json

CodexExecLibrarian runs read-only by construction: -s read-only with
approval_policy=never and NO MCP server. The exec spike proved MCP tools
only run under danger-full-access (any sandbox blocks the loopback socket
on macOS), which would leave codex's shell ungated -- so the librarian
reads the on-disk wiki and greps the tree with codex's built-in shell
instead. Read commands need no escalation; a write escalation fails under
never (no hang, no write). Tradeoff: no record_wiki_suggestion, no index
tools -- acceptable for read-only Q&A.

Chat routing keys on the CHAT backend: Codex/openai chats (which cannot
spawn the --agents subagent) advertise the ask_wiki_librarian MCP tool;
the shared primer anchor is mechanism-neutral.

Verified: compileKotlin + compileTestKotlin green; 18/18 unit tests pass
(CodexExecLibrarianTest 7, LibrarianExecutionTest 6,
ClaudeSubprocessLibrarianTest 5); read-only sandbox confirmed live. Full
end-to-end Codex answer not yet proven live (OpenAI workspace spend cap).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The chat model selector was noticeably wider than its content. Two
independent Swing sizing quirks each added slack:

- The collapsed combo sized itself via prototypeDisplayValue, which
  routes through the cell renderer and picks up ~50px of internal
  padding; and its widest row was a signed-out third-party profile
  (long name + "(sign in)" suffix) the user never sees selected. Now
  the combo width is pinned to the widest *enabled* label + arrow +
  insets + a small margin.
- BasicComboPopup.show() hardcodes the popup width to the combo's own
  width (arrow button included). The popup is now resized to its list
  content width when shown.

Also refresh the selector on subscription/Codex auth status changes so
newly-signed-in providers become selectable without reopening the tab.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
NotesPaths/SessionScanner/TranscriptCostReader encoded the project base
path with `"-" + trimStart('/').replace("/", "-")`, which only stripped
forward slashes. On Windows the drive colon survived (e.g. `-C:-Users-…`)
and Path.resolve threw InvalidPathException, silently dropping the notes
primer section. It also failed to locate sessions for any POSIX path
containing `.` or `_` (e.g. `.claude/worktrees` sessions).

Centralize the scheme in ClaudeProjectDir.encode(), which replaces every
non-alphanumeric char with `-` — verified against real CLI-created dirs
(`.claude` → `--claude`, `enc.test_dir` → `enc-test-dir`). Route all three
consumers through it so the plugin points at the exact directory the CLI
creates.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…md.exe cap)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
spopescu and others added 12 commits July 22, 2026 00:36
The interactive chat CLI no longer injects wiki --agents; the librarian is
reached via the ask_wiki_librarian MCP tool and the author via the
out-of-band WikiAuthorInvoker. Update both pages to drop the deleted
chooseLibrarianMode / CLAUDE_SUBAGENT* framing and describe the single
MCP-tool dispatch path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Delete dead prompts/wiki-librarian-prompt.md (the interactive librarian
  now uses WIKI_LIBRARIAN_TOOL_PROMPT; the subprocess librarian loads
  /agents/wiki-librarian.md) and its now-orphaned resource test.
- Fix stale WIKI_LIBRARIAN_PROMPT comment references → WIKI_LIBRARIAN_TOOL_PROMPT.
- runAuthorNow KDoc: describe the invoker generically (not "--agents"),
  since the openai-compatible/codex invokers don't use --agents.
- Drop a stray double blank line in ChatPanel.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…not merely unused

Reflects that the subagent-persona constant and its prompt resource are now
gone entirely (Task 4 + the minor-cleanup sweep), not "still exists but
unreferenced".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The one-shot claude/codex subprocesses redirected stdin from a hardcoded
File("/dev/null") to hand the process immediate EOF (it otherwise blocks on
an open stdin pipe). On Windows "/dev/null" is a non-existent relative path,
so Redirect.from throws at process start — the in-chat wiki-librarian tool,
the codex librarian, and the out-of-band wiki-author all failed on Windows.

Centralize the null-device path in NullDevice (NUL on Windows, /dev/null
elsewhere) and route all three ProcessBuilder stdin redirects through it,
preserving the immediate-EOF behavior cross-platform.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ab model

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Provider/service/profile resolution before the runner call was unguarded; a
throw there would escape the /seed-wiki coroutine (no CoroutineExceptionHandler)
and leave a dangling "Seeding…" line with no resolution. Wrap setup+run so any
failure surfaces as a Result(ok=false) the caller renders.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…horing entry point

Adds the seed-wiki path (WikiPromptRunner, tiered by WIKI-role backend, used by
/seed-wiki to bootstrap under the WIKI model) alongside rescan auto-apply and
runAuthorNow. Produced by the drift auto-author; verified accurate against source.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
AgentBackendFactory (and the wiki-librarian/drift authoring paths)
resolved model capability from profile modelRules only, ignoring the
per-model capability that "Refresh Models" already verified and
persisted in the catalog. A model probed as agentic with no matching
profile rule (e.g. NVIDIA NIM models outside a narrow rule list) fell
through to COMPLETION_ONLY and the backend refused to start.

Add ModelCapabilityResolver.catalogCapability to read a DEFINITE
verified verdict (agentic/completion_only) from the catalog and feed
it into resolve()'s endpointCapability slot, which already outranks
profile rules. An unknown/absent entry still yields null so profile
rules decide — a failed probe must never override a user-authored
rule.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@spopescu

Copy link
Copy Markdown
Collaborator Author

Superseded by #174 — this branch's history diverged from main because #172 already squash-merged the same content; re-opening as a clean single-commit branch off current main.

@spopescu spopescu closed this Jul 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant