@gajae-code/coding-agent 0.4.5 → 0.5.0
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 +43 -0
- package/dist/types/commands/harness.d.ts +3 -0
- package/dist/types/config/model-profile-activation.d.ts +11 -2
- package/dist/types/config/model-profiles.d.ts +7 -0
- package/dist/types/config/model-registry.d.ts +3 -0
- package/dist/types/config/model-resolver.d.ts +2 -0
- package/dist/types/config/models-config-schema.d.ts +30 -0
- package/dist/types/config/settings-schema.d.ts +4 -3
- package/dist/types/gjc-runtime/team-runtime.d.ts +0 -1
- package/dist/types/gjc-runtime/tmux-common.d.ts +3 -0
- package/dist/types/harness-control-plane/owner.d.ts +1 -1
- package/dist/types/harness-control-plane/receipt-spool.d.ts +19 -0
- package/dist/types/harness-control-plane/state-machine.d.ts +6 -1
- package/dist/types/harness-control-plane/types.d.ts +4 -0
- package/dist/types/hindsight/mental-models.d.ts +5 -5
- package/dist/types/modes/components/model-selector.d.ts +1 -12
- package/dist/types/modes/rpc/rpc-client.d.ts +2 -2
- package/dist/types/modes/rpc/rpc-types.d.ts +4 -1
- package/dist/types/sdk.d.ts +5 -0
- package/dist/types/session/agent-session.d.ts +2 -0
- package/dist/types/session/blob-store.d.ts +20 -1
- package/dist/types/session/session-manager.d.ts +24 -6
- package/dist/types/session/streaming-output.d.ts +3 -2
- package/dist/types/session/tool-choice-queue.d.ts +6 -0
- package/dist/types/task/receipt.d.ts +1 -0
- package/dist/types/task/types.d.ts +7 -0
- package/dist/types/thinking-metadata.d.ts +16 -0
- package/dist/types/thinking.d.ts +3 -12
- package/dist/types/tools/index.d.ts +2 -0
- package/dist/types/tools/resolve.d.ts +0 -10
- package/dist/types/utils/tool-choice.d.ts +14 -1
- package/package.json +7 -7
- package/src/cli.ts +8 -4
- package/src/commands/harness.ts +36 -2
- package/src/commands/launch.ts +2 -2
- package/src/commands/session.ts +3 -1
- package/src/config/model-profile-activation.ts +15 -3
- package/src/config/model-profiles.ts +255 -56
- package/src/config/model-resolver.ts +9 -6
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings-schema.ts +6 -3
- package/src/coordinator-mcp/server.ts +54 -23
- package/src/cursor.ts +16 -2
- package/src/defaults/gjc/skills/team/SKILL.md +3 -2
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +8 -2
- package/src/export/html/index.ts +13 -9
- package/src/gjc-runtime/team-runtime.ts +33 -7
- package/src/gjc-runtime/tmux-common.ts +15 -0
- package/src/gjc-runtime/tmux-sessions.ts +19 -11
- package/src/gjc-runtime/ultragoal-runtime.ts +505 -41
- package/src/gjc-runtime/workflow-manifest.generated.json +27 -1
- package/src/gjc-runtime/workflow-manifest.ts +16 -1
- package/src/harness-control-plane/owner.ts +78 -27
- package/src/harness-control-plane/receipt-spool.ts +128 -0
- package/src/harness-control-plane/state-machine.ts +27 -6
- package/src/harness-control-plane/storage.ts +23 -0
- package/src/harness-control-plane/types.ts +4 -0
- package/src/hindsight/mental-models.ts +17 -16
- package/src/internal-urls/docs-index.generated.ts +2 -2
- package/src/modes/components/assistant-message.ts +26 -14
- package/src/modes/components/diff.ts +97 -0
- package/src/modes/components/model-selector.ts +353 -181
- package/src/modes/components/tool-execution.ts +30 -13
- package/src/modes/controllers/selector-controller.ts +33 -42
- package/src/modes/rpc/rpc-client.ts +3 -2
- package/src/modes/rpc/rpc-mode.ts +44 -14
- package/src/modes/rpc/rpc-types.ts +5 -2
- package/src/modes/shared/agent-wire/command-dispatch.ts +10 -5
- package/src/modes/shared/agent-wire/command-validation.ts +11 -0
- package/src/sdk.ts +29 -2
- package/src/secrets/obfuscator.ts +102 -27
- package/src/session/agent-session.ts +105 -20
- package/src/session/blob-store.ts +89 -3
- package/src/session/session-manager.ts +309 -58
- package/src/session/streaming-output.ts +185 -122
- package/src/session/tool-choice-queue.ts +23 -0
- package/src/task/executor.ts +69 -6
- package/src/task/receipt.ts +5 -0
- package/src/task/render.ts +21 -1
- package/src/task/types.ts +8 -0
- package/src/thinking-metadata.ts +51 -0
- package/src/thinking.ts +26 -46
- package/src/tools/bash.ts +1 -1
- package/src/tools/index.ts +2 -0
- package/src/tools/resolve.ts +93 -18
- package/src/utils/edit-mode.ts +1 -1
- package/src/utils/tool-choice.ts +45 -16
package/src/task/executor.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { type JsonSchemaValidationIssue, validateJsonSchemaValue } from "@gajae-
|
|
|
13
13
|
import { logger, prompt, untilAborted } from "@gajae-code/utils";
|
|
14
14
|
import { AsyncJobManager } from "../async";
|
|
15
15
|
import { ModelRegistry } from "../config/model-registry";
|
|
16
|
-
import { resolveModelOverrideWithAuthFallback } from "../config/model-resolver";
|
|
16
|
+
import { formatModelString, resolveModelOverrideWithAuthFallback } from "../config/model-resolver";
|
|
17
17
|
import type { PromptTemplate } from "../config/prompt-templates";
|
|
18
18
|
import { Settings } from "../config/settings";
|
|
19
19
|
import { SETTINGS_SCHEMA, type SettingPath } from "../config/settings-schema";
|
|
@@ -38,7 +38,7 @@ import { jtdToJsonSchema, normalizeSchema } from "../tools/jtd-to-json-schema";
|
|
|
38
38
|
import { type ReportFindingDetails, toReviewFinding } from "../tools/review";
|
|
39
39
|
import { ToolAbortError } from "../tools/tool-errors";
|
|
40
40
|
import type { EventBus } from "../utils/event-bus";
|
|
41
|
-
import {
|
|
41
|
+
import { buildNamedToolChoiceResult } from "../utils/tool-choice";
|
|
42
42
|
import type { WorkspaceTree } from "../workspace-tree";
|
|
43
43
|
import { subprocessToolRegistry } from "./subprocess-tool-registry";
|
|
44
44
|
import {
|
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
type AgentProgress,
|
|
47
47
|
MAX_OUTPUT_BYTES,
|
|
48
48
|
MAX_OUTPUT_LINES,
|
|
49
|
+
type ModelSubstitutionWarning,
|
|
49
50
|
type ReviewFinding,
|
|
50
51
|
type SingleResult,
|
|
51
52
|
TASK_SUBAGENT_EVENT_CHANNEL,
|
|
@@ -627,6 +628,10 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
627
628
|
let yieldCalled = false;
|
|
628
629
|
let pauseRequested = false;
|
|
629
630
|
let paused = false;
|
|
631
|
+
let modelSubstitutionWarning: ModelSubstitutionWarning | undefined;
|
|
632
|
+
let resolvedModelString: string | undefined;
|
|
633
|
+
let lastAssistantModelString: string | undefined;
|
|
634
|
+
let effectiveThinkingLevelForWarning: ThinkingLevel | undefined;
|
|
630
635
|
|
|
631
636
|
// Accumulate usage incrementally from message_end events (no memory for streaming events)
|
|
632
637
|
const accumulatedUsage = {
|
|
@@ -762,6 +767,14 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
762
767
|
return undefined;
|
|
763
768
|
};
|
|
764
769
|
|
|
770
|
+
const getMessageModelString = (message: unknown): string | undefined => {
|
|
771
|
+
if (!message || typeof message !== "object") return undefined;
|
|
772
|
+
const record = message as { provider?: unknown; model?: unknown };
|
|
773
|
+
return typeof record.provider === "string" && typeof record.model === "string"
|
|
774
|
+
? `${record.provider}/${record.model}`
|
|
775
|
+
: undefined;
|
|
776
|
+
};
|
|
777
|
+
|
|
765
778
|
const updateRecentOutputLines = () => {
|
|
766
779
|
const lines = recentOutputTail.split("\n").filter(line => line.trim());
|
|
767
780
|
progress.recentOutput = lines.slice(-8).reverse();
|
|
@@ -964,6 +977,29 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
964
977
|
}
|
|
965
978
|
}
|
|
966
979
|
}
|
|
980
|
+
const assistantModel = getMessageModelString(event.message);
|
|
981
|
+
if (assistantModel) {
|
|
982
|
+
lastAssistantModelString = assistantModel;
|
|
983
|
+
if (resolvedModelString && assistantModel !== resolvedModelString && !modelSubstitutionWarning) {
|
|
984
|
+
modelSubstitutionWarning = {
|
|
985
|
+
requested: resolvedModelString,
|
|
986
|
+
effective: assistantModel,
|
|
987
|
+
reason: "assistant_model_mismatch",
|
|
988
|
+
};
|
|
989
|
+
progress.modelSubstitutionWarning = modelSubstitutionWarning;
|
|
990
|
+
activeSession?.sessionManager.appendModelChange(assistantModel, undefined, {
|
|
991
|
+
previousModel: resolvedModelString,
|
|
992
|
+
reason: modelSubstitutionWarning.reason,
|
|
993
|
+
thinkingLevel: effectiveThinkingLevelForWarning ?? null,
|
|
994
|
+
});
|
|
995
|
+
logger.warn("Subagent assistant response reported a substituted effective model", {
|
|
996
|
+
requested: resolvedModelString,
|
|
997
|
+
effective: assistantModel,
|
|
998
|
+
agent: agent.name,
|
|
999
|
+
id,
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
967
1003
|
}
|
|
968
1004
|
// Extract and accumulate usage (prefer message.usage, fallback to event.usage)
|
|
969
1005
|
const messageUsage = getMessageUsage(event.message) || (event as AgentEvent & { usage?: unknown }).usage;
|
|
@@ -1090,6 +1126,8 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1090
1126
|
thinkingLevel: resolvedThinkingLevel,
|
|
1091
1127
|
explicitThinkingLevel,
|
|
1092
1128
|
authFallbackUsed,
|
|
1129
|
+
requestedModel,
|
|
1130
|
+
fallbackReason,
|
|
1093
1131
|
} = await awaitAbortable(
|
|
1094
1132
|
resolveModelOverrideWithAuthFallback(
|
|
1095
1133
|
modelPatterns,
|
|
@@ -1099,9 +1137,18 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1099
1137
|
options.parentSessionId,
|
|
1100
1138
|
),
|
|
1101
1139
|
);
|
|
1102
|
-
if (
|
|
1140
|
+
if (model) {
|
|
1141
|
+
resolvedModelString = formatModelString(model);
|
|
1142
|
+
}
|
|
1143
|
+
if (authFallbackUsed && model && requestedModel) {
|
|
1144
|
+
modelSubstitutionWarning = {
|
|
1145
|
+
requested: formatModelString(requestedModel),
|
|
1146
|
+
effective: formatModelString(model),
|
|
1147
|
+
reason: fallbackReason ?? "auth_unavailable",
|
|
1148
|
+
};
|
|
1149
|
+
progress.modelSubstitutionWarning = modelSubstitutionWarning;
|
|
1103
1150
|
logger.warn("Subagent model has no working credentials; falling back to parent session model", {
|
|
1104
|
-
requested:
|
|
1151
|
+
requested: modelSubstitutionWarning.requested,
|
|
1105
1152
|
parentModel: options.parentActiveModelPattern,
|
|
1106
1153
|
resolvedProvider: model.provider,
|
|
1107
1154
|
resolvedModel: model.id,
|
|
@@ -1113,6 +1160,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1113
1160
|
const effectiveThinkingLevel = explicitThinkingLevel
|
|
1114
1161
|
? resolvedThinkingLevel
|
|
1115
1162
|
: (thinkingLevel ?? resolvedThinkingLevel);
|
|
1163
|
+
effectiveThinkingLevelForWarning = effectiveThinkingLevel;
|
|
1116
1164
|
|
|
1117
1165
|
const sessionManager = sessionFile
|
|
1118
1166
|
? await awaitAbortable(SessionManager.open(sessionFile))
|
|
@@ -1174,6 +1222,10 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1174
1222
|
settings: subagentSettings,
|
|
1175
1223
|
model,
|
|
1176
1224
|
thinkingLevel: effectiveThinkingLevel,
|
|
1225
|
+
modelSubstitution:
|
|
1226
|
+
modelSubstitutionWarning?.reason === "auth_unavailable" && requestedModel
|
|
1227
|
+
? { requestedModel, reason: modelSubstitutionWarning.reason }
|
|
1228
|
+
: undefined,
|
|
1177
1229
|
toolNames,
|
|
1178
1230
|
outputSchema,
|
|
1179
1231
|
requireYieldTool: true,
|
|
@@ -1412,7 +1464,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1412
1464
|
await awaitAbortable(session.waitForIdle());
|
|
1413
1465
|
}
|
|
1414
1466
|
|
|
1415
|
-
const
|
|
1467
|
+
const reminderToolChoiceResult = buildNamedToolChoiceResult("yield", session.model);
|
|
1416
1468
|
|
|
1417
1469
|
let retryCount = 0;
|
|
1418
1470
|
while (!paused && !yieldCalled && retryCount < MAX_YIELD_RETRIES && !abortSignal.aborted) {
|
|
@@ -1433,7 +1485,9 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1433
1485
|
await awaitAbortable(
|
|
1434
1486
|
session.prompt(reminder, {
|
|
1435
1487
|
attribution: "agent",
|
|
1436
|
-
...(isFinalRetry &&
|
|
1488
|
+
...(isFinalRetry && reminderToolChoiceResult.exactNamed && reminderToolChoiceResult.choice
|
|
1489
|
+
? { toolChoice: reminderToolChoiceResult.choice }
|
|
1490
|
+
: {}),
|
|
1437
1491
|
}),
|
|
1438
1492
|
);
|
|
1439
1493
|
await awaitAbortable(session.waitForIdle());
|
|
@@ -1466,6 +1520,14 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1466
1520
|
error = undefined;
|
|
1467
1521
|
}
|
|
1468
1522
|
}
|
|
1523
|
+
if (lastAssistantModelString && resolvedModelString && lastAssistantModelString !== resolvedModelString) {
|
|
1524
|
+
modelSubstitutionWarning ??= {
|
|
1525
|
+
requested: resolvedModelString,
|
|
1526
|
+
effective: lastAssistantModelString,
|
|
1527
|
+
reason: "assistant_model_mismatch",
|
|
1528
|
+
};
|
|
1529
|
+
progress.modelSubstitutionWarning = modelSubstitutionWarning;
|
|
1530
|
+
}
|
|
1469
1531
|
} catch (err) {
|
|
1470
1532
|
exitCode = 1;
|
|
1471
1533
|
if (!abortSignal.aborted) {
|
|
@@ -1642,6 +1704,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1642
1704
|
contextTokens: progress.contextTokens,
|
|
1643
1705
|
contextWindow: progress.contextWindow,
|
|
1644
1706
|
modelOverride,
|
|
1707
|
+
modelSubstitutionWarning,
|
|
1645
1708
|
error: exitCode !== 0 && stderr ? stderr : undefined,
|
|
1646
1709
|
aborted: wasAborted,
|
|
1647
1710
|
abortReason: finalAbortReason,
|
package/src/task/receipt.ts
CHANGED
|
@@ -29,6 +29,7 @@ export interface TaskResultReceipt {
|
|
|
29
29
|
contextTokens?: number;
|
|
30
30
|
contextWindow?: number;
|
|
31
31
|
modelOverride?: string | string[];
|
|
32
|
+
modelSubstitutionWarning?: SingleResult["modelSubstitutionWarning"];
|
|
32
33
|
usage?: SingleResult["usage"];
|
|
33
34
|
cost?: number;
|
|
34
35
|
branchName?: string;
|
|
@@ -78,6 +79,9 @@ function truncateText(value: string | undefined, maxChars: number): string | und
|
|
|
78
79
|
|
|
79
80
|
function buildSafeSynopsis(raw: SingleResult, outputRef: TaskResultReceipt["outputRef"]): string {
|
|
80
81
|
const status = getStatus(raw);
|
|
82
|
+
if (raw.modelSubstitutionWarning) {
|
|
83
|
+
return `Task ${status}; requested model substituted from ${raw.modelSubstitutionWarning.requested} to ${raw.modelSubstitutionWarning.effective}.`;
|
|
84
|
+
}
|
|
81
85
|
if (raw.retryFailure) {
|
|
82
86
|
return `Task ${status}; retry stopped after attempt ${raw.retryFailure.attempt}.`;
|
|
83
87
|
}
|
|
@@ -220,6 +224,7 @@ export function buildTaskReceipt(raw: SingleResult): TaskResultReceipt {
|
|
|
220
224
|
contextTokens: raw.contextTokens,
|
|
221
225
|
contextWindow: raw.contextWindow,
|
|
222
226
|
modelOverride: raw.modelOverride,
|
|
227
|
+
modelSubstitutionWarning: raw.modelSubstitutionWarning,
|
|
223
228
|
usage: raw.usage,
|
|
224
229
|
cost: raw.usage?.cost.total,
|
|
225
230
|
branchName: raw.branchName,
|
package/src/task/render.ts
CHANGED
|
@@ -119,6 +119,10 @@ function normalizeReportFindings(value: unknown): ReportFindingDetails[] {
|
|
|
119
119
|
return findings;
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
function formatModelSubstitutionWarning(warning: { requested: string; effective: string }): string {
|
|
123
|
+
return `Requested model substituted: ${warning.requested} -> ${warning.effective}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
122
126
|
function formatJsonScalar(value: unknown, _theme: Theme): string {
|
|
123
127
|
if (value === null) return "null";
|
|
124
128
|
if (typeof value === "string") {
|
|
@@ -566,6 +570,14 @@ function renderAgentProgress(
|
|
|
566
570
|
lines.push(statusLine);
|
|
567
571
|
|
|
568
572
|
lines.push(...renderTaskSection(progress.assignment ?? progress.task, continuePrefix, expanded, theme));
|
|
573
|
+
if (progress.modelSubstitutionWarning) {
|
|
574
|
+
lines.push(
|
|
575
|
+
`${continuePrefix}${theme.fg(
|
|
576
|
+
"warning",
|
|
577
|
+
truncateToWidth(replaceTabs(formatModelSubstitutionWarning(progress.modelSubstitutionWarning)), 90),
|
|
578
|
+
)}`,
|
|
579
|
+
);
|
|
580
|
+
}
|
|
569
581
|
|
|
570
582
|
// Current tool (if running) or most recent completed tool
|
|
571
583
|
if (progress.status === "running") {
|
|
@@ -862,9 +874,17 @@ function renderAgentResult(result: TaskResultReceipt, isLast: boolean, expanded:
|
|
|
862
874
|
}
|
|
863
875
|
}
|
|
864
876
|
}
|
|
865
|
-
} else {
|
|
877
|
+
} else if (!result.modelSubstitutionWarning) {
|
|
866
878
|
lines.push(...renderOutputSection(result.preview, continuePrefix, expanded, theme, 3, 12));
|
|
867
879
|
}
|
|
880
|
+
if (result.modelSubstitutionWarning) {
|
|
881
|
+
lines.push(
|
|
882
|
+
`${continuePrefix}${theme.fg(
|
|
883
|
+
"warning",
|
|
884
|
+
truncateToWidth(replaceTabs(formatModelSubstitutionWarning(result.modelSubstitutionWarning)), 90),
|
|
885
|
+
)}`,
|
|
886
|
+
);
|
|
887
|
+
}
|
|
868
888
|
if (result.roi?.lowRoi) {
|
|
869
889
|
lines.push(`${continuePrefix}${theme.fg("warning", "low ROI: produced no material contribution")}`);
|
|
870
890
|
}
|
package/src/task/types.ts
CHANGED
|
@@ -215,6 +215,12 @@ export interface AgentDefinition {
|
|
|
215
215
|
filePath?: string;
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
+
export interface ModelSubstitutionWarning {
|
|
219
|
+
requested: string;
|
|
220
|
+
effective: string;
|
|
221
|
+
reason: "auth_unavailable" | "assistant_model_mismatch";
|
|
222
|
+
}
|
|
223
|
+
|
|
218
224
|
/** Progress tracking for a single agent */
|
|
219
225
|
export interface AgentProgress {
|
|
220
226
|
index: number;
|
|
@@ -247,6 +253,7 @@ export interface AgentProgress {
|
|
|
247
253
|
cost: number;
|
|
248
254
|
durationMs: number;
|
|
249
255
|
modelOverride?: string | string[];
|
|
256
|
+
modelSubstitutionWarning?: ModelSubstitutionWarning;
|
|
250
257
|
/** Data extracted by registered subprocess tool handlers (keyed by tool name) */
|
|
251
258
|
extractedToolData?: Record<string, unknown[]>;
|
|
252
259
|
/**
|
|
@@ -306,6 +313,7 @@ export interface SingleResult {
|
|
|
306
313
|
/** Model's context window in tokens, when known. */
|
|
307
314
|
contextWindow?: number;
|
|
308
315
|
modelOverride?: string | string[];
|
|
316
|
+
modelSubstitutionWarning?: ModelSubstitutionWarning;
|
|
309
317
|
error?: string;
|
|
310
318
|
aborted?: boolean;
|
|
311
319
|
abortReason?: string;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export type ThinkingLevelValue = "inherit" | "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Metadata used to render thinking selector values in the coding-agent UI.
|
|
5
|
+
*
|
|
6
|
+
* This module is intentionally provider/native-free so schema generation can
|
|
7
|
+
* import settings metadata before native addons have been built in CI.
|
|
8
|
+
*/
|
|
9
|
+
export interface ThinkingLevelMetadata {
|
|
10
|
+
value: ThinkingLevelValue;
|
|
11
|
+
label: string;
|
|
12
|
+
description: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const THINKING_LEVEL_METADATA: Record<ThinkingLevelValue, ThinkingLevelMetadata> = {
|
|
16
|
+
inherit: {
|
|
17
|
+
value: "inherit",
|
|
18
|
+
label: "inherit",
|
|
19
|
+
description: "Inherit session default",
|
|
20
|
+
},
|
|
21
|
+
off: { value: "off", label: "off", description: "No reasoning" },
|
|
22
|
+
minimal: {
|
|
23
|
+
value: "minimal",
|
|
24
|
+
label: "min",
|
|
25
|
+
description: "Very brief reasoning (~1k tokens)",
|
|
26
|
+
},
|
|
27
|
+
low: { value: "low", label: "low", description: "Light reasoning (~2k tokens)" },
|
|
28
|
+
medium: {
|
|
29
|
+
value: "medium",
|
|
30
|
+
label: "medium",
|
|
31
|
+
description: "Moderate reasoning (~8k tokens)",
|
|
32
|
+
},
|
|
33
|
+
high: { value: "high", label: "high", description: "Deep reasoning (~16k tokens)" },
|
|
34
|
+
xhigh: {
|
|
35
|
+
value: "xhigh",
|
|
36
|
+
label: "xhigh",
|
|
37
|
+
description: "Maximum reasoning (~32k tokens)",
|
|
38
|
+
},
|
|
39
|
+
max: {
|
|
40
|
+
value: "max",
|
|
41
|
+
label: "max",
|
|
42
|
+
description: "Opus maximum reasoning",
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Returns display metadata for a thinking selector.
|
|
48
|
+
*/
|
|
49
|
+
export function getThinkingLevelMetadata(level: ThinkingLevelValue): ThinkingLevelMetadata {
|
|
50
|
+
return THINKING_LEVEL_METADATA[level];
|
|
51
|
+
}
|
package/src/thinking.ts
CHANGED
|
@@ -2,45 +2,7 @@ import { type ResolvedThinkingLevel, ThinkingLevel } from "@gajae-code/agent-cor
|
|
|
2
2
|
import { clampThinkingLevelForModel, type Effort, THINKING_EFFORTS } from "@gajae-code/ai/model-thinking";
|
|
3
3
|
import type { Model } from "@gajae-code/ai/types";
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
* Metadata used to render thinking selector values in the coding-agent UI.
|
|
7
|
-
*/
|
|
8
|
-
export interface ThinkingLevelMetadata {
|
|
9
|
-
value: ThinkingLevel;
|
|
10
|
-
label: string;
|
|
11
|
-
description: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const THINKING_LEVEL_METADATA: Record<ThinkingLevel, ThinkingLevelMetadata> = {
|
|
15
|
-
[ThinkingLevel.Inherit]: {
|
|
16
|
-
value: ThinkingLevel.Inherit,
|
|
17
|
-
label: "inherit",
|
|
18
|
-
description: "Inherit session default",
|
|
19
|
-
},
|
|
20
|
-
[ThinkingLevel.Off]: { value: ThinkingLevel.Off, label: "off", description: "No reasoning" },
|
|
21
|
-
[ThinkingLevel.Minimal]: {
|
|
22
|
-
value: ThinkingLevel.Minimal,
|
|
23
|
-
label: "min",
|
|
24
|
-
description: "Very brief reasoning (~1k tokens)",
|
|
25
|
-
},
|
|
26
|
-
[ThinkingLevel.Low]: { value: ThinkingLevel.Low, label: "low", description: "Light reasoning (~2k tokens)" },
|
|
27
|
-
[ThinkingLevel.Medium]: {
|
|
28
|
-
value: ThinkingLevel.Medium,
|
|
29
|
-
label: "medium",
|
|
30
|
-
description: "Moderate reasoning (~8k tokens)",
|
|
31
|
-
},
|
|
32
|
-
[ThinkingLevel.High]: { value: ThinkingLevel.High, label: "high", description: "Deep reasoning (~16k tokens)" },
|
|
33
|
-
[ThinkingLevel.XHigh]: {
|
|
34
|
-
value: ThinkingLevel.XHigh,
|
|
35
|
-
label: "xhigh",
|
|
36
|
-
description: "Maximum reasoning (~32k tokens)",
|
|
37
|
-
},
|
|
38
|
-
[ThinkingLevel.Max]: {
|
|
39
|
-
value: ThinkingLevel.Max,
|
|
40
|
-
label: "max",
|
|
41
|
-
description: "Opus maximum reasoning",
|
|
42
|
-
},
|
|
43
|
-
};
|
|
5
|
+
export { getThinkingLevelMetadata, type ThinkingLevelMetadata } from "./thinking-metadata";
|
|
44
6
|
|
|
45
7
|
const THINKING_LEVELS = new Set<string>([ThinkingLevel.Inherit, ThinkingLevel.Off, ...THINKING_EFFORTS]);
|
|
46
8
|
const EFFORT_LEVELS = new Set<string>(THINKING_EFFORTS);
|
|
@@ -59,13 +21,6 @@ export function parseThinkingLevel(value: string | null | undefined): ThinkingLe
|
|
|
59
21
|
return value !== undefined && value !== null && THINKING_LEVELS.has(value) ? (value as ThinkingLevel) : undefined;
|
|
60
22
|
}
|
|
61
23
|
|
|
62
|
-
/**
|
|
63
|
-
* Returns display metadata for a thinking selector.
|
|
64
|
-
*/
|
|
65
|
-
export function getThinkingLevelMetadata(level: ThinkingLevel): ThinkingLevelMetadata {
|
|
66
|
-
return THINKING_LEVEL_METADATA[level];
|
|
67
|
-
}
|
|
68
|
-
|
|
69
24
|
/**
|
|
70
25
|
* Converts an agent-local selector into the effort sent to providers.
|
|
71
26
|
*/
|
|
@@ -91,3 +46,28 @@ export function resolveThinkingLevelForModel(
|
|
|
91
46
|
}
|
|
92
47
|
return clampThinkingLevelForModel(model, level);
|
|
93
48
|
}
|
|
49
|
+
|
|
50
|
+
export function clampExplicitThinkingLevelForModel(
|
|
51
|
+
model: Model | undefined,
|
|
52
|
+
level: ThinkingLevel | undefined,
|
|
53
|
+
): ThinkingLevel | undefined {
|
|
54
|
+
if (level === undefined || level === ThinkingLevel.Inherit || level === ThinkingLevel.Off) {
|
|
55
|
+
return level;
|
|
56
|
+
}
|
|
57
|
+
return clampThinkingLevelForModel(model, level);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function formatClampedModelSelector(selector: string, model: Model | undefined): string {
|
|
61
|
+
const slashIdx = selector.indexOf("/");
|
|
62
|
+
if (slashIdx <= 0) return selector;
|
|
63
|
+
const id = selector.slice(slashIdx + 1);
|
|
64
|
+
const colonIdx = id.lastIndexOf(":");
|
|
65
|
+
if (colonIdx === -1) return selector;
|
|
66
|
+
const suffix = id.slice(colonIdx + 1);
|
|
67
|
+
const thinkingLevel = parseThinkingLevel(suffix);
|
|
68
|
+
if (!thinkingLevel) return selector;
|
|
69
|
+
const clamped = clampExplicitThinkingLevelForModel(model, thinkingLevel);
|
|
70
|
+
return clamped && clamped !== ThinkingLevel.Inherit
|
|
71
|
+
? `${selector.slice(0, slashIdx + 1)}${id.slice(0, colonIdx)}:${clamped}`
|
|
72
|
+
: selector.slice(0, slashIdx + 1) + id.slice(0, colonIdx);
|
|
73
|
+
}
|
package/src/tools/bash.ts
CHANGED
|
@@ -51,7 +51,7 @@ async function saveBashOriginalArtifact(session: ToolSession, originalText: stri
|
|
|
51
51
|
const bashSchemaBase = z.object({
|
|
52
52
|
command: z.string().describe("command to execute"),
|
|
53
53
|
env: z.record(z.string().regex(BASH_ENV_NAME_PATTERN), z.string()).optional().describe("extra env vars"),
|
|
54
|
-
timeout: z.number().default(300).describe("timeout in seconds").optional(),
|
|
54
|
+
timeout: z.number().default(300).describe("timeout in seconds, NOT milliseconds (30 = 30s)").optional(),
|
|
55
55
|
cwd: z.string().describe("working directory").optional(),
|
|
56
56
|
pty: z.boolean().describe("run in pty mode").optional(),
|
|
57
57
|
});
|
package/src/tools/index.ts
CHANGED
|
@@ -240,6 +240,8 @@ export interface ToolSession {
|
|
|
240
240
|
getToolChoiceQueue?(): ToolChoiceQueue;
|
|
241
241
|
/** Build a model-provider-specific ToolChoice that targets the named tool, or undefined if unsupported. */
|
|
242
242
|
buildToolChoice?(toolName: string): ToolChoice | undefined;
|
|
243
|
+
/** Build a named tool-choice decision, preserving whether exact named forcing survived capability degradation. */
|
|
244
|
+
buildToolChoiceResult?(toolName: string): import("../utils/tool-choice").NamedToolChoiceResult;
|
|
243
245
|
/** Steer a hidden custom message into the conversation (e.g. a preview reminder). */
|
|
244
246
|
steer?(message: { customType: string; content: string; details?: unknown }): void;
|
|
245
247
|
/** Peek the currently in-flight tool-choice queue directive's invocation handler. Used by the `resolve` tool to dispatch to the pending action. */
|
package/src/tools/resolve.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@gajae-code/agent-core";
|
|
2
|
+
import type { ToolChoice } from "@gajae-code/ai";
|
|
2
3
|
import type { Component } from "@gajae-code/tui";
|
|
3
4
|
import { Text } from "@gajae-code/tui";
|
|
4
5
|
import { prompt, untilAborted } from "@gajae-code/utils";
|
|
@@ -38,6 +39,21 @@ export interface ResolveToolDetails {
|
|
|
38
39
|
* semantics. No session-level abstraction is needed: callers pass their
|
|
39
40
|
* apply/reject functions directly.
|
|
40
41
|
*/
|
|
42
|
+
/**
|
|
43
|
+
* Tags preview-fallback handlers installed in the session's standing-resolve
|
|
44
|
+
* slot so newer previews can replace older ones (latest-preview-wins) without
|
|
45
|
+
* ever displacing a mode-owned handler such as plan mode's approval handler.
|
|
46
|
+
*/
|
|
47
|
+
const previewResolveFallbacks = new WeakSet<object>();
|
|
48
|
+
|
|
49
|
+
function markPreviewResolveFallback(handler: (input: unknown) => Promise<unknown> | unknown): void {
|
|
50
|
+
previewResolveFallbacks.add(handler);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isPreviewResolveFallback(handler: (input: unknown) => Promise<unknown> | unknown): boolean {
|
|
54
|
+
return previewResolveFallbacks.has(handler);
|
|
55
|
+
}
|
|
56
|
+
|
|
41
57
|
export function queueResolveHandler(
|
|
42
58
|
session: ToolSession,
|
|
43
59
|
options: {
|
|
@@ -48,8 +64,6 @@ export function queueResolveHandler(
|
|
|
48
64
|
},
|
|
49
65
|
): void {
|
|
50
66
|
const queue = session.getToolChoiceQueue?.();
|
|
51
|
-
const forced = session.buildToolChoice?.("resolve");
|
|
52
|
-
if (!queue || !forced || typeof forced === "string") return;
|
|
53
67
|
|
|
54
68
|
const steerReminder = (): void => {
|
|
55
69
|
session.steer?.({
|
|
@@ -63,27 +77,88 @@ export function queueResolveHandler(
|
|
|
63
77
|
});
|
|
64
78
|
};
|
|
65
79
|
|
|
66
|
-
|
|
80
|
+
// Re-evaluated on every push (including apply-error re-pushes) so a runtime
|
|
81
|
+
// incapability discovered mid-turn degrades the NEXT push instead of
|
|
82
|
+
// replaying a stale forced choice the model can never satisfy.
|
|
83
|
+
const resolveForcedChoice = (): { forced: ToolChoice | undefined; exactNamed: boolean } => {
|
|
84
|
+
const toolChoiceResult = session.buildToolChoiceResult?.("resolve");
|
|
85
|
+
if (toolChoiceResult !== undefined) {
|
|
86
|
+
return { forced: toolChoiceResult.choice, exactNamed: toolChoiceResult.exactNamed };
|
|
87
|
+
}
|
|
88
|
+
// Legacy bridge fallback: sessions that only provide buildToolChoice
|
|
89
|
+
// (older SDK embedders, test harnesses) keep the previous behavior — a
|
|
90
|
+
// named object choice is treated as exact named forcing.
|
|
91
|
+
const legacyChoice = session.buildToolChoice?.("resolve");
|
|
92
|
+
const isNamedObject = typeof legacyChoice === "object" && legacyChoice !== null;
|
|
93
|
+
return { forced: isNamedObject ? legacyChoice : undefined, exactNamed: isNamedObject };
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const clearFallback = (): void => {
|
|
97
|
+
// Identity-aware: only clear the shared standing slot when it still holds
|
|
98
|
+
// THIS preview's fallback. Plan mode (or a newer preview) may have
|
|
99
|
+
// replaced it in the meantime — leave theirs untouched.
|
|
100
|
+
if (session.peekStandingResolveHandler?.() === invoke) {
|
|
101
|
+
session.setStandingResolveHandler?.(null);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const invoke = async (input: unknown): Promise<AgentToolResult<unknown>> => {
|
|
106
|
+
const result = await runResolveInvocation(input as ResolveParams, {
|
|
107
|
+
sourceToolName: options.sourceToolName,
|
|
108
|
+
label: options.label,
|
|
109
|
+
apply: options.apply,
|
|
110
|
+
reject: options.reject,
|
|
111
|
+
onApplyError: () => {
|
|
112
|
+
// Apply threw (e.g. ast_edit overlapping replacements). Re-push the
|
|
113
|
+
// same directive so the preview remains pending and the model can
|
|
114
|
+
// `discard` or fix-and-retry on the next turn instead of being
|
|
115
|
+
// stranded with no pending action to address. The re-push goes
|
|
116
|
+
// through the exactNamed gate again — degraded models fall back
|
|
117
|
+
// to the reminder alone. The standing fallback stays installed so
|
|
118
|
+
// a voluntary resolve can still reach the pending action.
|
|
119
|
+
pushDirective();
|
|
120
|
+
steerReminder();
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
// Apply succeeded or the preview was discarded: the pending action is
|
|
124
|
+
// finished, so the voluntary-dispatch fallback must not linger.
|
|
125
|
+
clearFallback();
|
|
126
|
+
return result;
|
|
127
|
+
};
|
|
128
|
+
markPreviewResolveFallback(invoke);
|
|
129
|
+
|
|
130
|
+
// Voluntary-dispatch fallback: when forcing is unavailable (statically
|
|
131
|
+
// degraded) or later removed (runtime degradeInFlight drops the queue
|
|
132
|
+
// directive that owns the invoker), the model can still call `resolve`.
|
|
133
|
+
// ResolveTool.execute consults the queue invoker first, so the standing
|
|
134
|
+
// handler only serves degraded paths. Latest preview wins (mirroring the
|
|
135
|
+
// queue's pushOnce now:true semantics): a newer preview's fallback replaces
|
|
136
|
+
// an older preview's, but NEVER clobbers a non-preview standing handler
|
|
137
|
+
// (e.g. plan mode's approval handler).
|
|
138
|
+
const installFallback = (): void => {
|
|
139
|
+
if (!session.setStandingResolveHandler) return;
|
|
140
|
+
const existing = session.peekStandingResolveHandler?.();
|
|
141
|
+
if (existing === invoke) return;
|
|
142
|
+
if (existing !== undefined && !isPreviewResolveFallback(existing)) return;
|
|
143
|
+
session.setStandingResolveHandler(invoke);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const pushDirective = (): boolean => {
|
|
147
|
+
const { forced, exactNamed } = resolveForcedChoice();
|
|
148
|
+
if (!queue || !forced || !exactNamed) {
|
|
149
|
+
installFallback();
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
67
152
|
queue.pushOnce(forced, {
|
|
68
153
|
label: `pending-action:${options.sourceToolName}`,
|
|
69
154
|
now: true,
|
|
70
155
|
onRejected: () => "requeue",
|
|
71
|
-
onInvoked:
|
|
72
|
-
runResolveInvocation(input as ResolveParams, {
|
|
73
|
-
sourceToolName: options.sourceToolName,
|
|
74
|
-
label: options.label,
|
|
75
|
-
apply: options.apply,
|
|
76
|
-
reject: options.reject,
|
|
77
|
-
onApplyError: () => {
|
|
78
|
-
// Apply threw (e.g. ast_edit overlapping replacements). Re-push the
|
|
79
|
-
// same directive so the preview remains pending and the model can
|
|
80
|
-
// `discard` or fix-and-retry on the next turn instead of being
|
|
81
|
-
// stranded with no pending action to address.
|
|
82
|
-
pushDirective();
|
|
83
|
-
steerReminder();
|
|
84
|
-
},
|
|
85
|
-
}),
|
|
156
|
+
onInvoked: invoke,
|
|
86
157
|
});
|
|
158
|
+
// Forced directive may still be degraded mid-turn by a runtime
|
|
159
|
+
// incapability discovery; keep the fallback armed for that case.
|
|
160
|
+
installFallback();
|
|
161
|
+
return true;
|
|
87
162
|
};
|
|
88
163
|
|
|
89
164
|
pushDirective();
|
package/src/utils/edit-mode.ts
CHANGED
package/src/utils/tool-choice.ts
CHANGED
|
@@ -1,33 +1,62 @@
|
|
|
1
|
-
import type { Api, Model, ToolChoice } from "@gajae-code/ai";
|
|
1
|
+
import type { Api, Model, ResolveToolChoiceResult, ToolChoice } from "@gajae-code/ai";
|
|
2
|
+
import { resolveToolChoice } from "@gajae-code/ai";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Build a provider-aware tool choice that targets one specific tool when supported.
|
|
5
6
|
* Providers that only expose required/any forcing may still honor named choices by
|
|
6
7
|
* narrowing their request tool list before transport.
|
|
7
8
|
*/
|
|
8
|
-
export
|
|
9
|
-
|
|
9
|
+
export interface NamedToolChoiceResult {
|
|
10
|
+
choice: ToolChoice | undefined;
|
|
11
|
+
exactNamed: boolean;
|
|
12
|
+
resolved?: ResolveToolChoiceResult;
|
|
13
|
+
}
|
|
10
14
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
15
|
+
export function buildNamedToolChoiceResult(toolName: string, model?: Model<Api>): NamedToolChoiceResult {
|
|
16
|
+
if (!model) return { choice: undefined, exactNamed: false };
|
|
14
17
|
|
|
15
|
-
|
|
18
|
+
let namedChoice: ToolChoice | undefined;
|
|
19
|
+
let namedShape = false;
|
|
20
|
+
|
|
21
|
+
if (model.api === "anthropic-messages" || model.api === "bedrock-converse-stream") {
|
|
22
|
+
namedChoice = { type: "tool", name: toolName };
|
|
23
|
+
namedShape = true;
|
|
24
|
+
} else if (
|
|
16
25
|
model.api === "openai-codex-responses" ||
|
|
17
26
|
model.api === "openai-responses" ||
|
|
18
27
|
model.api === "openai-completions" ||
|
|
19
|
-
model.api === "azure-openai-responses"
|
|
28
|
+
model.api === "azure-openai-responses" ||
|
|
29
|
+
model.api === "ollama-chat"
|
|
30
|
+
) {
|
|
31
|
+
namedChoice = { type: "function", name: toolName };
|
|
32
|
+
namedShape = true;
|
|
33
|
+
} else if (
|
|
34
|
+
model.api === "google-generative-ai" ||
|
|
35
|
+
model.api === "google-gemini-cli" ||
|
|
36
|
+
model.api === "google-vertex"
|
|
20
37
|
) {
|
|
21
|
-
|
|
38
|
+
namedChoice = "required";
|
|
22
39
|
}
|
|
23
40
|
|
|
24
|
-
if (
|
|
25
|
-
return { type: "function", name: toolName };
|
|
26
|
-
}
|
|
41
|
+
if (!namedChoice) return { choice: undefined, exactNamed: false };
|
|
27
42
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
43
|
+
const resolved = resolveToolChoice(model, namedChoice);
|
|
44
|
+
const exactNamed = namedShape && resolved.resolvedLevel === "named" && resolved.targetToolName === toolName;
|
|
45
|
+
return {
|
|
46
|
+
choice: exactNamed ? resolved.resolvedChoice : undefined,
|
|
47
|
+
exactNamed,
|
|
48
|
+
resolved,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
31
51
|
|
|
32
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Legacy capability-aware wrapper. May return a lossy `"required"` when named
|
|
54
|
+
* forcing degrades (e.g. Google APIs, or compat `toolChoiceSupport: "required"`),
|
|
55
|
+
* which forces *some* tool rather than `toolName` specifically. Queue directives
|
|
56
|
+
* that need exact tool identity (resolve / todo_write / yield) MUST use
|
|
57
|
+
* `buildNamedToolChoiceResult` and gate on `exactNamed` instead.
|
|
58
|
+
*/
|
|
59
|
+
export function buildNamedToolChoice(toolName: string, model?: Model<Api>): ToolChoice | undefined {
|
|
60
|
+
const result = buildNamedToolChoiceResult(toolName, model);
|
|
61
|
+
return result.choice ?? result.resolved?.resolvedChoice;
|
|
33
62
|
}
|