@golba98/codexa 1.0.3 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/README.md +17 -18
  2. package/bin/codexa.js +62 -144
  3. package/package.json +4 -3
  4. package/src/app.tsx +642 -303
  5. package/src/commands/handler.ts +7 -18
  6. package/src/config/buildInfo.ts +2 -2
  7. package/src/config/layeredConfig.ts +1 -1
  8. package/src/config/runtimeConfig.ts +1 -1
  9. package/src/config/settings.ts +1 -1
  10. package/src/config/trustStore.ts +1 -1
  11. package/src/core/README.md +52 -0
  12. package/src/core/agent/loop.ts +282 -0
  13. package/src/core/agent/protocol.ts +211 -0
  14. package/src/core/agent/tools.ts +414 -0
  15. package/src/core/{codexExecArgs.ts → codex/codexExecArgs.ts} +2 -2
  16. package/src/core/{codexLaunch.ts → codex/codexLaunch.ts} +3 -3
  17. package/src/core/{codexPrompt.ts → codex/codexPrompt.ts} +3 -3
  18. package/src/core/debug/modelStateDebug.ts +34 -0
  19. package/src/core/executables/antigravityExecutable.ts +48 -0
  20. package/src/core/executables/executableResolver.ts +5 -1
  21. package/src/core/models/codexModelCapabilities.ts +57 -4
  22. package/src/core/models/codexModelsCacheSeed.ts +153 -0
  23. package/src/core/models/providerModelCache.ts +106 -0
  24. package/src/core/perf/renderDebug.ts +10 -6
  25. package/src/core/process/CommandRunner.ts +12 -1
  26. package/src/core/providerLauncher/registry.ts +64 -18
  27. package/src/core/providerLauncher/types.ts +12 -9
  28. package/src/core/providerLauncher/workspaceConfig.ts +41 -26
  29. package/src/core/providerRuntime/anthropic.ts +10 -6
  30. package/src/core/providerRuntime/antigravity.ts +461 -0
  31. package/src/core/providerRuntime/claudeCodeDiscovery.ts +724 -446
  32. package/src/core/providerRuntime/claudeCodeDiscoveryDebug.ts +55 -0
  33. package/src/core/providerRuntime/contextMetadata.ts +12 -24
  34. package/src/core/providerRuntime/local.ts +129 -51
  35. package/src/core/providerRuntime/mistralVibe.ts +663 -0
  36. package/src/core/providerRuntime/models.ts +40 -15
  37. package/src/core/providerRuntime/reasoning.ts +9 -6
  38. package/src/core/providerRuntime/registry.ts +76 -4
  39. package/src/core/providerRuntime/types.ts +20 -14
  40. package/src/core/providers/codexSubprocess.ts +2 -2
  41. package/src/core/providers/types.ts +1 -1
  42. package/src/core/{attachments.ts → shared/attachments.ts} +27 -4
  43. package/src/core/{cleanupFastFail.ts → shared/cleanupFastFail.ts} +1 -1
  44. package/src/core/{hollowResponseFormat.ts → shared/hollowResponseFormat.ts} +1 -1
  45. package/src/core/terminal/clearFrameBoundary.ts +814 -0
  46. package/src/core/terminal/frameLock.ts +109 -0
  47. package/src/core/terminal/inkRenderReset.ts +123 -0
  48. package/src/core/terminal/terminalControl.ts +22 -0
  49. package/src/core/terminal/terminalTitle.ts +16 -102
  50. package/src/core/{channel.ts → version/channel.ts} +1 -1
  51. package/src/core/{updateCheck.ts → version/updateCheck.ts} +10 -5
  52. package/src/core/{launchContext.ts → workspace/launchContext.ts} +9 -16
  53. package/src/core/{planStorage.ts → workspace/planStorage.ts} +2 -2
  54. package/src/core/{workspaceGuard.ts → workspace/workspaceGuard.ts} +97 -13
  55. package/src/headless/execRunner.ts +2 -2
  56. package/src/index.tsx +66 -98
  57. package/src/session/appSession.ts +10 -7
  58. package/src/session/chatLifecycle.ts +1 -1
  59. package/src/session/liveRenderScheduler.ts +1 -1
  60. package/src/session/types.ts +1 -1
  61. package/src/test/runtimeTestUtils.ts +90 -0
  62. package/src/ui/{ActivityBars.tsx → chrome/ActivityBars.tsx} +1 -1
  63. package/src/ui/{ActivityIndicator.tsx → chrome/ActivityIndicator.tsx} +3 -3
  64. package/src/ui/{AnimatedStatusText.tsx → chrome/AnimatedStatusText.tsx} +3 -3
  65. package/src/ui/chrome/AppShell.tsx +672 -0
  66. package/src/ui/{BottomComposer.tsx → chrome/BottomComposer.tsx} +159 -158
  67. package/src/ui/{DashCard.tsx → chrome/DashCard.tsx} +2 -2
  68. package/src/ui/{RunFooter.tsx → chrome/RunFooter.tsx} +3 -3
  69. package/src/ui/chrome/RuntimeStatusBar.tsx +108 -0
  70. package/src/ui/{Spinner.tsx → chrome/Spinner.tsx} +1 -1
  71. package/src/ui/{TopHeader.tsx → chrome/TopHeader.tsx} +54 -38
  72. package/src/ui/{UpdateAvailableCard.tsx → chrome/UpdateAvailableCard.tsx} +5 -5
  73. package/src/ui/{commandNormalize.ts → input/commandNormalize.ts} +1 -1
  74. package/src/ui/{focus.ts → input/focus.ts} +1 -1
  75. package/src/ui/{inputBuffer.ts → input/inputBuffer.ts} +3 -3
  76. package/src/ui/layout.ts +298 -24
  77. package/src/ui/{AttachmentImportPanel.tsx → panels/AttachmentImportPanel.tsx} +3 -3
  78. package/src/ui/{AuthPanel.tsx → panels/AuthPanel.tsx} +5 -5
  79. package/src/ui/{BackendPicker.tsx → panels/BackendPicker.tsx} +2 -2
  80. package/src/ui/{ModePicker.tsx → panels/ModePicker.tsx} +2 -2
  81. package/src/ui/{ModelPicker.tsx → panels/ModelPicker.tsx} +2 -2
  82. package/src/ui/{ModelPickerScreen.tsx → panels/ModelPickerScreen.tsx} +224 -42
  83. package/src/ui/{ModelReasoningPicker.tsx → panels/ModelReasoningPicker.tsx} +5 -5
  84. package/src/ui/{Panel.tsx → panels/Panel.tsx} +1 -1
  85. package/src/ui/{PermissionsPanel.tsx → panels/PermissionsPanel.tsx} +3 -3
  86. package/src/ui/{PlanActionPicker.tsx → panels/PlanActionPicker.tsx} +2 -2
  87. package/src/ui/{PlanReviewPanel.tsx → panels/PlanReviewPanel.tsx} +5 -5
  88. package/src/ui/panels/ProviderPicker.tsx +737 -0
  89. package/src/ui/{ReasoningPicker.tsx → panels/ReasoningPicker.tsx} +3 -3
  90. package/src/ui/{SelectionPanel.tsx → panels/SelectionPanel.tsx} +5 -1
  91. package/src/ui/{SettingsPanel.tsx → panels/SettingsPanel.tsx} +4 -4
  92. package/src/ui/{TextEntryPanel.tsx → panels/TextEntryPanel.tsx} +4 -4
  93. package/src/ui/{ThemePicker.tsx → panels/ThemePicker.tsx} +2 -2
  94. package/src/ui/{UpdatePromptPanel.tsx → panels/UpdatePromptPanel.tsx} +17 -23
  95. package/src/ui/{Markdown.tsx → render/Markdown.tsx} +2 -2
  96. package/src/ui/{diffRenderer.ts → render/diffRenderer.ts} +1 -1
  97. package/src/ui/{logoVariants.ts → render/logoVariants.ts} +4 -8
  98. package/src/ui/{modeDisplay.ts → render/modeDisplay.ts} +2 -2
  99. package/src/ui/{outputPipeline.ts → render/outputPipeline.ts} +2 -2
  100. package/src/ui/{runtimeDisplay.ts → render/runtimeDisplay.ts} +23 -10
  101. package/src/ui/{textLayout.ts → render/textLayout.ts} +15 -4
  102. package/src/ui/{ActionRequiredBlock.tsx → timeline/ActionRequiredBlock.tsx} +3 -3
  103. package/src/ui/{AgentBlock.tsx → timeline/AgentBlock.tsx} +10 -10
  104. package/src/ui/{StaticIntroItem.tsx → timeline/StaticIntroItem.tsx} +2 -2
  105. package/src/ui/{ThinkingBlock.tsx → timeline/ThinkingBlock.tsx} +4 -4
  106. package/src/ui/{Timeline.tsx → timeline/Timeline.tsx} +1620 -1471
  107. package/src/ui/timeline/TranscriptShell.tsx +322 -0
  108. package/src/ui/{TurnGroup.tsx → timeline/TurnGroup.tsx} +15 -15
  109. package/src/ui/timeline/layoutListWindow.ts +145 -0
  110. package/src/ui/{progressEntries.ts → timeline/progressEntries.ts} +3 -3
  111. package/src/ui/{runActivityView.ts → timeline/runActivityView.ts} +1 -1
  112. package/src/ui/{timelineMeasure.ts → timeline/timelineMeasure.ts} +206 -134
  113. package/src/core/codex.ts +0 -124
  114. package/src/ui/AppShell.tsx +0 -706
  115. package/src/ui/ProviderPicker.tsx +0 -321
  116. package/src/ui/StaticTranscriptItem.tsx +0 -56
  117. /package/src/core/{inputDebug.ts → debug/inputDebug.ts} +0 -0
  118. /package/src/core/{clipboard.ts → shared/clipboard.ts} +0 -0
  119. /package/src/core/{githubDiagnostics.ts → shared/githubDiagnostics.ts} +0 -0
  120. /package/src/core/{projectInstructions.ts → workspace/projectInstructions.ts} +0 -0
  121. /package/src/core/{workspaceActivity.ts → workspace/workspaceActivity.ts} +0 -0
  122. /package/src/core/{workspaceRoot.ts → workspace/workspaceRoot.ts} +0 -0
  123. /package/src/ui/{busyStatusAnimation.ts → chrome/busyStatusAnimation.ts} +0 -0
  124. /package/src/ui/{slashCommands.ts → input/slashCommands.ts} +0 -0
  125. /package/src/ui/{terminalAnswerFormat.ts → render/terminalAnswerFormat.ts} +0 -0
