@gajae-code/coding-agent 0.4.5 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/dist/types/async/job-manager.d.ts +26 -0
  3. package/dist/types/cli/args.d.ts +1 -0
  4. package/dist/types/cli/list-models.d.ts +6 -0
  5. package/dist/types/commands/gc.d.ts +26 -0
  6. package/dist/types/commands/harness.d.ts +3 -0
  7. package/dist/types/config/file-lock-gc.d.ts +5 -0
  8. package/dist/types/config/file-lock.d.ts +7 -0
  9. package/dist/types/config/model-profile-activation.d.ts +11 -2
  10. package/dist/types/config/model-profiles.d.ts +7 -0
  11. package/dist/types/config/model-registry.d.ts +3 -0
  12. package/dist/types/config/model-resolver.d.ts +2 -0
  13. package/dist/types/config/models-config-schema.d.ts +30 -0
  14. package/dist/types/config/settings-schema.d.ts +4 -3
  15. package/dist/types/coordinator/contract.d.ts +1 -1
  16. package/dist/types/defaults/gjc/extensions/grok-build/index.d.ts +1 -0
  17. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/index.d.ts +1 -0
  18. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.d.ts +25 -0
  19. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.d.ts +27 -0
  20. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/billing.d.ts +8 -0
  21. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.d.ts +5 -0
  22. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.d.ts +10 -0
  23. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/usage.d.ts +2 -0
  24. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.d.ts +2 -0
  25. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/shared/errors.d.ts +38 -0
  26. package/dist/types/defaults/gjc-grok-cli.d.ts +5 -0
  27. package/dist/types/extensibility/extensions/index.d.ts +1 -0
  28. package/dist/types/extensibility/extensions/prefix-command-bridge.d.ts +35 -0
  29. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +103 -0
  30. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +2 -0
  31. package/dist/types/gjc-runtime/deep-interview-state.d.ts +112 -0
  32. package/dist/types/gjc-runtime/gc-render.d.ts +6 -0
  33. package/dist/types/gjc-runtime/gc-runtime.d.ts +134 -0
  34. package/dist/types/gjc-runtime/ledger-event-renderer.d.ts +68 -0
  35. package/dist/types/gjc-runtime/team-gc.d.ts +7 -0
  36. package/dist/types/gjc-runtime/team-runtime.d.ts +5 -1
  37. package/dist/types/gjc-runtime/tmux-common.d.ts +14 -0
  38. package/dist/types/gjc-runtime/tmux-gc.d.ts +7 -0
  39. package/dist/types/gjc-runtime/tmux-sessions.d.ts +13 -0
  40. package/dist/types/harness-control-plane/gc-adapter.d.ts +3 -0
  41. package/dist/types/harness-control-plane/owner.d.ts +8 -1
  42. package/dist/types/harness-control-plane/receipt-spool.d.ts +19 -0
  43. package/dist/types/harness-control-plane/state-machine.d.ts +6 -1
  44. package/dist/types/harness-control-plane/storage.d.ts +20 -0
  45. package/dist/types/harness-control-plane/types.d.ts +4 -0
  46. package/dist/types/hindsight/mental-models.d.ts +5 -5
  47. package/dist/types/modes/components/hook-selector.d.ts +7 -1
  48. package/dist/types/modes/components/model-selector.d.ts +1 -12
  49. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  50. package/dist/types/modes/rpc/rpc-client.d.ts +2 -2
  51. package/dist/types/modes/rpc/rpc-mode.d.ts +16 -1
  52. package/dist/types/modes/rpc/rpc-types.d.ts +4 -1
  53. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +13 -0
  54. package/dist/types/modes/shared/agent-wire/session-registry.d.ts +25 -0
  55. package/dist/types/modes/shared/agent-wire/unattended-action-policy.d.ts +2 -0
  56. package/dist/types/sdk.d.ts +5 -0
  57. package/dist/types/session/agent-session.d.ts +3 -1
  58. package/dist/types/session/blob-store.d.ts +59 -4
  59. package/dist/types/session/session-manager.d.ts +24 -6
  60. package/dist/types/session/streaming-output.d.ts +3 -2
  61. package/dist/types/session/tool-choice-queue.d.ts +6 -0
  62. package/dist/types/skill-state/workflow-hud.d.ts +14 -0
  63. package/dist/types/task/receipt.d.ts +1 -0
  64. package/dist/types/task/types.d.ts +7 -0
  65. package/dist/types/thinking-metadata.d.ts +16 -0
  66. package/dist/types/thinking.d.ts +3 -12
  67. package/dist/types/tools/ask.d.ts +15 -1
  68. package/dist/types/tools/index.d.ts +2 -0
  69. package/dist/types/tools/resolve.d.ts +0 -10
  70. package/dist/types/tools/subagent.d.ts +6 -0
  71. package/dist/types/utils/tool-choice.d.ts +14 -1
  72. package/package.json +7 -7
  73. package/src/async/job-manager.ts +52 -0
  74. package/src/cli/args.ts +3 -0
  75. package/src/cli/auth-broker-cli.ts +1 -0
  76. package/src/cli/list-models.ts +13 -1
  77. package/src/cli.ts +9 -4
  78. package/src/commands/gc.ts +22 -0
  79. package/src/commands/harness.ts +43 -5
  80. package/src/commands/launch.ts +2 -2
  81. package/src/commands/session.ts +3 -1
  82. package/src/config/file-lock-gc.ts +181 -0
  83. package/src/config/file-lock.ts +14 -0
  84. package/src/config/model-profile-activation.ts +15 -3
  85. package/src/config/model-profiles.ts +264 -56
  86. package/src/config/model-resolver.ts +9 -6
  87. package/src/config/models-config-schema.ts +1 -0
  88. package/src/config/settings-schema.ts +6 -3
  89. package/src/coordinator/contract.ts +1 -0
  90. package/src/coordinator-mcp/server.ts +513 -26
  91. package/src/cursor.ts +16 -2
  92. package/src/defaults/gjc/agent.models.grok-cli.yml +36 -0
  93. package/src/defaults/gjc/extensions/grok-build/index.ts +1 -0
  94. package/src/defaults/gjc/extensions/grok-build/package.json +7 -0
  95. package/src/defaults/gjc/extensions/grok-cli-vendor/biome.json +39 -0
  96. package/src/defaults/gjc/extensions/grok-cli-vendor/package.json +8 -0
  97. package/src/defaults/gjc/extensions/grok-cli-vendor/src/index.ts +1 -0
  98. package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +155 -0
  99. package/src/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.ts +361 -0
  100. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/billing.ts +57 -0
  101. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +99 -0
  102. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.ts +50 -0
  103. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/usage.ts +56 -0
  104. package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.ts +36 -0
  105. package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/errors.ts +44 -0
  106. package/src/defaults/gjc/skills/deep-interview/SKILL.md +131 -113
  107. package/src/defaults/gjc/skills/deep-interview/lateral-review-panel.md +49 -0
  108. package/src/defaults/gjc/skills/team/SKILL.md +3 -2
  109. package/src/defaults/gjc/skills/ultragoal/SKILL.md +8 -2
  110. package/src/defaults/gjc-defaults.ts +7 -0
  111. package/src/defaults/gjc-grok-cli.ts +22 -0
  112. package/src/export/html/index.ts +13 -9
  113. package/src/extensibility/extensions/index.ts +1 -0
  114. package/src/extensibility/extensions/prefix-command-bridge.ts +128 -0
  115. package/src/gjc-runtime/deep-interview-recorder.ts +417 -0
  116. package/src/gjc-runtime/deep-interview-runtime.ts +18 -26
  117. package/src/gjc-runtime/deep-interview-state.ts +324 -0
  118. package/src/gjc-runtime/gc-render.ts +70 -0
  119. package/src/gjc-runtime/gc-runtime.ts +403 -0
  120. package/src/gjc-runtime/ledger-event-renderer.ts +164 -0
  121. package/src/gjc-runtime/ralplan-runtime.ts +58 -7
  122. package/src/gjc-runtime/state-renderer.ts +12 -3
  123. package/src/gjc-runtime/state-runtime.ts +46 -29
  124. package/src/gjc-runtime/team-gc.ts +49 -0
  125. package/src/gjc-runtime/team-runtime.ts +211 -8
  126. package/src/gjc-runtime/tmux-common.ts +29 -0
  127. package/src/gjc-runtime/tmux-gc.ts +176 -0
  128. package/src/gjc-runtime/tmux-sessions.ts +68 -12
  129. package/src/gjc-runtime/ultragoal-runtime.ts +517 -41
  130. package/src/gjc-runtime/workflow-manifest.generated.json +27 -1
  131. package/src/gjc-runtime/workflow-manifest.ts +16 -1
  132. package/src/harness-control-plane/gc-adapter.ts +184 -0
  133. package/src/harness-control-plane/owner.ts +89 -27
  134. package/src/harness-control-plane/receipt-spool.ts +128 -0
  135. package/src/harness-control-plane/state-machine.ts +27 -6
  136. package/src/harness-control-plane/storage.ts +93 -0
  137. package/src/harness-control-plane/types.ts +4 -0
  138. package/src/hindsight/mental-models.ts +17 -16
  139. package/src/internal-urls/docs-index.generated.ts +14 -8
  140. package/src/main.ts +7 -2
  141. package/src/modes/components/assistant-message.ts +26 -14
  142. package/src/modes/components/diff.ts +97 -0
  143. package/src/modes/components/hook-selector.ts +19 -0
  144. package/src/modes/components/model-selector.ts +370 -181
  145. package/src/modes/components/status-line/segments.ts +1 -1
  146. package/src/modes/components/tool-execution.ts +30 -13
  147. package/src/modes/controllers/command-controller.ts +25 -6
  148. package/src/modes/controllers/extension-ui-controller.ts +3 -0
  149. package/src/modes/controllers/selector-controller.ts +34 -42
  150. package/src/modes/rpc/rpc-client.ts +3 -2
  151. package/src/modes/rpc/rpc-mode.ts +187 -39
  152. package/src/modes/rpc/rpc-types.ts +5 -2
  153. package/src/modes/shared/agent-wire/command-dispatch.ts +279 -257
  154. package/src/modes/shared/agent-wire/command-validation.ts +11 -0
  155. package/src/modes/shared/agent-wire/deep-interview-gate.ts +30 -1
  156. package/src/modes/shared/agent-wire/session-registry.ts +109 -0
  157. package/src/modes/shared/agent-wire/unattended-action-policy.ts +24 -0
  158. package/src/modes/shared/agent-wire/unattended-run-controller.ts +23 -3
  159. package/src/modes/shared/agent-wire/unattended-session.ts +16 -1
  160. package/src/sdk.ts +46 -5
  161. package/src/secrets/obfuscator.ts +102 -27
  162. package/src/session/agent-session.ts +179 -25
  163. package/src/session/blob-store.ts +148 -6
  164. package/src/session/session-manager.ts +311 -60
  165. package/src/session/streaming-output.ts +185 -122
  166. package/src/session/tool-choice-queue.ts +23 -0
  167. package/src/setup/hermes/templates/operator-instructions.v1.md +7 -1
  168. package/src/skill-state/workflow-hud.ts +106 -10
  169. package/src/slash-commands/builtin-registry.ts +3 -2
  170. package/src/task/executor.ts +78 -6
  171. package/src/task/receipt.ts +5 -0
  172. package/src/task/render.ts +21 -1
  173. package/src/task/types.ts +8 -0
  174. package/src/thinking-metadata.ts +51 -0
  175. package/src/thinking.ts +26 -46
  176. package/src/tools/ask.ts +56 -1
  177. package/src/tools/bash.ts +1 -1
  178. package/src/tools/index.ts +2 -0
  179. package/src/tools/job.ts +3 -2
  180. package/src/tools/monitor.ts +36 -1
  181. package/src/tools/resolve.ts +93 -18
  182. package/src/tools/subagent-render.ts +9 -0
  183. package/src/tools/subagent.ts +26 -2
  184. package/src/utils/edit-mode.ts +1 -1
  185. package/src/utils/tool-choice.ts +45 -16
