@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.
Files changed (87) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/dist/types/commands/harness.d.ts +3 -0
  3. package/dist/types/config/model-profile-activation.d.ts +11 -2
  4. package/dist/types/config/model-profiles.d.ts +7 -0
  5. package/dist/types/config/model-registry.d.ts +3 -0
  6. package/dist/types/config/model-resolver.d.ts +2 -0
  7. package/dist/types/config/models-config-schema.d.ts +30 -0
  8. package/dist/types/config/settings-schema.d.ts +4 -3
  9. package/dist/types/gjc-runtime/team-runtime.d.ts +0 -1
  10. package/dist/types/gjc-runtime/tmux-common.d.ts +3 -0
  11. package/dist/types/harness-control-plane/owner.d.ts +1 -1
  12. package/dist/types/harness-control-plane/receipt-spool.d.ts +19 -0
  13. package/dist/types/harness-control-plane/state-machine.d.ts +6 -1
  14. package/dist/types/harness-control-plane/types.d.ts +4 -0
  15. package/dist/types/hindsight/mental-models.d.ts +5 -5
  16. package/dist/types/modes/components/model-selector.d.ts +1 -12
  17. package/dist/types/modes/rpc/rpc-client.d.ts +2 -2
  18. package/dist/types/modes/rpc/rpc-types.d.ts +4 -1
  19. package/dist/types/sdk.d.ts +5 -0
  20. package/dist/types/session/agent-session.d.ts +2 -0
  21. package/dist/types/session/blob-store.d.ts +20 -1
  22. package/dist/types/session/session-manager.d.ts +24 -6
  23. package/dist/types/session/streaming-output.d.ts +3 -2
  24. package/dist/types/session/tool-choice-queue.d.ts +6 -0
  25. package/dist/types/task/receipt.d.ts +1 -0
  26. package/dist/types/task/types.d.ts +7 -0
  27. package/dist/types/thinking-metadata.d.ts +16 -0
  28. package/dist/types/thinking.d.ts +3 -12
  29. package/dist/types/tools/index.d.ts +2 -0
  30. package/dist/types/tools/resolve.d.ts +0 -10
  31. package/dist/types/utils/tool-choice.d.ts +14 -1
  32. package/package.json +7 -7
  33. package/src/cli.ts +8 -4
  34. package/src/commands/harness.ts +36 -2
  35. package/src/commands/launch.ts +2 -2
  36. package/src/commands/session.ts +3 -1
  37. package/src/config/model-profile-activation.ts +15 -3
  38. package/src/config/model-profiles.ts +255 -56
  39. package/src/config/model-resolver.ts +9 -6
  40. package/src/config/models-config-schema.ts +1 -0
  41. package/src/config/settings-schema.ts +6 -3
  42. package/src/coordinator-mcp/server.ts +54 -23
  43. package/src/cursor.ts +16 -2
  44. package/src/defaults/gjc/skills/team/SKILL.md +3 -2
  45. package/src/defaults/gjc/skills/ultragoal/SKILL.md +8 -2
  46. package/src/export/html/index.ts +13 -9
  47. package/src/gjc-runtime/team-runtime.ts +33 -7
  48. package/src/gjc-runtime/tmux-common.ts +15 -0
  49. package/src/gjc-runtime/tmux-sessions.ts +19 -11
  50. package/src/gjc-runtime/ultragoal-runtime.ts +505 -41
  51. package/src/gjc-runtime/workflow-manifest.generated.json +27 -1
  52. package/src/gjc-runtime/workflow-manifest.ts +16 -1
  53. package/src/harness-control-plane/owner.ts +78 -27
  54. package/src/harness-control-plane/receipt-spool.ts +128 -0
  55. package/src/harness-control-plane/state-machine.ts +27 -6
  56. package/src/harness-control-plane/storage.ts +23 -0
  57. package/src/harness-control-plane/types.ts +4 -0
  58. package/src/hindsight/mental-models.ts +17 -16
  59. package/src/internal-urls/docs-index.generated.ts +2 -2
  60. package/src/modes/components/assistant-message.ts +26 -14
  61. package/src/modes/components/diff.ts +97 -0
  62. package/src/modes/components/model-selector.ts +353 -181
  63. package/src/modes/components/tool-execution.ts +30 -13
  64. package/src/modes/controllers/selector-controller.ts +33 -42
  65. package/src/modes/rpc/rpc-client.ts +3 -2
  66. package/src/modes/rpc/rpc-mode.ts +44 -14
  67. package/src/modes/rpc/rpc-types.ts +5 -2
  68. package/src/modes/shared/agent-wire/command-dispatch.ts +10 -5
  69. package/src/modes/shared/agent-wire/command-validation.ts +11 -0
  70. package/src/sdk.ts +29 -2
  71. package/src/secrets/obfuscator.ts +102 -27
  72. package/src/session/agent-session.ts +105 -20
  73. package/src/session/blob-store.ts +89 -3
  74. package/src/session/session-manager.ts +309 -58
  75. package/src/session/streaming-output.ts +185 -122
  76. package/src/session/tool-choice-queue.ts +23 -0
  77. package/src/task/executor.ts +69 -6
  78. package/src/task/receipt.ts +5 -0
  79. package/src/task/render.ts +21 -1
  80. package/src/task/types.ts +8 -0
  81. package/src/thinking-metadata.ts +51 -0
  82. package/src/thinking.ts +26 -46
  83. package/src/tools/bash.ts +1 -1
  84. package/src/tools/index.ts +2 -0
  85. package/src/tools/resolve.ts +93 -18
  86. package/src/utils/edit-mode.ts +1 -1
  87. package/src/utils/tool-choice.ts +45 -16
