Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
- Bkper CLI Agent has a new look: the startup screen shows the Bkper logo in its account type colors, and new Bkper light and dark themes replace Pi's default colors, following your terminal's light or dark background. Custom themes you selected are kept
- The startup screen no longer breaks in narrow terminals
- Updated Pi to 0.99.2
- The agent can now run several tool calls in one JavaScript script (codemode), so batch lookups and large outputs are handled in one step and only the result reaches the conversation. Scripts that write to a Book follow the same rule as single commands: the agent shows the exact script and every change first, and runs it only after you confirm. Codemode is on by default; a `defaultTools` setting you set is kept, and `+codemode` or `-codemode` turns it on or off

## [5.2.0] - 2026-10-01

Expand Down
3 changes: 3 additions & 0 deletions src/agent/extensions/builtins.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
createCodemodeExtension,
getAgentDir,
type Extension,
type ExtensionAPI,
Expand Down Expand Up @@ -120,4 +121,6 @@ export function registerBkperAgentBuiltins(
dispatchHandoffCommand,
getBkperHandoffShortcutFromFile(getAgentDir())
);
// Registered inactive; the Bkper default tool selection activates it.
createCodemodeExtension()(pi);
}
129 changes: 105 additions & 24 deletions src/agent/interactive/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,43 +21,116 @@ export interface ResolvedBkperAgentTools {
warning?: string;
}

// Always selected so the tool set, and with it the prompt cache, stays stable across turns.
const CODEMODE_TOOL = 'codemode';

function isToolModifier(entry: unknown): boolean {
return typeof entry === 'string' && (entry.startsWith('+') || entry.startsWith('-'));
}

/** Applies `+name`/`-name` entries in order, as Pi does for `defaultTools`. */
function applyToolModifiers(tools: string[], modifiers: string[]): string[] {
const result = [...tools];
for (const modifier of modifiers) {
const name = modifier.slice(1);
const index = result.indexOf(name);
if (modifier.startsWith('+') && index === -1 && name) {
result.push(name);
} else if (modifier.startsWith('-') && index !== -1) {
result.splice(index, 1);
}
}
return result;
}

/**
* Pi resolves a `defaultTools` selection without plain tool names in any settings layer against
* its own defaults, which lack codemode and PowerShell. Returns those modifiers, global layer
* first, so they can be applied to the Bkper defaults instead.
*/
function getModifierOnlyToolSelection(layers: unknown[]): string[] | undefined {
const modifiers: string[] = [];
for (const layer of layers) {
if (layer === undefined) {
continue;
}
if (!isModifierList(layer)) {
return undefined;
}
modifiers.push(...layer);
}
return modifiers.length > 0 ? modifiers : undefined;
}

function isModifierList(layer: unknown): layer is string[] {
return Array.isArray(layer) && layer.every(isToolModifier);
}

function disableUnavailableShells(
configuredTools: string[],
availability: ShellAvailability
): ResolvedBkperAgentTools {
const unavailableShells = configuredTools.filter(
tool =>
(tool === 'bash' && !availability.bash) ||
(tool === 'powershell' && !availability.powershell)
);
const tools = configuredTools.filter(tool => !unavailableShells.includes(tool));
return unavailableShells.length > 0
? {
tools,
warning: `Unavailable configured shell tools were disabled: ${unavailableShells.join(
', '
)}.`,
}
: {tools};
}

export function resolveBkperAgentTools(
configuredTools: string[] | undefined,
platform: NodeJS.Platform,
availability: ShellAvailability
availability: ShellAvailability,
toolModifiers: string[] = []
): ResolvedBkperAgentTools {
if (configuredTools) {
const unavailableShells = configuredTools.filter(
tool =>
(tool === 'bash' && !availability.bash) ||
(tool === 'powershell' && !availability.powershell)
);
const tools = configuredTools.filter(tool => !unavailableShells.includes(tool));
return unavailableShells.length > 0
? {
tools,
warning: `Unavailable configured shell tools were disabled: ${unavailableShells.join(
', '
)}.`,
}
: {tools};
return disableUnavailableShells(configuredTools, availability);
}

const defaults = selectDefaultBkperAgentTools(platform, availability);
if (toolModifiers.length === 0) {
return defaults;
}
const modified = disableUnavailableShells(
applyToolModifiers(defaults.tools, toolModifiers),
availability
);
const warnings = [defaults.warning, modified.warning].filter(
(warning): warning is string => warning !== undefined
);
return warnings.length > 0
? {tools: modified.tools, warning: warnings.join(' ')}
: {tools: modified.tools};
}

function selectDefaultBkperAgentTools(
platform: NodeJS.Platform,
availability: ShellAvailability
): ResolvedBkperAgentTools {
if (platform === 'win32' && availability.powershell) {
return {tools: ['read', 'powershell', 'edit', 'write']};
return {tools: ['read', 'powershell', 'edit', 'write', CODEMODE_TOOL]};
}

if (availability.bash) {
return platform === 'win32'
? {
tools: ['read', 'bash', 'edit', 'write'],
tools: ['read', 'bash', 'edit', 'write', CODEMODE_TOOL],
warning: 'PowerShell is unavailable; using Bash instead.',
}
: {tools: ['read', 'bash', 'edit', 'write']};
: {tools: ['read', 'bash', 'edit', 'write', CODEMODE_TOOL]};
}

return {
tools: ['read', 'edit', 'write'],
tools: ['read', 'edit', 'write', CODEMODE_TOOL],
warning: 'No supported shell is available; command execution is disabled.',
};
}
Expand All @@ -68,6 +141,8 @@ type DefaultToolsSettingsManager = {
};