@@ -21,6 +21,8 @@ interface RalplanHudState extends WorkflowGateHudState {
21
21
  stage?: string;
22
22
  waiting?: string;
23
23
  iteration?: number;
24
+ iterationFromIndex?: number;
25
+ stages?: string;
24
26
  verdict?: string;
25
27
  latestSummary?: string;
26
28
  pendingApproval?: boolean;
@@ -100,6 +102,95 @@ export function buildDeepInterviewHudSummary(state: DeepInterviewHudState): Work
100
102
  };
101
103
  }
102
104
 
105
+ export interface DeepInterviewHudDeriveOptions {
106
+ phase?: string;
107
+ specStatus?: string;
108
+ updatedAt?: string;
109
+ }
110
+
111
+ function diIsPlainObject(value: unknown): value is Record<string, unknown> {
112
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
113
+ }
114
+
115
+ function latestScoredAmbiguity(rounds: unknown): number | undefined {
116
+ if (!Array.isArray(rounds)) return undefined;
117
+ for (let index = rounds.length - 1; index >= 0; index--) {
118
+ const round = rounds[index];
119
+ if (diIsPlainObject(round) && round.lifecycle === "scored" && typeof round.ambiguity === "number") {
120
+ return round.ambiguity;
121
+ }
122
+ }
123
+ return undefined;
124
+ }
125
+
126
+ function weakestDimensionFromTopology(
127
+ topology: Record<string, unknown>,
128
+ targetComponent: string | undefined,
129
+ ): string | undefined {
130
+ if (!Array.isArray(topology.components)) return undefined;
131
+ const components = topology.components.filter(diIsPlainObject);
132
+ const dimensionOf = (component: Record<string, unknown>): string | undefined =>
133
+ typeof component.weakest_dimension === "string" && component.weakest_dimension.trim()
134
+ ? component.weakest_dimension
135
+ : undefined;
136
+ if (targetComponent) {
137
+ const targeted = components.find(component => component.id === targetComponent && dimensionOf(component));
138
+ if (targeted) return dimensionOf(targeted);
139
+ }
140
+ const active = components.find(component => component.status !== "deferred" && dimensionOf(component));
141
+ if (active) return dimensionOf(active);
142
+ const any = components.find(component => dimensionOf(component));
143
+ return any ? dimensionOf(any) : undefined;
144
+ }
145
+
146
+ /**
147
+ * Single source of deep-interview HUD derivation. Reads a complete (normalized)
148
+ * mode-state envelope so recorder, `gjc state write`, reconcile, seed, and handoff
149
+ * all produce identical chips. Topology-aware `target`/`weakest` come from
150
+ * `state.topology`; `legacy_missing` topology omits those chips (no synthetic values).
151
+ */
152
+ export function deriveDeepInterviewHud(
153
+ payload: Record<string, unknown>,
154
+ options: DeepInterviewHudDeriveOptions = {},
155
+ ): WorkflowHudSummary {
156
+ const stateField = diIsPlainObject(payload.state) ? payload.state : {};
157
+ const isNumber = (value: unknown): value is number => typeof value === "number" && Number.isFinite(value);
158
+ const isArray = (value: unknown): value is unknown[] => Array.isArray(value);
159
+ const pick = <T>(key: string, guard: (value: unknown) => value is T): T | undefined => {
160
+ const value = stateField[key] ?? payload[key];
161
+ return guard(value) ? value : undefined;
162
+ };
163
+
164
+ const phase = options.phase ?? (typeof payload.current_phase === "string" ? payload.current_phase : undefined);
165
+ const rounds = pick("rounds", isArray);
166
+ const ambiguity = pick("current_ambiguity", isNumber) ?? latestScoredAmbiguity(rounds);
167
+ const threshold = pick("threshold", isNumber);
168
+ const rawTopology = diIsPlainObject(stateField.topology)
169
+ ? stateField.topology
170
+ : diIsPlainObject(payload.topology)
171
+ ? payload.topology
172
+ : undefined;
173
+ // `legacy_missing` topology was never confirmed: omit target/weakest even if stale fields linger.
174
+ const topology = rawTopology && rawTopology.status !== "legacy_missing" ? rawTopology : undefined;
175
+ const targetComponent =
176
+ topology && typeof topology.last_targeted_component_id === "string"
177
+ ? topology.last_targeted_component_id
178
+ : undefined;
179
+ const weakestDimension = topology ? weakestDimensionFromTopology(topology, targetComponent) : undefined;
180
+ const specStatus = options.specStatus ?? (typeof payload.spec_status === "string" ? payload.spec_status : undefined);
181
+
182
+ return buildDeepInterviewHudSummary({
183
+ phase,
184
+ ambiguity,
185
+ threshold,
186
+ roundCount: rounds?.length,
187
+ targetComponent,
188
+ weakestDimension,
189
+ specStatus,
190
+ updatedAt: options.updatedAt ?? new Date().toISOString(),
191
+ });
192
+ }
193
+
103
194
  export function buildRalplanHudSummary(state: RalplanHudState): WorkflowHudSummary {
104
195
  const verdict = state.verdict?.toUpperCase();
105
196
  const verdictSeverity =
@@ -118,7 +209,14 @@ export function buildRalplanHudSummary(state: RalplanHudState): WorkflowHudSumma
118
209
  ...gateChips(state, 6),
119
210
  chip("stage", state.stage, 10),
120
211
  chip("waiting", state.waiting, 20),
121
- chip("iter", state.iteration === undefined ? undefined : String(state.iteration), 30),
212
+ chip(
213
+ "iter",
214
+ (state.iterationFromIndex ?? state.iteration) === undefined
215
+ ? undefined
216
+ : String(state.iterationFromIndex ?? state.iteration),
217
+ 30,
218
+ ),
219
+ chip("stages", state.stages, 35),
122
220
  chip("verdict", verdict, 40, verdictSeverity),
123
221
  ]),