@@ -0,0 +1,16 @@
1
+ export type ThinkingLevelValue = "inherit" | "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
2
+ /**
3
+ * Metadata used to render thinking selector values in the coding-agent UI.
4
+ *
5
+ * This module is intentionally provider/native-free so schema generation can
6
+ * import settings metadata before native addons have been built in CI.
7
+ */
8
+ export interface ThinkingLevelMetadata {
9
+ value: ThinkingLevelValue;
10
+ label: string;
11
+ description: string;
12
+ }
13
+ /**
14
+ * Returns display metadata for a thinking selector.
15
+ */
16
+ export declare function getThinkingLevelMetadata(level: ThinkingLevelValue): ThinkingLevelMetadata;
@@ -1,14 +1,7 @@
1
1
  import { type ResolvedThinkingLevel, ThinkingLevel } from "@gajae-code/agent-core/thinking";
2
2
  import { type Effort } from "@gajae-code/ai/model-thinking";
3
3
  import type { Model } from "@gajae-code/ai/types";
4
- /**
5
- * Metadata used to render thinking selector values in the coding-agent UI.
6
- */
7
- export interface ThinkingLevelMetadata {
8
- value: ThinkingLevel;
9
- label: string;
10
- description: string;
11
- }
4
+ export { getThinkingLevelMetadata, type ThinkingLevelMetadata } from "./thinking-metadata";
12
5
  /**
13
6
  * Parses a provider-facing effort value.
14
7
  */
@@ -17,10 +10,6 @@ export declare function parseEffort(value: string | null | undefined): Effort |
17
10
  * Parses an agent-local thinking selector.
18
11
  */
19
12
  export declare function parseThinkingLevel(value: string | null | undefined): ThinkingLevel | undefined;
20
- /**
21
- * Returns display metadata for a thinking selector.
22
- */
23
- export declare function getThinkingLevelMetadata(level: ThinkingLevel): ThinkingLevelMetadata;
24
13
  /**
25
14
  * Converts an agent-local selector into the effort sent to providers.
26
15
  */
@@ -29,3 +18,5 @@ export declare function toReasoningEffort(level: ThinkingLevel | undefined): Eff
29
18
  * Resolves a selector against the current model while preserving explicit "off".
30
19
  */
31
20
  export declare function resolveThinkingLevelForModel(model: Model | undefined, level: ThinkingLevel | undefined): ResolvedThinkingLevel | undefined;
