@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.
- package/CHANGELOG.md +48 -0
- package/dist/types/cli/args.d.ts +1 -0
- package/dist/types/cli/notify-cli.d.ts +5 -0
- package/dist/types/cli.d.ts +3 -0
- package/dist/types/commands/launch.d.ts +3 -0
- package/dist/types/config/model-profile-activation.d.ts +7 -7
- package/dist/types/config/model-registry.d.ts +7 -3
- package/dist/types/config/model-resolver.d.ts +2 -2
- package/dist/types/config/settings-schema.d.ts +15 -1
- package/dist/types/config/settings.d.ts +7 -0
- package/dist/types/extensibility/runtime-skill-discovery.d.ts +20 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +9 -0
- package/dist/types/gjc-runtime/tmux-common.d.ts +1 -0
- package/dist/types/modes/components/settings-selector.d.ts +2 -2
- package/dist/types/modes/components/thinking-selector.d.ts +7 -2
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/rpc/rpc-mode.d.ts +1 -3
- package/dist/types/modes/rpc/rpc-socket-security.d.ts +3 -0
- package/dist/types/modes/types.d.ts +3 -0
- package/dist/types/modes/utils/injected-user-submission.d.ts +52 -0
- package/dist/types/notifications/config-commands.d.ts +38 -0
- package/dist/types/notifications/config.d.ts +16 -0
- package/dist/types/notifications/index.d.ts +14 -1
- package/dist/types/notifications/lifecycle-commands.d.ts +1 -0
- package/dist/types/notifications/lifecycle-control-runtime.d.ts +2 -1
- package/dist/types/notifications/reply-sent-store.d.ts +53 -0
- package/dist/types/notifications/rich-draft.d.ts +68 -0
- package/dist/types/notifications/rich-render.d.ts +90 -0
- package/dist/types/notifications/telegram-daemon-cli.d.ts +3 -3
- package/dist/types/notifications/telegram-daemon.d.ts +22 -0
- package/dist/types/notifications/telegram-reference.d.ts +7 -0
- package/dist/types/notifications/threaded-render.d.ts +10 -0
- package/dist/types/notifications/topic-registry.d.ts +4 -5
- package/dist/types/runtime-credential-selector.d.ts +7 -0
- package/dist/types/sdk.d.ts +7 -1
- package/dist/types/session/agent-session.d.ts +6 -3
- package/dist/types/tools/index.d.ts +1 -0
- package/dist/types/tools/skill-discovery.d.ts +40 -0
- package/dist/types/utils/pasted-image-path.d.ts +30 -0
- package/package.json +8 -11
- package/src/cli/args.ts +8 -0
- package/src/cli/notify-cli.ts +38 -10
- package/src/cli.ts +5 -0
- package/src/commands/launch.ts +5 -0
- package/src/commands/notify.ts +2 -2
- package/src/config/keybindings.ts +1 -1
- package/src/config/model-profile-activation.ts +23 -7
- package/src/config/model-registry.ts +21 -4
- package/src/config/model-resolver.ts +5 -16
- package/src/config/settings-schema.ts +11 -2
- package/src/config/settings.ts +24 -1
- package/src/extensibility/runtime-skill-discovery.ts +229 -0
- package/src/gjc-runtime/launch-tmux.ts +78 -12
- package/src/gjc-runtime/psmux-detect.ts +17 -2
- package/src/gjc-runtime/team-runtime.ts +46 -7
- package/src/gjc-runtime/tmux-common.ts +1 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +10 -1
- package/src/internal-urls/docs-index.generated.ts +6 -6
- package/src/lsp/client.ts +1 -0
- package/src/main.ts +26 -3
- package/src/modes/components/footer.ts +7 -2
- package/src/modes/components/settings-selector.ts +3 -3
- package/src/modes/components/thinking-selector.ts +83 -20
- package/src/modes/controllers/event-controller.ts +11 -2
- package/src/modes/controllers/extension-ui-controller.ts +8 -1
- package/src/modes/controllers/input-controller.ts +62 -26
- package/src/modes/controllers/selector-controller.ts +43 -0
- package/src/modes/interactive-mode.ts +5 -0
- package/src/modes/rpc/rpc-mode.ts +3 -9
- package/src/modes/rpc/rpc-socket-security.ts +13 -1
- package/src/modes/types.ts +3 -0
- package/src/modes/utils/injected-user-submission.ts +94 -0
- package/src/modes/utils/ui-helpers.ts +48 -15
- package/src/notifications/config-commands.ts +90 -0
- package/src/notifications/config.ts +27 -0
- package/src/notifications/index.ts +184 -14
- package/src/notifications/lifecycle-commands.ts +37 -16
- package/src/notifications/lifecycle-control-runtime.ts +8 -3
- package/src/notifications/reply-sent-store.ts +134 -0
- package/src/notifications/rich-draft.ts +107 -0
- package/src/notifications/rich-render.ts +143 -0
- package/src/notifications/telegram-daemon-cli.ts +60 -5
- package/src/notifications/telegram-daemon.ts +431 -91
- package/src/notifications/telegram-reference.ts +17 -0
- package/src/notifications/threaded-render.ts +42 -1
- package/src/notifications/topic-registry.ts +9 -8
- package/src/prompts/tools/search-tool-bm25.md +5 -0
- package/src/prompts/tools/skill-discovery.md +13 -0
- package/src/runtime-credential-selector.ts +35 -0
- package/src/sdk.ts +104 -13
- package/src/session/agent-session.ts +48 -22
- package/src/slash-commands/builtin-registry.ts +78 -1
- package/src/system-prompt.ts +4 -6
- package/src/tools/index.ts +4 -0
- package/src/tools/skill-discovery.ts +73 -0
- package/src/tools/skill.ts +15 -2
- 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
|
-
? {
|
|
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
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
1056
|
-
//
|
|
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
|
|
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
|
-
//
|
|
1874
|
-
//
|
|
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 =
|
|
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:
|
|
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
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
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, {
|
|
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(
|
|
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
|
-
|
|
5836
|
-
|
|
5837
|
-
|
|
5838
|
-
|
|
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?: {
|
|
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(
|
|
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
|
|
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
|
}
|