@gajae-code/coding-agent 0.9.0 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/dist/types/cli/args.d.ts +1 -0
  3. package/dist/types/cli/notify-cli.d.ts +5 -0
  4. package/dist/types/cli.d.ts +3 -0
  5. package/dist/types/commands/launch.d.ts +3 -0
  6. package/dist/types/config/model-profile-activation.d.ts +7 -7
  7. package/dist/types/config/model-registry.d.ts +7 -3
  8. package/dist/types/config/model-resolver.d.ts +2 -2
  9. package/dist/types/config/settings-schema.d.ts +15 -1
  10. package/dist/types/config/settings.d.ts +7 -0
  11. package/dist/types/extensibility/runtime-skill-discovery.d.ts +20 -0
  12. package/dist/types/gjc-runtime/team-runtime.d.ts +9 -0
  13. package/dist/types/gjc-runtime/tmux-common.d.ts +1 -0
  14. package/dist/types/modes/components/settings-selector.d.ts +2 -2
  15. package/dist/types/modes/components/thinking-selector.d.ts +7 -2
  16. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  17. package/dist/types/modes/interactive-mode.d.ts +2 -0
  18. package/dist/types/modes/rpc/rpc-mode.d.ts +1 -3
  19. package/dist/types/modes/rpc/rpc-socket-security.d.ts +3 -0
  20. package/dist/types/modes/types.d.ts +3 -0
  21. package/dist/types/modes/utils/injected-user-submission.d.ts +52 -0
  22. package/dist/types/notifications/config-commands.d.ts +38 -0
  23. package/dist/types/notifications/config.d.ts +16 -0
  24. package/dist/types/notifications/index.d.ts +14 -1
  25. package/dist/types/notifications/lifecycle-commands.d.ts +1 -0
  26. package/dist/types/notifications/lifecycle-control-runtime.d.ts +2 -1
  27. package/dist/types/notifications/reply-sent-store.d.ts +53 -0
  28. package/dist/types/notifications/rich-draft.d.ts +68 -0
  29. package/dist/types/notifications/rich-render.d.ts +90 -0
  30. package/dist/types/notifications/telegram-daemon-cli.d.ts +3 -3
  31. package/dist/types/notifications/telegram-daemon.d.ts +22 -0
  32. package/dist/types/notifications/telegram-reference.d.ts +7 -0
  33. package/dist/types/notifications/threaded-render.d.ts +10 -0
  34. package/dist/types/notifications/topic-registry.d.ts +4 -5
  35. package/dist/types/runtime-credential-selector.d.ts +7 -0
  36. package/dist/types/sdk.d.ts +7 -1
  37. package/dist/types/session/agent-session.d.ts +6 -3
  38. package/dist/types/tools/index.d.ts +1 -0
  39. package/dist/types/tools/skill-discovery.d.ts +40 -0
  40. package/dist/types/utils/pasted-image-path.d.ts +30 -0
  41. package/package.json +8 -11
  42. package/src/cli/args.ts +8 -0
  43. package/src/cli/notify-cli.ts +38 -10
  44. package/src/cli.ts +5 -0
  45. package/src/commands/launch.ts +5 -0
  46. package/src/commands/notify.ts +2 -2
  47. package/src/config/keybindings.ts +1 -1
  48. package/src/config/model-profile-activation.ts +23 -7
  49. package/src/config/model-registry.ts +21 -4
  50. package/src/config/model-resolver.ts +5 -16
  51. package/src/config/settings-schema.ts +11 -2
  52. package/src/config/settings.ts +24 -1
  53. package/src/extensibility/runtime-skill-discovery.ts +229 -0
  54. package/src/gjc-runtime/launch-tmux.ts +78 -12
  55. package/src/gjc-runtime/psmux-detect.ts +17 -2
  56. package/src/gjc-runtime/team-runtime.ts +46 -7
  57. package/src/gjc-runtime/tmux-common.ts +1 -0
  58. package/src/gjc-runtime/ultragoal-runtime.ts +10 -1
  59. package/src/internal-urls/docs-index.generated.ts +6 -6
  60. package/src/lsp/client.ts +1 -0
  61. package/src/main.ts +26 -3
  62. package/src/modes/components/footer.ts +7 -2
  63. package/src/modes/components/settings-selector.ts +3 -3
  64. package/src/modes/components/thinking-selector.ts +83 -20
  65. package/src/modes/controllers/event-controller.ts +11 -2
  66. package/src/modes/controllers/extension-ui-controller.ts +8 -1
  67. package/src/modes/controllers/input-controller.ts +62 -26
  68. package/src/modes/controllers/selector-controller.ts +43 -0
  69. package/src/modes/interactive-mode.ts +5 -0
  70. package/src/modes/rpc/rpc-mode.ts +3 -9
  71. package/src/modes/rpc/rpc-socket-security.ts +13 -1
  72. package/src/modes/types.ts +3 -0
  73. package/src/modes/utils/injected-user-submission.ts +94 -0
  74. package/src/modes/utils/ui-helpers.ts +48 -15
  75. package/src/notifications/config-commands.ts +90 -0
  76. package/src/notifications/config.ts +27 -0
  77. package/src/notifications/index.ts +184 -14
  78. package/src/notifications/lifecycle-commands.ts +37 -16
  79. package/src/notifications/lifecycle-control-runtime.ts +8 -3
  80. package/src/notifications/reply-sent-store.ts +134 -0
  81. package/src/notifications/rich-draft.ts +107 -0
  82. package/src/notifications/rich-render.ts +143 -0
  83. package/src/notifications/telegram-daemon-cli.ts +60 -5
  84. package/src/notifications/telegram-daemon.ts +431 -91
  85. package/src/notifications/telegram-reference.ts +17 -0
  86. package/src/notifications/threaded-render.ts +42 -1
  87. package/src/notifications/topic-registry.ts +9 -8
  88. package/src/prompts/tools/search-tool-bm25.md +5 -0
  89. package/src/prompts/tools/skill-discovery.md +13 -0
  90. package/src/runtime-credential-selector.ts +35 -0
  91. package/src/sdk.ts +104 -13
  92. package/src/session/agent-session.ts +48 -22
  93. package/src/slash-commands/builtin-registry.ts +78 -1
  94. package/src/system-prompt.ts +4 -6
  95. package/src/tools/index.ts +4 -0
  96. package/src/tools/skill-discovery.ts +73 -0
  97. package/src/tools/skill.ts +15 -2
  98. package/src/utils/pasted-image-path.ts +170 -0