21
+ export declare function clampExplicitThinkingLevelForModel(model: Model | undefined, level: ThinkingLevel | undefined): ThinkingLevel | undefined;
22
+ export declare function formatClampedModelSelector(selector: string, model: Model | undefined): string;
@@ -193,6 +193,8 @@ export interface ToolSession {
193
193
  getToolChoiceQueue?(): ToolChoiceQueue;
194
194
  /** Build a model-provider-specific ToolChoice that targets the named tool, or undefined if unsupported. */
195
195
  buildToolChoice?(toolName: string): ToolChoice | undefined;
196
+ /** Build a named tool-choice decision, preserving whether exact named forcing survived capability degradation. */
197
+ buildToolChoiceResult?(toolName: string): import("../utils/tool-choice").NamedToolChoiceResult;
196
198
  /** Steer a hidden custom message into the conversation (e.g. a preview reminder). */
197
199
  steer?(message: {
198
200
  customType: string;
@@ -21,16 +21,6 @@ export interface ResolveToolDetails {
21
21
  label?: string;
22
22
  sourceResultDetails?: unknown;
23
23
  }
24
- /**
25
- * Queue a resolve-protocol handler on the tool-choice queue. Forces the next
26
- * LLM call to invoke the hidden `resolve` tool, wraps the caller's apply/reject
27
- * callbacks into an onInvoked closure that matches the resolve schema, and
28
- * steers a preview reminder so the model understands why.
29
- *
30
- * This is the canonical entry point for any tool that wants preview/apply
31
- * semantics. No session-level abstraction is needed: callers pass their
32
- * apply/reject functions directly.
33
- */
34
24
  export declare function queueResolveHandler(session: ToolSession, options: {
35
25
  label: string;
36
26
  sourceToolName: string;
@@ -1,7 +1,20 @@
1
- import type { Api, Model, ToolChoice } from "@gajae-code/ai";
1
+ import type { Api, Model, ResolveToolChoiceResult, ToolChoice } from "@gajae-code/ai";
2
2
  /**
3
3
  * Build a provider-aware tool choice that targets one specific tool when supported.
4
4
  * Providers that only expose required/any forcing may still honor named choices by
5
5
  * narrowing their request tool list before transport.
6
6
  */
7
+ export interface NamedToolChoiceResult {
8
+ choice: ToolChoice | undefined;
9
+ exactNamed: boolean;
10
+ resolved?: ResolveToolChoiceResult;
11
+ }
12
+ export declare function buildNamedToolChoiceResult(toolName: string, model?: Model<Api>): NamedToolChoiceResult;
13
+ /**
14
+ * Legacy capability-aware wrapper. May return a lossy `"required"` when named
15
+ * forcing degrades (e.g. Google APIs, or compat `toolChoiceSupport: "required"`),
16
+ * which forces *some* tool rather than `toolName` specifically. Queue directives
17
+ * that need exact tool identity (resolve / todo_write / yield) MUST use
18
+ * `buildNamedToolChoiceResult` and gate on `exactNamed` instead.
19
+ */
7
20
  export declare function buildNamedToolChoice(toolName: string, model?: Model<Api>): ToolChoice | undefined;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@gajae-code/coding-agent",
4
- "version": "0.4.5",
4
+ "version": "0.5.0",
5
5
  "description": "Gajae Code CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://gaebal-gajae.dev",
7
7
  "author": "Yeachan-Heo",
@@ -51,12 +51,12 @@
51
51
  "@agentclientprotocol/sdk": "0.21.0",
52
52
  "@babel/parser": "^7.29.3",
53
53
  "@mozilla/readability": "^0.6.0",
54
- "@gajae-code/stats": "0.4.5",
55
- "@gajae-code/agent-core": "0.4.5",
56
- "@gajae-code/ai": "0.4.5",
57
- "@gajae-code/natives": "0.4.5",
58
- "@gajae-code/tui": "0.4.5",
59
- "@gajae-code/utils": "0.4.5",
54
+ "@gajae-code/stats": "0.5.0",
55
+ "@gajae-code/agent-core": "0.5.0",
56
+ "@gajae-code/ai": "0.5.0",
57
+ "@gajae-code/natives": "0.5.0",
58
+ "@gajae-code/tui": "0.5.0",
59
+ "@gajae-code/utils": "0.5.0",
60
60
  "@puppeteer/browsers": "^2.13.0",
61
61
  "@types/turndown": "5.0.6",
62
62
  "@xterm/headless": "^6.0.0",
package/src/cli.ts CHANGED
@@ -5,11 +5,15 @@
5
5
  * lightweight CLI runner from pi-utils.
6
6
  */
7
7
  import { Args, type CliConfig, Command, type CommandEntry, Flags, run } from "@gajae-code/utils/cli";
8
- import { APP_NAME, MIN_BUN_VERSION, VERSION } from "@gajae-code/utils/dirs";
8
+ import { APP_NAME, formatBunRuntimeError, MIN_BUN_VERSION, VERSION } from "@gajae-code/utils/dirs";
9
9
 
10
10
  if (Bun.semver.order(Bun.version, MIN_BUN_VERSION) < 0) {
11
11
  process.stderr.write(
12
- `error: Bun runtime must be >= ${MIN_BUN_VERSION} (found v${Bun.version}). Please upgrade: bun upgrade\n`,
12
+ formatBunRuntimeError({
13
+ currentVersion: Bun.version,
14
+ minVersion: MIN_BUN_VERSION,
15
+ execPath: process.execPath,
16
+ }),
13
17
  );
14
18
  process.exit(1);
15
19
  }
@@ -130,8 +134,8 @@ class RootHelpCommand extends Command {
130
134
  `# Launch in a sibling git worktree\n ${APP_NAME} --worktree`,
131
135
  `# Use different model (fuzzy matching)\n ${APP_NAME} --model opus "Help me refactor this code"`,
132
136
  `# Limit model cycling to specific models\n ${APP_NAME} --models claude-sonnet,claude-haiku,gpt-4o`,
133
- `# Activate a model profile for this session\n ${APP_NAME} --mpreset codex-standard`,
134
- `# Persist a model profile as the default\n ${APP_NAME} --mpreset opencode-go-pro --default`,
137
+ `# Activate a model profile for this session\n ${APP_NAME} --mpreset codex-medium`,
138
+ `# Persist a model profile as the default\n ${APP_NAME} --mpreset opencodego --default`,
135
139
  `# Export a session file to HTML\n ${APP_NAME} --export ~/.gjc/agent/sessions/--path--/session.jsonl`,
136
140
  ];
137
141
  static strict = false;
@@ -11,16 +11,18 @@
11
11
  import { execFileSync } from "node:child_process";
12
12
  import { randomBytes } from "node:crypto";
13
13
  import { existsSync, readFileSync } from "node:fs";
14
+ import * as path from "node:path";
14
15
  import { Args, Command, Flags } from "@gajae-code/utils/cli";
15
16
  import { resolveGjcTmuxCommand, sanitizeTmuxToken } from "../gjc-runtime/tmux-common";
16
17
  import { classifyRecovery } from "../harness-control-plane/classifier";
17
18
  import { callEndpoint, EndpointUnreachableError } from "../harness-control-plane/control-endpoint";
18
19
  import { type ResolvedOwner, RuntimeOwner, resolveOwner } from "../harness-control-plane/owner";
19
20
  import { preserveDirtyWorktree } from "../harness-control-plane/preserve";
21
+ import { RECEIPT_SPOOL_DIR_ENV } from "../harness-control-plane/receipt-spool";
20
22
  import { buildReceipt, requiresVanishBeforeAction, type VanishEvidence } from "../harness-control-plane/receipts";
21
23
  import { GajaeCodeRpc } from "../harness-control-plane/rpc-adapter";
22
24
  import { classifyLeaseStatus, readLease } from "../harness-control-plane/session-lease";
23
- import { buildResponse, buildStateView } from "../harness-control-plane/state-machine";
25
+ import { buildResponse, buildStateView, submitUnavailableReason } from "../harness-control-plane/state-machine";
24
26
  import {
25
27
  canonicalWorkspacePath,
26
28
  generateSessionId,
@@ -513,6 +515,9 @@ export default class Harness extends Command {
513
515
  cursor: Flags.string({ description: "Event cursor for events --follow (exclusive)", default: "0" }),
514
516
  follow: Flags.boolean({ description: "Tail the owner-written event log", default: false }),
515
517
  json: Flags.boolean({ char: "j", description: "Emit machine-readable JSON", default: true }),
518
+ "receipt-spool-dir": Flags.string({
519
+ description: "Append persisted ReceiptEnvelope records to spool.jsonl under this directory",
520
+ }),
516
521
  };
517
522
 
518
523
  static examples = [
@@ -527,6 +532,11 @@ export default class Harness extends Command {
527
532
  const verb = String(args.verb);
528
533
  let root = resolveHarnessRoot();
529
534
  try {
535
+ const receiptSpoolDir = flags["receipt-spool-dir"];
536
+ if (receiptSpoolDir !== undefined) {
537
+ if (!receiptSpoolDir.trim()) throw new Error("receipt_spool_dir_empty");
538
+ process.env[RECEIPT_SPOOL_DIR_ENV] = path.resolve(receiptSpoolDir.trim());
539
+ }
530
540
  const input = parseInput(flags.input);
531
541
  const promptFile = flags["prompt-file"];
532
542
  if (promptFile !== undefined) {
@@ -676,6 +686,9 @@ export default class Harness extends Command {
676
686
  }
677
687
  const sessionName = deterministicHarnessTmuxSessionName(sessionId);
678
688
  const envAssignments = [`GJC_HARNESS_STATE_ROOT=${shellQuote(root)}`];
689
+ if (process.env[RECEIPT_SPOOL_DIR_ENV]) {
690
+ envAssignments.push(`${RECEIPT_SPOOL_DIR_ENV}=${shellQuote(process.env[RECEIPT_SPOOL_DIR_ENV])}`);
691
+ }
679
692
  if (process.env.GJC_HARNESS_RPC_COMMAND) {
680
693
  envAssignments.push(`GJC_HARNESS_RPC_COMMAND=${shellQuote(process.env.GJC_HARNESS_RPC_COMMAND)}`);
681
694
  }
@@ -715,6 +728,9 @@ export default class Harness extends Command {
715
728
  env: {
716
729
  ...process.env,
717
730
  GJC_HARNESS_STATE_ROOT: root,
731
+ ...(process.env[RECEIPT_SPOOL_DIR_ENV]
732
+ ? { [RECEIPT_SPOOL_DIR_ENV]: process.env[RECEIPT_SPOOL_DIR_ENV] }
733
+ : {}),
718
734
  ...(process.env.GJC_HARNESS_TEST_NODE_MODULES
719
735
  ? { GJC_HARNESS_TEST_NODE_MODULES: process.env.GJC_HARNESS_TEST_NODE_MODULES }
720
736
  : {}),
@@ -878,7 +894,9 @@ export default class Harness extends Command {
878
894
  ): Promise<boolean> {
879
895
  const owner = await resolveOwner(root, sessionId);
880
896
  if (!owner.live || !owner.socketPath) return false;
897
+ const priorSpoolDir = input[RECEIPT_SPOOL_DIR_ENV];
881
898
  try {
899
+ if (process.env[RECEIPT_SPOOL_DIR_ENV]) input[RECEIPT_SPOOL_DIR_ENV] = process.env[RECEIPT_SPOOL_DIR_ENV];
882
900
  const res = (await callEndpoint(owner.socketPath, { verb, input })) as { ok?: boolean };
883
901
  writeJson(res);
884
902
  if (res?.ok === false) process.exitCode = 1;
@@ -886,6 +904,9 @@ export default class Harness extends Command {
886
904
  } catch (error) {
887
905
  if (error instanceof EndpointUnreachableError) return false;
888
906
  throw error;
907
+ } finally {
908
+ if (priorSpoolDir === undefined) delete input[RECEIPT_SPOOL_DIR_ENV];
909
+ else input[RECEIPT_SPOOL_DIR_ENV] = priorSpoolDir;
889
910
  }
890
911
  }
891
912
 
@@ -978,8 +999,21 @@ export default class Harness extends Command {
978
999
 
979
1000
  async #submit(root: string, input: Record<string, unknown>, flagSession: string | undefined): Promise<void> {
980
1001
  const sessionId = requireSessionId(input, flagSession);
981
- if (await this.#tryOwnerRoute(root, sessionId, "submit", { ...input, sessionId })) return;
982
1002
  let state = await loadState(root, sessionId);
1003
+ const noOwnerGate = submitUnavailableReason(state.lifecycle, false);
1004
+ if (!noOwnerGate || noOwnerGate === "owner-not-live") {
1005
+ if (await this.#tryOwnerRoute(root, sessionId, "submit", { ...input, sessionId })) return;
1006
+ state = await loadState(root, sessionId);
1007
+ }
1008
+ const blockedByOwnerLiveness = state.blockers.some(
1009
+ blocker => isOwnerLivenessBlocker(blocker) || blocker === OWNER_STARTUP_BLOCKER,
1010
+ );
1011
+ const lifecycleGate = submitUnavailableReason(state.lifecycle, false);
1012
+ if (lifecycleGate && lifecycleGate !== "owner-not-live" && !blockedByOwnerLiveness) {
1013
+ writeJson(buildResponse(state, false, { accepted: false, submitted: false, reason: lifecycleGate }, false));
1014
+ process.exitCode = 1;
1015
+ return;
1016
+ }
983
1017
  // No live owner: submission is blocked (never echoed-as-accepted). Surface owner exit
984
1018
  // evidence + explicit recovery guidance so the caller is not left with a bare gate.
985
1019
  const ownerExit = await buildOwnerExitEvidence(root, state);
@@ -142,8 +142,8 @@ export default class Index extends Command {
142
142
  `# Launch in a sibling git worktree\n ${APP_NAME} --worktree`,
143
143
  `# Use different model (fuzzy matching)\n ${APP_NAME} --model opus "Help me refactor this code"`,
144
144
  `# Limit model cycling to specific models\n ${APP_NAME} --models claude-sonnet,claude-haiku,gpt-4o`,
145
- `# Activate a model profile for this session\n ${APP_NAME} --mpreset codex-standard`,
146
- `# Persist a model profile as the default\n ${APP_NAME} --mpreset opencode-go-pro --default`,
145
+ `# Activate a model profile for this session\n ${APP_NAME} --mpreset codex-medium`,
146
+ `# Persist a model profile as the default\n ${APP_NAME} --mpreset opencodego --default`,
147
147
  `# Export a session file to HTML\n ${APP_NAME} --export ~/.gjc/agent/sessions/--path--/session.jsonl`,
148
148
  ];
149
149
 
@@ -18,7 +18,9 @@ function writeText(lines: string[]): void {
18
18
  function writeJsonFailure(error: unknown): void {
19
19
  const message = error instanceof Error ? error.message : String(error);
20
20
  const [reason = "session_error"] = message.split(":");
21
- writeJson({ ok: false, reason });
21
+ const hintIndex = message.indexOf(" — ");
22
+ const detail = hintIndex >= 0 ? message.slice(hintIndex + " — ".length).trim() : "";
23
+ writeJson(detail ? { ok: false, reason, detail } : { ok: false, reason });
22
24
  }
23
25
 
24
26
  interface SessionJsonDto {
@@ -1,6 +1,7 @@
1
1
  import type { ThinkingLevel } from "@gajae-code/agent-core";
2
2
  import type { Api, Model } from "@gajae-code/ai";
3
3
  import type { AgentSession } from "../session/agent-session";
4
+ import { formatClampedModelSelector } from "../thinking";
4
5
  import {
5
6
  aggregateModelProfileRequiredProviders,
6
7
  formatAvailableProfileNames,
@@ -10,8 +11,14 @@ import { type GjcModelAssignmentTargetId, isAuthenticated, type ModelRegistry }
10
11
  import { resolveModelRoleValue } from "./model-resolver";
11
12
  import type { Settings } from "./settings";
12
13
 
14
+ type ModelProfileActivationSession = Pick<AgentSession, "model" | "thinkingLevel" | "sessionId"> & {
15
+ setModelTemporary?: AgentSession["setModelTemporary"];
16
+ setActiveModelProfile?: (name: string | undefined) => void;
17
+ getActiveModelProfile?: () => string | undefined;
18
+ };
19
+
13
20
  export interface PrepareModelProfileActivationOptions {
14
- session: Pick<AgentSession, "model" | "thinkingLevel" | "sessionId">;
21
+ session: ModelProfileActivationSession;
15
22
  modelRegistry: Pick<
16
23
  ModelRegistry,
17
24
  | "getModelProfile"
@@ -29,7 +36,7 @@ export interface PrepareModelProfileActivationOptions {
29
36
 
30
37
  export interface PreparedModelProfileActivation {
31
38
  profileName: string;
32
- session: Pick<AgentSession, "model" | "thinkingLevel" | "sessionId" | "setModelTemporary">;
39
+ session: ModelProfileActivationSession & { setModelTemporary: AgentSession["setModelTemporary"] };
33
40
  settings: Pick<Settings, "get" | "override" | "set" | "flush">;
34
41
  previousModel: Model<Api> | undefined;
35
42
  previousThinkingLevel: ThinkingLevel | undefined;
@@ -37,6 +44,7 @@ export interface PreparedModelProfileActivation {
37
44
  defaultModel: Model<Api> | undefined;
38
45
  defaultThinkingLevel: ThinkingLevel | undefined;
39
46
  agentModelOverrides: Record<string, string>;
47
+ previousActiveModelProfile: string | undefined;
40
48
  }
41
49
 
42
50
  export function formatModelProfileCredentialError(profileName: string, providers: readonly string[]): string {
@@ -89,7 +97,7 @@ export async function prepareModelProfileActivation(
89
97
  if (!resolved.model) {
90
98
  throw new Error(`Model profile "${options.profileName}" ${role} selector did not resolve: ${selector}`);
91
99
  }
92
- agentModelOverrides[role] = selector;
100
+ agentModelOverrides[role] = formatClampedModelSelector(selector, resolved.model);
93
101
  }
94
102
 
95
103
  return {
@@ -102,6 +110,7 @@ export async function prepareModelProfileActivation(
102
110
  defaultModel: resolvedDefault?.model,
103
111
  defaultThinkingLevel: resolvedDefault?.thinkingLevel,
104
112
  agentModelOverrides,
113
+ previousActiveModelProfile: options.session.getActiveModelProfile?.(),
105
114
  };
106
115
  }
107
116
 
@@ -113,6 +122,7 @@ export async function applyPreparedModelProfileActivation(
113
122
  const previousThinkingLevel = prepared.previousThinkingLevel;
114
123
  const previousAgentModelOverrides = prepared.previousAgentModelOverrides;
115
124
  const previousPersistedDefault = prepared.settings.get("modelProfile.default");
125
+ const previousActiveModelProfile = prepared.previousActiveModelProfile;
116
126
  let modelChanged = false;
117
127
  let overridesChanged = false;
118
128
  let defaultChanged = false;
@@ -134,6 +144,7 @@ export async function applyPreparedModelProfileActivation(
134
144
  defaultChanged = true;
135
145
  await prepared.settings.flush();
136
146
  }
147
+ prepared.session.setActiveModelProfile?.(prepared.profileName);
137
148
  } catch (error) {
138
149
  if (defaultChanged) {
139
150
  prepared.settings.set("modelProfile.default", previousPersistedDefault);
@@ -141,6 +152,7 @@ export async function applyPreparedModelProfileActivation(
141
152
  if (overridesChanged) {
142
153
  prepared.settings.override("task.agentModelOverrides", previousAgentModelOverrides);
143
154
  }
155
+ prepared.session.setActiveModelProfile?.(previousActiveModelProfile);
144
156
  if (modelChanged && previousModel) {
145
157
  await prepared.session.setModelTemporary(previousModel, previousThinkingLevel);
146
158
  }