feat(coding-agent): expose session metadata to bash tools (#6967)
This commit is contained in:
@@ -2,6 +2,10 @@
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Exposed `PI_SESSION_ID`, `PI_SESSION_FILE`, `PI_PROVIDER`, `PI_MODEL`, and `PI_REASONING_LEVEL` to commands run by built-in and factory-created bash tools.
|
||||||
|
|
||||||
## [0.81.1] - 2026-07-21
|
## [0.81.1] - 2026-07-21
|
||||||
|
|
||||||
### New Features
|
### New Features
|
||||||
|
|||||||
@@ -662,6 +662,7 @@ pi --thinking high "Solve this complex problem"
|
|||||||
|
|
||||||
| Variable | Description |
|
| Variable | Description |
|
||||||
|----------|-------------|
|
|----------|-------------|
|
||||||
|
| `PI_CODING_AGENT` | Set to `true` by the CLI and RPC entry points so child processes can detect that they run inside Pi |
|
||||||
| `PI_CODING_AGENT_DIR` | Override config directory (default: `~/.pi/agent`) |
|
| `PI_CODING_AGENT_DIR` | Override config directory (default: `~/.pi/agent`) |
|
||||||
| `PI_CODING_AGENT_SESSION_DIR` | Override session storage directory (overridden by `--session-dir`) |
|
| `PI_CODING_AGENT_SESSION_DIR` | Override session storage directory (overridden by `--session-dir`) |
|
||||||
| `PI_PACKAGE_DIR` | Override package directory (useful for Nix/Guix where store paths tokenize poorly) |
|
| `PI_PACKAGE_DIR` | Override package directory (useful for Nix/Guix where store paths tokenize poorly) |
|
||||||
@@ -671,6 +672,18 @@ pi --thinking high "Solve this complex problem"
|
|||||||
| `PI_CACHE_RETENTION` | Set to `long` for extended prompt cache (Anthropic: 1h, OpenAI: 24h) |
|
| `PI_CACHE_RETENTION` | Set to `long` for extended prompt cache (Anthropic: 1h, OpenAI: 24h) |
|
||||||
| `VISUAL`, `EDITOR` | Fallback external editor for Ctrl+G when `externalEditor` is unset; defaults to Notepad on Windows and `nano` elsewhere |
|
| `VISUAL`, `EDITOR` | Fallback external editor for Ctrl+G when `externalEditor` is unset; defaults to Notepad on Windows and `nano` elsewhere |
|
||||||
|
|
||||||
|
Commands run by the LLM-callable bash tool also receive current session metadata:
|
||||||
|
|
||||||
|
| Variable | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| `PI_SESSION_ID` | Current session ID |
|
||||||
|
| `PI_SESSION_FILE` | Absolute session JSONL path; unset for ephemeral sessions |
|
||||||
|
| `PI_PROVIDER` | Currently selected model provider |
|
||||||
|
| `PI_MODEL` | Currently selected model ID |
|
||||||
|
| `PI_REASONING_LEVEL` | Current effective reasoning level |
|
||||||
|
|
||||||
|
These values are resolved when each command starts. See [Environment Variables](docs/environment-variables.md#bash-tool-session-environment) for semantics, examples, and custom-tool opt-out.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Contributing & Development
|
## Contributing & Development
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
# Environment Variables
|
||||||
|
|
||||||
|
Pi uses environment variables in three ways:
|
||||||
|
|
||||||
|
- Variables such as `PI_OFFLINE` configure the Pi process.
|
||||||
|
- Pi sets `PI_CODING_AGENT` so child processes can detect that they run inside Pi.
|
||||||
|
- Commands run by the LLM-callable bash tool receive `PI_*` variables describing the current session.
|
||||||
|
|
||||||
|
Provider API-key variables are documented separately in [Providers](providers.md#environment-variables-or-auth-file).
|
||||||
|
|
||||||
|
## Process Marker
|
||||||
|
|
||||||
|
The CLI and RPC entry points set `PI_CODING_AGENT=true`. Child processes inherit it and can use it to detect that they run inside Pi. It is not session-specific and is not set automatically when Pi is embedded through the SDK.
|
||||||
|
|
||||||
|
## Bash Tool Session Environment
|
||||||
|
|
||||||
|
Commands run by the bash tool receive the current Pi session state:
|
||||||
|
|
||||||
|
| Variable | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| `PI_SESSION_ID` | Current session ID |
|
||||||
|
| `PI_SESSION_FILE` | Absolute path to the current session JSONL file; unset for ephemeral sessions |
|
||||||
|
| `PI_PROVIDER` | Currently selected model provider |
|
||||||
|
| `PI_MODEL` | Currently selected model ID |
|
||||||
|
| `PI_REASONING_LEVEL` | Current effective reasoning level: `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, or `max` |
|
||||||
|
|
||||||
|
The values are resolved when each command starts. Switching models or changing the reasoning level therefore affects the next bash command without restarting Pi. `PI_PROVIDER` and `PI_MODEL` identify the selected Pi model, not a different upstream model that a router may choose internally.
|
||||||
|
|
||||||
|
When asked which model or provider is running, inspect these variables instead of inferring the answer from the system prompt:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
printf '%s/%s\n' "$PI_PROVIDER" "$PI_MODEL"
|
||||||
|
printf 'reasoning=%s session=%s\n' "$PI_REASONING_LEVEL" "$PI_SESSION_ID"
|
||||||
|
```
|
||||||
|
|
||||||
|
The session file can be inspected directly when the session is persistent:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
if [ -n "$PI_SESSION_FILE" ]; then
|
||||||
|
tail -n 1 "$PI_SESSION_FILE"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
These variables are injected into the LLM-callable bash tool. They are not injected into user-entered `!` or `!!` commands.
|
||||||
|
|
||||||
|
### Custom Bash Tools
|
||||||
|
|
||||||
|
Bash tools created with `createBashTool()` expose the session environment by default when registered with Pi. Injection happens before `spawnHook`, so a hook receives the variables in `ctx.env`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const bashTool = createBashTool(cwd, {
|
||||||
|
spawnHook: (ctx) => ({
|
||||||
|
...ctx,
|
||||||
|
env: { ...ctx.env, CI: "1" },
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Disable session metadata independently of the spawn hook:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const bashTool = createBashTool(cwd, {
|
||||||
|
exposeSessionEnvironment: false,
|
||||||
|
spawnHook: (ctx) => ctx,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
When disabled, Pi removes inherited values for these variables so nested Pi processes do not expose stale parent-session metadata.
|
||||||
|
|
||||||
|
## Pi Process Configuration
|
||||||
|
|
||||||
|
These variables are read by Pi itself:
|
||||||
|
|
||||||
|
| Variable | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| `PI_CODING_AGENT_DIR` | Override the config directory; default is `~/.pi/agent` |
|
||||||
|
| `PI_CODING_AGENT_SESSION_DIR` | Override session storage; overridden by `--session-dir` |
|
||||||
|
| `PI_PACKAGE_DIR` | Override the package directory, useful for Nix/Guix store paths |
|
||||||
|
| `PI_OFFLINE` | Disable startup network operations, including update checks, package updates, and install/update telemetry |
|
||||||
|
| `PI_SKIP_VERSION_CHECK` | Disable the `pi.dev` latest-version request |
|
||||||
|
| `PI_TELEMETRY` | Override install/update telemetry and provider attribution headers: `1`/`true`/`yes` or `0`/`false`/`no` |
|
||||||
|
| `PI_CACHE_RETENTION` | Set to `long` for extended provider prompt caching where supported |
|
||||||
|
| `PI_SHARE_VIEWER_URL` | Override the base URL used by `/share` |
|
||||||
|
| `PI_HARDWARE_CURSOR` | Set to `1` to show the hardware cursor; see [Terminal setup](terminal-setup.md) |
|
||||||
|
| `VISUAL`, `EDITOR` | External editor fallback when `externalEditor` is unset |
|
||||||
|
| `HTTP_PROXY`, `HTTPS_PROXY` | Proxy outbound HTTP requests |
|
||||||
|
|
||||||
|
Provider credentials such as `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, and cloud-provider configuration are listed in [Providers](providers.md#environment-variables-or-auth-file).
|
||||||
@@ -982,9 +982,9 @@ ctx.sessionManager.buildContextEntries() // Active branch entries with compac
|
|||||||
ctx.sessionManager.getLeafId() // Current leaf entry ID
|
ctx.sessionManager.getLeafId() // Current leaf entry ID
|
||||||
```
|
```
|
||||||
|
|
||||||
### ctx.modelRegistry / ctx.model
|
### ctx.modelRegistry / ctx.model / ctx.thinkingLevel
|
||||||
|
|
||||||
Access to models, providers, and resolved authentication. `ctx.modelRegistry.getProvider(id)` returns the effective pi-ai provider, while `getProviderAuth(id)` resolves its current API key, headers, base URL, and provider-scoped environment without requiring a loaded model. `ctx.model` is the active model.
|
Access to models, providers, and resolved authentication. `ctx.modelRegistry.getProvider(id)` returns the effective pi-ai provider, while `getProviderAuth(id)` resolves its current API key, headers, base URL, and provider-scoped environment without requiring a loaded model. `ctx.model` is the active model, and `ctx.thinkingLevel` is its current effective thinking level.
|
||||||
|
|
||||||
### ctx.signal
|
### ctx.signal
|
||||||
|
|
||||||
@@ -2096,7 +2096,15 @@ const bashTool = createBashTool(cwd, {
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
See [examples/extensions/ssh.ts](../examples/extensions/ssh.ts) for a complete SSH example with `--ssh` flag.
|
`createBashTool()` exposes the current session to commands through `PI_SESSION_ID`, `PI_SESSION_FILE`, `PI_PROVIDER`, `PI_MODEL`, and `PI_REASONING_LEVEL`. Injection happens before `spawnHook`, so hooks receive these values in `env` and preserve them when they spread the existing environment as above. Set `exposeSessionEnvironment: false` to disable them:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const bashTool = createBashTool(cwd, {
|
||||||
|
exposeSessionEnvironment: false,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
See [Bash tool session environment](environment-variables.md#bash-tool-session-environment) for variable semantics. See [examples/extensions/ssh.ts](../examples/extensions/ssh.ts) for a complete SSH example with `--ssh` flag.
|
||||||
|
|
||||||
### Output Truncation
|
### Output Truncation
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ For the full first-run flow, see [Quickstart](quickstart.md).
|
|||||||
|
|
||||||
## Reference
|
## Reference
|
||||||
|
|
||||||
|
- [Environment variables](environment-variables.md) - Pi process configuration and session metadata available to bash tools.
|
||||||
- [Session format](session-format.md) - JSONL session file format, entry types, and SessionManager API.
|
- [Session format](session-format.md) - JSONL session file format, entry types, and SessionManager API.
|
||||||
|
|
||||||
## Platform setup
|
## Platform setup
|
||||||
|
|||||||
@@ -289,19 +289,6 @@ pi --tools read,grep,find,ls -p "Review the code"
|
|||||||
pi --exclude-tools ask_question
|
pi --exclude-tools ask_question
|
||||||
```
|
```
|
||||||
|
|
||||||
### Environment Variables
|
|
||||||
|
|
||||||
| Variable | Description |
|
|
||||||
|----------|-------------|
|
|
||||||
| `PI_CODING_AGENT_DIR` | Override config directory; default is `~/.pi/agent` |
|
|
||||||
| `PI_CODING_AGENT_SESSION_DIR` | Override session storage directory; overridden by `--session-dir` |
|
|
||||||
| `PI_PACKAGE_DIR` | Override package directory, useful for Nix/Guix store paths |
|
|
||||||
| `PI_OFFLINE` | Disable startup network operations, including update checks, package update checks, and install/update telemetry |
|
|
||||||
| `PI_SKIP_VERSION_CHECK` | Skip the Pi version update check at startup. This prevents the `pi.dev` latest-version request |
|
|
||||||
| `PI_TELEMETRY` | Override install/update telemetry and provider attribution headers: `1`/`true`/`yes` or `0`/`false`/`no`. This does not disable update checks |
|
|
||||||
| `PI_CACHE_RETENTION` | Set to `long` for extended prompt cache where supported |
|
|
||||||
| `VISUAL`, `EDITOR` | Fallback external editor for Ctrl+G when `externalEditor` is unset; defaults to Notepad on Windows and `nano` elsewhere |
|
|
||||||
|
|
||||||
## Design Principles
|
## Design Principles
|
||||||
|
|
||||||
Pi keeps the core small and pushes workflow-specific behavior into extensions, skills, prompt templates, and packages.
|
Pi keeps the core small and pushes workflow-specific behavior into extensions, skills, prompt templates, and packages.
|
||||||
|
|||||||
@@ -694,6 +694,10 @@ export class ExtensionRunner {
|
|||||||
runner.assertActive();
|
runner.assertActive();
|
||||||
return getModel();
|
return getModel();
|
||||||
},
|
},
|
||||||
|
get thinkingLevel() {
|
||||||
|
runner.assertActive();
|
||||||
|
return runner.runtime.getThinkingLevel();
|
||||||
|
},
|
||||||
isIdle: () => {
|
isIdle: () => {
|
||||||
runner.assertActive();
|
runner.assertActive();
|
||||||
return runner.isIdleFn();
|
return runner.isIdleFn();
|
||||||
|
|||||||
@@ -317,6 +317,8 @@ export interface ExtensionContext {
|
|||||||
modelRegistry: ModelRegistry;
|
modelRegistry: ModelRegistry;
|
||||||
/** Current model (may be undefined) */
|
/** Current model (may be undefined) */
|
||||||
model: Model<any> | undefined;
|
model: Model<any> | undefined;
|
||||||
|
/** Current thinking level, when provided by the session runtime. */
|
||||||
|
thinkingLevel?: ThinkingLevel;
|
||||||
/** Whether the agent is idle (not streaming) */
|
/** Whether the agent is idle (not streaming) */
|
||||||
isIdle(): boolean;
|
isIdle(): boolean;
|
||||||
/** Whether project-local trust is active for this context. */
|
/** Whether project-local trust is active for this context. */
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ Pi documentation (read only when the user asks about pi itself, its SDK, extensi
|
|||||||
- Additional docs: ${docsPath}
|
- Additional docs: ${docsPath}
|
||||||
- Examples: ${examplesPath} (extensions, custom tools, SDK)
|
- Examples: ${examplesPath} (extensions, custom tools, SDK)
|
||||||
- When reading pi docs or examples, resolve docs/... under Additional docs and examples/... under Examples, not the current working directory
|
- When reading pi docs or examples, resolve docs/... under Additional docs and examples/... under Examples, not the current working directory
|
||||||
- When asked about: extensions (docs/extensions.md, examples/extensions/), themes (docs/themes.md), skills (docs/skills.md), prompt templates (docs/prompt-templates.md), TUI components (docs/tui.md), keybindings (docs/keybindings.md), SDK integrations (docs/sdk.md), custom providers (docs/custom-provider.md), adding models (docs/models.md), pi packages (docs/packages.md)
|
- When asked about: extensions (docs/extensions.md, examples/extensions/), themes (docs/themes.md), skills (docs/skills.md), prompt templates (docs/prompt-templates.md), TUI components (docs/tui.md), keybindings (docs/keybindings.md), SDK integrations (docs/sdk.md), custom providers (docs/custom-provider.md), adding models (docs/models.md), pi packages (docs/packages.md), environment variables (docs/environment-variables.md)
|
||||||
- When working on pi topics, read the docs and examples, and follow .md cross-references before implementing
|
- When working on pi topics, read the docs and examples, and follow .md cross-references before implementing
|
||||||
- Always read pi .md files completely and follow links to related docs (e.g., tui.md for TUI API details)`;
|
- Always read pi .md files completely and follow links to related docs (e.g., tui.md for TUI API details)`;
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
trackDetachedChildPid,
|
trackDetachedChildPid,
|
||||||
untrackDetachedChildPid,
|
untrackDetachedChildPid,
|
||||||
} from "../../utils/shell.ts";
|
} from "../../utils/shell.ts";
|
||||||
import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.ts";
|
import type { ExtensionContext, ToolDefinition, ToolRenderResultOptions } from "../extensions/types.ts";
|
||||||
import { OutputAccumulator } from "./output-accumulator.ts";
|
import { OutputAccumulator } from "./output-accumulator.ts";
|
||||||
import { getTextOutput, invalidArgText, str } from "./render-utils.ts";
|
import { getTextOutput, invalidArgText, str } from "./render-utils.ts";
|
||||||
import { wrapToolDefinition } from "./tool-definition-wrapper.ts";
|
import { wrapToolDefinition } from "./tool-definition-wrapper.ts";
|
||||||
@@ -155,8 +155,31 @@ export interface BashSpawnContext {
|
|||||||
|
|
||||||
export type BashSpawnHook = (context: BashSpawnContext) => BashSpawnContext;
|
export type BashSpawnHook = (context: BashSpawnContext) => BashSpawnContext;
|
||||||
|
|
||||||
function resolveSpawnContext(command: string, cwd: string, spawnHook?: BashSpawnHook): BashSpawnContext {
|
function resolveSpawnContext(
|
||||||
const baseContext: BashSpawnContext = { command, cwd, env: { ...getShellEnv() } };
|
command: string,
|
||||||
|
cwd: string,
|
||||||
|
spawnHook: BashSpawnHook | undefined,
|
||||||
|
exposeSessionEnvironment: boolean,
|
||||||
|
ctx: ExtensionContext | undefined,
|
||||||
|
): BashSpawnContext {
|
||||||
|
const env = { ...getShellEnv() };
|
||||||
|
delete env.PI_SESSION_ID;
|
||||||
|
delete env.PI_SESSION_FILE;
|
||||||
|
delete env.PI_PROVIDER;
|
||||||
|
delete env.PI_MODEL;
|
||||||
|
delete env.PI_REASONING_LEVEL;
|
||||||
|
if (exposeSessionEnvironment && ctx) {
|
||||||
|
const model = ctx.model;
|
||||||
|
env.PI_SESSION_ID = ctx.sessionManager.getSessionId();
|
||||||
|
const sessionFile = ctx.sessionManager.getSessionFile();
|
||||||
|
if (sessionFile) env.PI_SESSION_FILE = sessionFile;
|
||||||
|
if (model) {
|
||||||
|
env.PI_PROVIDER = model.provider;
|
||||||
|
env.PI_MODEL = model.id;
|
||||||
|
}
|
||||||
|
if (ctx.thinkingLevel) env.PI_REASONING_LEVEL = ctx.thinkingLevel;
|
||||||
|
}
|
||||||
|
const baseContext: BashSpawnContext = { command, cwd, env };
|
||||||
return spawnHook ? spawnHook(baseContext) : baseContext;
|
return spawnHook ? spawnHook(baseContext) : baseContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,6 +190,8 @@ export interface BashToolOptions {
|
|||||||
commandPrefix?: string;
|
commandPrefix?: string;
|
||||||
/** Optional explicit shell path from settings */
|
/** Optional explicit shell path from settings */
|
||||||
shellPath?: string;
|
shellPath?: string;
|
||||||
|
/** Expose current Pi session metadata as PI_* environment variables. Default: true */
|
||||||
|
exposeSessionEnvironment?: boolean;
|
||||||
/** Hook to adjust command, cwd, or env before execution */
|
/** Hook to adjust command, cwd, or env before execution */
|
||||||
spawnHook?: BashSpawnHook;
|
spawnHook?: BashSpawnHook;
|
||||||
}
|
}
|
||||||
@@ -294,22 +319,26 @@ export function createBashToolDefinition(
|
|||||||
): ToolDefinition<typeof bashSchema, BashToolDetails | undefined, BashRenderState> {
|
): ToolDefinition<typeof bashSchema, BashToolDetails | undefined, BashRenderState> {
|
||||||
const ops = options?.operations ?? createLocalBashOperations({ shellPath: options?.shellPath });
|
const ops = options?.operations ?? createLocalBashOperations({ shellPath: options?.shellPath });
|
||||||
const commandPrefix = options?.commandPrefix;
|
const commandPrefix = options?.commandPrefix;
|
||||||
|
const exposeSessionEnvironment = options?.exposeSessionEnvironment ?? true;
|
||||||
const spawnHook = options?.spawnHook;
|
const spawnHook = options?.spawnHook;
|
||||||
return {
|
return {
|
||||||
name: "bash",
|
name: "bash",
|
||||||
label: "bash",
|
label: "bash",
|
||||||
description: `Execute a bash command in the current working directory. Returns stdout and stderr. Output is truncated to last ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). If truncated, full output is saved to a temp file. Optionally provide a timeout in seconds.`,
|
description: `Execute a bash command in the current working directory. Returns stdout and stderr. Output is truncated to last ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). If truncated, full output is saved to a temp file. Optionally provide a timeout in seconds.`,
|
||||||
promptSnippet: "Execute bash commands (ls, grep, find, etc.)",
|
promptSnippet: "Execute bash commands (ls, grep, find, etc.)",
|
||||||
|
promptGuidelines: exposeSessionEnvironment
|
||||||
|
? ["Inspect PI_* environment variables for current model and session details."]
|
||||||
|
: undefined,
|
||||||
parameters: bashSchema,
|
parameters: bashSchema,
|
||||||
async execute(
|
async execute(
|
||||||
_toolCallId,
|
_toolCallId,
|
||||||
{ command, timeout }: { command: string; timeout?: number },
|
{ command, timeout }: { command: string; timeout?: number },
|
||||||
signal?: AbortSignal,
|
signal?: AbortSignal,
|
||||||
onUpdate?,
|
onUpdate?,
|
||||||
_ctx?,
|
ctx?,
|
||||||
) {
|
) {
|
||||||
const resolvedCommand = commandPrefix ? `${commandPrefix}\n${command}` : command;
|
const resolvedCommand = commandPrefix ? `${commandPrefix}\n${command}` : command;
|
||||||
const spawnContext = resolveSpawnContext(resolvedCommand, cwd, spawnHook);
|
const spawnContext = resolveSpawnContext(resolvedCommand, cwd, spawnHook, exposeSessionEnvironment, ctx);
|
||||||
const output = new OutputAccumulator({ tempFilePrefix: "pi-bash" });
|
const output = new OutputAccumulator({ tempFilePrefix: "pi-bash" });
|
||||||
let acceptingOutput = true;
|
let acceptingOutput = true;
|
||||||
let updateTimer: NodeJS.Timeout | undefined;
|
let updateTimer: NodeJS.Timeout | undefined;
|
||||||
@@ -466,5 +495,11 @@ export function createBashToolDefinition(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function createBashTool(cwd: string, options?: BashToolOptions): AgentTool<typeof bashSchema> {
|
export function createBashTool(cwd: string, options?: BashToolOptions): AgentTool<typeof bashSchema> {
|
||||||
return wrapToolDefinition(createBashToolDefinition(cwd, options));
|
const definition = createBashToolDefinition(cwd, options);
|
||||||
|
const tool = wrapToolDefinition(definition);
|
||||||
|
Object.assign(tool, {
|
||||||
|
promptSnippet: definition.promptSnippet,
|
||||||
|
promptGuidelines: definition.promptGuidelines,
|
||||||
|
});
|
||||||
|
return tool;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ export function wrapToolDefinition<TDetails = unknown>(
|
|||||||
parameters: definition.parameters,
|
parameters: definition.parameters,
|
||||||
prepareArguments: definition.prepareArguments,
|
prepareArguments: definition.prepareArguments,
|
||||||
executionMode: definition.executionMode,
|
executionMode: definition.executionMode,
|
||||||
execute: (toolCallId, params, signal, onUpdate) =>
|
execute: (toolCallId, params, signal, onUpdate, ctx?: ExtensionContext) =>
|
||||||
definition.execute(toolCallId, params, signal, onUpdate, ctxFactory?.() as ExtensionContext),
|
definition.execute(toolCallId, params, signal, onUpdate, ctx ?? (ctxFactory?.() as ExtensionContext)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1783,6 +1783,7 @@ export class InteractiveMode {
|
|||||||
sessionManager: this.sessionManager,
|
sessionManager: this.sessionManager,
|
||||||
modelRegistry: extensionRunner.getModelRegistry(),
|
modelRegistry: extensionRunner.getModelRegistry(),
|
||||||
model: this.session.model,
|
model: this.session.model,
|
||||||
|
thinkingLevel: this.session.thinkingLevel,
|
||||||
isIdle: () => this.session.isIdle,
|
isIdle: () => this.session.isIdle,
|
||||||
isProjectTrusted: () => this.settingsManager.isProjectTrusted(),
|
isProjectTrusted: () => this.settingsManager.isProjectTrusted(),
|
||||||
signal: this.session.agent.signal,
|
signal: this.session.agent.signal,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { DefaultResourceLoader } from "../src/core/resource-loader.ts";
|
|||||||
import { createAgentSession } from "../src/core/sdk.ts";
|
import { createAgentSession } from "../src/core/sdk.ts";
|
||||||
import { SessionManager } from "../src/core/session-manager.ts";
|
import { SessionManager } from "../src/core/session-manager.ts";
|
||||||
import { SettingsManager } from "../src/core/settings-manager.ts";
|
import { SettingsManager } from "../src/core/settings-manager.ts";
|
||||||
|
import { createBashTool } from "../src/core/tools/bash.ts";
|
||||||
|
|
||||||
describe("AgentSession dynamic tool registration", () => {
|
describe("AgentSession dynamic tool registration", () => {
|
||||||
let tempDir: string;
|
let tempDir: string;
|
||||||
@@ -25,6 +26,76 @@ describe("AgentSession dynamic tool registration", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("exposes session state before custom bash spawn hooks and supports opting out", async () => {
|
||||||
|
const settingsManager = SettingsManager.create(tempDir, agentDir);
|
||||||
|
const sessionManager = SessionManager.create(tempDir, join(agentDir, "sessions"), { id: "bash-env-test" });
|
||||||
|
let sessionEnv: NodeJS.ProcessEnv | undefined;
|
||||||
|
let optedOutEnv: NodeJS.ProcessEnv | undefined;
|
||||||
|
const resourceLoader = new DefaultResourceLoader({
|
||||||
|
cwd: tempDir,
|
||||||
|
agentDir,
|
||||||
|
settingsManager,
|
||||||
|
extensionFactories: [
|
||||||
|
(pi) => {
|
||||||
|
pi.registerTool(
|
||||||
|
createBashTool(tempDir, {
|
||||||
|
spawnHook: (ctx) => {
|
||||||
|
sessionEnv = ctx.env;
|
||||||
|
return ctx;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
pi.registerTool({
|
||||||
|
...createBashTool(tempDir, {
|
||||||
|
exposeSessionEnvironment: false,
|
||||||
|
spawnHook: (ctx) => {
|
||||||
|
optedOutEnv = ctx.env;
|
||||||
|
return ctx;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
name: "bash_without_session_env",
|
||||||
|
label: "bash without session env",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
await resourceLoader.reload();
|
||||||
|
|
||||||
|
const model = getModel("anthropic", "claude-sonnet-4-5")!;
|
||||||
|
const { session } = await createAgentSession({
|
||||||
|
cwd: tempDir,
|
||||||
|
agentDir,
|
||||||
|
model,
|
||||||
|
thinkingLevel: "high",
|
||||||
|
settingsManager,
|
||||||
|
sessionManager,
|
||||||
|
resourceLoader,
|
||||||
|
});
|
||||||
|
|
||||||
|
const bashTool = session.agent.state.tools.find((tool) => tool.name === "bash")!;
|
||||||
|
expect(session.systemPrompt).toContain(
|
||||||
|
"Inspect PI_* environment variables for current model and session details.",
|
||||||
|
);
|
||||||
|
await bashTool.execute("bash-env", { command: "printf ok" });
|
||||||
|
expect(sessionEnv).toMatchObject({
|
||||||
|
PI_SESSION_ID: session.sessionId,
|
||||||
|
PI_SESSION_FILE: session.sessionFile,
|
||||||
|
PI_PROVIDER: model.provider,
|
||||||
|
PI_MODEL: model.id,
|
||||||
|
PI_REASONING_LEVEL: session.thinkingLevel,
|
||||||
|
});
|
||||||
|
|
||||||
|
const optedOutBashTool = session.agent.state.tools.find((tool) => tool.name === "bash_without_session_env")!;
|
||||||
|
await optedOutBashTool.execute("bash-no-env", { command: "printf ok" });
|
||||||
|
expect(optedOutEnv).not.toHaveProperty("PI_SESSION_ID");
|
||||||
|
expect(optedOutEnv).not.toHaveProperty("PI_SESSION_FILE");
|
||||||
|
expect(optedOutEnv).not.toHaveProperty("PI_PROVIDER");
|
||||||
|
expect(optedOutEnv).not.toHaveProperty("PI_MODEL");
|
||||||
|
expect(optedOutEnv).not.toHaveProperty("PI_REASONING_LEVEL");
|
||||||
|
|
||||||
|
session.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
it("refreshes tool registry when tools are registered after initialization", async () => {
|
it("refreshes tool registry when tools are registered after initialization", async () => {
|
||||||
const settingsManager = SettingsManager.create(tempDir, agentDir);
|
const settingsManager = SettingsManager.create(tempDir, agentDir);
|
||||||
const sessionManager = SessionManager.inMemory();
|
const sessionManager = SessionManager.inMemory();
|
||||||
|
|||||||
@@ -92,4 +92,40 @@ describe("createAgentSession session manager defaults", () => {
|
|||||||
|
|
||||||
session.dispose();
|
session.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("exposes current session state to the built-in bash tool", async () => {
|
||||||
|
const model = getModel("anthropic", "claude-sonnet-4-5");
|
||||||
|
expect(model).toBeTruthy();
|
||||||
|
|
||||||
|
const { session } = await createAgentSession({
|
||||||
|
cwd,
|
||||||
|
agentDir,
|
||||||
|
model: model!,
|
||||||
|
thinkingLevel: "high",
|
||||||
|
});
|
||||||
|
expect(session.sessionFile).toBeTruthy();
|
||||||
|
expect(session.systemPrompt).toContain(
|
||||||
|
"Inspect PI_* environment variables for current model and session details.",
|
||||||
|
);
|
||||||
|
|
||||||
|
const bashTool = session.agent.state.tools.find((tool) => tool.name === "bash");
|
||||||
|
expect(bashTool).toBeTruthy();
|
||||||
|
const result = await bashTool!.execute("test", {
|
||||||
|
command: `printf '%s\\n' "$PI_SESSION_ID" "$PI_SESSION_FILE" "$PI_PROVIDER" "$PI_MODEL" "$PI_REASONING_LEVEL"`,
|
||||||
|
});
|
||||||
|
const output = result.content
|
||||||
|
.filter((item): item is { type: "text"; text: string } => item.type === "text")
|
||||||
|
.map((item) => item.text)
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
expect(output.trim().split("\n")).toEqual([
|
||||||
|
session.sessionId,
|
||||||
|
session.sessionFile,
|
||||||
|
model!.provider,
|
||||||
|
model!.id,
|
||||||
|
session.thinkingLevel,
|
||||||
|
]);
|
||||||
|
|
||||||
|
session.dispose();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ describe("buildSystemPrompt", () => {
|
|||||||
expect(prompt).toContain(
|
expect(prompt).toContain(
|
||||||
"- When reading pi docs or examples, resolve docs/... under Additional docs and examples/... under Examples, not the current working directory",
|
"- When reading pi docs or examples, resolve docs/... under Additional docs and examples/... under Examples, not the current working directory",
|
||||||
);
|
);
|
||||||
|
expect(prompt).toContain("environment variables (docs/environment-variables.md)");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ describe("ToolExecutionComponent parity", () => {
|
|||||||
return { exitCode: 0 };
|
return { exitCode: 0 };
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const tool = createBashToolDefinition(process.cwd(), { operations });
|
const tool = createBashToolDefinition(process.cwd(), { operations, exposeSessionEnvironment: false });
|
||||||
const promise = tool.execute(
|
const promise = tool.execute(
|
||||||
"tool-bash-1",
|
"tool-bash-1",
|
||||||
{ command: "sleep 10" },
|
{ command: "sleep 10" },
|
||||||
@@ -163,7 +163,7 @@ describe("ToolExecutionComponent parity", () => {
|
|||||||
return { exitCode: 0 };
|
return { exitCode: 0 };
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const tool = createBashToolDefinition(process.cwd(), { operations });
|
const tool = createBashToolDefinition(process.cwd(), { operations, exposeSessionEnvironment: false });
|
||||||
const result = await tool.execute(
|
const result = await tool.execute(
|
||||||
"tool-bash-1b",
|
"tool-bash-1b",
|
||||||
{ command: "generate output" },
|
{ command: "generate output" },
|
||||||
|
|||||||
Reference in New Issue
Block a user