@gajae-code/coding-agent 0.4.5 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +43 -0
- package/dist/types/commands/harness.d.ts +3 -0
- package/dist/types/config/model-profile-activation.d.ts +11 -2
- package/dist/types/config/model-profiles.d.ts +7 -0
- package/dist/types/config/model-registry.d.ts +3 -0
- package/dist/types/config/model-resolver.d.ts +2 -0
- package/dist/types/config/models-config-schema.d.ts +30 -0
- package/dist/types/config/settings-schema.d.ts +4 -3
- package/dist/types/gjc-runtime/team-runtime.d.ts +0 -1
- package/dist/types/gjc-runtime/tmux-common.d.ts +3 -0
- package/dist/types/harness-control-plane/owner.d.ts +1 -1
- package/dist/types/harness-control-plane/receipt-spool.d.ts +19 -0
- package/dist/types/harness-control-plane/state-machine.d.ts +6 -1
- package/dist/types/harness-control-plane/types.d.ts +4 -0
- package/dist/types/hindsight/mental-models.d.ts +5 -5
- package/dist/types/modes/components/model-selector.d.ts +1 -12
- package/dist/types/modes/rpc/rpc-client.d.ts +2 -2
- package/dist/types/modes/rpc/rpc-types.d.ts +4 -1
- package/dist/types/sdk.d.ts +5 -0
- package/dist/types/session/agent-session.d.ts +2 -0
- package/dist/types/session/blob-store.d.ts +20 -1
- package/dist/types/session/session-manager.d.ts +24 -6
- package/dist/types/session/streaming-output.d.ts +3 -2
- package/dist/types/session/tool-choice-queue.d.ts +6 -0
- package/dist/types/task/receipt.d.ts +1 -0
- package/dist/types/task/types.d.ts +7 -0
- package/dist/types/thinking-metadata.d.ts +16 -0
- package/dist/types/thinking.d.ts +3 -12
- package/dist/types/tools/index.d.ts +2 -0
- package/dist/types/tools/resolve.d.ts +0 -10
- package/dist/types/utils/tool-choice.d.ts +14 -1
- package/package.json +7 -7
- package/src/cli.ts +8 -4
- package/src/commands/harness.ts +36 -2
- package/src/commands/launch.ts +2 -2
- package/src/commands/session.ts +3 -1
- package/src/config/model-profile-activation.ts +15 -3
- package/src/config/model-profiles.ts +255 -56
- package/src/config/model-resolver.ts +9 -6
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings-schema.ts +6 -3
- package/src/coordinator-mcp/server.ts +54 -23
- package/src/cursor.ts +16 -2
- package/src/defaults/gjc/skills/team/SKILL.md +3 -2
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +8 -2
- package/src/export/html/index.ts +13 -9
- package/src/gjc-runtime/team-runtime.ts +33 -7
- package/src/gjc-runtime/tmux-common.ts +15 -0
- package/src/gjc-runtime/tmux-sessions.ts +19 -11
- package/src/gjc-runtime/ultragoal-runtime.ts +505 -41
- package/src/gjc-runtime/workflow-manifest.generated.json +27 -1
- package/src/gjc-runtime/workflow-manifest.ts +16 -1
- package/src/harness-control-plane/owner.ts +78 -27
- package/src/harness-control-plane/receipt-spool.ts +128 -0
- package/src/harness-control-plane/state-machine.ts +27 -6
- package/src/harness-control-plane/storage.ts +23 -0
- package/src/harness-control-plane/types.ts +4 -0
- package/src/hindsight/mental-models.ts +17 -16
- package/src/internal-urls/docs-index.generated.ts +2 -2
- package/src/modes/components/assistant-message.ts +26 -14
- package/src/modes/components/diff.ts +97 -0
- package/src/modes/components/model-selector.ts +353 -181
- package/src/modes/components/tool-execution.ts +30 -13
- package/src/modes/controllers/selector-controller.ts +33 -42
- package/src/modes/rpc/rpc-client.ts +3 -2
- package/src/modes/rpc/rpc-mode.ts +44 -14
- package/src/modes/rpc/rpc-types.ts +5 -2
- package/src/modes/shared/agent-wire/command-dispatch.ts +10 -5
- package/src/modes/shared/agent-wire/command-validation.ts +11 -0
- package/src/sdk.ts +29 -2
- package/src/secrets/obfuscator.ts +102 -27
- package/src/session/agent-session.ts +105 -20
- package/src/session/blob-store.ts +89 -3
- package/src/session/session-manager.ts +309 -58
- package/src/session/streaming-output.ts +185 -122
- package/src/session/tool-choice-queue.ts +23 -0
- package/src/task/executor.ts +69 -6
- package/src/task/receipt.ts +5 -0
- package/src/task/render.ts +21 -1
- package/src/task/types.ts +8 -0
- package/src/thinking-metadata.ts +51 -0
- package/src/thinking.ts +26 -46
- package/src/tools/bash.ts +1 -1
- package/src/tools/index.ts +2 -0
- package/src/tools/resolve.ts +93 -18
- package/src/utils/edit-mode.ts +1 -1
- package/src/utils/tool-choice.ts +45 -16
|
@@ -152,6 +152,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
152
152
|
isError?: boolean;
|
|
153
153
|
details?: any;
|
|
154
154
|
};
|
|
155
|
+
#textOutputCache?: { content: unknown; showImages: boolean; terminalImageProtocol: unknown; output: string };
|
|
155
156
|
// Edit preview state
|
|
156
157
|
#editMode?: EditMode;
|
|
157
158
|
#editDiffPreview?: PerFileDiffPreview[];
|
|
@@ -197,9 +198,11 @@ export class ToolExecutionComponent extends Container {
|
|
|
197
198
|
|
|
198
199
|
this.addChild(new Spacer(1));
|
|
199
200
|
|
|
200
|
-
// Always create both - contentBox for custom tools/bash/tools with renderers, contentText for other built-ins
|
|
201
|
-
|
|
202
|
-
|
|
201
|
+
// Always create both - contentBox for custom tools/bash/tools with renderers, contentText for other built-ins.
|
|
202
|
+
// Vertical padding is 0: block separation comes solely from the leading Spacer
|
|
203
|
+
// (1 blank line above each block), matching reference TUIs (083.2).
|
|
204
|
+
this.#contentBox = new Box(1, 0, (text: string) => theme.bg("toolPendingBg", text));
|
|
205
|
+
this.#contentText = new Text("", 1, 0, (text: string) => theme.bg("toolPendingBg", text));
|
|
203
206
|
|
|
204
207
|
// Use Box for custom tools or built-in tools that have renderers
|
|
205
208
|
const hasRenderer = toolName in toolRenderers;
|
|
@@ -295,6 +298,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
295
298
|
isPartial = false,
|
|
296
299
|
_toolCallId?: string,
|
|
297
300
|
): void {
|
|
301
|
+
this.#textOutputCache = undefined;
|
|
298
302
|
this.#result = result;
|
|
299
303
|
this.#isPartial = isPartial;
|
|
300
304
|
// When tool is complete, ensure args are marked complete so spinner stops
|
|
@@ -397,6 +401,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
397
401
|
|
|
398
402
|
setShowImages(show: boolean): void {
|
|
399
403
|
this.#showImages = show;
|
|
404
|
+
this.#textOutputCache = undefined;
|
|
400
405
|
this.#updateDisplay();
|
|
401
406
|
}
|
|
402
407
|
|
|
@@ -514,7 +519,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
514
519
|
const fileBgFn = fileResult.isError
|
|
515
520
|
? (text: string) => theme.bg("toolErrorBg", text)
|
|
516
521
|
: (text: string) => theme.bg("toolSuccessBg", text);
|
|
517
|
-
const fileBox = new Box(1,
|
|
522
|
+
const fileBox = new Box(1, 0, fileBgFn);
|
|
518
523
|
try {
|
|
519
524
|
const resultComponent = renderer.renderResult(
|
|
520
525
|
{ content: [], details: fileResult, isError: fileResult.isError },
|
|
@@ -540,7 +545,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
540
545
|
const pendingSpacer = new Spacer(1);
|
|
541
546
|
this.#multiFileBoxes.push(pendingSpacer);
|
|
542
547
|
this.addChild(pendingSpacer);
|
|
543
|
-
const pendingBox = new Box(1,
|
|
548
|
+
const pendingBox = new Box(1, 0, (text: string) => theme.bg("toolPendingBg", text));
|
|
544
549
|
const pendingText = renderStatusLine(
|
|
545
550
|
{
|
|
546
551
|
icon: "pending",
|
|
@@ -723,16 +728,27 @@ export class ToolExecutionComponent extends Container {
|
|
|
723
728
|
#getTextOutput(): string {
|
|
724
729
|
if (!this.#result) return "";
|
|
725
730
|
|
|
726
|
-
const
|
|
727
|
-
const
|
|
731
|
+
const content = this.#result.content;
|
|
732
|
+
const terminalImageProtocol = TERMINAL.imageProtocol;
|
|
733
|
+
const cached = this.#textOutputCache;
|
|
734
|
+
if (
|
|
735
|
+
cached?.content === content &&
|
|
736
|
+
cached.showImages === this.#showImages &&
|
|
737
|
+
cached.terminalImageProtocol === terminalImageProtocol
|
|
738
|
+
) {
|
|
739
|
+
return cached.output;
|
|
740
|
+
}
|
|
728
741
|
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
.
|
|
742
|
+
const textParts: string[] = [];
|
|
743
|
+
for (const block of content ?? []) {
|
|
744
|
+
if (block.type !== "text") continue;
|
|
745
|
+
const text = block.text || "";
|
|
746
|
+
textParts.push(sanitizeWithOptionalSixelPassthrough(text, sanitizeText));
|
|
747
|
+
}
|
|
748
|
+
let output = textParts.join("\n");
|
|
734
749
|
|
|
735
|
-
|
|
750
|
+
const imageBlocks = this.#getAllImageBlocks();
|
|
751
|
+
if (imageBlocks.length > 0 && (!terminalImageProtocol || !this.#showImages)) {
|
|
736
752
|
const imageIndicators = imageBlocks
|
|
737
753
|
.map((img: any) => {
|
|
738
754
|
const dims = img.data ? (getImageDimensions(img.data, img.mimeType) ?? undefined) : undefined;
|
|
@@ -742,6 +758,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
742
758
|
output = output ? `${output}\n${imageIndicators}` : imageIndicators;
|
|
743
759
|
}
|
|
744
760
|
|
|
761
|
+
this.#textOutputCache = { content, showImages: this.#showImages, terminalImageProtocol, output };
|
|
745
762
|
return output;
|
|
746
763
|
}
|
|
747
764
|
|
|
@@ -5,6 +5,7 @@ import type { Component, OverlayHandle } from "@gajae-code/tui";
|
|
|
5
5
|
import { Input, Loader, Spacer, Text } from "@gajae-code/tui";
|
|
6
6
|
import { getAgentDbPath, getProjectDir } from "@gajae-code/utils";
|
|
7
7
|
import { activateModelProfile } from "../../config/model-profile-activation";
|
|
8
|
+
import { recommendModelProfileForProvider } from "../../config/model-profiles";
|
|
8
9
|
import { settings } from "../../config/settings";
|
|
9
10
|
import { DebugSelectorComponent } from "../../debug";
|
|
10
11
|
import { disableProvider, enableProvider } from "../../discovery";
|
|
@@ -45,7 +46,7 @@ import { CustomProviderWizardComponent, type CustomProviderWizardSubmit } from "
|
|
|
45
46
|
import { ExtensionDashboard } from "../components/extensions";
|
|
46
47
|
import { HistorySearchComponent } from "../components/history-search";
|
|
47
48
|
import { JobsOverlayComponent } from "../components/jobs-overlay";
|
|
48
|
-
import { ModelSelectorComponent
|
|
49
|
+
import { ModelSelectorComponent } from "../components/model-selector";
|
|
49
50
|
import { OAuthSelectorComponent } from "../components/oauth-selector";
|
|
50
51
|
import { PluginSelectorComponent } from "../components/plugin-selector";
|
|
51
52
|
import {
|
|
@@ -537,12 +538,6 @@ export class SelectorController {
|
|
|
537
538
|
this.ctx.session.scopedModels,
|
|
538
539
|
async selection => {
|
|
539
540
|
try {
|
|
540
|
-
if (selection.kind === "preset") {
|
|
541
|
-
await this.#applyModelAssignmentPreset(selection);
|
|
542
|
-
done();
|
|
543
|
-
this.ctx.ui.requestRender();
|
|
544
|
-
return;
|
|
545
|
-
}
|
|
546
541
|
if (selection.kind === "profile") {
|
|
547
542
|
await activateModelProfile(
|
|
548
543
|
{
|
|
@@ -609,45 +604,12 @@ export class SelectorController {
|
|
|
609
604
|
done();
|
|
610
605
|
this.ctx.ui.requestRender();
|
|
611
606
|
},
|
|
612
|
-
options,
|
|
607
|
+
{ ...options, sessionId: this.ctx.session.sessionId },
|
|
613
608
|
);
|
|
614
609
|
return { component: selector, focus: selector };
|
|
615
610
|
});
|
|
616
611
|
}
|
|
617
612
|
|
|
618
|
-
async #applyModelAssignmentPreset(selection: Extract<ModelSelectorSelection, { kind: "preset" }>): Promise<void> {
|
|
619
|
-
const { assignments, model, preset, selector } = selection;
|
|
620
|
-
const apiKey = await this.ctx.session.modelRegistry.getApiKey(model, this.ctx.session.sessionId);
|
|
621
|
-
if (!apiKey) {
|
|
622
|
-
throw new Error(`No API key for ${model.provider}/${model.id}`);
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
const defaultThinkingLevel = assignments.default;
|
|
626
|
-
await this.ctx.session.setModel(model, "default", {
|
|
627
|
-
selector,
|
|
628
|
-
thinkingLevel: defaultThinkingLevel,
|
|
629
|
-
});
|
|
630
|
-
if (defaultThinkingLevel && defaultThinkingLevel !== ThinkingLevel.Inherit) {
|
|
631
|
-
this.ctx.session.setThinkingLevel(defaultThinkingLevel);
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
const overrides = this.ctx.settings.get("task.agentModelOverrides");
|
|
635
|
-
const nextOverrides = { ...overrides };
|
|
636
|
-
for (const [targetRole, presetThinkingLevel] of Object.entries(assignments) as [
|
|
637
|
-
keyof Extract<ModelSelectorSelection, { kind: "preset" }>["assignments"],
|
|
638
|
-
ThinkingLevel,
|
|
639
|
-
][]) {
|
|
640
|
-
if (!targetRole || targetRole === "default") continue;
|
|
641
|
-
nextOverrides[targetRole] =
|
|
642
|
-
presetThinkingLevel === ThinkingLevel.Inherit ? selector : `${selector}:${presetThinkingLevel}`;
|
|
643
|
-
}
|
|
644
|
-
this.ctx.settings.set("task.agentModelOverrides", nextOverrides);
|
|
645
|
-
this.ctx.settings.getStorage()?.recordModelUsage(`${model.provider}/${model.id}`);
|
|
646
|
-
this.ctx.statusLine.invalidate();
|
|
647
|
-
this.ctx.updateEditorBorderColor();
|
|
648
|
-
this.ctx.showStatus(`${preset.label}: ${selector}`);
|
|
649
|
-
}
|
|
650
|
-
|
|
651
613
|
async showPluginSelector(mode: "install" | "uninstall" = "install"): Promise<void> {
|
|
652
614
|
const mgr = new MarketplaceManager({
|
|
653
615
|
marketplacesRegistryPath: getMarketplacesRegistryPath(),
|
|
@@ -1034,6 +996,34 @@ export class SelectorController {
|
|
|
1034
996
|
await this.showSessionSelector();
|
|
1035
997
|
}
|
|
1036
998
|
|
|
999
|
+
async #handlePostLoginModelProfileRecommendation(providerId: string): Promise<void> {
|
|
1000
|
+
const recommendedProfile = recommendModelProfileForProvider(
|
|
1001
|
+
providerId,
|
|
1002
|
+
this.ctx.session.modelRegistry.getModelProfiles(),
|
|
1003
|
+
);
|
|
1004
|
+
if (!recommendedProfile) {
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
const activeProfile = this.ctx.session.getActiveModelProfile?.() ?? this.ctx.settings.get("modelProfile.default");
|
|
1009
|
+
if (activeProfile) {
|
|
1010
|
+
this.ctx.showStatus(`Preset ${recommendedProfile.name} is available in /model.`);
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
const confirmed = await this.ctx.showHookConfirm(`Apply ${recommendedProfile.name} now?`, "");
|
|
1015
|
+
if (!confirmed) {
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
await activateModelProfile({
|
|
1020
|
+
session: this.ctx.session,
|
|
1021
|
+
modelRegistry: this.ctx.session.modelRegistry,
|
|
1022
|
+
settings: this.ctx.settings,
|
|
1023
|
+
profileName: recommendedProfile.name,
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1037
1027
|
async #handleOAuthLogin(providerId: string): Promise<void> {
|
|
1038
1028
|
this.ctx.showStatus(`Logging in to ${providerId}…`);
|
|
1039
1029
|
const manualInput = this.ctx.oauthManualInput;
|
|
@@ -1090,6 +1080,7 @@ export class SelectorController {
|
|
|
1090
1080
|
new Text(theme.fg("success", `${theme.status.success} Successfully logged in to ${providerId}`), 1, 0),
|
|
1091
1081
|
);
|
|
1092
1082
|
this.ctx.chatContainer.addChild(new Text(theme.fg("dim", `Credentials saved to ${getAgentDbPath()}`), 1, 0));
|
|
1083
|
+
await this.#handlePostLoginModelProfileRecommendation(providerId);
|
|
1093
1084
|
this.ctx.ui.requestRender();
|
|
1094
1085
|
} catch (error: unknown) {
|
|
1095
1086
|
this.ctx.showError(`Login failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -1120,7 +1111,7 @@ export class SelectorController {
|
|
|
1120
1111
|
async showOAuthSelector(mode: "login" | "logout", providerId?: string): Promise<void> {
|
|
1121
1112
|
if (providerId) {
|
|
1122
1113
|
const oauthProvider = getOAuthProviders().find(provider => provider.id === providerId);
|
|
1123
|
-
if (!oauthProvider) {
|
|
1114
|
+
if (!oauthProvider && !this.ctx.session.modelRegistry.getModelProfiles().has(providerId)) {
|
|
1124
1115
|
this.ctx.showError(`Unknown OAuth provider: ${providerId}`);
|
|
1125
1116
|
return;
|
|
1126
1117
|
}
|
|
@@ -12,6 +12,7 @@ import type { SessionStats } from "../../session/agent-session";
|
|
|
12
12
|
import type {
|
|
13
13
|
RpcCommand,
|
|
14
14
|
RpcExtensionUIRequest,
|
|
15
|
+
RpcGetStateInclude,
|
|
15
16
|
RpcHandoffResult,
|
|
16
17
|
RpcHostToolCallRequest,
|
|
17
18
|
RpcHostToolCancelRequest,
|
|
@@ -442,8 +443,8 @@ export class RpcClient {
|
|
|
442
443
|
/**
|
|
443
444
|
* Get current session state.
|
|
444
445
|
*/
|
|
445
|
-
async getState(): Promise<RpcSessionState> {
|
|
446
|
-
const response = await this.#send({ type: "get_state" });
|
|
446
|
+
async getState(include?: RpcGetStateInclude[]): Promise<RpcSessionState> {
|
|
447
|
+
const response = await this.#send(include ? { type: "get_state", include } : { type: "get_state" });
|
|
447
448
|
return this.#getData(response);
|
|
448
449
|
}
|
|
449
450
|
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* - Extension UI: Extension UI requests are emitted, client responds with extension_ui_response
|
|
12
12
|
*/
|
|
13
13
|
import * as path from "node:path";
|
|
14
|
-
import { $env,
|
|
14
|
+
import { $env, readLines, Snowflake } from "@gajae-code/utils";
|
|
15
15
|
import type {
|
|
16
16
|
ExtensionUIContext,
|
|
17
17
|
ExtensionUIDialogOptions,
|
|
@@ -169,6 +169,7 @@ export async function runRpcMode(
|
|
|
169
169
|
process.stdout.write(`${JSON.stringify(obj)}\n`);
|
|
170
170
|
};
|
|
171
171
|
const emitRpcTitles = shouldEmitRpcTitles();
|
|
172
|
+
const decodeError = (err: unknown): string => (err instanceof Error ? err.message : String(err));
|
|
172
173
|
|
|
173
174
|
const pendingExtensionRequests = new Map<string, PendingExtensionRequest>();
|
|
174
175
|
const hostToolBridge = new RpcHostToolBridge(output);
|
|
@@ -229,6 +230,28 @@ export async function runRpcMode(
|
|
|
229
230
|
|
|
230
231
|
// Shutdown request flag (wrapped in object to allow mutation with const)
|
|
231
232
|
const shutdownState = { requested: false };
|
|
233
|
+
let shutdownStarted = false;
|
|
234
|
+
async function shutdown(exitCode: number, reason: string): Promise<never> {
|
|
235
|
+
if (shutdownStarted) {
|
|
236
|
+
process.exit(exitCode);
|
|
237
|
+
}
|
|
238
|
+
shutdownStarted = true;
|
|
239
|
+
hostToolBridge.rejectAllPending(`${reason} before host tool execution completed`);
|
|
240
|
+
hostUriBridge.clear(`${reason} before host URI request completed`);
|
|
241
|
+
try {
|
|
242
|
+
await session.sessionManager.ensureOnDisk();
|
|
243
|
+
} catch (err) {
|
|
244
|
+
output(error(undefined, "shutdown", decodeError(err)));
|
|
245
|
+
process.exit(1);
|
|
246
|
+
}
|
|
247
|
+
try {
|
|
248
|
+
await session.dispose();
|
|
249
|
+
} catch (err) {
|
|
250
|
+
output(error(undefined, "shutdown", decodeError(err)));
|
|
251
|
+
process.exit(1);
|
|
252
|
+
}
|
|
253
|
+
process.exit(exitCode);
|
|
254
|
+
}
|
|
232
255
|
|
|
233
256
|
/**
|
|
234
257
|
* Extension UI context that uses the RPC protocol.
|
|
@@ -497,16 +520,24 @@ export async function runRpcMode(
|
|
|
497
520
|
*/
|
|
498
521
|
async function checkShutdownRequested(): Promise<void> {
|
|
499
522
|
if (!shutdownState.requested) return;
|
|
523
|
+
await shutdown(0, "RPC shutdown requested");
|
|
524
|
+
}
|
|
500
525
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
526
|
+
// Listen for JSONL input using Bun's stdin. Parse frame-by-frame so a malformed
|
|
527
|
+
// command reports a parse error without poisoning the whole long-lived RPC session.
|
|
528
|
+
const inputDecoder = new TextDecoder("utf-8", { fatal: false });
|
|
529
|
+
for await (const line of readLines(Bun.stdin.stream())) {
|
|
530
|
+
const text = inputDecoder.decode(line).trim();
|
|
531
|
+
if (!text) continue;
|
|
504
532
|
|
|
505
|
-
|
|
506
|
-
|
|
533
|
+
let parsed: unknown;
|
|
534
|
+
try {
|
|
535
|
+
parsed = JSON.parse(text);
|
|
536
|
+
} catch (err) {
|
|
537
|
+
output(error(undefined, "parse", `Failed to parse command: ${decodeError(err)}`));
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
507
540
|
|
|
508
|
-
// Listen for JSON input using Bun's stdin
|
|
509
|
-
for await (const parsed of readJsonl(Bun.stdin.stream())) {
|
|
510
541
|
try {
|
|
511
542
|
// Handle extension UI responses
|
|
512
543
|
if ((parsed as RpcExtensionUIResponse).type === "extension_ui_response") {
|
|
@@ -540,13 +571,12 @@ export async function runRpcMode(
|
|
|
540
571
|
|
|
541
572
|
// Check for deferred shutdown request (idle between commands)
|
|
542
573
|
await checkShutdownRequested();
|
|
543
|
-
} catch (
|
|
544
|
-
output(error(undefined, "parse", `Failed to parse command: ${
|
|
574
|
+
} catch (err) {
|
|
575
|
+
output(error(undefined, "parse", `Failed to parse command: ${decodeError(err)}`));
|
|
545
576
|
}
|
|
546
577
|
}
|
|
547
578
|
|
|
548
|
-
// stdin closed — RPC client is gone, exit cleanly
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
process.exit(0);
|
|
579
|
+
// stdin closed — RPC client is gone, flush durable state and exit cleanly
|
|
580
|
+
await shutdown(0, "RPC client disconnected");
|
|
581
|
+
throw new Error("RPC shutdown returned unexpectedly");
|
|
552
582
|
}
|
|
@@ -12,6 +12,8 @@ import type { ContextUsage } from "../../extensibility/extensions/types";
|
|
|
12
12
|
import type { SessionStats } from "../../session/agent-session";
|
|
13
13
|
import type { TodoPhase } from "../../tools/todo-write";
|
|
14
14
|
|
|
15
|
+
export type RpcGetStateInclude = "tools" | "dumpTools" | "systemPrompt";
|
|
16
|
+
|
|
15
17
|
// ============================================================================
|
|
16
18
|
// RPC Commands (stdin)
|
|
17
19
|
// ============================================================================
|
|
@@ -26,7 +28,7 @@ export type RpcCommand =
|
|
|
26
28
|
| { id?: string; type: "new_session"; parentSession?: string }
|
|
27
29
|
|
|
28
30
|
// State
|
|
29
|
-
| { id?: string; type: "get_state" }
|
|
31
|
+
| { id?: string; type: "get_state"; include?: RpcGetStateInclude[] }
|
|
30
32
|
| { id?: string; type: "set_todos"; phases: TodoPhase[] }
|
|
31
33
|
| { id?: string; type: "set_host_tools"; tools: RpcHostToolDefinition[] }
|
|
32
34
|
| { id?: string; type: "set_host_uri_schemes"; schemes: RpcHostUriSchemeDefinition[] }
|
|
@@ -99,8 +101,9 @@ export interface RpcSessionState {
|
|
|
99
101
|
messageCount: number;
|
|
100
102
|
queuedMessageCount: number;
|
|
101
103
|
todoPhases: TodoPhase[];
|
|
102
|
-
/**
|
|
104
|
+
/** Optional static system prompt blocks. Omitted by default; request with get_state include ["systemPrompt"]. */
|
|
103
105
|
systemPrompt?: string[];
|
|
106
|
+
/** Optional static tool schemas. Omitted by default; request with get_state include ["tools"]. */
|
|
104
107
|
dumpTools?: Array<{ name: string; description: string; parameters: unknown }>;
|
|
105
108
|
/** Current context window usage. Null tokens/percent when unknown (e.g. right after compaction). */
|
|
106
109
|
contextUsage?: ContextUsage;
|
|
@@ -183,14 +183,19 @@ export async function dispatchRpcCommand(
|
|
|
183
183
|
messageCount: session.messages.length,
|
|
184
184
|
queuedMessageCount: session.queuedMessageCount,
|
|
185
185
|
todoPhases: session.getTodoPhases(),
|
|
186
|
-
|
|
187
|
-
|
|
186
|
+
contextUsage: session.getContextUsage(),
|
|
187
|
+
};
|
|
188
|
+
const include = new Set(command.include ?? []);
|
|
189
|
+
if (include.has("systemPrompt")) {
|
|
190
|
+
state.systemPrompt = session.systemPrompt;
|
|
191
|
+
}
|
|
192
|
+
if (include.has("tools") || include.has("dumpTools")) {
|
|
193
|
+
state.dumpTools = session.agent.state.tools.map(tool => ({
|
|
188
194
|
name: tool.name,
|
|
189
195
|
description: tool.description,
|
|
190
196
|
parameters: tool.parameters,
|
|
191
|
-
}))
|
|
192
|
-
|
|
193
|
-
};
|
|
197
|
+
}));
|
|
198
|
+
}
|
|
194
199
|
return rpcSuccess(id, "get_state", state);
|
|
195
200
|
}
|
|
196
201
|
|
|
@@ -28,6 +28,15 @@ function stringArray(value: unknown): value is string[] {
|
|
|
28
28
|
return Array.isArray(value) && value.every(item => typeof item === "string");
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
const GET_STATE_INCLUDES = new Set(["tools", "dumpTools", "systemPrompt"]);
|
|
32
|
+
|
|
33
|
+
function optionalGetStateInclude(value: unknown): boolean {
|
|
34
|
+
return (
|
|
35
|
+
value === undefined ||
|
|
36
|
+
(Array.isArray(value) && value.every(item => typeof item === "string" && GET_STATE_INCLUDES.has(item)))
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
31
40
|
function todoPhase(value: unknown): boolean {
|
|
32
41
|
if (!isRecord(value) || typeof value.name !== "string" || !Array.isArray(value.tasks)) return false;
|
|
33
42
|
return value.tasks.every(
|
|
@@ -96,7 +105,9 @@ export function isRpcCommand(value: unknown): value is RpcCommand {
|
|
|
96
105
|
case "follow_up":
|
|
97
106
|
return stringField(value, "message") && optionalArray(value.images);
|
|
98
107
|
case "abort":
|
|
108
|
+
return true;
|
|
99
109
|
case "get_state":
|
|
110
|
+
return optionalGetStateInclude(value.include);
|
|
100
111
|
case "cycle_model":
|
|
101
112
|
case "get_available_models":
|
|
102
113
|
case "cycle_thinking_level":
|
package/src/sdk.ts
CHANGED
|
@@ -133,7 +133,7 @@ import { ToolContextStore } from "./tools/context";
|
|
|
133
133
|
import { getImageGenTools } from "./tools/image-gen";
|
|
134
134
|
import { wrapToolWithMetaNotice } from "./tools/output-meta";
|
|
135
135
|
import { EventBus } from "./utils/event-bus";
|
|
136
|
-
import { buildNamedToolChoice } from "./utils/tool-choice";
|
|
136
|
+
import { buildNamedToolChoice, buildNamedToolChoiceResult } from "./utils/tool-choice";
|
|
137
137
|
import { buildWorkspaceTree, type WorkspaceTree } from "./workspace-tree";
|
|
138
138
|
|
|
139
139
|
type AsyncResultEntry = {
|
|
@@ -234,6 +234,8 @@ export interface CreateAgentSessionOptions {
|
|
|
234
234
|
modelPattern?: string;
|
|
235
235
|
/** Thinking selector. Default: from settings, else unset */
|
|
236
236
|
thinkingLevel?: ThinkingLevel;
|
|
237
|
+
/** Runtime substitution metadata for the initial model_change session event. */
|
|
238
|
+
modelSubstitution?: { requestedModel: Model; reason: string };
|
|
237
239
|
/** Models available for cycling (Ctrl+P in interactive mode) */
|
|
238
240
|
scopedModels?: ScopedModelSelection[];
|
|
239
241
|
|
|
@@ -1212,6 +1214,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1212
1214
|
const m = session.model;
|
|
1213
1215
|
return m ? buildNamedToolChoice(name, m) : undefined;
|
|
1214
1216
|
},
|
|
1217
|
+
buildToolChoiceResult: name => buildNamedToolChoiceResult(name, session.model),
|
|
1215
1218
|
steer: msg =>
|
|
1216
1219
|
session.agent.steer({
|
|
1217
1220
|
role: "custom",
|
|
@@ -1898,6 +1901,19 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1898
1901
|
}
|
|
1899
1902
|
return result;
|
|
1900
1903
|
},
|
|
1904
|
+
onToolChoiceIncapability: event => {
|
|
1905
|
+
const droppedLabel = session?.toolChoiceQueue.degradeInFlight(event.reason);
|
|
1906
|
+
logger.debug("Dropped in-flight tool choice after runtime incapability", {
|
|
1907
|
+
droppedLabel,
|
|
1908
|
+
api: event.api,
|
|
1909
|
+
provider: event.provider,
|
|
1910
|
+
model: event.model,
|
|
1911
|
+
requestedLevel: event.requestedLevel,
|
|
1912
|
+
resolvedLevel: event.resolvedLevel,
|
|
1913
|
+
reason: event.reason,
|
|
1914
|
+
registryKey: event.registryKey,
|
|
1915
|
+
});
|
|
1916
|
+
},
|
|
1901
1917
|
intentTracing: !!intentField,
|
|
1902
1918
|
getToolChoice: () => session?.nextToolChoice(),
|
|
1903
1919
|
telemetry: options.telemetry,
|
|
@@ -1912,7 +1928,18 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1912
1928
|
} else {
|
|
1913
1929
|
// Save initial model, thinking level, and service tier for new sessions so they can be restored on resume.
|
|
1914
1930
|
if (model) {
|
|
1915
|
-
|
|
1931
|
+
const substitution = options.modelSubstitution;
|
|
1932
|
+
sessionManager.appendModelChange(
|
|
1933
|
+
`${model.provider}/${model.id}`,
|
|
1934
|
+
undefined,
|
|
1935
|
+
substitution
|
|
1936
|
+
? {
|
|
1937
|
+
previousModel: `${substitution.requestedModel.provider}/${substitution.requestedModel.id}`,
|
|
1938
|
+
reason: substitution.reason,
|
|
1939
|
+
thinkingLevel: thinkingLevel ?? null,
|
|
1940
|
+
}
|
|
1941
|
+
: undefined,
|
|
1942
|
+
);
|
|
1916
1943
|
}
|
|
1917
1944
|
sessionManager.appendThinkingLevelChange(thinkingLevel);
|
|
1918
1945
|
if (initialServiceTier) {
|
|
@@ -73,9 +73,24 @@ export class SecretObfuscator {
|
|
|
73
73
|
/** Replace-mode plain mappings: secret → replacement */
|
|
74
74
|
#replaceMappings = new Map<string, string>();
|
|
75
75
|
|
|
76
|
+
/** Replace-mode plain mappings sorted longest-first for deterministic longest-match replacement. */
|
|
77
|
+
#sortedReplaceMappings: Array<{ secret: string; replacement: string }> = [];
|
|
78
|
+
|
|
79
|
+
/** Obfuscate-mode plain and regex-discovered mappings sorted longest-first. */
|
|
80
|
+
#sortedObfuscateMappings: Array<{ secret: string; index: number; placeholder: string }> = [];
|
|
81
|
+
|
|
82
|
+
/** Reverse lookup for obfuscate-mode secrets to avoid scanning mappings. */
|
|
83
|
+
#obfuscateIndexBySecret = new Map<string, number>();
|
|
84
|
+
|
|
76
85
|
/** Reverse lookup for deobfuscation: placeholder → secret */
|
|
77
86
|
#deobfuscateMap = new Map<string, string>();
|
|
78
87
|
|
|
88
|
+
/** Combined plain-secret regex cache for single-pass replacement. */
|
|
89
|
+
#combinedPlainRegex: RegExp | undefined;
|
|
90
|
+
#combinedPlainReplacementBySecret = new Map<string, string>();
|
|
91
|
+
#combinedPlainRegexDirty = true;
|
|
92
|
+
#useSequentialPlainReplacement = false;
|
|
93
|
+
|
|
79
94
|
/** Next available index for regex match discoveries */
|
|
80
95
|
#nextIndex: number;
|
|
81
96
|
|
|
@@ -93,6 +108,7 @@ export class SecretObfuscator {
|
|
|
93
108
|
this.#plainMappings.set(entry.content, index);
|
|
94
109
|
this.#obfuscateMappings.set(index, { secret: entry.content, placeholder });
|
|
95
110
|
this.#deobfuscateMap.set(placeholder, entry.content);
|
|
111
|
+
this.#obfuscateIndexBySecret.set(entry.content, index);
|
|
96
112
|
index++;
|
|
97
113
|
} else {
|
|
98
114
|
// replace mode
|
|
@@ -111,6 +127,16 @@ export class SecretObfuscator {
|
|
|
111
127
|
}
|
|
112
128
|
|
|
113
129
|
this.#nextIndex = index;
|
|
130
|
+
this.#sortedReplaceMappings = [...this.#replaceMappings]
|
|
131
|
+
.sort((a, b) => b[0].length - a[0].length)
|
|
132
|
+
.map(([secret, replacement]) => ({ secret, replacement }));
|
|
133
|
+
this.#sortedObfuscateMappings = [...this.#plainMappings]
|
|
134
|
+
.sort((a, b) => b[0].length - a[0].length)
|
|
135
|
+
.map(([secret, mappingIndex]) => ({
|
|
136
|
+
secret,
|
|
137
|
+
index: mappingIndex,
|
|
138
|
+
placeholder: this.#obfuscateMappings.get(mappingIndex)!.placeholder,
|
|
139
|
+
}));
|
|
114
140
|
this.#hasAny = entries.length > 0;
|
|
115
141
|
}
|
|
116
142
|
|
|
@@ -121,18 +147,7 @@ export class SecretObfuscator {
|
|
|
121
147
|
/** Obfuscate all secrets in text. Bidirectional placeholders for obfuscate mode, one-way for replace. */
|
|
122
148
|
obfuscate(text: string): string {
|
|
123
149
|
if (!this.#hasAny) return text;
|
|
124
|
-
let result = text;
|
|
125
|
-
|
|
126
|
-
// 1. Process replace-mode plain secrets
|
|
127
|
-
for (const [secret, replacement] of [...this.#replaceMappings].sort((a, b) => b[0].length - a[0].length)) {
|
|
128
|
-
result = replaceAll(result, secret, replacement);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// 2. Process obfuscate-mode plain secrets
|
|
132
|
-
for (const [secret, index] of [...this.#plainMappings].sort((a, b) => b[0].length - a[0].length)) {
|
|
133
|
-
const mapping = this.#obfuscateMappings.get(index)!;
|
|
134
|
-
result = replaceAll(result, secret, mapping.placeholder);
|
|
135
|
-
}
|
|
150
|
+
let result = this.#obfuscatePlainMappings(text);
|
|
136
151
|
|
|
137
152
|
// 3. Process regex entries — discover new matches
|
|
138
153
|
for (const entry of this.#regexEntries) {
|
|
@@ -160,6 +175,9 @@ export class SecretObfuscator {
|
|
|
160
175
|
const placeholder = buildPlaceholder(index);
|
|
161
176
|
this.#obfuscateMappings.set(index, { secret: matchValue, placeholder });
|
|
162
177
|
this.#deobfuscateMap.set(placeholder, matchValue);
|
|
178
|
+
this.#obfuscateIndexBySecret.set(matchValue, index);
|
|
179
|
+
this.#insertSortedObfuscateMapping({ secret: matchValue, index, placeholder });
|
|
180
|
+
this.#combinedPlainRegexDirty = true;
|
|
163
181
|
}
|
|
164
182
|
const mapping = this.#obfuscateMappings.get(index)!;
|
|
165
183
|
result = replaceAll(result, matchValue, mapping.placeholder);
|
|
@@ -186,15 +204,74 @@ export class SecretObfuscator {
|
|
|
186
204
|
|
|
187
205
|
/** Find the obfuscate index for a known secret value. */
|
|
188
206
|
#findObfuscateIndex(secret: string): number | undefined {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
207
|
+
return this.#obfuscateIndexBySecret.get(secret);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
#insertSortedObfuscateMapping(mapping: { secret: string; index: number; placeholder: string }): void {
|
|
211
|
+
let lo = 0;
|
|
212
|
+
let hi = this.#sortedObfuscateMappings.length;
|
|
213
|
+
while (lo < hi) {
|
|
214
|
+
const mid = (lo + hi) >> 1;
|
|
215
|
+
if (this.#sortedObfuscateMappings[mid]!.secret.length < mapping.secret.length) {
|
|
216
|
+
hi = mid;
|
|
217
|
+
} else {
|
|
218
|
+
lo = mid + 1;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
this.#sortedObfuscateMappings.splice(lo, 0, mapping);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
#obfuscatePlainMappings(text: string): string {
|
|
225
|
+
this.#ensureCombinedPlainRegex();
|
|
226
|
+
if (this.#useSequentialPlainReplacement) return this.#obfuscatePlainMappingsSequential(text);
|
|
227
|
+
if (!this.#combinedPlainRegex) return text;
|
|
228
|
+
return text.replace(
|
|
229
|
+
this.#combinedPlainRegex,
|
|
230
|
+
match => this.#combinedPlainReplacementBySecret.get(match) ?? match,
|
|
231
|
+
);
|
|
232
|
+
}
|
|
192
233
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
234
|
+
#obfuscatePlainMappingsSequential(text: string): string {
|
|
235
|
+
let result = text;
|
|
236
|
+
for (const mapping of this.#sortedReplaceMappings) {
|
|
237
|
+
result = replaceAll(result, mapping.secret, mapping.replacement);
|
|
238
|
+
}
|
|
239
|
+
for (const mapping of this.#sortedObfuscateMappings) {
|
|
240
|
+
result = replaceAll(result, mapping.secret, mapping.placeholder);
|
|
196
241
|
}
|
|
197
|
-
return
|
|
242
|
+
return result;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
#ensureCombinedPlainRegex(): void {
|
|
246
|
+
if (!this.#combinedPlainRegexDirty) return;
|
|
247
|
+
this.#combinedPlainRegexDirty = false;
|
|
248
|
+
this.#combinedPlainReplacementBySecret = new Map<string, string>();
|
|
249
|
+
|
|
250
|
+
const mappings = [
|
|
251
|
+
...this.#sortedReplaceMappings.map(mapping => ({ secret: mapping.secret, replacement: mapping.replacement })),
|
|
252
|
+
...this.#sortedObfuscateMappings.map(mapping => ({
|
|
253
|
+
secret: mapping.secret,
|
|
254
|
+
replacement: mapping.placeholder,
|
|
255
|
+
})),
|
|
256
|
+
];
|
|
257
|
+
|
|
258
|
+
this.#useSequentialPlainReplacement = mappings.some((mapping, index) =>
|
|
259
|
+
mappings.some(
|
|
260
|
+
(other, otherIndex) =>
|
|
261
|
+
other.secret.length > 0 &&
|
|
262
|
+
(mapping.replacement.includes(other.secret) ||
|
|
263
|
+
(index !== otherIndex &&
|
|
264
|
+
(mapping.secret.includes(other.secret) || other.secret.includes(mapping.secret)))),
|
|
265
|
+
),
|
|
266
|
+
);
|
|
267
|
+
for (const mapping of mappings) {
|
|
268
|
+
if (!this.#combinedPlainReplacementBySecret.has(mapping.secret))
|
|
269
|
+
this.#combinedPlainReplacementBySecret.set(mapping.secret, mapping.replacement);
|
|
270
|
+
}
|
|
271
|
+
this.#combinedPlainRegex =
|
|
272
|
+
mappings.length > 0
|
|
273
|
+
? new RegExp(mappings.map(mapping => escapeRegex(mapping.secret)).join("|"), "g")
|
|
274
|
+
: undefined;
|
|
198
275
|
}
|
|
199
276
|
}
|
|
200
277
|
|
|
@@ -238,14 +315,12 @@ export function obfuscateMessages(obfuscator: SecretObfuscator, messages: Messag
|
|
|
238
315
|
|
|
239
316
|
/** Replace all occurrences of `search` in `text` with `replacement`. */
|
|
240
317
|
function replaceAll(text: string, search: string, replacement: string): string {
|
|
241
|
-
if (search.length === 0) return text;
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
}
|
|
248
|
-
return result;
|
|
318
|
+
if (search.length === 0 || !text.includes(search)) return text;
|
|
319
|
+
return text.split(search).join(replacement);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function escapeRegex(value: string): string {
|
|
323
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
249
324
|
}
|
|
250
325
|
|
|
251
326
|
/** Deep-walk an object, transforming all string values. */
|