124
222
  ...(state.updatedAt ? { updated_at: state.updatedAt } : {}),
@@ -136,17 +234,15 @@ export function buildUltragoalHudSummary(state: UltragoalHudState): WorkflowHudS
136
234
  chip("goals", `${complete}/${total}`, 10),
137
235
  chip("current", state.currentGoal ? `${state.currentGoal.id}:${state.currentGoal.title}` : state.status, 20),
138
236
  chip("status", state.status, 30, state.status === "complete" ? "success" : undefined),
237
+ chip(
238
+ "ledger",
239
+ state.latestLedgerEvent?.event
240
+ ? [state.latestLedgerEvent.event, state.latestLedgerEvent.goalId].filter(Boolean).join(":")
241
+ : undefined,
242
+ 35,
243
+ ),
139
244
  ...gateChips(state, 40),
140
245
  ]),
141
- details: state.latestLedgerEvent
142
- ? compactChips([
143
- chip(
144
- "ledger",
145
- [state.latestLedgerEvent.event, state.latestLedgerEvent.goalId].filter(Boolean).join(":"),
146
- 100,
147
- ),
148
- ])
149
- : undefined,
150
246
  ...(state.updatedAt ? { updated_at: state.updatedAt } : {}),
151
247
  };
152
248
  }
@@ -4,6 +4,7 @@ import type { ThinkingLevel } from "@gajae-code/agent-core";
4
4
  import { type Model, modelsAreEqual } from "@gajae-code/ai";
