- Added IPython-backed Python tool with streaming output and image/JSON rendering. - Implemented Jupyter kernel gateway integration with WebSocket communication. - Added Python prelude with 30+ shell-like utility functions for file operations. - Migrated environment variables from PI_ to OMP_ prefix with automatic migration. - Added streaming output system with automatic spill-to-disk for large outputs. - Reorganized settings interface into behavior, tools, display, voice, status, lsp, and exa tabs.
7.6 KiB
Dynamic Python Tool Documentation
Problem
packages/coding-agent/src/prompts/tools/python.md contains hardcoded documentation for prelude helpers:
### File I/O
- `read(path, limit=None)` — read file, optional char limit
- `write(path, content)` — write file (creates parents)
...
This duplicates information already present in python-prelude.py (function signatures and docstrings). When the prelude changes, the markdown must be manually updated — they drift.
Goal
Extract helper documentation from the running Python environment via runtime introspection, then populate python.md using Handlebars templating (same system as system-prompt.md).
Current Flow
startup
└─> checkPythonKernelAvailability(cwd) // just checks if kernel_gateway is installed
└─> returns { ok: true/false, reason }
createTools(session)
└─> if python available && mode != "bash-only"
└─> createPythonTool(session)
└─> description: renderPromptTemplate(pythonDescription) // no context, static
Proposed Flow
startup
└─> warmPythonKernel(cwd)
├─> start kernel gateway + kernel
├─> run prelude
├─> run introspection snippet
│ └─> returns JSON: [{ name, signature, docstring, category }, ...]
├─> cache extracted docs in module-level state
└─> returns { ok, reason, kernel }
createTools(session)
└─> if python available && mode != "bash-only"
└─> createPythonTool(session)
└─> description: renderPromptTemplate(pythonDescription, { helpers: cachedDocs })
session reset
└─> kernel.restart() // reuse same gateway, just restart kernel
Key Changes
1. python-kernel.ts — Add introspection method
interface PreludeHelper {
name: string;
signature: string;
docstring: string;
}
class PythonKernel {
// existing...
async introspectPrelude(): Promise<PreludeHelper[]> {
const result = await this.execute(INTROSPECTION_SNIPPET);
return JSON.parse(result.output);
}
}
2. python-executor.ts — Expose warm kernel + cached docs
let cachedPreludeDocs: PreludeHelper[] | null = null;
export async function warmPythonEnvironment(cwd: string): Promise<{
ok: boolean;
reason?: string;
docs: PreludeHelper[];
}> {
// 1. Check availability (existing)
// 2. Start kernel (new - currently deferred to first execute)
// 3. Run prelude (already happens on kernel start)
// 4. Introspect and cache
// 5. Return docs
}
export function getPreludeDocs(): PreludeHelper[] {
return cachedPreludeDocs ?? [];
}
3. tools/index.ts — Warm kernel during tool creation
export async function createTools(session: ToolSession, toolNames?: string[]): Promise<Tool[]> {
// ...existing python availability check...
if (shouldCheckPython) {
const warmup = await warmPythonEnvironment(session.cwd);
pythonAvailable = warmup.ok;
// docs now cached for use by createPythonTool
}
// ...rest unchanged...
}
4. tools/python.ts — Pass docs to template
import { getPreludeDocs } from "../python-executor";
export function createPythonTool(session: ToolSession): AgentTool<typeof pythonSchema> {
const helpers = getPreludeDocs();
const categories = groupByCategory(helpers); // group into File I/O, Navigation, etc.
return {
name: "python",
description: renderPromptTemplate(pythonDescription, { categories }),
// ...
};
}
5. prompts/tools/python.md — Use Handlebars
## Prelude helpers
All helpers auto-print results and return values for chaining.
{{#each categories}}
### {{name}}
{{#each functions}}
- `{{name}}{{signature}}` — {{docstring}}
{{/each}}
{{/each}}
Introspection Snippet
import inspect, json
CATEGORIES = {
'read': 'File I/O', 'write': 'File I/O', 'append': 'File I/O', ...
'cp': 'File operations', 'mv': 'File operations', ...
}
helpers = []
for name, cat in CATEGORIES.items():
obj = globals().get(name)
if not callable(obj):
continue
sig = str(inspect.signature(obj))
doc = (inspect.getdoc(obj) or '').split('\n')[0]
helpers.append({'name': name, 'signature': sig, 'docstring': doc, 'category': cat})
print(json.dumps(helpers))
Category Mapping
Categories are defined in the introspection snippet (Python side), not TypeScript. This keeps the source of truth in one place. Order:
- File I/O:
read,write,append,touch,cat - File operations:
cp,mv,rm,mkdir - Navigation:
pwd,cd,ls,tree,stat - Search:
find,glob_files,grep,rgrep - Text processing:
head,tail,sort_lines,uniq,cols,wc - Find and replace:
replace,sed,rsed - Batch operations:
batch,diff - Shell bridge:
run,bash,env
Session Reset Behavior
Currently: reset: true parameter triggers full kernel restart via restartKernelSession().
Proposed: Same behavior, but the pre-warmed kernel is the one being restarted. No change needed — the existing session management already handles this.
Environment Variable Override
Add OMP_PY environment variable to override the settings preference:
| Value | Mode | Description |
|---|---|---|
0 or bash |
bash-only | Disable Python tool, use bash only |
1 or py |
ipy-only | Disable bash tool, use Python only |
mix or both |
both | Enable both bash and Python tools |
Priority: OMP_PY env var > settings preference > default (ipy-only)
Implementation
In tools/index.ts:
function getPythonModeFromEnv(): "ipy-only" | "bash-only" | "both" | null {
const value = process.env.OMP_PY?.toLowerCase();
if (!value) return null;
switch (value) {
case "0":
case "bash":
return "bash-only";
case "1":
case "py":
return "ipy-only";
case "mix":
case "both":
return "both";
default:
return null;
}
}
export async function createTools(session: ToolSession, toolNames?: string[]): Promise<Tool[]> {
// ...
const pythonMode = getPythonModeFromEnv() ?? session.settings?.getPythonToolMode?.() ?? "ipy-only";
// ...
}
Use Cases
OMP_PY=0 omp— Force bash mode for compatibility testingOMP_PY=1 omp— Force Python mode even if settings say otherwiseOMP_PY=mix omp— Enable both for users who want choice
Fallback
If kernel warmup fails (no Python, no kernel_gateway), getPreludeDocs() returns empty array. The template should handle this gracefully:
{{#if categories.length}}
## Prelude helpers
...
{{else}}
## Prelude helpers
(Documentation unavailable — Python kernel failed to start)
{{/if}}
Files to Modify
packages/coding-agent/src/core/python-kernel.ts— AddintrospectPrelude()methodpackages/coding-agent/src/core/python-executor.ts— AddwarmPythonEnvironment(),getPreludeDocs()packages/coding-agent/src/core/tools/index.ts— Call warmup duringcreateTools(), addOMP_PYenv overridepackages/coding-agent/src/core/tools/python.ts— Pass docs context to templatepackages/coding-agent/src/prompts/tools/python.md— Convert to Handlebars templatepackages/coding-agent/src/core/python-prelude.py— Add category markers or rely on introspection snippet
Testing
- Unit test:
introspectPrelude()returns expected structure - Unit test:
warmPythonEnvironment()populates cache - Unit test: Template renders correctly with mock docs
- Integration test: Full flow from startup to tool description containing extracted docs
- Fallback test: Empty docs when Python unavailable