type BkperAgentToolSettingsManager = DefaultToolsSettingsManager & {
getGlobalSettings(): {defaultTools?: string[]};
getProjectSettings(): {defaultTools?: string[]};
getShellPath(): string | undefined;
};

Expand Down Expand Up @@ -109,11 +184,17 @@ export function applyBkperAgentToolSelection(
effectivePlatform === 'win32' &&
(forceWindows || canResolveShell(() => getPowerShellConfig())),
};
const resolved = resolveBkperAgentTools(
settingsManager.getDefaultTools(),
effectivePlatform,
availability
);
const toolModifiers = getModifierOnlyToolSelection([
settingsManager.getGlobalSettings().defaultTools,
settingsManager.getProjectSettings().defaultTools,
]);
const resolved = toolModifiers
? resolveBkperAgentTools(undefined, effectivePlatform, availability, toolModifiers)
: resolveBkperAgentTools(
settingsManager.getDefaultTools(),
effectivePlatform,
availability
);

overrideDefaultTools(settingsManager, resolved.tools);

Expand Down
22 changes: 18 additions & 4 deletions src/agent/system-prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,14 +81,25 @@ function getCodingToolDefinitions(selectedTools: string[]) {
].filter(definition => selectedTools.includes(definition.name));
}

// Pi drops tool prompt snippets and guidelines when the system prompt is replaced, and does not
// export codemode's, so Bkper describes codemode itself.
const CODEMODE_PROMPT_SNIPPET =
'Run JavaScript that calls other tools (chains, loops, Promise.all, filtering large results)';
const CODEMODE_PROMPT_GUIDELINES = [
'Use codemode, not shell loops or jq pipelines, to repeat commands across items, chain their output, or filter large results.',
'Book writes in a codemode script need the same confirmation as single commands: resolve targets read-only, show the script and changes, run only after the user confirms, and report each item\'s result.',
];

function buildToolPromptSection(selectedTools: string[]): string {
const toolDefinitions = getCodingToolDefinitions(selectedTools);
const toolLines = toolDefinitions
.flatMap(definition => {
const hasCodemode = selectedTools.includes('codemode');
const toolLines = [
...toolDefinitions.flatMap(definition => {
const snippet = normalizePromptSnippet(definition.promptSnippet);
return snippet ? [`- ${definition.name}: ${snippet}`] : [];
})
.join('\n');
}),
...(hasCodemode ? [`- codemode: ${CODEMODE_PROMPT_SNIPPET}`] : []),
].join('\n');

const guidelineLines: string[] = [];
const seenGuidelines = new Set<string>();
Expand All @@ -115,6 +126,9 @@ function buildToolPromptSection(selectedTools: string[]): string {
addGuideline(guideline);
}
}
if (hasCodemode) {
CODEMODE_PROMPT_GUIDELINES.forEach(addGuideline);
}
addGuideline('Do not claim builds, tests, or command results unless you actually ran them.');

const toolsList = toolLines.length > 0 ? toolLines : '(none)';
Expand Down
27 changes: 27 additions & 0 deletions test/unit/agent/extensions/builtins.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ describe('Bkper agent built-in extensions', function () {
registerShortcut: sinon.stub(),
registerMessageRenderer: sinon.stub(),
registerProvider: sinon.stub(),
registerTool: sinon.stub(),
} as unknown as ExtensionAPI);

expect(registeredEvents).to.deep.equal([
Expand All @@ -56,6 +57,30 @@ describe('Bkper agent built-in extensions', function () {
]);
});

it('registers the codemode tool inactive so tool selection decides activation', function () {
const registerTool = sinon.stub();

registerBkperAgentBuiltins(
{
on: sinon.stub() as unknown as ExtensionAPI['on'],
registerCommand: sinon.stub(),
registerShortcut: sinon.stub(),
registerMessageRenderer: sinon.stub(),
registerProvider: sinon.stub(),
registerTool,
} as unknown as ExtensionAPI,
sinon.stub().resolves(),
undefined,
{}
);

const codemodeTools = registerTool.args
.map(([tool]) => tool as {name: string; defaultActive?: boolean})
.filter(tool => tool.name === 'codemode');
expect(codemodeTools).to.have.length(1);
expect(codemodeTools[0]?.defaultActive).to.equal(false);
});

it('registers Bkper AI through Pi standard OpenAI Responses transport', function () {
const providers: Array<{name: string; config: ProviderConfig}> = [];

Expand All @@ -68,6 +93,7 @@ describe('Bkper agent built-in extensions', function () {
registerProvider: (name: string, config: ProviderConfig) => {
providers.push({name, config});
},
registerTool: sinon.stub(),
} as unknown as ExtensionAPI,
sinon.stub().resolves(),
undefined,
Expand Down Expand Up @@ -100,6 +126,7 @@ describe('Bkper agent built-in extensions', function () {
registerShortcut: sinon.stub(),
registerMessageRenderer: sinon.stub(),
registerProvider,
registerTool: sinon.stub(),
} as unknown as ExtensionAPI,
sinon.stub().resolves(),
undefined,
Expand Down
2 changes: 2 additions & 0 deletions test/unit/agent/interactive/run-agent-mode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,12 @@ describe('runAgentMode', function () {
'powershell',
'edit',
'write',
'codemode',
]);
expect(runtime.session.systemPrompt).to.include(
'- powershell: Execute PowerShell commands'
);
expect(runtime.session.systemPrompt).to.include('- codemode:');
expect(runtime.session.systemPrompt).to.not.include('- bash:');
} finally {
await runtime.dispose();
Expand Down
Loading
Loading