mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-08-31 02:14:58 +00:00
* feat: support multi-level thinking effort switching
- kimi provider: emit thinking.effort in the new wire format; keep reasoning_effort mirrored during the transition
- model catalog: thread support_efforts / default_effort from oauth through to /models
- config schema: add supportEfforts / defaultEffort on model aliases
- TUI: multi-segment thinking control in /model, new /effort command, footer effort display
- switch status uses displayName and distinguishes model vs effort-only changes
* docs: add thinking effort design plans
- thinking-effort-switching.md: implemented multi-level effort switching
- thinking-model-overhaul.md: follow-up refactor plan for the thinking state model
* docs: collapse thinking overhaul plan into a single PR
* refactor!: overhaul thinking config and effort resolution
Replace default_thinking and thinking.mode with a single [thinking] enabled/effort table. ThinkingEffort is now an open string ('off' | 'on' | model-declared effort); effort levels come from each model's support_efforts instead of a fixed enum.
Centralize default and always_thinking clamp logic in resolveThinkingEffort/defaultThinkingEffortFor, and honor an explicitly configured effort when an always_thinking model is forced back on.
TUI keeps a single thinkingEffort field instead of the boolean + level pair; 'on' is normalized to the model default at the UI boundary.
BREAKING CHANGE: default_thinking and thinking.mode are removed from config; migrate to [thinking] enabled/effort.
* refactor: rename residual thinking level wording to effort
Rename comments, error messages, parameter names, the SetThinkingPayload wire field (level -> effort), and TUI local variables so the thinking effort naming is consistent throughout. No behavior change.
* refactor: rename remaining camelCase thinking level identifiers to effort
Rename liveLevel/prevLevel/levelChanged/commitLevel/effectiveLevel to liveEffort/prevEffort/effortChanged/commitEffort/effectiveEffort in the TUI model picker and config commands.
* refactor: eliminate remaining thinking level wording in comments and tests
Rename levelLabel -> effortLabel, EffortSelectorOptions.levels -> efforts, and 'effort level(s)' / 'default level' / 'requested level' wording in comments, error messages, slash-command description, and test titles to effort. Also restore the withThinking(effort) parameter rename in the Kimi provider that was accidentally reverted.
* fix: address codex review feedback on thinking effort handling
- OpenAI thinkingEffortToReasoningEffort and Anthropic clampEffort now normalize 'on' / unrecognized efforts instead of throwing, so boolean non-Kimi models no longer crash on session start.
- ACP resolveCurrentThinkingEnabled treats a non-empty thinking.effort as enabled, matching agent-core's resolveThinkingEffort.
- REST promptThinkingSchema accepts any non-empty effort string so model-declared efforts are not rejected at the API boundary.
* test: align kimi e2e expectations with supportEfforts-gated reasoning_effort
The kimi provider now sends reasoning_effort only when the model declares support_efforts; boolean models (no support_efforts) send only thinking.type. Update the kimi e2e tests to drop the stale reasoning_effort expectation for the boolean test model.
* test: cover [thinking] effort parsing in config.test
Add effort = "high" to the documented [thinking] table in the config parse test and assert config.thinking.effort is resolved, so the new [thinking] effort field has direct parse coverage.
* docs: add thinking test coverage gap analysis
Capture the explore agent's test coverage review for the thinking overhaul PR, including P1/P2 gaps and the two open design questions, for follow-up test additions.
* feat(oauth): parse nested think_efforts from /models response
The /models endpoint now returns effort levels under a nested think_efforts object ({ support, valid_efforts, default_effort }). Parse it preferentially in both managed-kimi-code and open-platform model parsing, falling back to the legacy flat support_efforts / default_effort fields for older servers.
* refactor(oauth): only read nested think_efforts; gate on support=true
Drop the legacy flat support_efforts / default_effort fallback. The think_efforts object is now the single source, and its support flag gates the whole object — when support is not true, valid_efforts and default_effort are ignored entirely.
* chore: remove unused parseStringArray import in open-platform
* docs: finalize thinking effort release notes
Downgrade the changeset to minor with an English summary, drop the version-specific 'added in 1.0.0' info block, and present the deprecated config fields as a table (field / deprecated in 0.21.0 / description).
* refactor: drop temporary refresh toggles and kimi reasoning_effort mirror
Remove the always-true REFRESH_MODELS_ON_PICKER_OPEN / REFRESH_PROVIDER_MODELS_ON_STARTUP toggles and their stale re-enable TODOs, and stop sending reasoning_effort from the kimi provider (thinking.effort is the only wire field now).
* fix(tui): avoid persisting "on" as thinking effort
* fix: preserve persisted thinking effort across login and provider setup
* fix(tui): show actual thinking effort in /status and footer
* test(tui): align message-flow expectations with effort persistence and /status display
* fix(vis): rename thinkingLevel to thinkingEffort in config.update analysis
774 lines
22 KiB
TypeScript
774 lines
22 KiB
TypeScript
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
|
|
import {
|
|
ErrorCodes,
|
|
makeErrorPayload,
|
|
type AgentContextData,
|
|
type ApprovalRequest,
|
|
type ApprovalResponse,
|
|
type CoreAPI,
|
|
type Event,
|
|
type ExperimentalFeatureState,
|
|
type QuestionRequest,
|
|
type QuestionResult,
|
|
type RPCMethods,
|
|
type SDKAPI,
|
|
type ToolCallRequest,
|
|
type ToolCallResponse,
|
|
type SwarmModeTrigger,
|
|
} from '@moonshot-ai/agent-core';
|
|
import type { Kaos } from '@moonshot-ai/kaos';
|
|
|
|
import type { ApprovalHandler, QuestionHandler } from '#/events';
|
|
import type {
|
|
AddAdditionalDirInput,
|
|
AddAdditionalDirResult,
|
|
BackgroundTaskInfo,
|
|
ConfigDiagnostics,
|
|
CreateSessionOptions,
|
|
ExportSessionInput,
|
|
ExportSessionResult,
|
|
CreateGoalInput,
|
|
ForkSessionInput,
|
|
GetConfigOptions,
|
|
GoalSnapshot,
|
|
GoalToolResult,
|
|
KimiConfig,
|
|
KimiConfigPatch,
|
|
ListSessionsOptions,
|
|
McpServerInfo,
|
|
McpStartupMetrics,
|
|
PermissionMode,
|
|
PluginInfo,
|
|
PluginSummary,
|
|
ReloadSummary,
|
|
CompactOptions,
|
|
SessionPlan,
|
|
SessionStatus,
|
|
SessionUsage,
|
|
PromptInput,
|
|
RenameSessionInput,
|
|
ResumeSessionInput,
|
|
ResumedSessionSummary,
|
|
SessionSummary,
|
|
SkillSummary,
|
|
PluginCommandDef,
|
|
Unsubscribe,
|
|
} from '#/types';
|
|
|
|
const MAIN_AGENT_ID = 'main';
|
|
|
|
export interface SessionPromptRpcInput {
|
|
readonly sessionId: string;
|
|
readonly input: PromptInput;
|
|
}
|
|
|
|
export interface SessionIdRpcInput {
|
|
readonly sessionId: string;
|
|
}
|
|
|
|
export interface ReloadSessionRpcInput extends SessionIdRpcInput {
|
|
readonly forcePluginSessionStartReminder?: boolean;
|
|
}
|
|
|
|
export interface SetSessionModelRpcInput extends SessionIdRpcInput {
|
|
readonly model: string;
|
|
}
|
|
|
|
export interface SetSessionModelRpcResult {
|
|
readonly model: string;
|
|
readonly providerName?: string | undefined;
|
|
}
|
|
|
|
export interface SetSessionThinkingRpcInput extends SessionIdRpcInput {
|
|
readonly effort: string;
|
|
}
|
|
|
|
export interface SetSessionPermissionRpcInput extends SessionIdRpcInput {
|
|
readonly mode: PermissionMode;
|
|
}
|
|
|
|
export interface SetSessionPlanModeRpcInput extends SessionIdRpcInput {
|
|
readonly enabled: boolean;
|
|
}
|
|
|
|
export type SetSessionSwarmModeRpcInput =
|
|
| (SessionIdRpcInput & { readonly enabled: true; readonly trigger: SwarmModeTrigger })
|
|
| (SessionIdRpcInput & { readonly enabled: false });
|
|
|
|
export interface ActivateSkillRpcInput extends SessionIdRpcInput {
|
|
readonly name: string;
|
|
readonly args?: string | undefined;
|
|
}
|
|
|
|
export interface ActivatePluginCommandRpcInput extends SessionIdRpcInput {
|
|
readonly pluginId: string;
|
|
readonly commandName: string;
|
|
readonly args?: string | undefined;
|
|
}
|
|
|
|
export interface ReconnectMcpServerRpcInput extends SessionIdRpcInput {
|
|
readonly name: string;
|
|
}
|
|
|
|
type ResolvedCoreAPI = RPCMethods<CoreAPI>;
|
|
|
|
export abstract class SDKRpcClientBase {
|
|
private readonly interactiveAgentScope = new AsyncLocalStorage<string>();
|
|
private readonly eventListeners = new Set<(event: Event) => void>();
|
|
private readonly approvalHandlers = new Map<string, ApprovalHandler>();
|
|
private readonly questionHandlers = new Map<string, QuestionHandler>();
|
|
|
|
get interactiveAgentId(): string {
|
|
return this.interactiveAgentScope.getStore() ?? MAIN_AGENT_ID;
|
|
}
|
|
|
|
withInteractiveAgent<T>(agentId: string, fn: () => T): T {
|
|
return this.interactiveAgentScope.run(agentId, fn);
|
|
}
|
|
|
|
protected abstract getRpc(): Promise<ResolvedCoreAPI>;
|
|
|
|
async createSession(input: CreateSessionOptions): Promise<SessionSummary> {
|
|
const rpc = await this.getRpc();
|
|
const { planMode, ...coreInput } = input;
|
|
void planMode;
|
|
return rpc.createSession(coreInput);
|
|
}
|
|
|
|
async createSessionWithKaos(
|
|
input: CreateSessionOptions,
|
|
kaos: Kaos,
|
|
persistenceKaos?: Kaos,
|
|
): Promise<SessionSummary> {
|
|
void kaos;
|
|
void persistenceKaos;
|
|
return this.createSession(input);
|
|
}
|
|
|
|
async resumeSession(input: ResumeSessionInput): Promise<ResumedSessionSummary> {
|
|
const rpc = await this.getRpc();
|
|
return rpc.resumeSession({ ...input, sessionId: input.id });
|
|
}
|
|
|
|
async resumeSessionWithKaos(
|
|
input: ResumeSessionInput,
|
|
kaos: Kaos,
|
|
persistenceKaos?: Kaos,
|
|
): Promise<ResumedSessionSummary> {
|
|
void kaos;
|
|
void persistenceKaos;
|
|
return this.resumeSession(input);
|
|
}
|
|
|
|
async reloadSession(input: ReloadSessionRpcInput): Promise<ResumedSessionSummary> {
|
|
const rpc = await this.getRpc();
|
|
return rpc.reloadSession({
|
|
sessionId: input.sessionId,
|
|
forcePluginSessionStartReminder: input.forcePluginSessionStartReminder,
|
|
});
|
|
}
|
|
|
|
async forkSession(input: ForkSessionInput): Promise<SessionSummary> {
|
|
const rpc = await this.getRpc();
|
|
return rpc.forkSession({
|
|
sessionId: input.id,
|
|
id: input.forkId,
|
|
title: input.title,
|
|
metadata: input.metadata,
|
|
});
|
|
}
|
|
|
|
async closeSession(input: SessionIdRpcInput): Promise<void> {
|
|
const rpc = await this.getRpc();
|
|
return rpc.closeSession({ sessionId: input.sessionId });
|
|
}
|
|
|
|
async listSessions(input: ListSessionsOptions = {}): Promise<readonly SessionSummary[]> {
|
|
const rpc = await this.getRpc();
|
|
return rpc.listSessions(input);
|
|
}
|
|
|
|
async renameSession(input: RenameSessionInput): Promise<void> {
|
|
const rpc = await this.getRpc();
|
|
return rpc.renameSession({
|
|
sessionId: input.id,
|
|
title: input.title,
|
|
});
|
|
}
|
|
|
|
async exportSession(input: ExportSessionInput): Promise<ExportSessionResult> {
|
|
const rpc = await this.getRpc();
|
|
return rpc.exportSession({
|
|
sessionId: input.id,
|
|
outputPath: input.outputPath,
|
|
includeGlobalLog: input.includeGlobalLog,
|
|
version: input.version,
|
|
installSource: input.installSource,
|
|
shellEnv: input.shellEnv,
|
|
});
|
|
}
|
|
|
|
async getConfig(input?: GetConfigOptions): Promise<KimiConfig> {
|
|
const rpc = await this.getRpc();
|
|
return rpc.getKimiConfig(input ?? {});
|
|
}
|
|
|
|
async getConfigDiagnostics(): Promise<ConfigDiagnostics> {
|
|
const rpc = await this.getRpc();
|
|
return rpc.getConfigDiagnostics({});
|
|
}
|
|
|
|
async getExperimentalFeatures(): Promise<readonly ExperimentalFeatureState[]> {
|
|
const rpc = await this.getRpc();
|
|
return rpc.getExperimentalFeatures({});
|
|
}
|
|
|
|
async setConfig(input: KimiConfigPatch): Promise<KimiConfig> {
|
|
const rpc = await this.getRpc();
|
|
return rpc.setKimiConfig(input);
|
|
}
|
|
|
|
async removeProvider(providerId: string): Promise<KimiConfig> {
|
|
const rpc = await this.getRpc();
|
|
return rpc.removeKimiProvider({ providerId });
|
|
}
|
|
|
|
async prompt(input: SessionPromptRpcInput): Promise<void> {
|
|
const agentId = this.interactiveAgentId;
|
|
const rpc = await this.getRpc();
|
|
return rpc.prompt({
|
|
sessionId: input.sessionId,
|
|
agentId,
|
|
input: input.input,
|
|
});
|
|
}
|
|
|
|
async runShellCommand(input: {
|
|
sessionId: string;
|
|
command: string;
|
|
commandId?: string;
|
|
}): Promise<{ stdout: string; stderr: string; isError?: boolean; backgrounded?: boolean }> {
|
|
const agentId = this.interactiveAgentId;
|
|
const rpc = await this.getRpc();
|
|
return rpc.runShellCommand({
|
|
sessionId: input.sessionId,
|
|
agentId,
|
|
command: input.command,
|
|
commandId: input.commandId,
|
|
});
|
|
}
|
|
|
|
async cancelShellCommand(input: { sessionId: string; commandId: string }): Promise<void> {
|
|
const agentId = this.interactiveAgentId;
|
|
const rpc = await this.getRpc();
|
|
return rpc.cancelShellCommand({
|
|
sessionId: input.sessionId,
|
|
agentId,
|
|
commandId: input.commandId,
|
|
});
|
|
}
|
|
|
|
async steer(input: SessionPromptRpcInput): Promise<void> {
|
|
const agentId = this.interactiveAgentId;
|
|
const rpc = await this.getRpc();
|
|
return rpc.steer({
|
|
sessionId: input.sessionId,
|
|
agentId,
|
|
input: input.input,
|
|
});
|
|
}
|
|
|
|
async generateAgentsMd(input: SessionIdRpcInput): Promise<void> {
|
|
const rpc = await this.getRpc();
|
|
return rpc.generateAgentsMd({ sessionId: input.sessionId });
|
|
}
|
|
|
|
async getSessionWarnings(input: SessionIdRpcInput) {
|
|
const rpc = await this.getRpc();
|
|
return rpc.getSessionWarnings({ sessionId: input.sessionId });
|
|
}
|
|
|
|
async addAdditionalDir(input: AddAdditionalDirInput): Promise<AddAdditionalDirResult> {
|
|
const rpc = await this.getRpc();
|
|
return rpc.addAdditionalDir({ sessionId: input.id, path: input.path, persist: input.persist });
|
|
}
|
|
|
|
async startBtw(input: SessionIdRpcInput): Promise<string> {
|
|
const agentId = this.interactiveAgentId;
|
|
const rpc = await this.getRpc();
|
|
return rpc.startBtw({
|
|
sessionId: input.sessionId,
|
|
agentId,
|
|
});
|
|
}
|
|
|
|
async cancel(input: SessionIdRpcInput): Promise<void> {
|
|
const agentId = this.interactiveAgentId;
|
|
const rpc = await this.getRpc();
|
|
return rpc.cancel({
|
|
sessionId: input.sessionId,
|
|
agentId,
|
|
});
|
|
}
|
|
|
|
async setModel(input: SetSessionModelRpcInput): Promise<SetSessionModelRpcResult> {
|
|
const rpc = await this.getRpc();
|
|
return rpc.setModel({
|
|
sessionId: input.sessionId,
|
|
agentId: this.interactiveAgentId,
|
|
model: input.model,
|
|
});
|
|
}
|
|
|
|
async setThinking(input: SetSessionThinkingRpcInput): Promise<void> {
|
|
const rpc = await this.getRpc();
|
|
return rpc.setThinking({
|
|
sessionId: input.sessionId,
|
|
agentId: this.interactiveAgentId,
|
|
effort: input.effort,
|
|
});
|
|
}
|
|
|
|
async setPermission(input: SetSessionPermissionRpcInput): Promise<void> {
|
|
const rpc = await this.getRpc();
|
|
return rpc.setPermission({
|
|
sessionId: input.sessionId,
|
|
agentId: this.interactiveAgentId,
|
|
mode: input.mode,
|
|
});
|
|
}
|
|
|
|
async setPlanMode(input: SetSessionPlanModeRpcInput): Promise<void> {
|
|
const rpc = await this.getRpc();
|
|
if (!input.enabled) {
|
|
return rpc.cancelPlan({
|
|
sessionId: input.sessionId,
|
|
agentId: this.interactiveAgentId,
|
|
});
|
|
}
|
|
return rpc.enterPlan({
|
|
sessionId: input.sessionId,
|
|
agentId: this.interactiveAgentId,
|
|
});
|
|
}
|
|
|
|
async setSwarmMode(input: SetSessionSwarmModeRpcInput): Promise<void> {
|
|
if (input.enabled) return this.enterSwarmMode(input);
|
|
return this.exitSwarmMode(input);
|
|
}
|
|
|
|
async swarm(input: SessionPromptRpcInput): Promise<void> {
|
|
await this.enterSwarmMode({ sessionId: input.sessionId, trigger: 'task' });
|
|
return this.prompt(input);
|
|
}
|
|
|
|
private async enterSwarmMode(
|
|
input: SessionIdRpcInput & { readonly trigger: SwarmModeTrigger },
|
|
): Promise<void> {
|
|
const rpc = await this.getRpc();
|
|
return rpc.enterSwarm({
|
|
sessionId: input.sessionId,
|
|
agentId: this.interactiveAgentId,
|
|
trigger: input.trigger,
|
|
});
|
|
}
|
|
|
|
private async exitSwarmMode(input: SessionIdRpcInput): Promise<void> {
|
|
const rpc = await this.getRpc();
|
|
return rpc.exitSwarm({
|
|
sessionId: input.sessionId,
|
|
agentId: this.interactiveAgentId,
|
|
});
|
|
}
|
|
|
|
async getPlan(input: SessionIdRpcInput): Promise<SessionPlan> {
|
|
const rpc = await this.getRpc();
|
|
return rpc.getPlan({
|
|
sessionId: input.sessionId,
|
|
agentId: this.interactiveAgentId,
|
|
});
|
|
}
|
|
|
|
async clearPlan(input: SessionIdRpcInput): Promise<void> {
|
|
const rpc = await this.getRpc();
|
|
await rpc.clearPlan({
|
|
sessionId: input.sessionId,
|
|
agentId: this.interactiveAgentId,
|
|
});
|
|
}
|
|
|
|
async compact(input: SessionIdRpcInput & CompactOptions): Promise<void> {
|
|
const rpc = await this.getRpc();
|
|
return rpc.beginCompaction({
|
|
sessionId: input.sessionId,
|
|
agentId: this.interactiveAgentId,
|
|
...(input.instruction !== undefined ? { instruction: input.instruction } : {}),
|
|
});
|
|
}
|
|
|
|
async cancelCompaction(input: SessionIdRpcInput): Promise<void> {
|
|
const rpc = await this.getRpc();
|
|
return rpc.cancelCompaction({
|
|
sessionId: input.sessionId,
|
|
agentId: this.interactiveAgentId,
|
|
});
|
|
}
|
|
|
|
async undoHistory(input: SessionIdRpcInput & { count: number }): Promise<void> {
|
|
const rpc = await this.getRpc();
|
|
return rpc.undoHistory({
|
|
sessionId: input.sessionId,
|
|
agentId: this.interactiveAgentId,
|
|
count: input.count,
|
|
});
|
|
}
|
|
|
|
async getContext(input: SessionIdRpcInput): Promise<AgentContextData> {
|
|
const rpc = await this.getRpc();
|
|
return rpc.getContext({
|
|
sessionId: input.sessionId,
|
|
agentId: this.interactiveAgentId,
|
|
});
|
|
}
|
|
|
|
async getUsage(input: SessionIdRpcInput): Promise<SessionUsage> {
|
|
const rpc = await this.getRpc();
|
|
return rpc.getUsage({
|
|
sessionId: input.sessionId,
|
|
agentId: this.interactiveAgentId,
|
|
});
|
|
}
|
|
|
|
async getStatus(input: SessionIdRpcInput): Promise<SessionStatus> {
|
|
const rpc = await this.getRpc();
|
|
const agentId = this.interactiveAgentId;
|
|
const config = await rpc.getConfig({
|
|
sessionId: input.sessionId,
|
|
agentId,
|
|
});
|
|
const context = await rpc.getContext({
|
|
sessionId: input.sessionId,
|
|
agentId,
|
|
});
|
|
const permission = await rpc.getPermission({
|
|
sessionId: input.sessionId,
|
|
agentId,
|
|
});
|
|
const plan = await rpc.getPlan({
|
|
sessionId: input.sessionId,
|
|
agentId,
|
|
});
|
|
const swarmMode = await rpc.getSwarmMode({
|
|
sessionId: input.sessionId,
|
|
agentId,
|
|
});
|
|
const usage = await rpc.getUsage({
|
|
sessionId: input.sessionId,
|
|
agentId,
|
|
});
|
|
const maxContextTokens = config.modelCapabilities?.max_context_tokens ?? 0;
|
|
const contextTokens = context.tokenCount;
|
|
const contextUsage = maxContextTokens > 0 ? contextTokens / maxContextTokens : 0;
|
|
const hasUsage =
|
|
usage.byModel !== undefined || usage.total !== undefined || usage.currentTurn !== undefined;
|
|
return {
|
|
model: config.modelAlias ?? config.provider?.model,
|
|
thinkingEffort: config.thinkingEffort,
|
|
permission: permission.mode,
|
|
planMode: plan !== null,
|
|
swarmMode,
|
|
contextTokens,
|
|
maxContextTokens,
|
|
contextUsage,
|
|
usage: hasUsage ? usage : undefined,
|
|
};
|
|
}
|
|
|
|
async listSkills(input: SessionIdRpcInput): Promise<readonly SkillSummary[]> {
|
|
const rpc = await this.getRpc();
|
|
return rpc.listSkills({ sessionId: input.sessionId });
|
|
}
|
|
|
|
async listPluginCommands(input: SessionIdRpcInput): Promise<readonly PluginCommandDef[]> {
|
|
const rpc = await this.getRpc();
|
|
return rpc.listPluginCommands({ sessionId: input.sessionId });
|
|
}
|
|
|
|
async listBackgroundTasks(
|
|
input: SessionIdRpcInput & { activeOnly?: boolean; limit?: number },
|
|
): Promise<readonly BackgroundTaskInfo[]> {
|
|
const rpc = await this.getRpc();
|
|
return rpc.getBackground({
|
|
sessionId: input.sessionId,
|
|
agentId: this.interactiveAgentId,
|
|
activeOnly: input.activeOnly,
|
|
limit: input.limit,
|
|
});
|
|
}
|
|
|
|
async getBackgroundTaskOutput(
|
|
input: SessionIdRpcInput & { taskId: string; tail?: number },
|
|
): Promise<string> {
|
|
const rpc = await this.getRpc();
|
|
return rpc.getBackgroundOutput({
|
|
sessionId: input.sessionId,
|
|
agentId: this.interactiveAgentId,
|
|
taskId: input.taskId,
|
|
tail: input.tail,
|
|
});
|
|
}
|
|
|
|
async stopBackgroundTask(
|
|
input: SessionIdRpcInput & { taskId: string; reason?: string },
|
|
): Promise<void> {
|
|
const rpc = await this.getRpc();
|
|
return rpc.stopBackground({
|
|
sessionId: input.sessionId,
|
|
agentId: this.interactiveAgentId,
|
|
taskId: input.taskId,
|
|
reason: input.reason,
|
|
});
|
|
}
|
|
|
|
async detachBackgroundTask(
|
|
input: SessionIdRpcInput & { taskId: string },
|
|
): Promise<BackgroundTaskInfo | undefined> {
|
|
const rpc = await this.getRpc();
|
|
return rpc.detachBackground({
|
|
sessionId: input.sessionId,
|
|
agentId: this.interactiveAgentId,
|
|
taskId: input.taskId,
|
|
});
|
|
}
|
|
|
|
async createGoal(input: SessionIdRpcInput & CreateGoalInput): Promise<GoalSnapshot> {
|
|
const rpc = await this.getRpc();
|
|
return rpc.createGoal({
|
|
sessionId: input.sessionId,
|
|
agentId: this.interactiveAgentId,
|
|
objective: input.objective,
|
|
replace: input.replace,
|
|
});
|
|
}
|
|
|
|
async getGoal(input: SessionIdRpcInput): Promise<GoalToolResult> {
|
|
const rpc = await this.getRpc();
|
|
return rpc.getGoal({ sessionId: input.sessionId, agentId: this.interactiveAgentId });
|
|
}
|
|
|
|
async pauseGoal(input: SessionIdRpcInput): Promise<GoalSnapshot> {
|
|
const rpc = await this.getRpc();
|
|
return rpc.pauseGoal({
|
|
sessionId: input.sessionId,
|
|
agentId: this.interactiveAgentId,
|
|
});
|
|
}
|
|
|
|
async resumeGoal(input: SessionIdRpcInput): Promise<GoalSnapshot> {
|
|
const rpc = await this.getRpc();
|
|
return rpc.resumeGoal({
|
|
sessionId: input.sessionId,
|
|
agentId: this.interactiveAgentId,
|
|
});
|
|
}
|
|
|
|
async cancelGoal(input: SessionIdRpcInput): Promise<GoalSnapshot> {
|
|
const rpc = await this.getRpc();
|
|
return rpc.cancelGoal({
|
|
sessionId: input.sessionId,
|
|
agentId: this.interactiveAgentId,
|
|
});
|
|
}
|
|
|
|
async listMcpServers(input: SessionIdRpcInput): Promise<readonly McpServerInfo[]> {
|
|
const rpc = await this.getRpc();
|
|
return rpc.listMcpServers({ sessionId: input.sessionId });
|
|
}
|
|
|
|
async getMcpStartupMetrics(input: SessionIdRpcInput): Promise<McpStartupMetrics> {
|
|
const rpc = await this.getRpc();
|
|
return rpc.getMcpStartupMetrics({ sessionId: input.sessionId });
|
|
}
|
|
|
|
async reconnectMcpServer(input: ReconnectMcpServerRpcInput): Promise<void> {
|
|
const rpc = await this.getRpc();
|
|
return rpc.reconnectMcpServer({ sessionId: input.sessionId, name: input.name });
|
|
}
|
|
|
|
async listPlugins(): Promise<readonly PluginSummary[]> {
|
|
const rpc = await this.getRpc();
|
|
return rpc.listPlugins({});
|
|
}
|
|
|
|
async installPlugin(source: string): Promise<PluginSummary> {
|
|
const rpc = await this.getRpc();
|
|
return rpc.installPlugin({ source });
|
|
}
|
|
|
|
async setPluginEnabled(id: string, enabled: boolean): Promise<void> {
|
|
const rpc = await this.getRpc();
|
|
return rpc.setPluginEnabled({ id, enabled });
|
|
}
|
|
|
|
async setPluginMcpServerEnabled(
|
|
id: string,
|
|
server: string,
|
|
enabled: boolean,
|
|
): Promise<void> {
|
|
const rpc = await this.getRpc();
|
|
return rpc.setPluginMcpServerEnabled({ id, server, enabled });
|
|
}
|
|
|
|
async removePlugin(id: string): Promise<void> {
|
|
const rpc = await this.getRpc();
|
|
return rpc.removePlugin({ id });
|
|
}
|
|
|
|
async reloadPlugins(): Promise<ReloadSummary> {
|
|
const rpc = await this.getRpc();
|
|
return rpc.reloadPlugins({});
|
|
}
|
|
|
|
async getPluginInfo(id: string): Promise<PluginInfo> {
|
|
const rpc = await this.getRpc();
|
|
return rpc.getPluginInfo({ id });
|
|
}
|
|
|
|
async activateSkill(input: ActivateSkillRpcInput): Promise<void> {
|
|
const rpc = await this.getRpc();
|
|
return rpc.activateSkill({
|
|
sessionId: input.sessionId,
|
|
agentId: this.interactiveAgentId,
|
|
name: input.name,
|
|
args: input.args,
|
|
});
|
|
}
|
|
|
|
async activatePluginCommand(input: ActivatePluginCommandRpcInput): Promise<void> {
|
|
const rpc = await this.getRpc();
|
|
return rpc.activatePluginCommand({
|
|
sessionId: input.sessionId,
|
|
agentId: this.interactiveAgentId,
|
|
pluginId: input.pluginId,
|
|
commandName: input.commandName,
|
|
args: input.args,
|
|
});
|
|
}
|
|
|
|
onEvent(listener: (event: Event) => void): Unsubscribe {
|
|
this.eventListeners.add(listener);
|
|
return () => {
|
|
this.eventListeners.delete(listener);
|
|
};
|
|
}
|
|
|
|
receiveEvent(event: Event): void {
|
|
for (const listener of this.eventListeners) {
|
|
listener(event);
|
|
}
|
|
}
|
|
|
|
setApprovalHandler(sessionId: string, handler: ApprovalHandler | undefined): void {
|
|
if (handler === undefined) {
|
|
this.approvalHandlers.delete(sessionId);
|
|
return;
|
|
}
|
|
this.approvalHandlers.set(sessionId, handler);
|
|
}
|
|
|
|
setQuestionHandler(sessionId: string, handler: QuestionHandler | undefined): void {
|
|
if (handler === undefined) {
|
|
this.questionHandlers.delete(sessionId);
|
|
return;
|
|
}
|
|
this.questionHandlers.set(sessionId, handler);
|
|
}
|
|
|
|
clearSessionHandlers(sessionId: string): void {
|
|
this.approvalHandlers.delete(sessionId);
|
|
this.questionHandlers.delete(sessionId);
|
|
}
|
|
|
|
async requestApproval(
|
|
request: ApprovalRequest & { sessionId: string; agentId: string },
|
|
): Promise<ApprovalResponse> {
|
|
const handler = this.approvalHandlers.get(request.sessionId);
|
|
if (handler === undefined) {
|
|
return {
|
|
decision: 'cancelled',
|
|
feedback: 'No approval handler registered.',
|
|
};
|
|
}
|
|
|
|
try {
|
|
return await handler(request);
|
|
} catch (error) {
|
|
this.receiveEvent({
|
|
type: 'error',
|
|
sessionId: request.sessionId,
|
|
agentId: request.agentId,
|
|
...makeErrorPayload(ErrorCodes.SESSION_APPROVAL_HANDLER_ERROR, errorMessage(error)),
|
|
});
|
|
return {
|
|
decision: 'cancelled',
|
|
feedback: 'Approval handler failed.',
|
|
};
|
|
}
|
|
}
|
|
|
|
async requestQuestion(
|
|
request: QuestionRequest & { sessionId: string; agentId: string },
|
|
): Promise<QuestionResult> {
|
|
const handler = this.questionHandlers.get(request.sessionId);
|
|
if (handler === undefined) return null;
|
|
|
|
try {
|
|
return await handler(request);
|
|
} catch (error) {
|
|
this.receiveEvent({
|
|
type: 'error',
|
|
sessionId: request.sessionId,
|
|
agentId: request.agentId,
|
|
...makeErrorPayload(ErrorCodes.SESSION_QUESTION_HANDLER_ERROR, errorMessage(error)),
|
|
});
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async toolCall(request: ToolCallRequest): Promise<ToolCallResponse> {
|
|
return {
|
|
output: `SDK custom tool calls are not supported: ${request.toolCallId}`,
|
|
isError: true,
|
|
};
|
|
}
|
|
|
|
}
|
|
|
|
export class ClientAPI implements SDKAPI {
|
|
constructor(readonly client: SDKRpcClientBase) {}
|
|
|
|
emitEvent(event: Event): void {
|
|
this.client.receiveEvent(event);
|
|
}
|
|
|
|
requestApproval(
|
|
request: ApprovalRequest & { sessionId: string; agentId: string },
|
|
): Promise<ApprovalResponse> {
|
|
return this.client.requestApproval(request);
|
|
}
|
|
|
|
requestQuestion(
|
|
request: QuestionRequest & { sessionId: string; agentId: string },
|
|
): Promise<QuestionResult> {
|
|
return this.client.requestQuestion(request);
|
|
}
|
|
|
|
toolCall(request: ToolCallRequest): Promise<ToolCallResponse> {
|
|
return this.client.toolCall(request);
|
|
}
|
|
}
|
|
|
|
function errorMessage(error: unknown): string {
|
|
return error instanceof Error ? error.message : String(error);
|
|
}
|