5
5
  import { getOAuthProviders } from "@gajae-code/ai/utils/oauth";
6
6
  import { setProjectDir } from "@gajae-code/utils";
7
+ import { jobElapsedMs } from "../async";
7
8
  import {
8
9
  GJC_MODEL_ASSIGNMENT_TARGET_IDS,
9
10
  GJC_MODEL_ASSIGNMENT_TARGETS,
@@ -506,14 +507,14 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
506
507
  if (snapshot.running.length > 0) {
507
508
  lines.push("", "Running Jobs");
508
509
  for (const job of snapshot.running) {
509
- lines.push(` [${job.id}] ${job.type} (${job.status}) — ${formatDuration(now - job.startTime)}`);
510
+ lines.push(` [${job.id}] ${job.type} (${job.status}) — ${formatDuration(jobElapsedMs(job, now))}`);
510
511
  lines.push(` ${job.label}`);
511
512
  }
512
513
  }
513
514
  if (snapshot.recent.length > 0) {
514
515
  lines.push("", "Recent Jobs");
515
516
  for (const job of snapshot.recent) {
516
- lines.push(` [${job.id}] ${job.type} (${job.status}) — ${formatDuration(now - job.startTime)}`);
517
+ lines.push(` [${job.id}] ${job.type} (${job.status}) — ${formatDuration(jobElapsedMs(job, now))}`);
517
518
  lines.push(` ${job.label}`);
518
519
  }
519
520
  }
@@ -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 { buildNamedToolChoice } from "../utils/tool-choice";
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,20 +1137,39 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1099
1137
  options.parentSessionId,
1100
1138
  ),
