@gajae-code/coding-agent 0.5.1 → 0.5.3
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.
- package/CHANGELOG.md +31 -0
- package/README.md +1 -1
- package/dist/types/async/job-manager.d.ts +6 -0
- package/dist/types/cli/setup-cli.d.ts +8 -1
- package/dist/types/commands/setup.d.ts +7 -0
- package/dist/types/config/file-lock.d.ts +24 -2
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/models-config-schema.d.ts +5 -0
- package/dist/types/config/settings-schema.d.ts +62 -0
- package/dist/types/dap/client.d.ts +2 -1
- package/dist/types/edit/read-file.d.ts +6 -0
- package/dist/types/eval/js/context-manager.d.ts +3 -0
- package/dist/types/eval/js/executor.d.ts +1 -0
- package/dist/types/exec/bash-executor.d.ts +2 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +64 -2
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +7 -1
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +10 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +29 -0
- package/dist/types/lsp/types.d.ts +2 -0
- package/dist/types/modes/bridge/bridge-mode.d.ts +1 -0
- package/dist/types/modes/components/model-selector.d.ts +2 -0
- package/dist/types/modes/components/oauth-selector.d.ts +1 -0
- package/dist/types/modes/components/provider-onboarding-selector.d.ts +1 -1
- package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +1 -0
- package/dist/types/modes/components/tool-execution.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- package/dist/types/modes/rpc/rpc-mode.d.ts +56 -1
- package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +10 -0
- package/dist/types/modes/theme/defaults/index.d.ts +302 -0
- package/dist/types/modes/theme/theme.d.ts +1 -0
- package/dist/types/modes/types.d.ts +1 -1
- package/dist/types/runtime/process-lifecycle.d.ts +108 -0
- package/dist/types/runtime-mcp/transports/stdio.d.ts +1 -0
- package/dist/types/runtime-mcp/types.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +17 -1
- package/dist/types/session/artifacts.d.ts +4 -1
- package/dist/types/session/history-storage.d.ts +2 -2
- package/dist/types/session/session-manager.d.ts +10 -1
- package/dist/types/session/streaming-output.d.ts +5 -0
- package/dist/types/setup/credential-import.d.ts +79 -0
- package/dist/types/slash-commands/helpers/fast-status-report.d.ts +76 -0
- package/dist/types/task/executor.d.ts +1 -0
- package/dist/types/task/render.d.ts +1 -1
- package/dist/types/tools/bash.d.ts +1 -0
- package/dist/types/tools/browser/tab-supervisor.d.ts +9 -0
- package/dist/types/tools/sqlite-reader.d.ts +2 -1
- package/dist/types/tools/subagent-render.d.ts +7 -1
- package/dist/types/tools/subagent.d.ts +21 -0
- package/dist/types/tools/ultragoal-ask-guard.d.ts +5 -0
- package/dist/types/web/search/index.d.ts +4 -4
- package/dist/types/web/search/provider.d.ts +16 -20
- package/dist/types/web/search/providers/base.d.ts +2 -1
- package/dist/types/web/search/providers/openai-compatible.d.ts +9 -0
- package/dist/types/web/search/types.d.ts +14 -2
- package/package.json +7 -7
- package/scripts/build-binary.ts +7 -0
- package/src/async/job-manager.ts +153 -39
- package/src/cli/args.ts +2 -0
- package/src/cli/fast-help.ts +2 -0
- package/src/cli/setup-cli.ts +138 -3
- package/src/commands/setup.ts +5 -1
- package/src/commands/ultragoal.ts +3 -1
- package/src/config/file-lock-gc.ts +14 -2
- package/src/config/file-lock.ts +63 -13
- package/src/config/model-profile-activation.ts +15 -3
- package/src/config/model-profiles.ts +15 -15
- package/src/config/model-registry.ts +21 -1
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings-schema.ts +62 -0
- package/src/dap/client.ts +105 -64
- package/src/dap/session.ts +44 -7
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +30 -8
- package/src/edit/read-file.ts +19 -1
- package/src/eval/js/context-manager.ts +228 -65
- package/src/eval/js/executor.ts +2 -0
- package/src/eval/js/index.ts +1 -0
- package/src/eval/js/worker-core.ts +10 -6
- package/src/eval/py/executor.ts +68 -19
- package/src/eval/py/kernel.ts +46 -22
- package/src/eval/py/runner.py +68 -14
- package/src/exec/bash-executor.ts +49 -13
- package/src/gjc-runtime/deep-interview-recorder.ts +40 -0
- package/src/gjc-runtime/launch-tmux.ts +3 -4
- package/src/gjc-runtime/ralplan-runtime.ts +174 -12
- package/src/gjc-runtime/state-runtime.ts +2 -1
- package/src/gjc-runtime/state-writer.ts +254 -7
- package/src/gjc-runtime/tmux-gc.ts +88 -38
- package/src/gjc-runtime/tmux-sessions.ts +44 -6
- package/src/gjc-runtime/ultragoal-guard.ts +155 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +1227 -31
- package/src/gjc-runtime/workflow-manifest.generated.json +44 -0
- package/src/gjc-runtime/workflow-manifest.ts +12 -0
- package/src/harness-control-plane/owner.ts +3 -2
- package/src/harness-control-plane/rpc-adapter.ts +1 -1
- package/src/hooks/skill-state.ts +121 -2
- package/src/internal-urls/artifact-protocol.ts +10 -1
- package/src/internal-urls/docs-index.generated.ts +14 -10
- package/src/lsp/client.ts +64 -26
- package/src/lsp/defaults.json +1 -0
- package/src/lsp/index.ts +2 -1
- package/src/lsp/lspmux.ts +33 -9
- package/src/lsp/types.ts +2 -0
- package/src/main.ts +14 -4
- package/src/modes/acp/acp-agent.ts +4 -2
- package/src/modes/bridge/bridge-mode.ts +23 -1
- package/src/modes/components/assistant-message.ts +10 -2
- package/src/modes/components/bash-execution.ts +5 -1
- package/src/modes/components/eval-execution.ts +5 -1
- package/src/modes/components/history-search.ts +5 -2
- package/src/modes/components/model-selector.ts +60 -2
- package/src/modes/components/oauth-selector.ts +5 -0
- package/src/modes/components/provider-onboarding-selector.ts +6 -1
- package/src/modes/components/runtime-mcp-add-wizard.ts +58 -7
- package/src/modes/components/skill-message.ts +24 -16
- package/src/modes/components/tool-execution.ts +6 -0
- package/src/modes/controllers/extension-ui-controller.ts +33 -6
- package/src/modes/controllers/input-controller.ts +5 -0
- package/src/modes/controllers/selector-controller.ts +86 -2
- package/src/modes/interactive-mode.ts +11 -1
- package/src/modes/rpc/rpc-mode.ts +132 -18
- package/src/modes/shared/agent-wire/command-dispatch.ts +5 -2
- package/src/modes/shared/agent-wire/host-tool-bridge.ts +3 -0
- package/src/modes/shared/agent-wire/unattended-session.ts +16 -1
- package/src/modes/theme/defaults/claude-code.json +100 -0
- package/src/modes/theme/defaults/codex.json +100 -0
- package/src/modes/theme/defaults/index.ts +6 -0
- package/src/modes/theme/defaults/opencode.json +102 -0
- package/src/modes/theme/theme.ts +2 -2
- package/src/modes/types.ts +1 -1
- package/src/modes/utils/ui-helpers.ts +5 -2
- package/src/prompts/agents/executor.md +5 -2
- package/src/runtime/process-lifecycle.ts +400 -0
- package/src/runtime-mcp/manager.ts +164 -50
- package/src/runtime-mcp/transports/http.ts +12 -11
- package/src/runtime-mcp/transports/stdio.ts +64 -38
- package/src/runtime-mcp/types.ts +3 -0
- package/src/sdk.ts +39 -1
- package/src/session/agent-session.ts +190 -33
- package/src/session/artifacts.ts +17 -2
- package/src/session/blob-store.ts +36 -2
- package/src/session/history-storage.ts +32 -11
- package/src/session/session-manager.ts +99 -31
- package/src/session/streaming-output.ts +54 -3
- package/src/setup/credential-import.ts +429 -0
- package/src/skill-state/deep-interview-mutation-guard.ts +2 -1
- package/src/slash-commands/builtin-registry.ts +30 -3
- package/src/slash-commands/helpers/fast-status-report.ts +111 -0
- package/src/task/executor.ts +7 -1
- package/src/task/render.ts +18 -7
- package/src/tools/archive-reader.ts +10 -1
- package/src/tools/ask.ts +4 -2
- package/src/tools/bash.ts +11 -4
- package/src/tools/browser/tab-supervisor.ts +22 -0
- package/src/tools/browser.ts +38 -4
- package/src/tools/cron.ts +1 -1
- package/src/tools/read.ts +11 -12
- package/src/tools/sqlite-reader.ts +19 -5
- package/src/tools/subagent-render.ts +119 -29
- package/src/tools/subagent.ts +147 -7
- package/src/tools/ultragoal-ask-guard.ts +39 -0
- package/src/web/search/index.ts +25 -25
- package/src/web/search/provider.ts +178 -87
- package/src/web/search/providers/base.ts +2 -1
- package/src/web/search/providers/openai-compatible.ts +151 -0
- package/src/web/search/types.ts +47 -22
package/src/tools/subagent.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { prompt } from "@gajae-code/utils";
|
|
|
4
4
|
import * as z from "zod/v4";
|
|
5
5
|
import { type AsyncJob, AsyncJobManager, jobElapsedMs, type SubagentRecord } from "../async";
|
|
6
6
|
import subagentDescription from "../prompts/tools/subagent.md" with { type: "text" };
|
|
7
|
-
import type { AgentProgress, AgentSource } from "../task/types";
|
|
7
|
+
import type { AgentProgress, AgentSource, TaskToolDetails } from "../task/types";
|
|
8
8
|
import { Ellipsis, truncateToWidth } from "../tui";
|
|
9
9
|
import type { ToolSession } from "./index";
|
|
10
10
|
import { replaceTabs } from "./render-utils";
|
|
@@ -330,12 +330,21 @@ export class SubagentTool implements AgentTool<typeof subagentSchema, SubagentTo
|
|
|
330
330
|
);
|
|
331
331
|
const watchedJobIds = runningJobs.map(job => job.id);
|
|
332
332
|
manager.watchJobs(watchedJobIds);
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
333
|
+
let lastEmittedSignature: string | undefined;
|
|
334
|
+
const emitIfChanged = (force: boolean): void => {
|
|
335
|
+
if (!onUpdate) return;
|
|
336
|
+
const result = this.#progressResult(manager, records, true);
|
|
337
|
+
const signature = subagentAwaitRenderedStateSignature(result.details?.subagents ?? []);
|
|
338
|
+
if (!force && signature === lastEmittedSignature) return;
|
|
339
|
+
lastEmittedSignature = signature;
|
|
340
|
+
onUpdate(result);
|
|
341
|
+
};
|
|
342
|
+
const progressTimer = onUpdate ? setInterval(() => emitIfChanged(false), 500) : undefined;
|
|
343
|
+
// Initial emission so the panel appears immediately; later idle ticks are
|
|
344
|
+
// gated on a value-based rendered-state signature so unchanged progress no
|
|
345
|
+
// longer rebuilds the renderer component or mutates transcript lines above
|
|
346
|
+
// the viewport (the source of the await-panel repaint storms).
|
|
347
|
+
emitIfChanged(true);
|
|
339
348
|
|
|
340
349
|
let timedOut = false;
|
|
341
350
|
try {
|
|
@@ -720,3 +729,134 @@ function previewJobOutput(
|
|
|
720
729
|
const preview = truncateToWidth(normalized, width, Ellipsis.Unicode);
|
|
721
730
|
return { type: source.type, preview, truncated: preview !== normalized };
|
|
722
731
|
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* Canonical, value-based rendered-state signature for the `subagent` await panel.
|
|
735
|
+
*
|
|
736
|
+
* Producer-side await gating compares this signature against the last emitted one
|
|
737
|
+
* and only fires `onUpdate` when the *rendered* state actually changed. Unchanged
|
|
738
|
+
* idle ticks therefore stop rebuilding the renderer component and stop mutating
|
|
739
|
+
* transcript lines above the viewport, which is what triggers TUI full-redraw
|
|
740
|
+
* storms (`tui.ts` `firstChanged < viewportTop`).
|
|
741
|
+
*
|
|
742
|
+
* It is deliberately value-based, never object identity: `AsyncJobManager.record-
|
|
743
|
+
* SubagentProgress` stores a `structuredClone` but `getSubagentProgress` returns
|
|
744
|
+
* the retained object by reference, so identity comparison would be both noisy and
|
|
745
|
+
* unsafe.
|
|
746
|
+
*
|
|
747
|
+
* Time-derived fields are intentionally excluded so the panel does not churn while
|
|
748
|
+
* idle: raw durations (`durationMs`), current-tool elapsed (`currentToolStartMs`),
|
|
749
|
+
* and retry countdowns (`retryState.startedAtMs`) are omitted. Idle duration and
|
|
750
|
+
* countdown ticking is sacrificed by design; every real transition still changes
|
|
751
|
+
* the signature.
|
|
752
|
+
*/
|
|
753
|
+
export function subagentAwaitRenderedStateSignature(subagents: readonly SubagentSnapshot[]): string {
|
|
754
|
+
return JSON.stringify(subagents.map(canonicalizeSnapshotForSignature));
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function canonicalizeSnapshotForSignature(snapshot: SubagentSnapshot): unknown {
|
|
758
|
+
return {
|
|
759
|
+
id: snapshot.id,
|
|
760
|
+
jobId: snapshot.jobId,
|
|
761
|
+
status: snapshot.status,
|
|
762
|
+
label: snapshot.label,
|
|
763
|
+
agent: snapshot.agent,
|
|
764
|
+
agentSource: snapshot.agentSource,
|
|
765
|
+
description: snapshot.description ?? null,
|
|
766
|
+
assignment: snapshot.assignment ?? null,
|
|
767
|
+
resultText: snapshot.resultText ?? null,
|
|
768
|
+
errorText: snapshot.errorText ?? null,
|
|
769
|
+
resultPreview: snapshot.resultPreview ?? null,
|
|
770
|
+
outputRef: snapshot.outputRef ?? null,
|
|
771
|
+
truncated: snapshot.truncated ?? false,
|
|
772
|
+
guidance: snapshot.guidance ?? null,
|
|
773
|
+
liveProgressAvailable: snapshot.liveProgressAvailable ?? null,
|
|
774
|
+
effectiveModel: snapshot.effectiveModel ?? null,
|
|
775
|
+
requestedModel: snapshot.requestedModel ?? null,
|
|
776
|
+
modelFellBack: snapshot.modelFellBack ?? false,
|
|
777
|
+
// durationMs intentionally excluded (time-derived; would defeat idle gating).
|
|
778
|
+
progress: snapshot.progress ? canonicalizeProgressForSignature(snapshot.progress) : null,
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
function canonicalizeProgressForSignature(progress: AgentProgress): unknown {
|
|
783
|
+
return {
|
|
784
|
+
id: progress.id,
|
|
785
|
+
agent: progress.agent,
|
|
786
|
+
agentSource: progress.agentSource,
|
|
787
|
+
status: progress.status,
|
|
788
|
+
task: progress.task,
|
|
789
|
+
assignment: progress.assignment ?? null,
|
|
790
|
+
description: progress.description ?? null,
|
|
791
|
+
lastIntent: progress.lastIntent ?? null,
|
|
792
|
+
currentTool: progress.currentTool ?? null,
|
|
793
|
+
currentToolArgs: progress.currentToolArgs ?? null,
|
|
794
|
+
// currentToolStartMs intentionally excluded (only drives elapsed rendering).
|
|
795
|
+
recentTools: progress.recentTools.map(tool => ({ tool: tool.tool, args: tool.args })),
|
|
796
|
+
recentOutput: progress.recentOutput,
|
|
797
|
+
toolCount: progress.toolCount,
|
|
798
|
+
tokens: progress.tokens,
|
|
799
|
+
contextTokens: progress.contextTokens ?? null,
|
|
800
|
+
contextWindow: progress.contextWindow ?? null,
|
|
801
|
+
cost: progress.cost,
|
|
802
|
+
modelOverride: progress.modelOverride ?? null,
|
|
803
|
+
modelSubstitutionWarning: progress.modelSubstitutionWarning ?? null,
|
|
804
|
+
// durationMs intentionally excluded (time-derived).
|
|
805
|
+
extractedToolData: progress.extractedToolData
|
|
806
|
+
? canonicalizeExtractedToolDataForSignature(progress.extractedToolData)
|
|
807
|
+
: null,
|
|
808
|
+
retryState: progress.retryState
|
|
809
|
+
? {
|
|
810
|
+
attempt: progress.retryState.attempt,
|
|
811
|
+
maxAttempts: progress.retryState.maxAttempts,
|
|
812
|
+
unbounded: progress.retryState.unbounded ?? false,
|
|
813
|
+
delayMs: progress.retryState.delayMs,
|
|
814
|
+
errorMessage: progress.retryState.errorMessage,
|
|
815
|
+
// startedAtMs intentionally excluded (drives countdown only).
|
|
816
|
+
}
|
|
817
|
+
: null,
|
|
818
|
+
retryFailure: progress.retryFailure ?? null,
|
|
819
|
+
inflightTaskDetails: progress.inflightTaskDetails
|
|
820
|
+
? canonicalizeTaskDetailsForSignature(progress.inflightTaskDetails)
|
|
821
|
+
: null,
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
/**
|
|
826
|
+
* Nested `task` data (`extractedToolData.task` and `inflightTaskDetails`) is the
|
|
827
|
+
* one place the await signature reaches into a live, ticking structure: nested
|
|
828
|
+
* `AgentProgress` carries the same time-derived fields excluded above, and
|
|
829
|
+
* `TaskToolDetails` adds `totalDurationMs` / per-result `durationMs`. Signing it
|
|
830
|
+
* wholesale would defeat idle gating whenever an awaited subagent is itself inside
|
|
831
|
+
* a live `task` call, so these helpers canonicalize the rendered, non-time subset
|
|
832
|
+
* recursively (mutually recursive with `canonicalizeProgressForSignature`).
|
|
833
|
+
*/
|
|
834
|
+
function canonicalizeExtractedToolDataForSignature(data: Record<string, unknown[]>): Record<string, unknown> {
|
|
835
|
+
const out: Record<string, unknown> = {};
|
|
836
|
+
for (const key of Object.keys(data)) {
|
|
837
|
+
// Only the `task` key holds time-ticking `TaskToolDetails`; other handler
|
|
838
|
+
// data (yield/report_finding/generic) is stable and passes through as-is.
|
|
839
|
+
out[key] = key === "task" ? (data[key] as TaskToolDetails[]).map(canonicalizeTaskDetailsForSignature) : data[key];
|
|
840
|
+
}
|
|
841
|
+
return out;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
function canonicalizeTaskDetailsForSignature(details: TaskToolDetails): unknown {
|
|
845
|
+
// `extractedToolData` is an untyped boundary (`Record<string, unknown[]>`), so
|
|
846
|
+
// guard each field instead of trusting the `TaskToolDetails` cast.
|
|
847
|
+
return {
|
|
848
|
+
// totalDurationMs intentionally excluded (time-derived).
|
|
849
|
+
results: Array.isArray(details.results) ? details.results.map(canonicalizeTaskResultForSignature) : null,
|
|
850
|
+
progress: Array.isArray(details.progress) ? details.progress.map(canonicalizeProgressForSignature) : null,
|
|
851
|
+
async: details.async
|
|
852
|
+
? { state: details.async.state, jobId: details.async.jobId, type: details.async.type }
|
|
853
|
+
: null,
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
function canonicalizeTaskResultForSignature(result: TaskToolDetails["results"][number]): unknown {
|
|
858
|
+
// Completed results do not tick, but drop `durationMs` so the only time-derived
|
|
859
|
+
// field in the receipt can never reintroduce idle churn.
|
|
860
|
+
const { durationMs: _durationMs, ...rest } = result;
|
|
861
|
+
return rest;
|
|
862
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { AgentTool } from "@gajae-code/agent-core";
|
|
2
|
+
import { isUltragoalAskBlocked, type UltragoalAskBlockDiagnostic } from "../gjc-runtime/ultragoal-guard";
|
|
3
|
+
import { ToolError } from "./tool-errors";
|
|
4
|
+
|
|
5
|
+
const ULTRAGOAL_ASK_GUARD = Symbol.for("gajae-code.ultragoalAskGuard");
|
|
6
|
+
|
|
7
|
+
type GuardedTool = AgentTool & { [ULTRAGOAL_ASK_GUARD]?: true };
|
|
8
|
+
|
|
9
|
+
export function formatUltragoalAskBlockMessage(diagnostic: UltragoalAskBlockDiagnostic): string {
|
|
10
|
+
return [
|
|
11
|
+
diagnostic.message,
|
|
12
|
+
`Ultragoal ask guard blocked ask (source: ${diagnostic.source}; reason: ${diagnostic.reason}).`,
|
|
13
|
+
"Use `gjc ultragoal record-review-blockers` to record the blocker instead of asking the user.",
|
|
14
|
+
].join("\n");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function assertUltragoalAskAllowed(cwd: string): Promise<void> {
|
|
18
|
+
const diagnostic = await isUltragoalAskBlocked(cwd);
|
|
19
|
+
if (!diagnostic.active) return;
|
|
20
|
+
throw new ToolError(formatUltragoalAskBlockMessage(diagnostic));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function guardToolForUltragoalAsk<T extends AgentTool>(tool: T, getCwd: () => string): T {
|
|
24
|
+
if (tool.name !== "ask") return tool;
|
|
25
|
+
const candidate = tool as GuardedTool;
|
|
26
|
+
if (candidate[ULTRAGOAL_ASK_GUARD]) return tool;
|
|
27
|
+
const wrapped = new Proxy(tool, {
|
|
28
|
+
get(target, prop, receiver) {
|
|
29
|
+
if (prop === ULTRAGOAL_ASK_GUARD) return true;
|
|
30
|
+
if (prop !== "execute") return Reflect.get(target, prop, receiver);
|
|
31
|
+
return async (...args: unknown[]): Promise<unknown> => {
|
|
32
|
+
await assertUltragoalAskAllowed(getCwd());
|
|
33
|
+
return Reflect.apply(target.execute, target, args);
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
}) as T & GuardedTool;
|
|
37
|
+
wrapped[ULTRAGOAL_ASK_GUARD] = true;
|
|
38
|
+
return wrapped as T;
|
|
39
|
+
}
|
package/src/web/search/index.ts
CHANGED
|
@@ -8,7 +8,6 @@ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallb
|
|
|
8
8
|
import type { AuthStorage } from "@gajae-code/ai";
|
|
9
9
|
import { prompt } from "@gajae-code/utils";
|
|
10
10
|
import * as z from "zod/v4";
|
|
11
|
-
import { parseModelString } from "../../config/model-resolver";
|
|
12
11
|
import type { CustomTool, CustomToolContext, RenderResultOptions } from "../../extensibility/custom-tools/types";
|
|
13
12
|
import type { Theme } from "../../modes/theme/theme";
|
|
14
13
|
import webSearchSystemPrompt from "../../prompts/system/web-search.md" with { type: "text" };
|
|
@@ -19,7 +18,7 @@ import { formatAge } from "../../tools/render-utils";
|
|
|
19
18
|
import { throwIfAborted } from "../../tools/tool-errors";
|
|
20
19
|
import { getSearchProviderLabel, resolveProviderChain, type SearchProvider } from "./provider";
|
|
21
20
|
import { renderSearchCall, renderSearchResult, type SearchRenderDetails } from "./render";
|
|
22
|
-
import type { SearchProviderId, SearchResponse } from "./types";
|
|
21
|
+
import type { ActiveSearchModelContext, SearchProviderId, SearchResponse } from "./types";
|
|
23
22
|
import { SearchProviderError } from "./types";
|
|
24
23
|
|
|
25
24
|
/** Web search tool parameters schema */
|
|
@@ -116,21 +115,11 @@ function formatForLLM(response: SearchResponse): string {
|
|
|
116
115
|
return parts.join("\n");
|
|
117
116
|
}
|
|
118
117
|
|
|
119
|
-
/** Best-effort active model provider: prefer the resolved Model, fall back to parsing the model string. */
|
|
120
|
-
function resolveActiveModelProvider(
|
|
121
|
-
modelProvider: string | undefined,
|
|
122
|
-
modelString: string | undefined,
|
|
123
|
-
): string | undefined {
|
|
124
|
-
if (modelProvider) return modelProvider;
|
|
125
|
-
if (modelString) return parseModelString(modelString)?.provider;
|
|
126
|
-
return undefined;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
118
|
interface ExecuteSearchOptions {
|
|
130
119
|
authStorage: AuthStorage;
|
|
131
120
|
sessionId?: string;
|
|
132
121
|
signal?: AbortSignal;
|
|
133
|
-
|
|
122
|
+
activeModelContext?: ActiveSearchModelContext;
|
|
134
123
|
}
|
|
135
124
|
|
|
136
125
|
/** Execute web search */
|
|
@@ -139,11 +128,17 @@ async function executeSearch(
|
|
|
139
128
|
params: SearchQueryParams,
|
|
140
129
|
options: ExecuteSearchOptions,
|
|
141
130
|
): Promise<{ content: Array<{ type: "text"; text: string }>; details: SearchRenderDetails }> {
|
|
142
|
-
const { authStorage, sessionId, signal,
|
|
131
|
+
const { authStorage, sessionId, signal, activeModelContext } = options;
|
|
143
132
|
// Pass `params.provider` straight through: when omitted (the normal model-facing
|
|
144
133
|
// path) it is `undefined`, so `resolveProviderChain` applies the settings-configured
|
|
145
134
|
// preferred provider. Coalescing to "auto" here would silently bypass that preference.
|
|
146
|
-
const providers = await resolveProviderChain(
|
|
135
|
+
const providers = await resolveProviderChain({
|
|
136
|
+
authStorage,
|
|
137
|
+
sessionId,
|
|
138
|
+
signal,
|
|
139
|
+
preferredProvider: params.provider,
|
|
140
|
+
activeModelContext,
|
|
141
|
+
});
|
|
147
142
|
|
|
148
143
|
const failures: Array<{ provider: SearchProvider; error: unknown }> = [];
|
|
149
144
|
let lastProvider = providers[0];
|
|
@@ -161,6 +156,7 @@ async function executeSearch(
|
|
|
161
156
|
signal,
|
|
162
157
|
authStorage,
|
|
163
158
|
sessionId,
|
|
159
|
+
activeModelContext,
|
|
164
160
|
});
|
|
165
161
|
|
|
166
162
|
const text = formatForLLM(response);
|
|
@@ -210,14 +206,19 @@ async function executeSearch(
|
|
|
210
206
|
*/
|
|
211
207
|
export async function runSearchQuery(
|
|
212
208
|
params: SearchQueryParams,
|
|
213
|
-
options: {
|
|
209
|
+
options: {
|
|
210
|
+
authStorage?: AuthStorage;
|
|
211
|
+
sessionId?: string;
|
|
212
|
+
signal?: AbortSignal;
|
|
213
|
+
activeModelContext?: ActiveSearchModelContext;
|
|
214
|
+
} = {},
|
|
214
215
|
): Promise<{ content: Array<{ type: "text"; text: string }>; details: SearchRenderDetails }> {
|
|
215
216
|
const authStorage = options.authStorage ?? (await discoverAuthStorage());
|
|
216
217
|
return executeSearch("cli-web-search", params, {
|
|
217
218
|
authStorage,
|
|
218
219
|
sessionId: options.sessionId,
|
|
219
220
|
signal: options.signal,
|
|
220
|
-
|
|
221
|
+
activeModelContext: options.activeModelContext,
|
|
221
222
|
});
|
|
222
223
|
}
|
|
223
224
|
|
|
@@ -251,11 +252,10 @@ export class WebSearchTool implements AgentTool<typeof webSearchSchema, SearchRe
|
|
|
251
252
|
): Promise<AgentToolResult<SearchRenderDetails>> {
|
|
252
253
|
const authStorage = this.#session.authStorage ?? (await discoverAuthStorage());
|
|
253
254
|
const sessionId = this.#session.getSessionId?.() ?? undefined;
|
|
254
|
-
const
|
|
255
|
-
this.#session.model
|
|
256
|
-
|
|
257
|
-
);
|
|
258
|
-
return executeSearch(_toolCallId, params, { authStorage, sessionId, signal, activeModelProvider });
|
|
255
|
+
const activeModelContext = this.#session.model
|
|
256
|
+
? this.#session.modelRegistry?.getActiveSearchModelContext(this.#session.model)
|
|
257
|
+
: undefined;
|
|
258
|
+
return executeSearch(_toolCallId, params, { authStorage, sessionId, signal, activeModelContext });
|
|
259
259
|
}
|
|
260
260
|
}
|
|
261
261
|
|
|
@@ -279,7 +279,7 @@ export const webSearchCustomTool: CustomTool<typeof webSearchSchema, SearchRende
|
|
|
279
279
|
authStorage,
|
|
280
280
|
sessionId,
|
|
281
281
|
signal,
|
|
282
|
-
|
|
282
|
+
activeModelContext: ctx.model ? ctx.modelRegistry?.getActiveSearchModelContext(ctx.model) : undefined,
|
|
283
283
|
});
|
|
284
284
|
},
|
|
285
285
|
|
|
@@ -296,6 +296,6 @@ export function getSearchTools(): CustomTool<any, any>[] {
|
|
|
296
296
|
return [webSearchCustomTool];
|
|
297
297
|
}
|
|
298
298
|
|
|
299
|
-
export { getSearchProvider, setPreferredSearchProvider } from "./provider";
|
|
299
|
+
export { getSearchProvider, setPreferredSearchProvider, setSearchFallbackProviders } from "./provider";
|
|
300
300
|
export type { SearchProviderId as SearchProvider, SearchResponse } from "./types";
|
|
301
|
-
export { isSearchProviderPreference } from "./types";
|
|
301
|
+
export { isConfigurableSearchProviderId, isSearchProviderPreference } from "./types";
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
|
|
11
11
|
import type { AuthStorage } from "@gajae-code/ai";
|
|
12
12
|
import type { SearchProvider } from "./providers/base";
|
|
13
|
-
import type { SearchProviderId } from "./types";
|
|
13
|
+
import type { ActiveSearchModelContext, SearchProviderId } from "./types";
|
|
14
|
+
import { isConfigurableSearchProviderId } from "./types";
|
|
14
15
|
|
|
15
16
|
export type { SearchParams } from "./providers/base";
|
|
16
17
|
export { SearchProvider } from "./providers/base";
|
|
@@ -23,36 +24,16 @@ interface ProviderMeta {
|
|
|
23
24
|
|
|
24
25
|
/** Lazy factories. Each `load()` dynamic-imports its provider module on first call. */
|
|
25
26
|
const PROVIDER_META: Record<SearchProviderId, ProviderMeta> = {
|
|
26
|
-
exa: {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
load: async () => new (await import("./providers/exa")).ExaProvider(),
|
|
30
|
-
},
|
|
31
|
-
brave: {
|
|
32
|
-
id: "brave",
|
|
33
|
-
label: "Brave",
|
|
34
|
-
load: async () => new (await import("./providers/brave")).BraveProvider(),
|
|
35
|
-
},
|
|
36
|
-
jina: {
|
|
37
|
-
id: "jina",
|
|
38
|
-
label: "Jina",
|
|
39
|
-
load: async () => new (await import("./providers/jina")).JinaProvider(),
|
|
40
|
-
},
|
|
27
|
+
exa: { id: "exa", label: "Exa", load: async () => new (await import("./providers/exa")).ExaProvider() },
|
|
28
|
+
brave: { id: "brave", label: "Brave", load: async () => new (await import("./providers/brave")).BraveProvider() },
|
|
29
|
+
jina: { id: "jina", label: "Jina", load: async () => new (await import("./providers/jina")).JinaProvider() },
|
|
41
30
|
perplexity: {
|
|
42
31
|
id: "perplexity",
|
|
43
32
|
label: "Perplexity",
|
|
44
33
|
load: async () => new (await import("./providers/perplexity")).PerplexityProvider(),
|
|
45
34
|
},
|
|
46
|
-
kimi: {
|
|
47
|
-
|
|
48
|
-
label: "Kimi",
|
|
49
|
-
load: async () => new (await import("./providers/kimi")).KimiProvider(),
|
|
50
|
-
},
|
|
51
|
-
zai: {
|
|
52
|
-
id: "zai",
|
|
53
|
-
label: "Z.AI",
|
|
54
|
-
load: async () => new (await import("./providers/zai")).ZaiProvider(),
|
|
55
|
-
},
|
|
35
|
+
kimi: { id: "kimi", label: "Kimi", load: async () => new (await import("./providers/kimi")).KimiProvider() },
|
|
36
|
+
zai: { id: "zai", label: "Z.AI", load: async () => new (await import("./providers/zai")).ZaiProvider() },
|
|
56
37
|
anthropic: {
|
|
57
38
|
id: "anthropic",
|
|
58
39
|
label: "Anthropic",
|
|
@@ -63,11 +44,7 @@ const PROVIDER_META: Record<SearchProviderId, ProviderMeta> = {
|
|
|
63
44
|
label: "Gemini",
|
|
64
45
|
load: async () => new (await import("./providers/gemini")).GeminiProvider(),
|
|
65
46
|
},
|
|
66
|
-
codex: {
|
|
67
|
-
id: "codex",
|
|
68
|
-
label: "OpenAI",
|
|
69
|
-
load: async () => new (await import("./providers/codex")).CodexProvider(),
|
|
70
|
-
},
|
|
47
|
+
codex: { id: "codex", label: "OpenAI", load: async () => new (await import("./providers/codex")).CodexProvider() },
|
|
71
48
|
tavily: {
|
|
72
49
|
id: "tavily",
|
|
73
50
|
label: "Tavily",
|
|
@@ -78,11 +55,7 @@ const PROVIDER_META: Record<SearchProviderId, ProviderMeta> = {
|
|
|
78
55
|
label: "Parallel",
|
|
79
56
|
load: async () => new (await import("./providers/parallel")).ParallelProvider(),
|
|
80
57
|
},
|
|
81
|
-
kagi: {
|
|
82
|
-
id: "kagi",
|
|
83
|
-
label: "Kagi",
|
|
84
|
-
load: async () => new (await import("./providers/kagi")).KagiProvider(),
|
|
85
|
-
},
|
|
58
|
+
kagi: { id: "kagi", label: "Kagi", load: async () => new (await import("./providers/kagi")).KagiProvider() },
|
|
86
59
|
synthetic: {
|
|
87
60
|
id: "synthetic",
|
|
88
61
|
label: "Synthetic",
|
|
@@ -98,26 +71,24 @@ const PROVIDER_META: Record<SearchProviderId, ProviderMeta> = {
|
|
|
98
71
|
label: "DuckDuckGo",
|
|
99
72
|
load: async () => new (await import("./providers/duckduckgo")).DuckDuckGoProvider(),
|
|
100
73
|
},
|
|
74
|
+
"openai-compatible": {
|
|
75
|
+
id: "openai-compatible",
|
|
76
|
+
label: "OpenAI-compatible",
|
|
77
|
+
load: async () => new (await import("./providers/openai-compatible")).OpenAICompatibleSearchProvider(),
|
|
78
|
+
},
|
|
101
79
|
};
|
|
102
80
|
|
|
103
81
|
const instanceCache = new Map<SearchProviderId, SearchProvider>();
|
|
104
82
|
|
|
105
|
-
/** Cheap, sync metadata accessor — never triggers a provider load. */
|
|
106
83
|
export function getSearchProviderLabel(id: SearchProviderId): string {
|
|
107
84
|
return PROVIDER_META[id]?.label ?? id;
|
|
108
85
|
}
|
|
109
86
|
|
|
110
|
-
/**
|
|
111
|
-
* Resolve and cache a provider instance. First call for a given id loads the
|
|
112
|
-
* underlying module; subsequent calls return the cached singleton.
|
|
113
|
-
*/
|
|
114
87
|
export async function getSearchProvider(id: SearchProviderId): Promise<SearchProvider> {
|
|
115
88
|
const cached = instanceCache.get(id);
|
|
116
89
|
if (cached) return cached;
|
|
117
90
|
const meta = PROVIDER_META[id];
|
|
118
|
-
if (!meta) {
|
|
119
|
-
throw new Error(`Unknown search provider: ${id}`);
|
|
120
|
-
}
|
|
91
|
+
if (!meta) throw new Error(`Unknown search provider: ${id}`);
|
|
121
92
|
const provider = await meta.load();
|
|
122
93
|
instanceCache.set(id, provider);
|
|
123
94
|
return provider;
|
|
@@ -141,13 +112,6 @@ export const SEARCH_PROVIDER_ORDER: SearchProviderId[] = [
|
|
|
141
112
|
"searxng",
|
|
142
113
|
];
|
|
143
114
|
|
|
144
|
-
/**
|
|
145
|
-
* Map an active model's provider string to its own native web-search provider.
|
|
146
|
-
* Keys are real model provider ids (see packages/ai/src/types.ts KnownProvider);
|
|
147
|
-
* a few aliases (gemini/kimi) and API strings (openai-responses) are tolerated
|
|
148
|
-
* defensively. Providers absent from this map (custom/unknown) fall through to
|
|
149
|
-
* DuckDuckGo.
|
|
150
|
-
*/
|
|
151
115
|
const MODEL_PROVIDER_TO_SEARCH: Record<string, SearchProviderId> = {
|
|
152
116
|
openai: "codex",
|
|
153
117
|
"openai-codex": "codex",
|
|
@@ -165,54 +129,181 @@ const MODEL_PROVIDER_TO_SEARCH: Record<string, SearchProviderId> = {
|
|
|
165
129
|
synthetic: "synthetic",
|
|
166
130
|
};
|
|
167
131
|
|
|
168
|
-
/** Preferred provider set via settings (default: auto) */
|
|
169
132
|
let preferredProvId: SearchProviderId | "auto" = "auto";
|
|
133
|
+
let fallbackProvIds: SearchProviderId[] = [];
|
|
170
134
|
|
|
171
|
-
/** Set the preferred web search provider from settings */
|
|
172
135
|
export function setPreferredSearchProvider(provider: SearchProviderId | "auto"): void {
|
|
173
136
|
preferredProvId = provider;
|
|
174
137
|
}
|
|
175
138
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
139
|
+
export function setSearchFallbackProviders(ids: readonly string[]): void {
|
|
140
|
+
fallbackProvIds = ids.filter(isConfigurableSearchProviderId);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface ResolveProviderChainOptions {
|
|
144
|
+
authStorage: AuthStorage;
|
|
145
|
+
sessionId?: string;
|
|
146
|
+
signal?: AbortSignal;
|
|
147
|
+
preferredProvider?: SearchProviderId | "auto";
|
|
148
|
+
activeModelContext?: ActiveSearchModelContext;
|
|
149
|
+
fallbackProviders?: readonly SearchProviderId[];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function appendAvailable(
|
|
153
|
+
chain: SearchProviderId[],
|
|
154
|
+
id: SearchProviderId,
|
|
155
|
+
authStorage: AuthStorage,
|
|
156
|
+
): Promise<void> {
|
|
157
|
+
if (chain.includes(id)) return;
|
|
158
|
+
const provider = await getSearchProvider(id);
|
|
159
|
+
if (await provider.isAvailable(authStorage)) chain.push(id);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function appendDeduped(chain: SearchProviderId[], id: SearchProviderId): void {
|
|
163
|
+
if (!chain.includes(id)) chain.push(id);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function isAnthropicWire(api: string): boolean {
|
|
167
|
+
return api === "anthropic-messages";
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function isGoogleWire(api: string): boolean {
|
|
171
|
+
return api === "google-generative-ai" || api === "google-vertex" || api === "google-gemini-cli";
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function isOpenAICompatWire(api: string): boolean {
|
|
175
|
+
return api === "openai-responses" || api === "openai-completions" || api === "azure-openai-responses";
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function looksHostedModelId(modelId: string | undefined): boolean {
|
|
179
|
+
if (!modelId) return false;
|
|
180
|
+
const id = modelId.toLowerCase();
|
|
181
|
+
return /^(gpt-|o\d|o-|chatgpt-|text-|davinci|babbage|curie)/.test(id);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function looksOpenAIFamilyModelId(ctx: ActiveSearchModelContext): boolean {
|
|
185
|
+
return looksHostedModelId(ctx.wireModelId) || looksHostedModelId(ctx.modelId);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function isLocalBaseUrl(baseUrl: string | undefined): boolean {
|
|
189
|
+
if (!baseUrl) return false;
|
|
190
|
+
let url: URL;
|
|
191
|
+
try {
|
|
192
|
+
url = new URL(baseUrl);
|
|
193
|
+
} catch {
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
const host = url.hostname.toLowerCase().replace(/^\[/, "").replace(/\]$/, "").replace(/\.$/, "");
|
|
197
|
+
if (
|
|
198
|
+
host === "localhost" ||
|
|
199
|
+
host.endsWith(".localhost") ||
|
|
200
|
+
host === "host.docker.internal" ||
|
|
201
|
+
host.endsWith(".local")
|
|
202
|
+
)
|
|
203
|
+
return true;
|
|
204
|
+
const v4 = host.match(/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/);
|
|
205
|
+
if (v4) {
|
|
206
|
+
const [a, b] = v4.slice(1, 3).map(Number);
|
|
207
|
+
if (
|
|
208
|
+
a === 127 ||
|
|
209
|
+
a === 0 ||
|
|
210
|
+
a === 10 ||
|
|
211
|
+
(a === 172 && b >= 16 && b <= 31) ||
|
|
212
|
+
(a === 192 && b === 168) ||
|
|
213
|
+
(a === 169 && b === 254)
|
|
214
|
+
)
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
if (host === "::1" || host === "::") return true;
|
|
218
|
+
if (host.startsWith("fc") || host.startsWith("fd")) return true;
|
|
219
|
+
if (host.startsWith("fe8") || host.startsWith("fe9") || host.startsWith("fea") || host.startsWith("feb"))
|
|
220
|
+
return true;
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function inferNativeProviderFromModel(ctx: ActiveSearchModelContext | undefined): SearchProviderId | undefined {
|
|
225
|
+
if (!ctx || ctx.webSearch === "off") return undefined;
|
|
226
|
+
const modelId = (ctx.wireModelId ?? ctx.modelId).toLowerCase();
|
|
227
|
+
if (modelId.startsWith("claude-") && isAnthropicWire(ctx.api)) return "anthropic";
|
|
228
|
+
if (modelId.startsWith("gemini-") && isGoogleWire(ctx.api)) return "gemini";
|
|
229
|
+
if (looksOpenAIFamilyModelId(ctx) && isOpenAICompatWire(ctx.api)) {
|
|
230
|
+
if (ctx.webSearch === "on" || !isLocalBaseUrl(ctx.baseUrl)) return "codex";
|
|
231
|
+
}
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function canUseDirectProviderMapping(ctx: ActiveSearchModelContext, id: SearchProviderId): boolean {
|
|
236
|
+
if (ctx.webSearch === "off") return false;
|
|
237
|
+
if (id !== "codex") return true;
|
|
238
|
+
if (!isOpenAICompatWire(ctx.api)) return true;
|
|
239
|
+
return ctx.webSearch === "on" || !isLocalBaseUrl(ctx.baseUrl);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export async function canUseGenericCredentials(
|
|
189
243
|
authStorage: AuthStorage,
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
244
|
+
ctx: ActiveSearchModelContext | undefined,
|
|
245
|
+
sessionId?: string,
|
|
246
|
+
signal?: AbortSignal,
|
|
247
|
+
): Promise<boolean> {
|
|
248
|
+
if (!ctx) return false;
|
|
249
|
+
const key = await authStorage.getApiKey(ctx.provider, sessionId, {
|
|
250
|
+
baseUrl: ctx.baseUrl,
|
|
251
|
+
modelId: ctx.modelId,
|
|
252
|
+
signal,
|
|
253
|
+
});
|
|
254
|
+
return Boolean(key);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export async function shouldTryGenericOpenAICompat(
|
|
258
|
+
authStorage: AuthStorage,
|
|
259
|
+
ctx: ActiveSearchModelContext | undefined,
|
|
260
|
+
sessionId?: string,
|
|
261
|
+
signal?: AbortSignal,
|
|
262
|
+
): Promise<boolean> {
|
|
263
|
+
if (!ctx || ctx.webSearch === "off" || !isOpenAICompatWire(ctx.api)) return false;
|
|
264
|
+
const autoAllowed =
|
|
265
|
+
ctx.webSearch === "on" ||
|
|
266
|
+
((ctx.api === "openai-responses" || looksOpenAIFamilyModelId(ctx)) && !isLocalBaseUrl(ctx.baseUrl));
|
|
267
|
+
return autoAllowed && (await canUseGenericCredentials(authStorage, ctx, sessionId, signal));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export async function resolveProviderChain(options: ResolveProviderChainOptions): Promise<SearchProvider[]> {
|
|
271
|
+
const {
|
|
272
|
+
authStorage,
|
|
273
|
+
sessionId,
|
|
274
|
+
signal,
|
|
275
|
+
preferredProvider = preferredProvId,
|
|
276
|
+
activeModelContext,
|
|
277
|
+
fallbackProviders = fallbackProvIds,
|
|
278
|
+
} = options;
|
|
193
279
|
const chain: SearchProviderId[] = [];
|
|
194
280
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
281
|
+
// A forced primary is honored only when it is a user-configurable provider.
|
|
282
|
+
// The internal `openai-compatible` adapter (and any non-configurable value) is
|
|
283
|
+
// never selectable as a forced primary; such inputs fall through to auto
|
|
284
|
+
// native resolution instead of being injected into the chain.
|
|
285
|
+
if (preferredProvider !== "auto" && isConfigurableSearchProviderId(preferredProvider)) {
|
|
286
|
+
await appendAvailable(chain, preferredProvider, authStorage);
|
|
287
|
+
} else if (activeModelContext) {
|
|
288
|
+
const directId = MODEL_PROVIDER_TO_SEARCH[activeModelContext.provider.toLowerCase()];
|
|
289
|
+
if (directId && canUseDirectProviderMapping(activeModelContext, directId))
|
|
290
|
+
await appendAvailable(chain, directId, authStorage);
|
|
291
|
+
const inferred = inferNativeProviderFromModel(activeModelContext);
|
|
292
|
+
if (inferred) await appendAvailable(chain, inferred, authStorage);
|
|
293
|
+
if (await shouldTryGenericOpenAICompat(authStorage, activeModelContext, sessionId, signal))
|
|
294
|
+
appendDeduped(chain, "openai-compatible");
|
|
208
295
|
}
|
|
209
296
|
|
|
210
|
-
//
|
|
211
|
-
|
|
297
|
+
// Configured fallbacks are user-facing only: the internal `openai-compatible`
|
|
298
|
+
// adapter (and any non-configurable id) can never enter the chain through the
|
|
299
|
+
// fallback list, regardless of how `fallbackProviders` was supplied.
|
|
300
|
+
for (const id of fallbackProviders) {
|
|
301
|
+
if (!isConfigurableSearchProviderId(id)) continue;
|
|
302
|
+
await appendAvailable(chain, id, authStorage);
|
|
303
|
+
}
|
|
304
|
+
appendDeduped(chain, "duckduckgo");
|
|
212
305
|
|
|
213
306
|
const providers: SearchProvider[] = [];
|
|
214
|
-
for (const id of chain)
|
|
215
|
-
providers.push(await getSearchProvider(id));
|
|
216
|
-
}
|
|
307
|
+
for (const id of chain) providers.push(await getSearchProvider(id));
|
|
217
308
|
return providers;
|
|
218
309
|
}
|