@oh-my-pi/pi-coding-agent 15.7.5 → 15.7.6
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 +30 -1
- package/dist/types/config/settings-schema.d.ts +9 -0
- package/dist/types/eval/__tests__/heartbeat.test.d.ts +1 -0
- package/dist/types/eval/heartbeat.d.ts +45 -0
- package/dist/types/extensibility/extensions/loader.d.ts +2 -2
- package/dist/types/extensibility/extensions/runner.d.ts +2 -1
- package/dist/types/extensibility/extensions/types.d.ts +24 -5
- package/dist/types/lsp/diagnostics-ledger.d.ts +10 -0
- package/dist/types/lsp/index.d.ts +2 -0
- package/dist/types/lsp/utils.d.ts +4 -0
- package/dist/types/modes/acp/acp-agent.d.ts +1 -1
- package/dist/types/modes/components/assistant-message.d.ts +3 -1
- package/dist/types/modes/components/custom-editor.d.ts +0 -1
- package/dist/types/modes/components/hook-selector.d.ts +7 -1
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +2 -2
- package/dist/types/modes/interactive-mode.d.ts +2 -2
- package/dist/types/modes/rpc/rpc-mode.d.ts +1 -1
- package/dist/types/modes/theme/theme.d.ts +1 -1
- package/dist/types/modes/types.d.ts +2 -2
- package/dist/types/session/agent-session.d.ts +8 -1
- package/dist/types/tools/ask.d.ts +8 -6
- package/dist/types/tools/eval-backends.d.ts +12 -0
- package/dist/types/tools/eval-render.d.ts +52 -0
- package/dist/types/tools/eval.d.ts +2 -35
- package/dist/types/tools/index.d.ts +4 -11
- package/dist/types/tui/output-block.d.ts +4 -3
- package/examples/extensions/README.md +1 -0
- package/examples/extensions/thinking-note.ts +13 -0
- package/package.json +9 -9
- package/src/config/model-registry.ts +33 -4
- package/src/config/settings-schema.ts +10 -0
- package/src/discovery/claude.ts +41 -22
- package/src/edit/index.ts +23 -3
- package/src/eval/__tests__/agent-bridge.test.ts +90 -0
- package/src/eval/__tests__/heartbeat.test.ts +84 -0
- package/src/eval/__tests__/llm-bridge.test.ts +30 -0
- package/src/eval/agent-bridge.ts +44 -38
- package/src/eval/heartbeat.ts +74 -0
- package/src/eval/js/executor.ts +13 -9
- package/src/eval/llm-bridge.ts +20 -14
- package/src/eval/py/executor.ts +14 -18
- package/src/exec/bash-executor.ts +31 -5
- package/src/extensibility/extensions/loader.ts +16 -18
- package/src/extensibility/extensions/runner.ts +22 -17
- package/src/extensibility/extensions/types.ts +39 -5
- package/src/internal-urls/docs-index.generated.ts +3 -3
- package/src/lsp/diagnostics-ledger.ts +51 -0
- package/src/lsp/index.ts +9 -22
- package/src/lsp/utils.ts +21 -0
- package/src/modes/acp/acp-agent.ts +8 -4
- package/src/modes/components/assistant-message.ts +28 -1
- package/src/modes/components/custom-editor.ts +9 -7
- package/src/modes/components/hook-selector.ts +159 -32
- package/src/modes/components/tool-execution.ts +20 -4
- package/src/modes/controllers/event-controller.ts +5 -2
- package/src/modes/controllers/extension-ui-controller.ts +3 -2
- package/src/modes/controllers/input-controller.ts +0 -15
- package/src/modes/interactive-mode.ts +2 -1
- package/src/modes/rpc/rpc-mode.ts +17 -6
- package/src/modes/theme/theme-schema.json +30 -0
- package/src/modes/theme/theme.ts +39 -2
- package/src/modes/types.ts +2 -1
- package/src/modes/utils/ui-helpers.ts +5 -2
- package/src/prompts/tools/ask.md +2 -1
- package/src/prompts/tools/eval.md +1 -1
- package/src/session/agent-session.ts +65 -11
- package/src/tools/ask.ts +74 -32
- package/src/tools/eval-backends.ts +38 -0
- package/src/tools/eval-render.ts +750 -0
- package/src/tools/eval.ts +27 -754
- package/src/tools/index.ts +7 -37
- package/src/tools/renderers.ts +1 -1
- package/src/tools/write.ts +9 -1
- package/src/tui/output-block.ts +5 -4
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
ExtensionError,
|
|
11
11
|
ExtensionUIContext,
|
|
12
12
|
ExtensionUIDialogOptions,
|
|
13
|
+
ExtensionUISelectItem,
|
|
13
14
|
ExtensionUiComponent,
|
|
14
15
|
ExtensionWidgetContent,
|
|
15
16
|
ExtensionWidgetOptions,
|
|
@@ -483,7 +484,7 @@ export class ExtensionUiController {
|
|
|
483
484
|
|
|
484
485
|
createBackgroundUiContext(): ExtensionUIContext {
|
|
485
486
|
return {
|
|
486
|
-
select: async (_title: string, _options:
|
|
487
|
+
select: async (_title: string, _options: ExtensionUISelectItem[], _dialogOptions) => undefined,
|
|
487
488
|
confirm: async (_title: string, _message: string, _dialogOptions) => false,
|
|
488
489
|
input: async (_title: string, _placeholder?: string, _dialogOptions?: unknown) => undefined,
|
|
489
490
|
notify: () => {},
|
|
@@ -581,7 +582,7 @@ export class ExtensionUiController {
|
|
|
581
582
|
*/
|
|
582
583
|
showHookSelector(
|
|
583
584
|
title: string,
|
|
584
|
-
options:
|
|
585
|
+
options: ExtensionUISelectItem[],
|
|
585
586
|
dialogOptions?: ExtensionUIDialogOptions,
|
|
586
587
|
extra?: { slider?: HookSelectorSlider },
|
|
587
588
|
): Promise<string | undefined> {
|
|
@@ -86,21 +86,6 @@ export class InputController {
|
|
|
86
86
|
|
|
87
87
|
setupKeyHandlers(): void {
|
|
88
88
|
this.ctx.editor.setActionKeys("app.interrupt", this.ctx.keybindings.getKeys("app.interrupt"));
|
|
89
|
-
this.ctx.editor.shouldBypassAutocompleteOnEscape = () =>
|
|
90
|
-
Boolean(
|
|
91
|
-
this.ctx.loadingAnimation ||
|
|
92
|
-
this.ctx.hasActiveBtw() ||
|
|
93
|
-
this.ctx.hasActiveOmfg() ||
|
|
94
|
-
this.ctx.session.isStreaming ||
|
|
95
|
-
this.ctx.session.isCompacting ||
|
|
96
|
-
this.ctx.session.isGeneratingHandoff ||
|
|
97
|
-
this.ctx.session.isBashRunning ||
|
|
98
|
-
this.ctx.session.isEvalRunning ||
|
|
99
|
-
this.ctx.autoCompactionLoader ||
|
|
100
|
-
this.ctx.retryLoader ||
|
|
101
|
-
this.ctx.autoCompactionEscapeHandler ||
|
|
102
|
-
this.ctx.retryEscapeHandler,
|
|
103
|
-
);
|
|
104
89
|
this.ctx.editor.onEscape = () => {
|
|
105
90
|
if (this.ctx.loopModeEnabled) {
|
|
106
91
|
this.ctx.pauseLoop();
|
|
@@ -40,6 +40,7 @@ import { isSettingsInitialized, Settings, settings } from "../config/settings";
|
|
|
40
40
|
import type {
|
|
41
41
|
ExtensionUIContext,
|
|
42
42
|
ExtensionUIDialogOptions,
|
|
43
|
+
ExtensionUISelectItem,
|
|
43
44
|
ExtensionWidgetContent,
|
|
44
45
|
ExtensionWidgetOptions,
|
|
45
46
|
} from "../extensibility/extensions";
|
|
@@ -2896,7 +2897,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2896
2897
|
|
|
2897
2898
|
showHookSelector(
|
|
2898
2899
|
title: string,
|
|
2899
|
-
options:
|
|
2900
|
+
options: ExtensionUISelectItem[],
|
|
2900
2901
|
dialogOptions?: ExtensionUIDialogOptions,
|
|
2901
2902
|
extra?: { slider?: HookSelectorSlider },
|
|
2902
2903
|
): Promise<string | undefined> {
|
|
@@ -12,10 +12,12 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { getOAuthProviders } from "@oh-my-pi/pi-ai/utils/oauth";
|
|
14
14
|
import { $env, readJsonl, Snowflake } from "@oh-my-pi/pi-utils";
|
|
15
|
-
import
|
|
16
|
-
ExtensionUIContext,
|
|
17
|
-
ExtensionUIDialogOptions,
|
|
18
|
-
|
|
15
|
+
import {
|
|
16
|
+
type ExtensionUIContext,
|
|
17
|
+
type ExtensionUIDialogOptions,
|
|
18
|
+
type ExtensionUISelectItem,
|
|
19
|
+
type ExtensionWidgetOptions,
|
|
20
|
+
getExtensionUISelectOptionLabel,
|
|
19
21
|
} from "../../extensibility/extensions";
|
|
20
22
|
import { type Theme, theme } from "../../modes/theme/theme";
|
|
21
23
|
import type { AgentSession } from "../../session/agent-session";
|
|
@@ -256,11 +258,20 @@ export async function runRpcMode(
|
|
|
256
258
|
return promise;
|
|
257
259
|
}
|
|
258
260
|
|
|
259
|
-
select(
|
|
261
|
+
select(
|
|
262
|
+
title: string,
|
|
263
|
+
options: ExtensionUISelectItem[],
|
|
264
|
+
dialogOptions?: ExtensionUIDialogOptions,
|
|
265
|
+
): Promise<string | undefined> {
|
|
260
266
|
return this.#createDialogPromise(
|
|
261
267
|
dialogOptions,
|
|
262
268
|
undefined,
|
|
263
|
-
{
|
|
269
|
+
{
|
|
270
|
+
method: "select",
|
|
271
|
+
title,
|
|
272
|
+
options: options.map(getExtensionUISelectOptionLabel),
|
|
273
|
+
timeout: dialogOptions?.timeout,
|
|
274
|
+
},
|
|
264
275
|
response => parseValueDialogResponse(response, dialogOptions),
|
|
265
276
|
);
|
|
266
277
|
}
|
|
@@ -404,6 +404,36 @@
|
|
|
404
404
|
"additionalProperties": {
|
|
405
405
|
"type": "string"
|
|
406
406
|
}
|
|
407
|
+
},
|
|
408
|
+
"spinnerFrames": {
|
|
409
|
+
"description": "Override the spinner frames. Use a flat array to set both `status` and `activity`, or an object to override each independently. Frames are advanced ~12.5fps for status spinners and ~60fps for activity spinners.",
|
|
410
|
+
"oneOf": [
|
|
411
|
+
{
|
|
412
|
+
"type": "array",
|
|
413
|
+
"minItems": 1,
|
|
414
|
+
"items": { "type": "string", "minLength": 1 }
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
"type": "object",
|
|
418
|
+
"properties": {
|
|
419
|
+
"status": {
|
|
420
|
+
"type": "array",
|
|
421
|
+
"minItems": 1,
|
|
422
|
+
"items": { "type": "string", "minLength": 1 }
|
|
423
|
+
},
|
|
424
|
+
"activity": {
|
|
425
|
+
"type": "array",
|
|
426
|
+
"minItems": 1,
|
|
427
|
+
"items": { "type": "string", "minLength": 1 }
|
|
428
|
+
}
|
|
429
|
+
},
|
|
430
|
+
"additionalProperties": false,
|
|
431
|
+
"anyOf": [
|
|
432
|
+
{ "required": ["status"] },
|
|
433
|
+
{ "required": ["activity"] }
|
|
434
|
+
]
|
|
435
|
+
}
|
|
436
|
+
]
|
|
407
437
|
}
|
|
408
438
|
},
|
|
409
439
|
"additionalProperties": false
|
package/src/modes/theme/theme.ts
CHANGED
|
@@ -812,6 +812,25 @@ const SPINNER_FRAMES: Record<SymbolPreset, Record<SpinnerType, string[]>> = {
|
|
|
812
812
|
},
|
|
813
813
|
};
|
|
814
814
|
|
|
815
|
+
/**
|
|
816
|
+
* Shape accepted by `themeJson.symbols.spinnerFrames`. A flat array applies to
|
|
817
|
+
* both spinner types; an object lets a theme override `status` and/or
|
|
818
|
+
* `activity` independently. Anything not specified falls back to the symbol
|
|
819
|
+
* preset's default frames.
|
|
820
|
+
*/
|
|
821
|
+
type SpinnerFramesOverride = string[] | { status?: string[]; activity?: string[] };
|
|
822
|
+
|
|
823
|
+
function normalizeSpinnerFramesOverride(
|
|
824
|
+
value: SpinnerFramesOverride | undefined,
|
|
825
|
+
): Partial<Record<SpinnerType, string[]>> {
|
|
826
|
+
if (value === undefined) return {};
|
|
827
|
+
if (Array.isArray(value)) return { status: value, activity: value };
|
|
828
|
+
const result: Partial<Record<SpinnerType, string[]>> = {};
|
|
829
|
+
if (value.status) result.status = value.status;
|
|
830
|
+
if (value.activity) result.activity = value.activity;
|
|
831
|
+
return result;
|
|
832
|
+
}
|
|
833
|
+
|
|
815
834
|
// ============================================================================
|
|
816
835
|
// Types & Schema
|
|
817
836
|
// ============================================================================
|
|
@@ -898,6 +917,19 @@ const themeColorsSchema = z.object(
|
|
|
898
917
|
},
|
|
899
918
|
);
|
|
900
919
|
|
|
920
|
+
const spinnerFramesArraySchema = z.array(z.string().min(1)).min(1);
|
|
921
|
+
const spinnerFramesSchema = z.union([
|
|
922
|
+
spinnerFramesArraySchema,
|
|
923
|
+
z
|
|
924
|
+
.object({
|
|
925
|
+
status: spinnerFramesArraySchema.optional(),
|
|
926
|
+
activity: spinnerFramesArraySchema.optional(),
|
|
927
|
+
})
|
|
928
|
+
.refine(value => value.status !== undefined || value.activity !== undefined, {
|
|
929
|
+
message: "spinnerFrames object must define `status` and/or `activity`",
|
|
930
|
+
}),
|
|
931
|
+
]);
|
|
932
|
+
|
|
901
933
|
const symbolPresetSchema = z.enum(["unicode", "nerd", "ascii"]);
|
|
902
934
|
|
|
903
935
|
const themeJsonSchema = z.object({
|
|
@@ -916,6 +948,7 @@ const themeJsonSchema = z.object({
|
|
|
916
948
|
.object({
|
|
917
949
|
preset: symbolPresetSchema.optional(),
|
|
918
950
|
overrides: z.record(z.string(), z.string()).optional(),
|
|
951
|
+
spinnerFrames: spinnerFramesSchema.optional(),
|
|
919
952
|
})
|
|
920
953
|
.optional(),
|
|
921
954
|
});
|
|
@@ -1243,6 +1276,7 @@ export class Theme {
|
|
|
1243
1276
|
#fgColors: Record<ThemeColor, string>;
|
|
1244
1277
|
#bgColors: Record<ThemeBg, string>;
|
|
1245
1278
|
#symbols: SymbolMap;
|
|
1279
|
+
#spinnerFramesOverrides: Partial<Record<SpinnerType, string[]>>;
|
|
1246
1280
|
|
|
1247
1281
|
constructor(
|
|
1248
1282
|
fgColors: Record<ThemeColor, string | number>,
|
|
@@ -1250,6 +1284,7 @@ export class Theme {
|
|
|
1250
1284
|
private readonly mode: ColorMode,
|
|
1251
1285
|
private readonly symbolPreset: SymbolPreset,
|
|
1252
1286
|
symbolOverrides: Partial<Record<SymbolKey, string>>,
|
|
1287
|
+
spinnerFramesOverrides: Partial<Record<SpinnerType, string[]>> = {},
|
|
1253
1288
|
) {
|
|
1254
1289
|
this.#fgColors = {} as Record<ThemeColor, string>;
|
|
1255
1290
|
for (const [key, value] of Object.entries(fgColors) as [ThemeColor, string | number][]) {
|
|
@@ -1269,6 +1304,7 @@ export class Theme {
|
|
|
1269
1304
|
logger.debug("Invalid symbol key in override", { key, availableKeys: Object.keys(this.#symbols) });
|
|
1270
1305
|
}
|
|
1271
1306
|
}
|
|
1307
|
+
this.#spinnerFramesOverrides = spinnerFramesOverrides;
|
|
1272
1308
|
}
|
|
1273
1309
|
|
|
1274
1310
|
fg(color: ThemeColor, text: string): string {
|
|
@@ -1563,7 +1599,7 @@ export class Theme {
|
|
|
1563
1599
|
* Get spinner frames by type.
|
|
1564
1600
|
*/
|
|
1565
1601
|
getSpinnerFrames(type: SpinnerType = "status"): string[] {
|
|
1566
|
-
return SPINNER_FRAMES[this.symbolPreset][type];
|
|
1602
|
+
return this.#spinnerFramesOverrides[type] ?? SPINNER_FRAMES[this.symbolPreset][type];
|
|
1567
1603
|
}
|
|
1568
1604
|
|
|
1569
1605
|
/**
|
|
@@ -1735,7 +1771,8 @@ function createTheme(themeJson: ThemeJson, options: CreateThemeOptions = {}): Th
|
|
|
1735
1771
|
// Extract symbol configuration - settings override takes precedence over theme
|
|
1736
1772
|
const symbolPreset: SymbolPreset = symbolPresetOverride ?? themeJson.symbols?.preset ?? "unicode";
|
|
1737
1773
|
const symbolOverrides = themeJson.symbols?.overrides ?? {};
|
|
1738
|
-
|
|
1774
|
+
const spinnerFramesOverrides = normalizeSpinnerFramesOverride(themeJson.symbols?.spinnerFrames);
|
|
1775
|
+
return new Theme(fgColors, bgColors, colorMode, symbolPreset, symbolOverrides, spinnerFramesOverrides);
|
|
1739
1776
|
}
|
|
1740
1777
|
|
|
1741
1778
|
async function loadTheme(name: string, options: CreateThemeOptions = {}): Promise<Theme> {
|
package/src/modes/types.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type { Settings } from "../config/settings";
|
|
|
7
7
|
import type {
|
|
8
8
|
ExtensionUIContext,
|
|
9
9
|
ExtensionUIDialogOptions,
|
|
10
|
+
ExtensionUISelectItem,
|
|
10
11
|
ExtensionWidgetContent,
|
|
11
12
|
ExtensionWidgetOptions,
|
|
12
13
|
} from "../extensibility/extensions";
|
|
@@ -297,7 +298,7 @@ export interface InteractiveModeContext {
|
|
|
297
298
|
setHookStatus(key: string, text: string | undefined): void;
|
|
298
299
|
showHookSelector(
|
|
299
300
|
title: string,
|
|
300
|
-
options:
|
|
301
|
+
options: ExtensionUISelectItem[],
|
|
301
302
|
dialogOptions?: ExtensionUIDialogOptions,
|
|
302
303
|
): Promise<string | undefined>;
|
|
303
304
|
hideHookSelector(): void;
|
|
@@ -262,8 +262,11 @@ export class UiHelpers {
|
|
|
262
262
|
break;
|
|
263
263
|
}
|
|
264
264
|
case "assistant": {
|
|
265
|
-
const assistantComponent = new AssistantMessageComponent(
|
|
266
|
-
|
|
265
|
+
const assistantComponent = new AssistantMessageComponent(
|
|
266
|
+
message,
|
|
267
|
+
this.ctx.hideThinkingBlock,
|
|
268
|
+
() => this.ctx.ui.requestRender(),
|
|
269
|
+
this.ctx.session.extensionRunner?.getAssistantThinkingRenderers(),
|
|
267
270
|
);
|
|
268
271
|
this.ctx.chatContainer.addChild(assistantComponent);
|
|
269
272
|
break;
|
package/src/prompts/tools/ask.md
CHANGED
|
@@ -8,6 +8,7 @@ Asks user when you need clarification or input during task execution.
|
|
|
8
8
|
- Use `recommended: <index>` to mark default (0-indexed); " (Recommended)" added automatically
|
|
9
9
|
- Use `questions` for multiple related questions instead of asking one at a time
|
|
10
10
|
- Set `multi: true` on question to allow multiple selections
|
|
11
|
+
- Use short option labels; put explanatory tradeoffs in `description` instead of merging them into the label
|
|
11
12
|
</instruction>
|
|
12
13
|
|
|
13
14
|
<caution>
|
|
@@ -22,7 +23,7 @@ Asks user when you need clarification or input during task execution.
|
|
|
22
23
|
|
|
23
24
|
<examples>
|
|
24
25
|
# Single question
|
|
25
|
-
questions: [{"id": "auth_method", "question": "Which authentication method should this API use?", "options": [{"label": "JWT"}, {"label": "OAuth2"}, {"label": "Session cookies"}], "recommended": 0}]
|
|
26
|
+
questions: [{"id": "auth_method", "question": "Which authentication method should this API use?", "options": [{"label": "JWT", "description": "Bearer tokens for stateless API clients."}, {"label": "OAuth2", "description": "Delegated authorization with external identity providers."}, {"label": "Session cookies", "description": "Browser-first authentication backed by server-side sessions."}], "recommended": 0}]
|
|
26
27
|
|
|
27
28
|
# Multiple questions
|
|
28
29
|
questions: [{"id": "storage_type", "question": "Which storage backend?", "options": [{"label": "SQLite"}, {"label": "PostgreSQL"}]}, {"id": "auth_method", "question": "Which auth method?", "options": [{"label": "JWT"}, {"label": "Session cookies"}]}]
|
|
@@ -8,7 +8,7 @@ Cell fields:
|
|
|
8
8
|
- `language` — {{#if py}}`"py"` for the IPython kernel{{/if}}{{#ifAll py js}}, {{/ifAll}}{{#if js}}`"js"` for the persistent JavaScript VM{{/if}}.
|
|
9
9
|
- `code` — cell body, verbatim. Newlines, quotes, and indentation are JSON-encoded; no fences, no headers.
|
|
10
10
|
- `title` (optional) — short label shown in the transcript (e.g. `"imports"`, `"load config"`).
|
|
11
|
-
- `timeout` (optional) — per-cell
|
|
11
|
+
- `timeout` (optional) — per-cell wall-clock budget in seconds (1-600). Default 30. It bounds the cell's **own** work, but is paused while an `agent()`/`parallel()`/`llm()` call is in flight — so a long fanout or a slow completion runs to completion, while the cell itself is still bounded. Compute, `print`/stdout, `log()`/`phase()`, and ordinary tool calls all count against the budget; raise `timeout` for a cell that does heavy local work or long non-agent tool calls.
|
|
12
12
|
- `reset` (optional) — wipe this cell's language kernel before running.{{#ifAll py js}} Reset is per-language: a `py` cell's reset does not touch the JavaScript VM and vice versa.{{/ifAll}}
|
|
13
13
|
|
|
14
14
|
**Work incrementally:**
|
|
@@ -159,6 +159,7 @@ import { containsOrchestrate, ORCHESTRATE_NOTICE } from "../modes/orchestrate";
|
|
|
159
159
|
import { getCurrentThemeName, theme } from "../modes/theme/theme";
|
|
160
160
|
import { parseTurnBudget } from "../modes/turn-budget";
|
|
161
161
|
import { containsUltrathink, ULTRATHINK_NOTICE } from "../modes/ultrathink";
|
|
162
|
+
import { computeNonMessageTokens } from "../modes/utils/context-usage";
|
|
162
163
|
import { containsWorkflow, WORKFLOW_NOTICE } from "../modes/workflow";
|
|
163
164
|
import type { PlanModeState } from "../plan-mode/state";
|
|
164
165
|
import autoContinuePrompt from "../prompts/system/auto-continue.md" with { type: "text" };
|
|
@@ -3617,8 +3618,13 @@ export class AgentSession {
|
|
|
3617
3618
|
/**
|
|
3618
3619
|
* Replace MCP tools in the registry and recompute the visible MCP tool set immediately.
|
|
3619
3620
|
* This allows /mcp add/remove/reauth to take effect without restarting the session.
|
|
3621
|
+
*
|
|
3622
|
+
* @param mcpTools The new MCP tools to register.
|
|
3623
|
+
* @param options.activateAll When true, force-activates every newly registered MCP tool
|
|
3624
|
+
* regardless of prior selection state. Used when an ACP client provisions MCP servers
|
|
3625
|
+
* for a session where MCP discovery is disabled.
|
|
3620
3626
|
*/
|
|
3621
|
-
async refreshMCPTools(mcpTools: CustomTool[]): Promise<void> {
|
|
3627
|
+
async refreshMCPTools(mcpTools: CustomTool[], options?: { activateAll?: boolean }): Promise<void> {
|
|
3622
3628
|
const previousSelectedMCPToolNames = this.getSelectedMCPToolNames();
|
|
3623
3629
|
const existingNames = Array.from(this.#toolRegistry.keys());
|
|
3624
3630
|
for (const name of existingNames) {
|
|
@@ -3659,6 +3665,18 @@ export class AgentSession {
|
|
|
3659
3665
|
this.#getConfiguredDefaultSelectedMCPToolNames(),
|
|
3660
3666
|
);
|
|
3661
3667
|
|
|
3668
|
+
if (options?.activateAll) {
|
|
3669
|
+
// Force-activate every newly registered MCP tool. This path is used
|
|
3670
|
+
// when an ACP client provisions MCP servers for a session where MCP
|
|
3671
|
+
// discovery is disabled — without it, getSelectedMCPToolNames()
|
|
3672
|
+
// returns only already-active tools (circular deadlock: tools can
|
|
3673
|
+
// only become active if they're already active).
|
|
3674
|
+
const newMcpNames = mcpTools.map(t => t.name);
|
|
3675
|
+
const nextActive = [...new Set([...this.#getActiveNonMCPToolNames(), ...newMcpNames])];
|
|
3676
|
+
await this.#applyActiveToolsByName(nextActive, { previousSelectedMCPToolNames });
|
|
3677
|
+
return;
|
|
3678
|
+
}
|
|
3679
|
+
|
|
3662
3680
|
const nextActive = [...this.#getActiveNonMCPToolNames(), ...this.getSelectedMCPToolNames()];
|
|
3663
3681
|
await this.#applyActiveToolsByName(nextActive, { previousSelectedMCPToolNames });
|
|
3664
3682
|
}
|
|
@@ -4279,7 +4297,7 @@ export class AgentSession {
|
|
|
4279
4297
|
// next microtask alongside the new turn.
|
|
4280
4298
|
const lastAssistant = this.#findLastAssistantMessage();
|
|
4281
4299
|
if (lastAssistant && !options?.skipCompactionCheck) {
|
|
4282
|
-
await this.#checkCompaction(lastAssistant, false, false);
|
|
4300
|
+
await this.#checkCompaction(lastAssistant, false, false, false);
|
|
4283
4301
|
}
|
|
4284
4302
|
|
|
4285
4303
|
// Build messages array (session context, eager todo prelude, then active prompt message)
|
|
@@ -4381,6 +4399,11 @@ export class AgentSession {
|
|
|
4381
4399
|
}
|
|
4382
4400
|
}
|
|
4383
4401
|
|
|
4402
|
+
await this.#runPrePromptCompactionIfNeeded(messages);
|
|
4403
|
+
if (this.#promptGeneration !== generation) {
|
|
4404
|
+
return;
|
|
4405
|
+
}
|
|
4406
|
+
|
|
4384
4407
|
const agentPromptOptions = options?.toolChoice ? { toolChoice: options.toolChoice } : undefined;
|
|
4385
4408
|
await this.#promptAgentWithIdleRetry(messages, agentPromptOptions);
|
|
4386
4409
|
if (!options?.skipPostPromptRecoveryWait) {
|
|
@@ -6091,6 +6114,34 @@ export class AgentSession {
|
|
|
6091
6114
|
}
|
|
6092
6115
|
}
|
|
6093
6116
|
|
|
6117
|
+
#estimatePendingPromptTokens(messages: AgentMessage[]): number {
|
|
6118
|
+
let tokens = computeNonMessageTokens(this);
|
|
6119
|
+
for (const message of this.messages) {
|
|
6120
|
+
tokens += estimateTokens(message);
|
|
6121
|
+
}
|
|
6122
|
+
for (const message of messages) {
|
|
6123
|
+
tokens += estimateTokens(message);
|
|
6124
|
+
}
|
|
6125
|
+
return tokens;
|
|
6126
|
+
}
|
|
6127
|
+
|
|
6128
|
+
async #runPrePromptCompactionIfNeeded(messages: AgentMessage[]): Promise<void> {
|
|
6129
|
+
const model = this.model;
|
|
6130
|
+
if (!model) return;
|
|
6131
|
+
const contextWindow = model.contextWindow ?? 0;
|
|
6132
|
+
if (contextWindow <= 0) return;
|
|
6133
|
+
const compactionSettings = this.settings.getGroup("compaction");
|
|
6134
|
+
const contextTokens = this.#estimatePendingPromptTokens(messages);
|
|
6135
|
+
if (!shouldCompact(contextTokens, contextWindow, compactionSettings)) return;
|
|
6136
|
+
|
|
6137
|
+
logger.debug("Pre-prompt context maintenance triggered by pending prompt size", {
|
|
6138
|
+
contextTokens,
|
|
6139
|
+
contextWindow,
|
|
6140
|
+
model: `${model.provider}/${model.id}`,
|
|
6141
|
+
});
|
|
6142
|
+
await this.#runAutoCompaction("threshold", false, false, false, { autoContinue: false });
|
|
6143
|
+
}
|
|
6144
|
+
|
|
6094
6145
|
/**
|
|
6095
6146
|
* Check if context maintenance or promotion is needed and run it.
|
|
6096
6147
|
* Called after agent_end and before prompt submission.
|
|
@@ -6111,6 +6162,7 @@ export class AgentSession {
|
|
|
6111
6162
|
* `agent_end` handler set this to true so `session.prompt()` resolves cleanly; callers
|
|
6112
6163
|
* on the pre-prompt path (where the next agent turn is about to start) set it to false
|
|
6113
6164
|
* to avoid racing the deferred handoff against the new turn.
|
|
6165
|
+
* @param autoContinue Whether maintenance may schedule the agent-authored continuation prompt.
|
|
6114
6166
|
* @returns true when a deferred handoff was scheduled. Callers MUST then skip any
|
|
6115
6167
|
* subsequent `#scheduleAgentContinue` / reminder appends for this turn — the
|
|
6116
6168
|
* handoff will replace session state and a concurrent `agent.continue()` would
|
|
@@ -6120,6 +6172,7 @@ export class AgentSession {
|
|
|
6120
6172
|
assistantMessage: AssistantMessage,
|
|
6121
6173
|
skipAbortedCheck = true,
|
|
6122
6174
|
allowDefer = true,
|
|
6175
|
+
autoContinue = true,
|
|
6123
6176
|
): Promise<boolean> {
|
|
6124
6177
|
// Skip if message was aborted (user cancelled) - unless skipAbortedCheck is false
|
|
6125
6178
|
if (skipAbortedCheck && assistantMessage.stopReason === "aborted") return false;
|
|
@@ -6157,7 +6210,7 @@ export class AgentSession {
|
|
|
6157
6210
|
// No promotion target available fall through to compaction
|
|
6158
6211
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
6159
6212
|
if (compactionSettings.enabled && compactionSettings.strategy !== "off") {
|
|
6160
|
-
await this.#runAutoCompaction("overflow", true, false, allowDefer);
|
|
6213
|
+
await this.#runAutoCompaction("overflow", true, false, allowDefer, { autoContinue });
|
|
6161
6214
|
}
|
|
6162
6215
|
return false;
|
|
6163
6216
|
}
|
|
@@ -6189,7 +6242,7 @@ export class AgentSession {
|
|
|
6189
6242
|
model: `${assistantMessage.provider}/${assistantMessage.model}`,
|
|
6190
6243
|
strategy: incompleteCompactionSettings.strategy,
|
|
6191
6244
|
});
|
|
6192
|
-
await this.#runAutoCompaction("incomplete", true, false, allowDefer);
|
|
6245
|
+
await this.#runAutoCompaction("incomplete", true, false, allowDefer, { autoContinue });
|
|
6193
6246
|
} else {
|
|
6194
6247
|
// Neither promotion nor compaction is available — surface the dead-end so
|
|
6195
6248
|
// the user understands why the turn yielded with nothing.
|
|
@@ -6215,7 +6268,7 @@ export class AgentSession {
|
|
|
6215
6268
|
// Try promotion first — if a larger model is available, switch instead of compacting
|
|
6216
6269
|
const promoted = await this.#tryContextPromotion(assistantMessage);
|
|
6217
6270
|
if (!promoted) {
|
|
6218
|
-
return await this.#runAutoCompaction("threshold", false, false, allowDefer);
|
|
6271
|
+
return await this.#runAutoCompaction("threshold", false, false, allowDefer, { autoContinue });
|
|
6219
6272
|
}
|
|
6220
6273
|
}
|
|
6221
6274
|
return false;
|
|
@@ -6939,17 +6992,18 @@ export class AgentSession {
|
|
|
6939
6992
|
willRetry: boolean,
|
|
6940
6993
|
deferred = false,
|
|
6941
6994
|
allowDefer = true,
|
|
6995
|
+
options: { autoContinue?: boolean } = {},
|
|
6942
6996
|
): Promise<boolean> {
|
|
6943
6997
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
6944
6998
|
if (compactionSettings.strategy === "off") return false;
|
|
6945
6999
|
if (reason !== "idle" && !compactionSettings.enabled) return false;
|
|
6946
7000
|
const generation = this.#promptGeneration;
|
|
6947
|
-
|
|
7001
|
+
const shouldAutoContinue = options.autoContinue !== false && compactionSettings.autoContinue !== false;
|
|
6948
7002
|
// Shake runs inline (cheap, no remote LLM). On overflow recovery, if shake
|
|
6949
7003
|
// reclaims nothing we fall through to the summary-compaction body below so
|
|
6950
7004
|
// the oversized input still gets resolved.
|
|
6951
7005
|
if (compactionSettings.strategy === "shake") {
|
|
6952
|
-
const outcome = await this.#runAutoShake(reason, willRetry, generation);
|
|
7006
|
+
const outcome = await this.#runAutoShake(reason, willRetry, generation, shouldAutoContinue);
|
|
6953
7007
|
if (outcome !== "fallback") return false;
|
|
6954
7008
|
}
|
|
6955
7009
|
// "overflow" and "incomplete" force inline execution because they are recovery
|
|
@@ -7018,7 +7072,7 @@ export class AgentSession {
|
|
|
7018
7072
|
aborted: false,
|
|
7019
7073
|
willRetry: false,
|
|
7020
7074
|
});
|
|
7021
|
-
if (!autoCompactionSignal.aborted && reason !== "idle" &&
|
|
7075
|
+
if (!autoCompactionSignal.aborted && reason !== "idle" && shouldAutoContinue) {
|
|
7022
7076
|
this.#scheduleAutoContinuePrompt(generation);
|
|
7023
7077
|
}
|
|
7024
7078
|
return false;
|
|
@@ -7270,7 +7324,7 @@ export class AgentSession {
|
|
|
7270
7324
|
};
|
|
7271
7325
|
await this.#emitSessionEvent({ type: "auto_compaction_end", action, result, aborted: false, willRetry });
|
|
7272
7326
|
|
|
7273
|
-
if (!willRetry && reason !== "idle" &&
|
|
7327
|
+
if (!willRetry && reason !== "idle" && shouldAutoContinue) {
|
|
7274
7328
|
this.#scheduleAutoContinuePrompt(generation);
|
|
7275
7329
|
}
|
|
7276
7330
|
|
|
@@ -7348,6 +7402,7 @@ export class AgentSession {
|
|
|
7348
7402
|
reason: "overflow" | "threshold" | "idle" | "incomplete",
|
|
7349
7403
|
willRetry: boolean,
|
|
7350
7404
|
generation: number,
|
|
7405
|
+
autoContinue: boolean,
|
|
7351
7406
|
): Promise<"handled" | "fallback"> {
|
|
7352
7407
|
const action = "shake";
|
|
7353
7408
|
await this.#emitSessionEvent({ type: "auto_compaction_start", reason, action });
|
|
@@ -7355,7 +7410,6 @@ export class AgentSession {
|
|
|
7355
7410
|
const controller = new AbortController();
|
|
7356
7411
|
this.#autoCompactionAbortController = controller;
|
|
7357
7412
|
const signal = controller.signal;
|
|
7358
|
-
const compactionSettings = this.settings.getGroup("compaction");
|
|
7359
7413
|
try {
|
|
7360
7414
|
const result = await this.shake("elide", { config: DEFAULT_SHAKE_CONFIG, signal });
|
|
7361
7415
|
if (signal.aborted) {
|
|
@@ -7391,7 +7445,7 @@ export class AgentSession {
|
|
|
7391
7445
|
skipped: !reclaimed,
|
|
7392
7446
|
});
|
|
7393
7447
|
|
|
7394
|
-
if (!willRetry && reason !== "idle" &&
|
|
7448
|
+
if (!willRetry && reason !== "idle" && autoContinue) {
|
|
7395
7449
|
this.#scheduleAutoContinuePrompt(generation);
|
|
7396
7450
|
}
|
|
7397
7451
|
if (willRetry) {
|