@@ -0,0 +1,55 @@
1
+ import { resolveClaudeExecutable } from "../executables/claudeExecutable.js";
2
+ import { runCommand } from "../process/CommandRunner.js";
3
+ import {
4
+ claudeCodeModelsToProviderModels,
5
+ discoverClaudeCodeCapabilities,
6
+ discoverModelsFromClaudePackageMetadata,
7
+ } from "./claudeCodeDiscovery.js";
8
+
9
+ async function main(): Promise<void> {
10
+ const cwd = process.cwd();
11
+ const resolvedCommand = await resolveClaudeExecutable({ cwd });
12
+ const versionResult = await runCommand({
13
+ executable: resolvedCommand,
14
+ args: ["--version"],
15
+ cwd,
16
+ timeoutMs: 5_000,
17
+ }).result;
18
+ const packageMetadata = discoverModelsFromClaudePackageMetadata(resolvedCommand);
19
+ const discovery = await discoverClaudeCodeCapabilities({ cwd });
20
+ const normalized = claudeCodeModelsToProviderModels(discovery.models);
21
+
22
+ const report = {
23
+ claudeCommand: resolvedCommand,
24
+ claudeBinaryPath: packageMetadata?.sourcePath ?? resolvedCommand,
25
+ claudeVersion: versionResult.status === "completed" && versionResult.exitCode === 0
26
+ ? versionResult.stdout.trim()
27
+ : null,
28
+ discoverySourceUsed: discovery.modelSource,
29
+ packageMetadataSource: packageMetadata?.sourcePath ?? null,
30
+ rawDiscoveredModelEntries: packageMetadata?.rawModelIds ?? [],
31
+ normalizedModelEntries: normalized.map((model) => ({
32
+ id: model.modelId,
33
+ label: model.label,
34
+ family: model.family,
35
+ version: model.version,
36
+ canonicalId: model.canonicalId,
37
+ source: model.source,
38
+ isFallback: model.isFallback,
39
+ discoveryKind: model.discoveryKind,
40
+ })),
41
+ fallbackReason: discovery.modelSource === "fallback"
42
+ ? discovery.diagnostics ?? { reason: "No Claude Code command, package metadata, settings, or config model source returned versioned models." }
43
+ : null,
44
+ };
45
+
46
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
47
+ }
48
+
49
+ try {
50
+ await main();
51
+ } catch (err) {
52
+ const message = err instanceof Error ? err.message : String(err);
53
+ process.stderr.write(`debug:claude-models failed: ${message}\n`);
54
+ process.exit(1);
55
+ }
@@ -67,14 +67,9 @@ const KNOWN_CONTEXT_REGISTRY: Record<string, KnownContextRegistryEntry> = {
67
67
  // Exact documented Anthropic model IDs only. Provider aliases such as
68
68
  // "haiku" remain unknown unless configured or discovered by CLI metadata.
69
69
  // Source: https://docs.anthropic.com/en/docs/about-claude/models
70
- "anthropic:claude-opus-4-7": {
71
- contextLength: 200_000,
72
- sourceUrl: "https://docs.anthropic.com/en/docs/about-claude/models",
73
- note: "Anthropic documented context window for this exact model ID.",
74
- },
75
- "anthropic:claude-sonnet-4-6": {
76
- contextLength: 200_000,
77
- sourceUrl: "https://docs.anthropic.com/en/docs/about-claude/models",
70
+ "anthropic:claude-sonnet-4-6": {
71
+ contextLength: 200_000,
72
+ sourceUrl: "https://docs.anthropic.com/en/docs/about-claude/models",
78
73
  note: "Anthropic documented context window for this exact model ID.",
79
74
  },
80
75
  "anthropic:claude-haiku-4-5": {
@@ -149,16 +144,9 @@ const KNOWN_CONTEXT_REGISTRY: Record<string, KnownContextRegistryEntry> = {
149
144
  },
150
145
  };
151
146
 
152
- // Short alias IDs used by ANTHROPIC_FALLBACK_MODELS canonical versioned IDs in the registry.
153
- const ANTHROPIC_MODEL_ALIAS_MAP: Record<string, string> = {
154
- opus: "claude-opus-4-7",
155
- sonnet: "claude-sonnet-4-6",
156
- haiku: "claude-haiku-4-5",
157
- };
158
-
159
- // Normalise OpenAI/Codex model IDs to lowercase-dashed form before registry lookup.
160
- // Handles display-label variants like "GPT-5 Codex" → "gpt-5-codex".
161
- function normalizeOpenAIModelId(modelId: string): string {
147
+ // Normalise OpenAI/Codex model IDs to lowercase-dashed form before registry lookup.
148
+ // Handles display-label variants like "GPT-5 Codex" → "gpt-5-codex".
149
+ function normalizeOpenAIModelId(modelId: string): string {
162
150
  return modelId.toLowerCase().replace(/\s+/g, "-");
163
151
  }
164
152
 
@@ -333,12 +321,12 @@ function resolveFromConfig(
333
321
  };
334
322
  }
335
323
 
336
- function resolveFromKnownRegistry(providerId: ProviderId, modelId: string): ModelContextMetadata | null {
337
- const lookupId = providerId === "anthropic"
338
- ? (ANTHROPIC_MODEL_ALIAS_MAP[modelId] ?? modelId)
339
- : providerId === "openai"
340
- ? normalizeOpenAIModelId(modelId)
341
- : modelId;
324
+ function resolveFromKnownRegistry(providerId: ProviderId, modelId: string): ModelContextMetadata | null {
325
+ const lookupId = providerId === "anthropic"
326
+ ? modelId
327
+ : providerId === "openai"
328
+ ? normalizeOpenAIModelId(modelId)
329
+ : modelId;
342
330
  const entry = KNOWN_CONTEXT_REGISTRY[`${providerId}:${lookupId}`];
343
331
  if (!entry) return null;
344
332
  return {
@@ -1,4 +1,6 @@
1
1
  import { sanitizeTerminalOutput } from "../terminal/terminalSanitize.js";
2
+ import { runAgentLoop, type AgentChatMessage, type AgentChatResponse } from "../agent/loop.js";
3
+ import { parseOpenAiToolCalls } from "../agent/protocol.js";
2
4
  import type { BackendRunHandlers } from "../providers/types.js";
3
5
  import type { ProviderWorkspaceOverride } from "../providerLauncher/types.js";
4
6
  import type {
@@ -456,21 +458,110 @@ function getCachedSelectedModel(config: LocalProviderConfig, routeModel: string)
456
458
  return selectFallbackLocalModel(config, discoveredIds) ?? routeModel;
457
459
  }
458
460
 
459
- function extractNonStreamingText(body: unknown): string {
460
- if (typeof body !== "object" || body === null) return "";
461
- const choices = (body as { choices?: unknown }).choices;
462
- if (!Array.isArray(choices)) return "";
463
- return choices
464
- .map((choice) => {
465
- if (typeof choice !== "object" || choice === null) return "";
466
- const message = (choice as { message?: { content?: unknown }; text?: unknown }).message;
467
- if (typeof message?.content === "string") return message.content;
468
- const text = (choice as { text?: unknown }).text;
469
- return typeof text === "string" ? text : "";
470
- })
471
- .join("")
472
- .trim();
473
- }
461
+ interface LocalResponseDiagnostics {
462
+ choiceCount: number;
463
+ finishReasons: string[];
464
+ recognizedFields: string[];
465
+ topLevelKeys: string[];
466
+ }
467
+
468
+ interface ExtractedLocalResponse extends AgentChatResponse {
469
+ diagnostics: LocalResponseDiagnostics;
470
+ }
471
+
472
+ function textFromContent(value: unknown): string {
473
+ if (typeof value === "string") return value;
474
+ if (Array.isArray(value)) return value.map(textFromContent).join("");
475
+ if (!isRecord(value)) return "";
476
+
477
+ // OpenAI-compatible servers commonly emit content as typed parts, such as
478
+ // { type: "text", text: "..." } or { type: "output_text", text: "..." }.
479
+ if (typeof value.text === "string") return value.text;
480
+ if (typeof value.content === "string" || Array.isArray(value.content)) {
481
+ return textFromContent(value.content);
482
+ }
483
+ if ((value.type === "text" || value.type === "output_text") && typeof value.value === "string") {
484
+ return value.value;
485
+ }
486
+ return "";
487
+ }
488
+
489
+ function messageFieldText(message: Record<string, unknown>, fields: readonly string[], recognizedFields: Set<string>): string {
490
+ for (const field of fields) {
491
+ const text = textFromContent(message[field]);
492
+ if (text.trim()) {
493
+ recognizedFields.add(`message.${field}`);
494
+ return text;
495
+ }
496
+ }
497
+ return "";
498
+ }
499
+
500
+ function extractNonStreamingResponse(body: unknown): ExtractedLocalResponse {
501
+ const topLevelKeys = isRecord(body) ? Object.keys(body).sort() : [];
502
+ const choices = isRecord(body) && Array.isArray(body.choices) ? body.choices : [];
503
+ const recognizedFields = new Set<string>();
504
+ const finishReasons: string[] = [];
505
+ const toolCalls = choices.flatMap((choice) => {
506
+ if (!isRecord(choice)) return [];
507
+ if (typeof choice.finish_reason === "string") finishReasons.push(choice.finish_reason);
508
+ const message = isRecord(choice.message) ? choice.message : null;
509
+ return message ? parseOpenAiToolCalls(message.tool_calls) : [];
510
+ });
511
+
512
+ const content: string[] = [];
513
+ const reasoning: string[] = [];
514
+ for (const choice of choices) {
515
+ if (!isRecord(choice)) continue;
516
+ const message = isRecord(choice.message) ? choice.message : null;
517
+ const messageContent = message
518
+ ? messageFieldText(message, ["content"], recognizedFields)
519
+ : "";
520
+ const legacyText = textFromContent(choice.text);
521
+ if (legacyText.trim()) recognizedFields.add("choice.text");
522
+ const answer = messageContent || legacyText;
523
+ if (answer.trim()) {
524
+ content.push(answer);
525
+ continue;
526
+ }
527
+
528
+ const reasoningText = message
529
+ ? messageFieldText(message, ["reasoning_content", "reasoning", "analysis"], recognizedFields)
530
+ : "";
531
+ const choiceReasoning = !reasoningText
532
+ ? messageFieldText(choice, ["reasoning_content", "reasoning", "analysis"], recognizedFields)
533
+ : "";
534
+ if ((reasoningText || choiceReasoning).trim()) {
535
+ reasoning.push(reasoningText || choiceReasoning);
536
+ }
537
+ }
538
+
539
+ return {
540
+ // Reasoning is a fallback only. A response with both fields should show its
541
+ // final answer, while reasoning-only servers still produce useful text.
542
+ text: (content.length > 0 ? content : reasoning).join("").trim(),
543
+ ...(toolCalls.length > 0 ? { toolCalls } : {}),
544
+ diagnostics: {
545
+ choiceCount: choices.length,
546
+ finishReasons: [...new Set(finishReasons)],
547
+ recognizedFields: [...recognizedFields].sort(),
548
+ topLevelKeys,
549
+ },
550
+ };
551
+ }
552
+
553
+ function emptyResponseError(config: LocalProviderConfig, model: string, diagnostics: LocalResponseDiagnostics): Error {
554
+ const finishReasons = diagnostics.finishReasons.length > 0 ? diagnostics.finishReasons.join(", ") : "none";
555
+ const fields = diagnostics.recognizedFields.length > 0 ? diagnostics.recognizedFields.join(", ") : "none";
556
+ const keys = diagnostics.topLevelKeys.length > 0 ? diagnostics.topLevelKeys.join(", ") : "none";
557
+ return new Error([
558
+ "Local OpenAI-compatible API returned no assistant text.",
559
+ `Endpoint: ${config.baseUrl}/chat/completions`,
560
+ `Model: ${model}`,
561
+ `Response details: choices=${diagnostics.choiceCount}; finish_reason=${finishReasons}; recognized_fields=${fields}; top_level_keys=${keys}.`,
562
+ "Check the local server response format or retry the prompt.",
563
+ ].join("\n"));
564
+ }
474
565
 
475
566
  function parseStreamDelta(line: string): string | null {
476
567
  const trimmed = line.trim();
@@ -521,18 +612,19 @@ async function postLocalChatCompletion(options: {
521
612
  request: ProviderChatRequest;
522
613
  config: LocalProviderConfig;
523
614
  stream: boolean;
615
+ messages?: readonly AgentChatMessage[];
524
616
  fetchImpl: FetchImpl;
525
617
  signal?: AbortSignal;
526
618
  handlers: BackendRunHandlers;
527
619
  capProfile: import("./capabilityProfile.js").ModelCapabilityProfile;
528
- }): Promise<string> {
620
+ }): Promise<AgentChatResponse> {
529
621
  const model = getCachedSelectedModel(options.config, options.request.route.modelId);
530
622
  const includeSystemPrompt = options.capProfile.supportsSystemPrompt !== false;
531
- const messages = [
623
+ const messages: readonly AgentChatMessage[] = options.messages ?? [
532
624
  ...(includeSystemPrompt && options.request.projectInstructions?.content
533
- ? [{ role: "system", content: options.request.projectInstructions.content }]
625
+ ? [{ role: "system" as const, content: options.request.projectInstructions.content }]
534
626
  : []),
535
- { role: "user", content: options.request.prompt },
627
+ { role: "user" as const, content: options.request.prompt },
536
628
  ];
537
629
  const response = await options.fetchImpl(`${options.config.baseUrl}/chat/completions`, {
538
630
  method: "POST",
@@ -560,11 +652,15 @@ async function postLocalChatCompletion(options: {
560
652
  }
561
653
 
562
654
  if (options.stream) {
563
- return readStreamingResponse(response, options.handlers);
655
+ return { text: await readStreamingResponse(response, options.handlers) };
564
656
  }
565
657
 
566
- const parsed = JSON.parse(await response.text()) as unknown;
567
- return extractNonStreamingText(parsed);
658
+ const parsed = JSON.parse(await response.text()) as unknown;
659
+ const extracted = extractNonStreamingResponse(parsed);
660
+ if (!extracted.text.trim() && (extracted.toolCalls?.length ?? 0) === 0) {
661
+ throw emptyResponseError(options.config, model, extracted.diagnostics);
662
+ }
663
+ return extracted;
568
664
  }
569
665
 
570
666
  function isModelNotLoadedError(status: number, body: string): boolean {
@@ -589,42 +685,24 @@ export async function runLocalOpenAiCompatible(
589
685
  providerConfig: request.localConfig ?? configuredOverride,
590
686
  rawMetadata: rawMeta,
591
687
  });
592
- const streamingSupported = capProfile.supportsStreaming !== false;
593
-
594
- if (streamingSupported) {
595
- try {
596
- const streamed = await postLocalChatCompletion({
688
+ const text = await runAgentLoop({
689
+ request,
690
+ handlers,
691
+ includeSystemPrompt: capProfile.supportsSystemPrompt !== false,
692
+ signal: options.signal,
693
+ sendMessages: async (messages) =>
694
+ postLocalChatCompletion({
597
695
  request,
598
696
  config,
599
- stream: true,
697
+ messages,
698
+ stream: false,
600
699
  fetchImpl,
601
700
  signal: options.signal,
602
701
  handlers,
603
702
  capProfile,
604
- });
605
- if (streamed) return streamed;
606
- } catch (error) {
607
- if (options.signal?.aborted) throw error;
608
- handlers.onProgress?.({
609
- id: "local-stream-fallback",
610
- source: "stderr",
611
- text: "Local streaming request failed; retrying without streaming.",
612
- });
613
- }
614
- }
615
-
616
- const text = await postLocalChatCompletion({
617
- request,
618
- config,
619
- stream: false,
620
- fetchImpl,
621
- signal: options.signal,
622
- handlers,
623
- capProfile,
703
+ }),
624
704
  });
625
- if (!text) {
626
- throw new Error("Local OpenAI-compatible API returned no assistant text.");
627
- }
705
+ if (!text) throw new Error("Local OpenAI-compatible API returned no assistant text after tool execution.");
628
706
  handlers.onAssistantDelta?.(text);
629
707
  return text;
630
708
  }