1101
1139
  );
1102
- if (authFallbackUsed && model) {
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: modelPatterns,
1151
+ requested: modelSubstitutionWarning.requested,
1105
1152
  parentModel: options.parentActiveModelPattern,
1106
1153
  resolvedProvider: model.provider,
1107
1154
  resolvedModel: model.id,
1108
1155
  });
1109
1156
  }
1157
+ // Record which model the subagent actually runs on (and any auth fallback,
1158
+ // see #985) so the subagent panel can surface it to the user.
1159
+ if (model) {
1160
+ AsyncJobManager.instance()?.updateSubagentModel?.(options.subagentId ?? id, {
1161
+ requestedModel: modelSubstitutionWarning?.requested ?? resolvedModelString,
1162
+ effectiveModel: resolvedModelString,
1163
+ modelFellBack: authFallbackUsed === true,
1164
+ });
1165
+ }
1110
1166
  if (model?.contextWindow && model.contextWindow > 0) {
1111
1167
  progress.contextWindow = model.contextWindow;
1112
1168
  }
1113
1169
  const effectiveThinkingLevel = explicitThinkingLevel
1114
1170
  ? resolvedThinkingLevel
1115
1171
  : (thinkingLevel ?? resolvedThinkingLevel);
1172
+ effectiveThinkingLevelForWarning = effectiveThinkingLevel;
1116
1173
 
