@gajae-code/coding-agent 0.5.1 → 0.5.2
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 +17 -0
- package/README.md +1 -1
- 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/gjc-runtime/state-writer.d.ts +64 -2
- 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/modes/components/provider-onboarding-selector.d.ts +1 -1
- 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/session/history-storage.d.ts +2 -2
- package/dist/types/session/session-manager.d.ts +10 -1
- package/dist/types/setup/credential-import.d.ts +79 -0
- package/dist/types/task/executor.d.ts +1 -0
- package/dist/types/task/render.d.ts +1 -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/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 +54 -12
- 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/defaults/gjc/skills/ultragoal/SKILL.md +30 -8
- 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 +2 -1
- 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/docs-index.generated.ts +13 -9
- package/src/lsp/defaults.json +1 -0
- package/src/main.ts +14 -4
- package/src/modes/acp/acp-agent.ts +4 -2
- package/src/modes/bridge/bridge-mode.ts +2 -1
- package/src/modes/components/history-search.ts +5 -2
- package/src/modes/components/model-selector.ts +26 -0
- package/src/modes/components/provider-onboarding-selector.ts +6 -1
- package/src/modes/controllers/selector-controller.ts +80 -1
- 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/prompts/agents/executor.md +5 -2
- package/src/sdk.ts +12 -1
- package/src/session/agent-session.ts +22 -11
- package/src/session/history-storage.ts +32 -11
- package/src/session/session-manager.ts +70 -18
- package/src/setup/credential-import.ts +429 -0
- package/src/skill-state/deep-interview-mutation-guard.ts +2 -1
- package/src/task/executor.ts +7 -1
- package/src/task/render.ts +18 -7
- package/src/tools/ask.ts +4 -2
- package/src/tools/cron.ts +1 -1
- 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/task/render.ts
CHANGED
|
@@ -525,6 +525,10 @@ function renderAgentProgress(
|
|
|
525
525
|
expanded: boolean,
|
|
526
526
|
theme: Theme,
|
|
527
527
|
spinnerFrame?: number,
|
|
528
|
+
/** When true, omit wall-clock-derived displays (current-tool elapsed, retry
|
|
529
|
+
* countdown) so the output is a pure function of `progress` — required when the
|
|
530
|
+
* caller caches these lines (the `subagent` await panel). */
|
|
531
|
+
staticTime = false,
|
|
528
532
|
): string[] {
|
|
529
533
|
const lines: string[] = [];
|
|
530
534
|
const prefix = isLast ? theme.fg("dim", theme.tree.last) : theme.fg("dim", theme.tree.branch);
|
|
@@ -587,7 +591,7 @@ function renderAgentProgress(
|
|
|
587
591
|
if (toolDetail) {
|
|
588
592
|
toolLine += `: ${theme.fg("dim", truncateToWidth(replaceTabs(toolDetail), 40))}`;
|
|
589
593
|
}
|
|
590
|
-
if (progress.currentToolStartMs) {
|
|
594
|
+
if (!staticTime && progress.currentToolStartMs) {
|
|
591
595
|
const elapsed = Date.now() - progress.currentToolStartMs;
|
|
592
596
|
if (elapsed > 5000) {
|
|
593
597
|
toolLine += `${theme.sep.dot}${theme.fg("warning", formatDuration(elapsed))}`;
|
|
@@ -610,12 +614,17 @@ function renderAgentProgress(
|
|
|
610
614
|
// long until the next attempt. Without this, the parent UI would just
|
|
611
615
|
// keep spinning while a child sleeps on a 3-hour provider rate-limit.
|
|
612
616
|
if (progress.retryState && progress.status === "running") {
|
|
613
|
-
const remainingMs = Math.max(0, progress.retryState.startedAtMs + progress.retryState.delayMs - Date.now());
|
|
614
|
-
const waitLabel = remainingMs > 0 ? `in ${formatDuration(remainingMs)}` : "now";
|
|
615
617
|
const attemptLabel = progress.retryState.unbounded
|
|
616
618
|
? `attempt ${progress.retryState.attempt}`
|
|
617
619
|
: `${progress.retryState.attempt}/${progress.retryState.maxAttempts}`;
|
|
618
|
-
|
|
620
|
+
// `staticTime` omits the wall-clock countdown so a cached await body stays a
|
|
621
|
+
// pure function of its key (the producer already drops time-only churn).
|
|
622
|
+
let waitLabel = "";
|
|
623
|
+
if (!staticTime) {
|
|
624
|
+
const remainingMs = Math.max(0, progress.retryState.startedAtMs + progress.retryState.delayMs - Date.now());
|
|
625
|
+
waitLabel = remainingMs > 0 ? ` in ${formatDuration(remainingMs)}` : " now";
|
|
626
|
+
}
|
|
627
|
+
const summary = `retrying ${attemptLabel}${waitLabel}: ${truncateToWidth(replaceTabs(progress.retryState.errorMessage), 60)}`;
|
|
619
628
|
lines.push(`${continuePrefix}${theme.tree.hook} ${theme.fg("warning", summary)}`);
|
|
620
629
|
} else if (progress.retryFailure && progress.status !== "running") {
|
|
621
630
|
const summary = `auto-retry gave up after ${progress.retryFailure.attempt} attempt${
|
|
@@ -687,7 +696,7 @@ function renderAgentProgress(
|
|
|
687
696
|
const inflight = progress.inflightTaskDetails;
|
|
688
697
|
if (completedTaskCalls.length > 0 || inflight) {
|
|
689
698
|
const snapshots = inflight ? [...completedTaskCalls, inflight] : completedTaskCalls;
|
|
690
|
-
const nestedLines = renderNestedTaskTree(snapshots, expanded, theme, spinnerFrame);
|
|
699
|
+
const nestedLines = renderNestedTaskTree(snapshots, expanded, theme, spinnerFrame, staticTime);
|
|
691
700
|
for (const line of nestedLines) {
|
|
692
701
|
lines.push(`${continuePrefix}${line}`);
|
|
693
702
|
}
|
|
@@ -712,8 +721,9 @@ export function renderSubagentLiveProgress(
|
|
|
712
721
|
expanded: boolean,
|
|
713
722
|
theme: Theme,
|
|
714
723
|
spinnerFrame?: number,
|
|
724
|
+
staticTime = false,
|
|
715
725
|
): string[] {
|
|
716
|
-
return renderAgentProgress(progress, true, expanded, theme, spinnerFrame);
|
|
726
|
+
return renderAgentProgress(progress, true, expanded, theme, spinnerFrame, staticTime);
|
|
717
727
|
}
|
|
718
728
|
|
|
719
729
|
/**
|
|
@@ -1051,6 +1061,7 @@ function renderNestedTaskTree(
|
|
|
1051
1061
|
expanded: boolean,
|
|
1052
1062
|
theme: Theme,
|
|
1053
1063
|
spinnerFrame?: number,
|
|
1064
|
+
staticTime = false,
|
|
1054
1065
|
): string[] {
|
|
1055
1066
|
const lines: string[] = [];
|
|
1056
1067
|
for (const details of detailsList) {
|
|
@@ -1066,7 +1077,7 @@ function renderNestedTaskTree(
|
|
|
1066
1077
|
if (inflight && inflight.length > 0) {
|
|
1067
1078
|
inflight.forEach((prog, index) => {
|
|
1068
1079
|
const isLast = index === inflight.length - 1;
|
|
1069
|
-
lines.push(...renderAgentProgress(prog, isLast, expanded, theme, spinnerFrame));
|
|
1080
|
+
lines.push(...renderAgentProgress(prog, isLast, expanded, theme, spinnerFrame, staticTime));
|
|
1070
1081
|
});
|
|
1071
1082
|
}
|
|
1072
1083
|
}
|
package/src/tools/ask.ts
CHANGED
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
visibleWidth,
|
|
27
27
|
wrapTextWithAnsi,
|
|
28
28
|
} from "@gajae-code/tui";
|
|
29
|
-
import { prompt, untilAborted } from "@gajae-code/utils";
|
|
29
|
+
import { logger, prompt, untilAborted } from "@gajae-code/utils";
|
|
30
30
|
import * as z from "zod/v4";
|
|
31
31
|
import {
|
|
32
32
|
formatDeepInterviewSelectorPrompt,
|
|
@@ -43,6 +43,7 @@ import { renderStatusLine } from "../tui";
|
|
|
43
43
|
import type { ToolSession } from ".";
|
|
44
44
|
import { formatErrorMessage, formatMeta, formatTitle } from "./render-utils";
|
|
45
45
|
import { ToolAbortError } from "./tool-errors";
|
|
46
|
+
import { assertUltragoalAskAllowed } from "./ultragoal-ask-guard";
|
|
46
47
|
|
|
47
48
|
// =============================================================================
|
|
48
49
|
// Types
|
|
@@ -501,7 +502,7 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
|
|
|
501
502
|
{ sessionId },
|
|
502
503
|
);
|
|
503
504
|
} catch (error) {
|
|
504
|
-
|
|
505
|
+
logger.warn(
|
|
505
506
|
`ask: deep-interview round recording failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
506
507
|
);
|
|
507
508
|
}
|
|
@@ -514,6 +515,7 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
|
|
|
514
515
|
_onUpdate?: AgentToolUpdateCallback<AskToolDetails>,
|
|
515
516
|
context?: AgentToolContext,
|
|
516
517
|
): Promise<AgentToolResult<AskToolDetails>> {
|
|
518
|
+
await assertUltragoalAskAllowed(this.session.cwd);
|
|
517
519
|
const gateEmitter = this.session.getWorkflowGateEmitter?.();
|
|
518
520
|
const canUseWorkflowGate = gateEmitter?.isUnattended() === true;
|
|
519
521
|
|
package/src/tools/cron.ts
CHANGED
|
@@ -391,7 +391,7 @@ export function calculateCronFireTimeMs(params: {
|
|
|
391
391
|
}
|
|
392
392
|
|
|
393
393
|
function setCronTimeout(callback: () => void, delayMs: number): CronTimerHandle {
|
|
394
|
-
let handle:
|
|
394
|
+
let handle: NodeJS.Timeout | undefined;
|
|
395
395
|
let cleared = false;
|
|
396
396
|
const schedule = (remainingMs: number) => {
|
|
397
397
|
if (cleared) return;
|
|
@@ -12,7 +12,7 @@ import { Text } from "@gajae-code/tui";
|
|
|
12
12
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
13
13
|
import type { Theme } from "../modes/theme/theme";
|
|
14
14
|
import { renderSubagentLiveProgress } from "../task/render";
|
|
15
|
-
import { Ellipsis, Hasher,
|
|
15
|
+
import { Ellipsis, Hasher, renderStatusLine } from "../tui";
|
|
16
16
|
import {
|
|
17
17
|
formatDuration,
|
|
18
18
|
formatStatusIcon,
|
|
@@ -21,12 +21,87 @@ import {
|
|
|
21
21
|
type ToolUIStatus,
|
|
22
22
|
truncateToWidth,
|
|
23
23
|
} from "./render-utils";
|
|
24
|
-
import type
|
|
24
|
+
import { type SubagentSnapshot, type SubagentToolDetails, subagentAwaitRenderedStateSignature } from "./subagent";
|
|
25
25
|
|
|
26
26
|
const PREVIEW_LINES_COLLAPSED = 1;
|
|
27
27
|
const PREVIEW_LINES_EXPANDED = 4;
|
|
28
28
|
const PREVIEW_LINE_WIDTH = 80;
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Bounded, content-addressed cache for each subagent's heavy body lines (the
|
|
32
|
+
* indented receipt fields + `renderSubagentLiveProgress` -> `renderAgentProgress`
|
|
33
|
+
* output). It is module-level so it survives the built-in renderer recreating the
|
|
34
|
+
* result component on every partial update (`tool-execution.ts` clears the content
|
|
35
|
+
* box and re-invokes `renderResult`), which a per-component `let cached` cannot.
|
|
36
|
+
*
|
|
37
|
+
* The cached body is a PURE function of its key: the per-subagent rendered-state
|
|
38
|
+
* signature (reused from the producer; excludes time-derived churn), expanded
|
|
39
|
+
* state, width, and the actual Theme instance identity. `spinnerFrame` and all
|
|
40
|
+
* wall-clock displays are deliberately kept OUT of the cached body — the animated
|
|
41
|
+
* spinner and the fresh duration live in the cheap per-subagent status line, and
|
|
42
|
+
* `renderSubagentLiveProgress` is invoked with `staticTime` so current-tool elapsed
|
|
43
|
+
* and retry countdowns are never baked into cached lines.
|
|
44
|
+
*/
|
|
45
|
+
const SUBAGENT_BODY_CACHE_MAX = 128;
|
|
46
|
+
const subagentBodyCache = new Map<bigint, string[]>();
|
|
47
|
+
let subagentBodyRenderCount = 0;
|
|
48
|
+
|
|
49
|
+
// Stable identity per Theme instance so a theme change (preview, symbol preset,
|
|
50
|
+
// color-blind reload, custom-theme reload, or in-memory swap) never reuses stale
|
|
51
|
+
// ANSI/glyph strings — distinct Theme objects get distinct ids even when the theme
|
|
52
|
+
// name is unchanged (e.g. the "<in-memory>" name).
|
|
53
|
+
const themeIdentity = new WeakMap<Theme, number>();
|
|
54
|
+
let nextThemeId = 1;
|
|
55
|
+
function themeIdentityId(theme: Theme): number {
|
|
56
|
+
let id = themeIdentity.get(theme);
|
|
57
|
+
if (id === undefined) {
|
|
58
|
+
id = nextThemeId++;
|
|
59
|
+
themeIdentity.set(theme, id);
|
|
60
|
+
}
|
|
61
|
+
return id;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Test-only seam (PR3 deterministic cache-hit assertions). */
|
|
65
|
+
export const subagentBodyCacheTestHooks = {
|
|
66
|
+
get bodyRenders(): number {
|
|
67
|
+
return subagentBodyRenderCount;
|
|
68
|
+
},
|
|
69
|
+
get size(): number {
|
|
70
|
+
return subagentBodyCache.size;
|
|
71
|
+
},
|
|
72
|
+
reset(): void {
|
|
73
|
+
subagentBodyRenderCount = 0;
|
|
74
|
+
subagentBodyCache.clear();
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
function renderCachedSubagentBody(
|
|
79
|
+
snapshot: SubagentSnapshot,
|
|
80
|
+
signature: string,
|
|
81
|
+
expanded: boolean,
|
|
82
|
+
width: number,
|
|
83
|
+
theme: Theme,
|
|
84
|
+
): string[] {
|
|
85
|
+
const key = new Hasher().str(signature).bool(expanded).u32(width).u32(themeIdentityId(theme)).digest();
|
|
86
|
+
const hit = subagentBodyCache.get(key);
|
|
87
|
+
if (hit) {
|
|
88
|
+
// Refresh LRU recency.
|
|
89
|
+
subagentBodyCache.delete(key);
|
|
90
|
+
subagentBodyCache.set(key, hit);
|
|
91
|
+
return hit;
|
|
92
|
+
}
|
|
93
|
+
const lines = renderSubagentSnapshotBody(snapshot, expanded, theme).map(line =>
|
|
94
|
+
line.length > 0 ? truncateToWidth(line, width, Ellipsis.Omit) : "",
|
|
95
|
+
);
|
|
96
|
+
subagentBodyRenderCount += 1;
|
|
97
|
+
subagentBodyCache.set(key, lines);
|
|
98
|
+
if (subagentBodyCache.size > SUBAGENT_BODY_CACHE_MAX) {
|
|
99
|
+
const oldest = subagentBodyCache.keys().next().value;
|
|
100
|
+
if (oldest !== undefined) subagentBodyCache.delete(oldest);
|
|
101
|
+
}
|
|
102
|
+
return lines;
|
|
103
|
+
}
|
|
104
|
+
|
|
30
105
|
function statusIconKind(status: SubagentSnapshot["status"]): ToolUIStatus {
|
|
31
106
|
switch (status) {
|
|
32
107
|
case "completed":
|
|
@@ -44,13 +119,10 @@ function statusIconKind(status: SubagentSnapshot["status"]): ToolUIStatus {
|
|
|
44
119
|
}
|
|
45
120
|
}
|
|
46
121
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
spinnerFrame: number | undefined,
|
|
52
|
-
): string[] {
|
|
53
|
-
const lines: string[] = [];
|
|
122
|
+
// Cheap, dynamic per-subagent status line: the spinner may animate and the duration
|
|
123
|
+
// is the snapshot's own (fresh) value, so this line is rebuilt every frame and is
|
|
124
|
+
// NOT part of the cached body.
|
|
125
|
+
function renderSubagentStatusLine(snapshot: SubagentSnapshot, theme: Theme, spinnerFrame: number | undefined): string {
|
|
54
126
|
const icon = formatStatusIcon(
|
|
55
127
|
statusIconKind(snapshot.status),
|
|
56
128
|
theme,
|
|
@@ -59,7 +131,14 @@ function renderSubagentSnapshot(
|
|
|
59
131
|
const id = theme.fg("muted", snapshot.id);
|
|
60
132
|
const status = theme.fg("dim", snapshot.status);
|
|
61
133
|
const duration = theme.fg("dim", formatDuration(snapshot.durationMs));
|
|
62
|
-
|
|
134
|
+
return `${icon} ${id} ${status} ${duration}`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Heavy, cacheable per-subagent body: a pure function of (snapshot content, expanded,
|
|
138
|
+
// theme). No spinner frame and no wall-clock displays leak in (live progress uses
|
|
139
|
+
// `staticTime`), so the module body cache can never serve stale or frozen-ticking lines.
|
|
140
|
+
function renderSubagentSnapshotBody(snapshot: SubagentSnapshot, expanded: boolean, theme: Theme): string[] {
|
|
141
|
+
const lines: string[] = [];
|
|
63
142
|
|
|
64
143
|
// Static receipt fields (parity with the markdown content for non-await actions).
|
|
65
144
|
if (snapshot.jobId !== snapshot.id) lines.push(` ${theme.fg("dim", `Job: ${snapshot.jobId}`)}`);
|
|
@@ -82,13 +161,13 @@ function renderSubagentSnapshot(
|
|
|
82
161
|
for (const al of snapshot.assignment.split("\n")) lines.push(` ${theme.fg("toolOutput", replaceTabs(al))}`);
|
|
83
162
|
}
|
|
84
163
|
|
|
85
|
-
// Defense in depth: the producer only attaches `progress` when a live
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
164
|
+
// Defense in depth: the producer only attaches `progress` when a live producer
|
|
165
|
+
// exists (subagent.ts #liveProgressFields), but the renderer also honors an
|
|
166
|
+
// explicit `liveProgressAvailable: false` so stale retained progress can never
|
|
167
|
+
// resurrect a live panel (AC5). `staticTime` keeps wall-clock displays out of
|
|
168
|
+
// these cached lines.
|
|
89
169
|
if (snapshot.progress && snapshot.liveProgressAvailable !== false) {
|
|
90
|
-
|
|
91
|
-
for (const pl of renderSubagentLiveProgress(snapshot.progress, expanded, theme, spinnerFrame)) {
|
|
170
|
+
for (const pl of renderSubagentLiveProgress(snapshot.progress, expanded, theme, undefined, true)) {
|
|
92
171
|
lines.push(` ${pl}`);
|
|
93
172
|
}
|
|
94
173
|
} else if (snapshot.liveProgressAvailable && (snapshot.status === "running" || snapshot.status === "queued")) {
|
|
@@ -133,14 +212,17 @@ export const subagentToolRenderer = {
|
|
|
133
212
|
|
|
134
213
|
const runningCount = subagents.filter(s => s.status === "running").length;
|
|
135
214
|
|
|
136
|
-
|
|
215
|
+
// Each snapshot's rendered-state signature is constant for this component
|
|
216
|
+
// instance, so compute them at most once; the heavy per-subagent bodies are
|
|
217
|
+
// cached module-side and keyed by that signature.
|
|
218
|
+
let snapshotSignatures: string[] | undefined;
|
|
137
219
|
return {
|
|
138
220
|
render(width: number): string[] {
|
|
139
221
|
const expanded = options.expanded;
|
|
140
|
-
const spinnerFrame = options.spinnerFrame ?? 0;
|
|
141
|
-
const key = new Hasher().bool(expanded).u32(width).u32(spinnerFrame).digest();
|
|
142
|
-
if (cached?.key === key) return cached.lines;
|
|
143
222
|
|
|
223
|
+
// Cheap dynamic header: may animate with `spinnerFrame` and is rebuilt
|
|
224
|
+
// every frame, but it is a single status line plus an optional hint, so
|
|
225
|
+
// it is never gated by the heavy body cache.
|
|
144
226
|
const header = renderStatusLine(
|
|
145
227
|
{
|
|
146
228
|
icon: runningCount > 0 ? "info" : "success",
|
|
@@ -153,23 +235,31 @@ export const subagentToolRenderer = {
|
|
|
153
235
|
},
|
|
154
236
|
theme,
|
|
155
237
|
);
|
|
156
|
-
|
|
157
|
-
const lines: string[] = [header];
|
|
238
|
+
const out: string[] = [truncateToWidth(header, width, Ellipsis.Omit)];
|
|
158
239
|
// Discoverability: the inline panel is a bounded preview; the session
|
|
159
240
|
// observer (ctrl+s) streams the full per-subagent message history.
|
|
160
241
|
if (runningCount > 0) {
|
|
161
|
-
|
|
162
|
-
}
|
|
163
|
-
for (const snapshot of subagents) {
|
|
164
|
-
lines.push(...renderSubagentSnapshot(snapshot, expanded, theme, options.spinnerFrame));
|
|
242
|
+
out.push(truncateToWidth(` ${theme.fg("dim", "(ctrl+s to observe sessions)")}`, width, Ellipsis.Omit));
|
|
165
243
|
}
|
|
166
244
|
|
|
167
|
-
|
|
168
|
-
|
|
245
|
+
snapshotSignatures ??= subagents.map(snapshot => subagentAwaitRenderedStateSignature([snapshot]));
|
|
246
|
+
subagents.forEach((snapshot, index) => {
|
|
247
|
+
// Fresh per-subagent status line (cheap), then the cached heavy body.
|
|
248
|
+
out.push(
|
|
249
|
+
truncateToWidth(
|
|
250
|
+
renderSubagentStatusLine(snapshot, theme, options.spinnerFrame),
|
|
251
|
+
width,
|
|
252
|
+
Ellipsis.Omit,
|
|
253
|
+
),
|
|
254
|
+
);
|
|
255
|
+
out.push(...renderCachedSubagentBody(snapshot, snapshotSignatures![index]!, expanded, width, theme));
|
|
256
|
+
});
|
|
169
257
|
return out;
|
|
170
258
|
},
|
|
171
259
|
invalidate() {
|
|
172
|
-
|
|
260
|
+
// The heavy body cache is content-addressed (keyed by the rendered-state
|
|
261
|
+
// signature, width, expanded, and theme), so there is no instance-local
|
|
262
|
+
// state to clear here.
|
|
173
263
|
},
|
|
174
264
|
};
|
|
175
265
|
},
|
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";
|