@@ -143,6 +143,23 @@ export function buildActionMessage(action: {
143
143
  return { text: body, inline_keyboard };
144
144
  }
145
145
 
146
+ /** Render an `action_needed` body as raw markdown (rich-message source; the HTML fallback stays on buildActionMessage). */
147
+ export function buildActionMarkdown(action: {
148
+ kind: "ask" | "idle";
149
+ question?: string;
150
+ options?: string[];
151
+ summary?: string;
152
+ }): string {
153
+ if (action.kind === "idle") {
154
+ return action.summary ? `🟢 Agent idle\n${action.summary}` : "🟢 Agent idle";
155
+ }
156
+ const heading = `❓ **${action.question ?? "Question"}**`;
157
+ const options = action.options ?? [];
158
+ if (options.length === 0) return `${heading}\n\n(reply with text)`;
159
+ const list = options.map((label, i) => `${i + 1}. ${label.replace(/^\s*\d+[.)]\s+/, "")}`).join("\n");
160
+ return `${heading}\n\n${list}`;
161
+ }
162
+
146
163
  /** Send Telegram HTML text chunks sequentially so long messages preserve order. */
147
164
  export async function sendTelegramHtmlChunks(
148
165
  send: TelegramSend,
@@ -38,6 +38,13 @@ export interface ThreadedSend {
38
38
  * message. Set for streamed turn frames so live + finalized share one message.
39
39
  */
40
40
  editable?: boolean;
41
+ /** Rich message class metadata. Only finalized final-answer sends are ever
42
+ * rich-promoted; the daemon gate (`shouldPromoteRich`) requires exactly this. */
43
+ richClass?: "final";
44
+ /** Rich final-answer markdown (raw). Delivery marker derived ONLY from a frame's `finalAnswer` bit; never inferred from `phase`. */
45
+ richMarkdown?: string;
46
+ /** Live-turn raw markdown for opt-in draft streaming (set ONLY on non-finalized turn frames; never triggers rich-final promotion, which requires `lane === "finalized"`). */
47
+ richDraftMarkdown?: string;
41
48
  }
42
49
 
43
50
  interface ThreadedFrame {
@@ -58,6 +65,7 @@ interface ThreadedFrame {
58
65
  cwd?: unknown;
59
66
  // turn_stream
60
67
  phase?: unknown;
68
+ finalAnswer?: boolean;
61
69
  text?: unknown;
62
70
  messageRef?: unknown;
63
71
  // image_attachment / file_attachment
@@ -69,12 +77,19 @@ interface ThreadedFrame {
69
77
  // config_update
70
78
  verbosity?: unknown;
71
79
  redact?: unknown;
80
+ // control_command_result
81
+ status?: unknown;
82
+ message?: unknown;
72
83
  }
73
84
 
74
85
  function str(v: unknown): string | undefined {
75
86
  return typeof v === "string" && v.length > 0 ? v : undefined;
76
87
  }
77
88
 
89
+ function isDotOnlyText(value: string): boolean {
90
+ return /^[.\s]+$/.test(value);
91
+ }
92
+
78
93
  /** Format the one-time identity header as pinned bullets. */
79
94
  export function formatIdentityHeader(frame: {
80
95
  repo?: unknown;
@@ -139,6 +154,7 @@ export function renderThreadedFrame(frame: ThreadedFrame): ThreadedSend | undefi
139
154
  case "turn_stream": {
140
155
  const raw = str(frame.text);
141
156
  if (!raw) return undefined;
157
+ if (frame.phase === "finalized" && isDotOnlyText(raw)) return undefined;
142
158
  const text = markdownToTelegramHtml(raw);
143
159
  const finalized = frame.phase === "finalized";
144
160
  // A per-turn ref ties the streamed live edits and the finalized text to
@@ -156,6 +172,16 @@ export function renderThreadedFrame(frame: ThreadedFrame): ThreadedSend | undefi
156
172
  text,
157
173
  coalesceKey,
158
174
  editable: coalesceKey !== undefined,
175
+ // Rich-final markers are set ONLY on a non-editable finalized final
176
+ // (no messageRef / coalesceKey). A streamed (editable) final owns a live
177
+ // message edited in place, so it is never rich-promoted and carries no
178
+ // rich marker to leak into split continuations.
179
+ richClass: frame.finalAnswer === true && coalesceKey === undefined ? "final" : undefined,
180
+ richMarkdown: frame.finalAnswer === true && coalesceKey === undefined ? raw : undefined,
181
+ // Live-only draft marker: carries the RAW markdown on non-finalized turn
182
+ // frames so the opt-in draft gate has the source. It never arms rich-final
183
+ // promotion (shouldPromoteRich requires lane === "finalized").
184
+ richDraftMarkdown: finalized ? undefined : raw,
159
185
  };
160
186
  }
161
187
  case "image_attachment": {
@@ -188,9 +214,24 @@ export function renderThreadedFrame(frame: ThreadedFrame): ThreadedSend | undefi
188
214
  const redact = typeof frame.redact === "boolean" ? `redact ${frame.redact ? "on" : "off"}` : undefined;
189
215
  const parts = [verbosity ? `verbosity ${verbosity}` : undefined, redact].filter(Boolean);
190
216
  return parts.length
191
- ? { method: "sendMessage", lane: "idle", text: finalizeTelegramHtml(`⚙ ${escapeHtml(parts.join(", "))}`) }
217
+ ? {
218
+ method: "sendMessage",
219
+ lane: "idle",
220
+ text: finalizeTelegramHtml(`⚙ ${escapeHtml(parts.join(", "))}`),
221
+ }
192
222
  : undefined;
193
223
  }
224
+ case "control_command_result": {
225
+ const message = str(frame.message);
226
+ if (!message) return undefined;
227
+ const status = str(frame.status);
228
+ const prefix = status === "ok" ? "✅" : status === "unavailable" ? "⚠️" : "❌";
229
+ return {
230
+ method: "sendMessage",
231
+ lane: "idle",
232
+ text: finalizeTelegramHtml(`${prefix} ${escapeHtml(truncate(message, 1200))}`),
233
+ };
234
+ }
194
235
  default:
195
236
  return undefined;
196
237
  }
@@ -120,15 +120,16 @@ export class TopicRegistry {
120
120
  return record ? !record.identitySent : true;
121
121
  }
122
122
 
123
- /**
124
- * Record the topic's applied title. Returns `true` when it changed (so the
125
- * caller should `editForumTopic`), `false` when already current or unknown.
126
- */
127
- applyName(sessionId: string, name: string): boolean {
123
+ /** Whether a known session topic's applied title differs from `name`. */
124
+ needsRename(sessionId: string, name: string): boolean {
128
125
  const record = this.topics.get(sessionId);
129
- if (!record || record.name === name) return false;
130
- record.name = name;
131
- return true;
126
+ return record !== undefined && record.name !== name;
127
+ }
128
+
129
+ /** Commit a successfully-applied Telegram topic title. */
130
+ markNameApplied(sessionId: string, name: string): void {
131
+ const record = this.topics.get(sessionId);
132
+ if (record) record.name = name;
132
133
  }
133
134
 
134
135
  /** Remove a session topic record after Telegram deletes the topic. */
@@ -21,3 +21,8 @@ Returns JSON with:
21
21
  - `activated_tools` — tools activated by this search call
22
22
  - `match_count` — number of ranked matches returned by the search
23
23
  - `total_tools`
24
+
25
+ Match details include:
26
+ - `server_name` — MCP server name when the activated result is an MCP tool
27
+ - `mcp_tool_name` — original MCP tool name when applicable
28
+ - `schema_keys` — searchable input property names
@@ -0,0 +1,13 @@
1
+ Discover project and user runtime skills without loading full skill content.
2
+
3
+ <instruction>
4
+ - Searches only custom runtime skill locations: project `.gjc/skills` and user `~/.gjc/skills`.
5
+ - Built-in, bundled, and internal workflow skills are intentionally excluded.
6
+ - Returns thin metadata only: name, description, source scope, path, and use conditions when present.
7
+ - To load a selected skill's full `SKILL.md`, invoke it through the existing `skill` tool with the exact `name` returned here.
8
+ </instruction>
9
+
10
+ Input:
11
+ - `query` (optional): words to match against skill name, description, source, or use conditions.
12
+ - `source` (optional): `all`, `project`, or `user`.
13
+ - `limit` (optional): maximum results, 1-50.
@@ -0,0 +1,35 @@
1
+ import type { AuthCredentialSelector, AuthCredentialSelectorKind } from "@gajae-code/ai";
2
+
3
+ export interface CliCredentialSelector {
4
+ provider?: string;
5
+ selector: AuthCredentialSelector;
6
+ raw: string;
7
+ }
8
+
9
+ const SELECTOR_KINDS = new Set<AuthCredentialSelectorKind>(["id", "email", "account", "project"]);
10
+
11
+ function parseSelectorBody(body: string): AuthCredentialSelector | undefined {
12
+ const separator = body.indexOf(":");
13
+ if (separator === -1) {
14
+ if (body.includes("@")) return { kind: "email", value: body };
15
+ return undefined;
16
+ }
17
+ const rawKind = body.slice(0, separator);
18
+ const value = body.slice(separator + 1).trim();
19
+ if (!SELECTOR_KINDS.has(rawKind as AuthCredentialSelectorKind) || value.length === 0) return undefined;
20
+ return { kind: rawKind as AuthCredentialSelectorKind, value };
21
+ }
22
+
23
+ export function parseCliCredentialSelector(raw: string): CliCredentialSelector {
24
+ const trimmed = raw.trim();
25
+ const slash = trimmed.indexOf("/");
26
+ const provider = slash === -1 ? undefined : trimmed.slice(0, slash).trim();
27
+ const body = slash === -1 ? trimmed : trimmed.slice(slash + 1).trim();
28
+ const selector = parseSelectorBody(body);
29
+ if (!selector || provider === "") {
30
+ throw new Error(
31
+ `Invalid --credential selector "${raw}". Use email:name@example.com, id:123, account:<id>, project:<id>, or provider/email:name@example.com.`,
32
+ );
33
+ }
34
+ return { ...(provider ? { provider } : {}), selector, raw };
35
+ }
package/src/sdk.ts CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  type ThinkingLevel,
10
10
  } from "@gajae-code/agent-core";
11
11
  import {
12
+ type AuthCredentialSelector,
12
13
  type CredentialDisabledEvent,
13
14
  type Message,
14
15
  type Model,
@@ -39,7 +40,7 @@ import {
39
40
  import { type AsyncJob, AsyncJobManager, isBackgroundJobSupportEnabled, jobElapsedMs } from "./async";
40
41
  import { loadCapability } from "./capability";
41
42
  import { type Rule, ruleCapability, setActiveRules } from "./capability/rule";
42
- import { ModelRegistry } from "./config/model-registry";
43
+ import { kNoAuth, ModelRegistry } from "./config/model-registry";
43
44
  import {
44
45
  formatModelString,
45
46
  parseModelPattern,
@@ -121,7 +122,12 @@ import {
121
122
  } from "./system-prompt";
122
123
  import { AgentOutputManager } from "./task/output-manager";
123
124
  import { parseThinkingLevel, resolveThinkingLevelForModel, toReasoningEffort } from "./thinking";
124
- import { collectDiscoverableTools, type DiscoverableTool } from "./tool-discovery/tool-index";
125
+ import {
126
+ collectDiscoverableTools,
127
+ type DiscoverableTool,
128
+ isMCPBridgeTool,
129
+ isMCPToolName,
130
+ } from "./tool-discovery/tool-index";
125
131
  import {
126
132
  applyConfiguredSearchTimeout,
127
133
  BashTool,
@@ -267,6 +273,8 @@ export interface CreateAgentSessionOptions {
267
273
  /** Optional provider-facing session identifier for prompt caches and sticky auth selection.
268
274
  * Keeps persisted session files isolated while reusing provider-side caches. */
269
275
  providerSessionId?: string;
276
+ /** Runtime credential selector for multi-account auth pools. */
277
+ credentialSelector?: { provider?: string; selector: AuthCredentialSelector; raw: string };
270
278
 
271
279
  /** Custom tools to register (in addition to built-in tools). Accepts both CustomTool and ToolDefinition. */
272
280
  customTools?: (CustomTool | ToolDefinition)[];
@@ -898,10 +906,22 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
898
906
  startupCredentialDisabledEvents.push(event);
899
907
  }
900
908
  });
909
+ let runtimeCredentialSelectorInstalled = false;
910
+ const installRuntimeCredentialSelector = (provider: string): void => {
911
+ if (!options.credentialSelector || runtimeCredentialSelectorInstalled) return;
912
+ authStorage.setRuntimeCredentialSelector(provider, options.credentialSelector.selector);
913
+ runtimeCredentialSelectorInstalled = true;
914
+ };
915
+ const earlyCredentialSelectorProvider = options.credentialSelector?.provider ?? options.model?.provider;
916
+ if (earlyCredentialSelectorProvider) {
917
+ installRuntimeCredentialSelector(earlyCredentialSelectorProvider);
918
+ }
901
919
  const settings = options.settings ?? (await logger.time("settings", Settings.init, { cwd, agentDir }));
902
920
  modelRegistry.applyConfiguredModelBindings(settings);
903
921
  logger.time("initializeWithSettings", initializeWithSettings, settings);
904
- if (!options.modelRegistry) {
922
+ const canRefreshModelsBeforeCredentialSelector =
923
+ !options.credentialSelector || runtimeCredentialSelectorInstalled || options.modelRegistry !== undefined;
924
+ if (!options.modelRegistry && canRefreshModelsBeforeCredentialSelector) {
905
925
  modelRegistry.refreshInBackground();
906
926
  }
907
927
  // Kick off workspace tree discovery early. The native workspace scan returns
@@ -967,7 +987,29 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
967
987
  return cached;
968
988
  }
969
989
 
970
- const hasKey = !!(await modelRegistry.getApiKey(candidate, providerSessionId));
990
+ const credentialSelector =
991
+ options.credentialSelector && !runtimeCredentialSelectorInstalled
992
+ ? options.credentialSelector.provider === undefined ||
993
+ options.credentialSelector.provider === candidate.provider
994
+ ? options.credentialSelector.selector
995
+ : undefined
996
+ : undefined;
997
+ if (options.credentialSelector?.provider && options.credentialSelector.provider !== candidate.provider) {
998
+ modelApiKeyAvailability.set(availabilityKey, false);
999
+ return false;
1000
+ }
1001
+ const key = await modelRegistry.getApiKey(candidate, providerSessionId, { credentialSelector }).catch(error => {
1002
+ if (credentialSelector) {
1003
+ logger.debug("Credential selector did not match model availability candidate", {
1004
+ provider: candidate.provider,
1005
+ model: candidate.id,
1006
+ error: error instanceof Error ? error.message : String(error),
1007
+ });
1008
+ return undefined;
1009
+ }
1010
+ throw error;
1011
+ });
1012
+ const hasKey = Boolean(key) && (!credentialSelector || key !== kNoAuth);
971
1013
  modelApiKeyAvailability.set(availabilityKey, hasKey);
972
1014
  return hasKey;
973
1015
  };
@@ -1042,6 +1084,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1042
1084
  const taskDepth = options.taskDepth ?? 0;
1043
1085
 
1044
1086
  let thinkingLevel = options.thinkingLevel;
1087
+ const hasExplicitDefaultThinkingLevel = settings.has("defaultThinkingLevel");
1088
+ let thinkingLevelFromSchemaDefault = false;
1045
1089
 
1046
1090
  // If session has data and includes a thinking entry, restore it
1047
1091
  if (thinkingLevel === undefined && hasExistingSession && hasThinkingEntry) {
@@ -1052,13 +1096,20 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1052
1096
  thinkingLevel = defaultRoleSpec.thinkingLevel;
1053
1097
  }
1054
1098
 
1055
- // Prefer the selected model's configured defaultLevel, otherwise fall back
1056
- // to the global settings default.
1099
+ // An explicit user/project default should win over the model's bundled
1100
+ // defaultLevel. The schema default is only a final fallback so model metadata
1101
+ // can keep driving first-run behavior until the user chooses "Set as default".
1102
+ if (thinkingLevel === undefined && hasExplicitDefaultThinkingLevel) {
1103
+ thinkingLevel = settings.get("defaultThinkingLevel");
1104
+ }
1105
+
1057
1106
  if (thinkingLevel === undefined && model?.thinking?.defaultLevel !== undefined) {
1058
1107
  thinkingLevel = model.thinking.defaultLevel;
1059
1108
  }
1109
+
1060
1110
  if (thinkingLevel === undefined) {
1061
1111
  thinkingLevel = settings.get("defaultThinkingLevel");
1112
+ thinkingLevelFromSchemaDefault = true;
1062
1113
  }
1063
1114
  if (model) {
1064
1115
  const resolvedModel = model;
@@ -1603,6 +1654,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1603
1654
  if (resolved) {
1604
1655
  model = resolved;
1605
1656
  modelFallbackMessage = undefined;
1657
+ if (thinkingLevelFromSchemaDefault && resolved.thinking?.defaultLevel !== undefined) {
1658
+ thinkingLevel = resolved.thinking.defaultLevel;
1659
+ thinkingLevelFromSchemaDefault = false;
1660
+ }
1661
+ thinkingLevel = resolveThinkingLevelForModel(resolved, thinkingLevel);
1606
1662
  } else {
1607
1663
  modelFallbackMessage = `Model "${options.modelPattern}" not found`;
1608
1664
  }
@@ -1634,6 +1690,18 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1634
1690
  }
1635
1691
  }
1636
1692
 
1693
+ if (options.credentialSelector && !runtimeCredentialSelectorInstalled) {
1694
+ const credentialProvider = options.credentialSelector.provider ?? model?.provider;
1695
+ if (!credentialProvider) {
1696
+ throw new Error(
1697
+ `--credential ${options.credentialSelector.raw} requires a resolved model or an explicit provider prefix`,
1698
+ );
1699
+ }
1700
+ installRuntimeCredentialSelector(credentialProvider);
1701
+ if (!options.modelRegistry && !canRefreshModelsBeforeCredentialSelector) {
1702
+ modelRegistry.refreshInBackground();
1703
+ }
1704
+ }
1637
1705
  const customCommandsResult: CustomCommandsLoadResult = { commands: [], errors: [] };
1638
1706
 
1639
1707
  let extensionRunner: ExtensionRunner | undefined;
@@ -1789,7 +1857,15 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1789
1857
  { source: "builtin" },
1790
1858
  )
1791
1859
  : [];
1792
- const discoverableToolsForDesc: DiscoverableTool[] = [...discoverableBuiltinTools];
1860
+ const discoverableMCPTools: DiscoverableTool[] = mcpDiscoveryEnabled
1861
+ ? collectDiscoverableTools(
1862
+ Array.from(tools.values()).filter(
1863
+ tool => isMCPToolName(tool.name) && !activeToolNames.has(tool.name),
1864
+ ),
1865
+ { source: "mcp" },
1866
+ )
1867
+ : [];
1868
+ const discoverableToolsForDesc: DiscoverableTool[] = [...discoverableBuiltinTools, ...discoverableMCPTools];
1793
1869
  const promptTools = buildSystemPromptToolMetadata(tools, {
1794
1870
  search_tool_bm25: { description: renderSearchToolBm25Description(discoverableToolsForDesc) },
1795
1871
  });
@@ -1838,8 +1914,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1838
1914
  intentField,
1839
1915
  mcpDiscoveryMode: false,
1840
1916
  mcpDiscoveryServerSummaries: [],
1841
- toolDiscoveryActive: effectiveDiscoveryMode === "all",
1842
- discoverableTools: discoverableBuiltinTools
1917
+ toolDiscoveryActive: effectiveDiscoveryMode === "all" || mcpDiscoveryEnabled,
1918
+ discoverableTools: [...discoverableBuiltinTools, ...discoverableMCPTools]
1843
1919
  .map(tool => ({ name: tool.name, summary: tool.summary }))
1844
1920
  .sort((a, b) => a.name.localeCompare(b.name)),
1845
1921
  eagerTasks,
@@ -1870,11 +1946,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1870
1946
  : toolNamesFromRegistry;
1871
1947
  const normalizedRequested = requestedToolNames.filter(name => toolRegistry.has(name));
1872
1948
  const requestedToolNameSet = new Set(normalizedRequested);
1873
- // Effective discovery mode only covers built-in tools; MCP tool discovery
1874
- // is quarantined from the GJC public surface.
1949
+ // Generic tool discovery hides non-essential built-ins behind search_tool_bm25.
1950
+ // Legacy MCP discovery remains separately controlled by mcp.discoveryMode so
1951
+ // loaded MCP catalogs can be searched without enabling every hidden built-in.
1875
1952
  const toolsDiscoveryModeSetting = settings.get("tools.discoveryMode");
1876
1953
  const effectiveDiscoveryMode: "off" | "all" = toolsDiscoveryModeSetting === "all" ? "all" : "off";
1877
- const mcpDiscoveryEnabled = false;
1954
+ const mcpDiscoveryEnabled = settings.get("mcp.discoveryMode") === true;
1878
1955
  const defaultInactiveToolNames = new Set(
1879
1956
  registeredTools.filter(tool => tool.definition.defaultInactive).map(tool => tool.definition.name),
1880
1957
  );
@@ -1887,6 +1964,20 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1887
1964
  const discoveryDefaultServerToolNames: string[] = [];
1888
1965
  let initialSelectedMCPToolNames: string[] = [];
1889
1966
  let defaultSelectedMCPToolNames: string[] = [];
1967
+ if (mcpDiscoveryEnabled) {
1968
+ const defaultServerNames = new Set(settings.get("mcp.discoveryDefaultServers") ?? []);
1969
+ for (const tool of toolRegistry.values()) {
1970
+ if (!isMCPBridgeTool(tool)) continue;
1971
+ discoverableMCPToolNames.add(tool.name);
1972
+ if (initialRequestedActiveToolNames.includes(tool.name)) {
1973
+ explicitlyRequestedMCPToolNames.push(tool.name);
1974
+ }
1975
+ const serverName = (tool as AgentTool & { mcpServerName?: string }).mcpServerName;
1976
+ if (serverName && defaultServerNames.has(serverName)) {
1977
+ discoveryDefaultServerToolNames.push(tool.name);
1978
+ }
1979
+ }
1980
+ }
1890
1981
  let initialToolNames = [...initialRequestedActiveToolNames];
1891
1982
  if (mcpDiscoveryEnabled) {
1892
1983
  const restoredSelectedMCPToolNames = existingSession.selectedMCPToolNames.filter(name =>
@@ -2247,7 +2338,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2247
2338
  initialSelectedMCPToolNames,
2248
2339
  defaultSelectedMCPToolNames,
2249
2340
  persistInitialMCPToolSelection: !hasExistingSession,
2250
- defaultSelectedMCPServerNames: [],
2341
+ defaultSelectedMCPServerNames: settings.get("mcp.discoveryDefaultServers") ?? [],
2251
2342
  ttsrManager,
2252
2343
  obfuscator,
2253
2344
  agentId: resolvedAgentId,
@@ -473,6 +473,8 @@ export interface PromptOptions {
473
473
  images?: ImageContent[];
474
474
  /** When streaming, how to queue the message: "steer" (interrupt) or "followUp" (wait). */
475
475
  streamingBehavior?: "steer" | "followUp";
476
+ /** When set to "sequential", this follow-up is delivered one prompt at a time even if followUpMode is "all". */
477
+ followUpQueuePolicy?: "respect-mode" | "sequential";
476
478
  /** Optional tool choice override for the next LLM call. */
477
479
  toolChoice?: ToolChoice;
478
480
  /** Send as developer/system message instead of user. Providers that support it use the developer role; others fall back to user. */
@@ -1786,7 +1788,7 @@ export class AgentSession {
1786
1788
  high = mid - 1;
1787
1789
  }
1788
1790
  }
1789
- return { message: bestMessage, tokens: Math.min(bestTokens, maxTokens) };
1791
+ return { message: bestMessage, tokens: bestTokens };
1790
1792
  };
1791
1793
 
1792
1794
  for (let i = providerMessages.length - 1; i >= 0; i--) {
@@ -1806,9 +1808,11 @@ export class AgentSession {
1806
1808
  recordSkip("token-limit");
1807
1809
  if (selected.length === 0) {
1808
1810
  const truncated = truncateMessageToTokenBudget(sanitized);
1809
- selected.unshift(truncated.message);
1810
- approximateTokens = truncated.tokens;
1811
- skippedReasons["newest-message-truncated"] = (skippedReasons["newest-message-truncated"] ?? 0) + 1;
1811
+ if (truncated.tokens <= maxTokens) {
1812
+ selected.unshift(truncated.message);
1813
+ approximateTokens = truncated.tokens;
1814
+ skippedReasons["newest-message-truncated"] = (skippedReasons["newest-message-truncated"] ?? 0) + 1;
1815
+ }
1812
1816
  }
1813
1817
  break;
1814
1818
  }
@@ -5302,7 +5306,9 @@ export class AgentSession {
5302
5306
  throw new AgentBusyError();
5303
5307
  }
5304
5308
  if (options.streamingBehavior === "followUp") {
5305
- await this.#queueFollowUp(expandedText, options?.images);
5309
+ await this.#queueFollowUp(expandedText, options?.images, {
5310
+ forceOneAtATime: options.followUpQueuePolicy === "sequential",
5311
+ });
5306
5312
  } else {
5307
5313
  await this.#queueSteer(expandedText, options?.images);
5308
5314
  }
@@ -5420,7 +5426,7 @@ export class AgentSession {
5420
5426
 
5421
5427
  async promptCustomMessage<T = unknown>(
5422
5428
  message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details" | "attribution">,
5423
- options?: Pick<PromptOptions, "streamingBehavior" | "toolChoice">,
5429
+ options?: Pick<PromptOptions, "streamingBehavior" | "toolChoice" | "followUpQueuePolicy">,
5424
5430
  ): Promise<void> {
5425
5431
  const textContent =
5426
5432
  typeof message.content === "string"
@@ -5434,7 +5440,10 @@ export class AgentSession {
5434
5440
  if (!options?.streamingBehavior) {
5435
5441
  throw new AgentBusyError();
5436
5442
  }
5437
- await this.sendCustomMessage(message, { deliverAs: options.streamingBehavior });
5443
+ await this.sendCustomMessage(message, {
5444
+ deliverAs: options.streamingBehavior,
5445
+ followUpQueuePolicy: options.followUpQueuePolicy,
5446
+ });
5438
5447
  return;
5439
5448
  }
5440
5449
 
@@ -5781,13 +5790,19 @@ export class AgentSession {
5781
5790
  /**
5782
5791
  * Queue a follow-up message to process after the agent would otherwise stop.
5783
5792
  */
5784
- async followUp(text: string, images?: ImageContent[]): Promise<void> {
5793
+ async followUp(
5794
+ text: string,
5795
+ images?: ImageContent[],
5796
+ options?: Pick<PromptOptions, "followUpQueuePolicy">,
5797
+ ): Promise<void> {
5785
5798
  if (text.startsWith("/")) {
5786
5799
  this.#throwIfExtensionCommand(text);
5787
5800
  }
5788
5801
 
5789
5802
  const expandedText = expandPromptTemplate(text, [...this.#promptTemplates]);
5790
- await this.#queueFollowUp(expandedText, images);
5803
+ await this.#queueFollowUp(expandedText, images, {
5804
+ forceOneAtATime: options?.followUpQueuePolicy === "sequential",
5805
+ });
5791
5806
  }
5792
5807
 
5793
5808
  /**
@@ -5824,19 +5839,22 @@ export class AgentSession {
5824
5839
  /**
5825
5840
  * Internal: Queue a follow-up message (already expanded, no extension command check).
5826
5841
  */
5827
- async #queueFollowUp(text: string, images?: ImageContent[]): Promise<void> {
5842
+ async #queueFollowUp(text: string, images?: ImageContent[], options?: { forceOneAtATime?: boolean }): Promise<void> {
5828
5843
  const displayText = text || (images && images.length > 0 ? "[Image]" : "");
5829
5844
  this.#followUpMessages.push(this.#createQueuedDisplayEntry(displayText));
5830
5845
  const content: (TextContent | ImageContent)[] = [{ type: "text", text }];
5831
5846
  if (images && images.length > 0) {
5832
5847
  content.push(...images);
5833
5848
  }
5834
- this.agent.followUp({
5835
- role: "user",
5836
- content,
5837
- attribution: "user",
5838
- timestamp: Date.now(),
5839
- });
5849
+ this.agent.followUp(
5850
+ {
5851
+ role: "user",
5852
+ content,
5853
+ attribution: "user",
5854
+ timestamp: Date.now(),
5855
+ },
5856
+ options?.forceOneAtATime ? { forceOneAtATime: true } : undefined,
5857
+ );
5840
5858
  // When fully idle AND the session is in a resumable assistant-ended state,
5841
5859
  // schedule an immediate continue so the queued follow-up is delivered
5842
5860
  // without waiting for the next user turn. We gate on isStreaming (model
@@ -5984,7 +6002,11 @@ export class AgentSession {
5984
6002
  */
5985
6003
  async sendCustomMessage<T = unknown>(
5986
6004
  message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details" | "attribution">,
5987
- options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" },
6005
+ options?: {
6006
+ triggerTurn?: boolean;
6007
+ deliverAs?: "steer" | "followUp" | "nextTurn";
6008
+ followUpQueuePolicy?: "respect-mode" | "sequential";
6009
+ },
5988
6010
  ): Promise<void> {
5989
6011
  const appMessage: CustomMessage<T> = {
5990
6012
  role: "custom",
@@ -6002,7 +6024,10 @@ export class AgentSession {
6002
6024
  }
6003
6025
 
6004
6026
  if (options?.deliverAs === "followUp") {
6005
- this.agent.followUp(appMessage);
6027
+ this.agent.followUp(
6028
+ appMessage,
6029
+ options.followUpQueuePolicy === "sequential" ? { forceOneAtATime: true } : undefined,
6030
+ );
6006
6031
  } else {
6007
6032
  this.agent.steer(appMessage);
6008
6033
  }
@@ -6928,7 +6953,7 @@ export class AgentSession {
6928
6953
 
6929
6954
  /**
6930
6955
  * Set thinking level.
6931
- * Saves the effective metadata-clamped level to session and settings only if it changes.
6956
+ * Saves the effective metadata-clamped level to the session, and to settings when requested.
6932
6957
  */
6933
6958
  setThinkingLevel(level: ThinkingLevel | undefined, persist: boolean = false): void {
6934
6959
  const effectiveLevel = resolveThinkingLevelForModel(this.model, level);
@@ -6937,11 +6962,12 @@ export class AgentSession {
6937
6962
  this.#thinkingLevel = effectiveLevel;
6938
6963
  this.agent.setThinkingLevel(toReasoningEffort(effectiveLevel));
6939
6964
 
6965
+ if (persist && effectiveLevel !== undefined) {
6966
+ this.settings.set("defaultThinkingLevel", effectiveLevel);
6967
+ }
6968
+
6940
6969
  if (isChanging) {
6941
6970
  this.sessionManager.appendThinkingLevelChange(effectiveLevel);
6942
- if (persist && effectiveLevel !== undefined && effectiveLevel !== ThinkingLevel.Off) {
6943
- this.settings.set("defaultThinkingLevel", effectiveLevel);
6944
- }
6945
6971
  this.#emit({ type: "thinking_level_changed", thinkingLevel: effectiveLevel });
6946
6972
  }
6947
6973
  }