1117
1174
  const sessionManager = sessionFile
1118
1175
  ? await awaitAbortable(SessionManager.open(sessionFile))
@@ -1174,6 +1231,10 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1174
1231
  settings: subagentSettings,
1175
1232
  model,
1176
1233
  thinkingLevel: effectiveThinkingLevel,
1234
+ modelSubstitution:
1235
+ modelSubstitutionWarning?.reason === "auth_unavailable" && requestedModel
1236
+ ? { requestedModel, reason: modelSubstitutionWarning.reason }
1237
+ : undefined,
1177
1238
  toolNames,
1178
1239
  outputSchema,
1179
1240
  requireYieldTool: true,
@@ -1412,7 +1473,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1412
1473
  await awaitAbortable(session.waitForIdle());
1413
1474
  }
1414
1475
 
1415
- const reminderToolChoice = buildNamedToolChoice("yield", session.model);
1476
+ const reminderToolChoiceResult = buildNamedToolChoiceResult("yield", session.model);
1416
1477
 
1417
1478
  let retryCount = 0;
1418
1479
  while (!paused && !yieldCalled && retryCount < MAX_YIELD_RETRIES && !abortSignal.aborted) {
@@ -1433,7 +1494,9 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1433
1494
  await awaitAbortable(
1434
1495
  session.prompt(reminder, {
1435
1496
  attribution: "agent",
1436
- ...(isFinalRetry && reminderToolChoice ? { toolChoice: reminderToolChoice } : {}),
1497
+ ...(isFinalRetry && reminderToolChoiceResult.exactNamed && reminderToolChoiceResult.choice
1498
+ ? { toolChoice: reminderToolChoiceResult.choice }
1499
+ : {}),
1437
1500
  }),
1438
1501
  );
1439
1502
  await awaitAbortable(session.waitForIdle());
@@ -1466,6 +1529,14 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1466
1529
  error = undefined;
1467
1530
  }
1468
1531
  }
1532
+ if (lastAssistantModelString && resolvedModelString && lastAssistantModelString !== resolvedModelString) {
1533
+ modelSubstitutionWarning ??= {
1534
+ requested: resolvedModelString,
1535
+ effective: lastAssistantModelString,
1536
+ reason: "assistant_model_mismatch",
1537
+ };
1538
+ progress.modelSubstitutionWarning = modelSubstitutionWarning;
1539
+ }
1469
1540
  } catch (err) {
1470
1541
  exitCode = 1;
1471
1542
  if (!abortSignal.aborted) {
@@ -1642,6 +1713,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1642
1713
  contextTokens: progress.contextTokens,
1643
1714
  contextWindow: progress.contextWindow,
1644
1715
  modelOverride,
1716
+ modelSubstitutionWarning,
1645
1717
  error: exitCode !== 0 && stderr ? stderr : undefined,
1646
1718
  aborted: wasAborted,
1647
1719
  abortReason: finalAbortReason,
@@ -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,
@@ -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
+ }