diff --git a/biome.json b/biome.json index aa8b9d63c..2272ac603 100644 --- a/biome.json +++ b/biome.json @@ -44,6 +44,7 @@ "!**/node_modules/**/*", "!**/test-sessions.ts", "!**/template.generated.ts", + "!**/docs-index.generated.ts", "!**/gen/agent_pb.ts", "!.worktrees/**/*" ] diff --git a/package.json b/package.json index 704330452..c1b8592ad 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "fmt:ts": "biome format --write .", "fmt:rs": "cargo fmt --all", "fix": "bun run fix:ts && bun run fix:rs", - "fix:ts": "biome check --write --unsafe . && bun --cwd=packages/coding-agent run format-prompts", + "fix:ts": "biome check --write --unsafe . && bun --cwd=packages/coding-agent run format-prompts && bun --cwd=packages/coding-agent run generate-docs-index", "fix:rs": "cargo clippy --fix --allow-dirty --all-targets --no-deps --allow-staged --broken-code --allow-no-vcs && cargo fmt --all", "build:native": "bun --cwd=packages/natives run build:native", "dev:native": "bun --cwd=packages/natives run dev:native", @@ -34,7 +34,8 @@ "publish": "bun run prepublishOnly && npm publish -ws --access public", "publish:dry": "bun run prepublishOnly && npm publish -ws --access public --dry-run", "release": "bun scripts/release.ts", - "generate-models": "bun --cwd=packages/ai scripts/generate-models.ts" + "generate-models": "bun --cwd=packages/ai scripts/generate-models.ts", + "generate-docs-index": "bun --cwd=packages/coding-agent run generate-docs-index", }, "devDependencies": { "@biomejs/biome": "^2.4.2", diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 0e4cd7ed8..90e0d414d 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,8 +1,12 @@ # Changelog ## [Unreleased] + ### Added +- Support for `docs://` internal URL protocol to access embedded documentation files (e.g., `docs://sdk.md`) +- Added `generate-docs-index` npm script to automatically index and embed documentation files at build time +- Support for executable tool files (.ts, .js, .sh, .bash, .py) in custom tools discovery alongside markdown files - Display streamed tool intent in working message during agent execution - Added `tools.intentTracing` setting to enable intent tracing, which asks the agent to describe the intent of each tool call before executing it - Support for file deletion in hashline edit mode via `delete: true` parameter @@ -12,6 +16,9 @@ ### Changed +- System prompt now identifies agent as operating inside Oh My Pi harness and instructs reading docs:// URLs for omp/pi topics +- Tool discovery now accepts executable script extensions (.ts, .js, .sh, .bash, .py) in addition to .json and .md files +- Updated bash and read tool documentation to reference `docs://` URL support - Hashline format separator changed from pipe (`|`) to colon (`:`) for improved readability (e.g., `LINE#ID:content` instead of `LINE#ID|content`) - Hashline hash representation changed from 4-character base36 to 2-character hexadecimal for more compact line references - Hashline edit API: renamed `delete` parameter to `rm` for consistency with standard file operations diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index cf8556719..14c6d7191 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -78,6 +78,7 @@ "scripts": { "check": "tsgo -p tsconfig.json", "format-prompts": "bun scripts/format-prompts.ts", + "generate-docs-index": "bun scripts/generate-docs-index.ts", "build:binary": "cd ../.. && bun --cwd=packages/stats scripts/generate-client-bundle.ts && bun --cwd=packages/natives run embed:native && bun build --compile --define PI_COMPILED=true --root . ./packages/coding-agent/src/cli.ts --outfile packages/coding-agent/dist/omp && bun --cwd=packages/natives run embed:native --reset && bun --cwd=packages/stats scripts/generate-client-bundle.ts --reset", "generate-template": "bun scripts/generate-template.ts", "test": "bun test" diff --git a/packages/coding-agent/scripts/generate-docs-index.ts b/packages/coding-agent/scripts/generate-docs-index.ts new file mode 100644 index 000000000..c916ad8b3 --- /dev/null +++ b/packages/coding-agent/scripts/generate-docs-index.ts @@ -0,0 +1,56 @@ +#!/usr/bin/env bun + +import { Glob } from "bun"; +import * as path from "node:path"; + +const docsDir = new URL("../../../docs/", import.meta.url).pathname; +const outputPath = new URL("../src/internal-urls/docs-index.generated.ts", import.meta.url).pathname; +const importBase = "../../../../docs"; + +function toIdentifier(relativePath: string): string { + const withoutExt = relativePath.replace(/\.md$/i, ""); + const parts = withoutExt + .split(/[^a-zA-Z0-9]+/) + .filter(Boolean) + .map((part, index) => { + if (index === 0) { + return part.toLowerCase(); + } + return part.charAt(0).toUpperCase() + part.slice(1).toLowerCase(); + }); + + const base = parts.length > 0 ? parts.join("") : "doc"; + const safeBase = /^[0-9]/.test(base) ? `doc${base}` : base; + return `${safeBase}Md`; +} + +const glob = new Glob("**/*.md"); +const entries: string[] = []; +for await (const relativePath of glob.scan(docsDir)) { + entries.push(relativePath.split(path.sep).join("/")); +} +entries.sort(); + +const usedIdentifiers = new Set(); +const docs = entries.map((relativePath) => { + let identifier = toIdentifier(relativePath); + let suffix = 2; + while (usedIdentifiers.has(identifier)) { + identifier = `${toIdentifier(relativePath)}${suffix}`; + suffix++; + } + usedIdentifiers.add(identifier); + return { relativePath, identifier }; +}); +docs.sort((a, b) => a.identifier.localeCompare(b.identifier) || a.relativePath.localeCompare(b.relativePath)); + +const imports = docs + .map(({ relativePath, identifier }) => `import ${identifier} from "${importBase}/${relativePath}" with { type: "text" };`) + .join("\n"); + +const mapEntries = docs.map(({ relativePath, identifier }) => `\t"${relativePath}": ${identifier},`).join("\n"); + +const output = `// Auto-generated by scripts/generate-docs-index.ts - DO NOT EDIT\n\n${imports}\n\nexport const EMBEDDED_DOCS: Readonly> = {\n${mapEntries}\n};\n\nexport const EMBEDDED_DOC_FILENAMES = Object.keys(EMBEDDED_DOCS).sort();\n`; + +await Bun.write(outputPath, output); +console.log(`Generated ${path.relative(process.cwd(), outputPath)} (${docs.length} docs)`); diff --git a/packages/coding-agent/src/discovery/builtin.ts b/packages/coding-agent/src/discovery/builtin.ts index 02afddb28..3bc04cdf6 100644 --- a/packages/coding-agent/src/discovery/builtin.ts +++ b/packages/coding-agent/src/discovery/builtin.ts @@ -656,7 +656,7 @@ async function loadTools(ctx: LoadContext): Promise> { fileLoadPromises.push( loadFilesFromDir(ctx, toolsDir, PROVIDER_ID, level, { - extensions: ["json", "md"], + extensions: ["json", "md", "ts", "js", "sh", "bash", "py"], transform: (name, content, path, source) => { if (name.endsWith(".json")) { const data = parseJSON<{ name?: string; description?: string }>(content); @@ -668,11 +668,21 @@ async function loadTools(ctx: LoadContext): Promise> { _source: source, }; } - const { frontmatter } = parseFrontmatter(content, { source: path }); + if (name.endsWith(".md")) { + const { frontmatter } = parseFrontmatter(content, { source: path }); + return { + name: (frontmatter.name as string) || name.replace(/\.md$/, ""), + path, + description: frontmatter.description as string | undefined, + level, + _source: source, + }; + } + // Executable tool files (.ts, .js, .sh, .bash, .py) + const toolName = name.replace(/\.(ts|js|sh|bash|py)$/, ""); return { - name: (frontmatter.name as string) || name.replace(/\.md$/, ""), + name: toolName, path, - description: frontmatter.description as string | undefined, level, _source: source, }; diff --git a/packages/coding-agent/src/internal-urls/docs-index.generated.ts b/packages/coding-agent/src/internal-urls/docs-index.generated.ts new file mode 100644 index 000000000..c45cab175 --- /dev/null +++ b/packages/coding-agent/src/internal-urls/docs-index.generated.ts @@ -0,0 +1,101 @@ +// Auto-generated by scripts/generate-docs-index.ts - DO NOT EDIT + +import bashToolRuntimeMd from "../../../../docs/bash-tool-runtime.md" with { type: "text" }; +import blobArtifactArchitectureMd from "../../../../docs/blob-artifact-architecture.md" with { type: "text" }; +import compactionMd from "../../../../docs/compaction.md" with { type: "text" }; +import configUsageMd from "../../../../docs/config-usage.md" with { type: "text" }; +import customToolsMd from "../../../../docs/custom-tools.md" with { type: "text" }; +import environmentVariablesMd from "../../../../docs/environment-variables.md" with { type: "text" }; +import extensionLoadingMd from "../../../../docs/extension-loading.md" with { type: "text" }; +import extensionsMd from "../../../../docs/extensions.md" with { type: "text" }; +import fsScanCacheArchitectureMd from "../../../../docs/fs-scan-cache-architecture.md" with { type: "text" }; +import geminiManifestExtensionsMd from "../../../../docs/gemini-manifest-extensions.md" with { type: "text" }; +import handoffGenerationPipelineMd from "../../../../docs/handoff-generation-pipeline.md" with { type: "text" }; +import hooksMd from "../../../../docs/hooks.md" with { type: "text" }; +import mcpProtocolTransportsMd from "../../../../docs/mcp-protocol-transports.md" with { type: "text" }; +import mcpRuntimeLifecycleMd from "../../../../docs/mcp-runtime-lifecycle.md" with { type: "text" }; +import mcpServerToolAuthoringMd from "../../../../docs/mcp-server-tool-authoring.md" with { type: "text" }; +import modelsMd from "../../../../docs/models.md" with { type: "text" }; +import nativesAddonLoaderRuntimeMd from "../../../../docs/natives-addon-loader-runtime.md" with { type: "text" }; +import nativesArchitectureMd from "../../../../docs/natives-architecture.md" with { type: "text" }; +import nativesBindingContractMd from "../../../../docs/natives-binding-contract.md" with { type: "text" }; +import nativesBuildReleaseDebuggingMd from "../../../../docs/natives-build-release-debugging.md" with { type: "text" }; +import nativesMediaSystemUtilsMd from "../../../../docs/natives-media-system-utils.md" with { type: "text" }; +import nativesRustTaskCancellationMd from "../../../../docs/natives-rust-task-cancellation.md" with { type: "text" }; +import nativesShellPtyProcessMd from "../../../../docs/natives-shell-pty-process.md" with { type: "text" }; +import nativesTextSearchPipelineMd from "../../../../docs/natives-text-search-pipeline.md" with { type: "text" }; +import nonCompactionRetryPolicyMd from "../../../../docs/non-compaction-retry-policy.md" with { type: "text" }; +import notebookToolRuntimeMd from "../../../../docs/notebook-tool-runtime.md" with { type: "text" }; +import pluginManagerInstallerPlumbingMd from "../../../../docs/plugin-manager-installer-plumbing.md" with { type: "text" }; +import portingFromPiMonoMd from "../../../../docs/porting-from-pi-mono.md" with { type: "text" }; +import portingToNativesMd from "../../../../docs/porting-to-natives.md" with { type: "text" }; +import providerStreamingInternalsMd from "../../../../docs/provider-streaming-internals.md" with { type: "text" }; +import pythonReplMd from "../../../../docs/python-repl.md" with { type: "text" }; +import rpcMd from "../../../../docs/rpc.md" with { type: "text" }; +import rulebookMatchingPipelineMd from "../../../../docs/rulebook-matching-pipeline.md" with { type: "text" }; +import sdkMd from "../../../../docs/sdk.md" with { type: "text" }; +import secretsMd from "../../../../docs/secrets.md" with { type: "text" }; +import sessionMd from "../../../../docs/session.md" with { type: "text" }; +import sessionOperationsExportShareForkResumeMd from "../../../../docs/session-operations-export-share-fork-resume.md" with { type: "text" }; +import sessionSwitchingAndRecentListingMd from "../../../../docs/session-switching-and-recent-listing.md" with { type: "text" }; +import sessionTreePlanMd from "../../../../docs/session-tree-plan.md" with { type: "text" }; +import skillsMd from "../../../../docs/skills.md" with { type: "text" }; +import slashCommandInternalsMd from "../../../../docs/slash-command-internals.md" with { type: "text" }; +import taskAgentDiscoveryMd from "../../../../docs/task-agent-discovery.md" with { type: "text" }; +import themeMd from "../../../../docs/theme.md" with { type: "text" }; +import treeMd from "../../../../docs/tree.md" with { type: "text" }; +import ttsrInjectionLifecycleMd from "../../../../docs/ttsr-injection-lifecycle.md" with { type: "text" }; +import tuiMd from "../../../../docs/tui.md" with { type: "text" }; +import tuiRuntimeInternalsMd from "../../../../docs/tui-runtime-internals.md" with { type: "text" }; + +export const EMBEDDED_DOCS: Readonly> = { + "bash-tool-runtime.md": bashToolRuntimeMd, + "blob-artifact-architecture.md": blobArtifactArchitectureMd, + "compaction.md": compactionMd, + "config-usage.md": configUsageMd, + "custom-tools.md": customToolsMd, + "environment-variables.md": environmentVariablesMd, + "extension-loading.md": extensionLoadingMd, + "extensions.md": extensionsMd, + "fs-scan-cache-architecture.md": fsScanCacheArchitectureMd, + "gemini-manifest-extensions.md": geminiManifestExtensionsMd, + "handoff-generation-pipeline.md": handoffGenerationPipelineMd, + "hooks.md": hooksMd, + "mcp-protocol-transports.md": mcpProtocolTransportsMd, + "mcp-runtime-lifecycle.md": mcpRuntimeLifecycleMd, + "mcp-server-tool-authoring.md": mcpServerToolAuthoringMd, + "models.md": modelsMd, + "natives-addon-loader-runtime.md": nativesAddonLoaderRuntimeMd, + "natives-architecture.md": nativesArchitectureMd, + "natives-binding-contract.md": nativesBindingContractMd, + "natives-build-release-debugging.md": nativesBuildReleaseDebuggingMd, + "natives-media-system-utils.md": nativesMediaSystemUtilsMd, + "natives-rust-task-cancellation.md": nativesRustTaskCancellationMd, + "natives-shell-pty-process.md": nativesShellPtyProcessMd, + "natives-text-search-pipeline.md": nativesTextSearchPipelineMd, + "non-compaction-retry-policy.md": nonCompactionRetryPolicyMd, + "notebook-tool-runtime.md": notebookToolRuntimeMd, + "plugin-manager-installer-plumbing.md": pluginManagerInstallerPlumbingMd, + "porting-from-pi-mono.md": portingFromPiMonoMd, + "porting-to-natives.md": portingToNativesMd, + "provider-streaming-internals.md": providerStreamingInternalsMd, + "python-repl.md": pythonReplMd, + "rpc.md": rpcMd, + "rulebook-matching-pipeline.md": rulebookMatchingPipelineMd, + "sdk.md": sdkMd, + "secrets.md": secretsMd, + "session.md": sessionMd, + "session-operations-export-share-fork-resume.md": sessionOperationsExportShareForkResumeMd, + "session-switching-and-recent-listing.md": sessionSwitchingAndRecentListingMd, + "session-tree-plan.md": sessionTreePlanMd, + "skills.md": skillsMd, + "slash-command-internals.md": slashCommandInternalsMd, + "task-agent-discovery.md": taskAgentDiscoveryMd, + "theme.md": themeMd, + "tree.md": treeMd, + "ttsr-injection-lifecycle.md": ttsrInjectionLifecycleMd, + "tui.md": tuiMd, + "tui-runtime-internals.md": tuiRuntimeInternalsMd, +}; + +export const EMBEDDED_DOC_FILENAMES = Object.keys(EMBEDDED_DOCS).sort(); diff --git a/packages/coding-agent/src/internal-urls/docs-protocol.ts b/packages/coding-agent/src/internal-urls/docs-protocol.ts new file mode 100644 index 000000000..439b11cfd --- /dev/null +++ b/packages/coding-agent/src/internal-urls/docs-protocol.ts @@ -0,0 +1,84 @@ +/** + * Protocol handler for docs:// URLs. + * + * Serves statically embedded documentation files bundled at build time. + * + * URL forms: + * - docs:// - Lists all available documentation files + * - docs://.md - Reads a specific documentation file + */ +import * as path from "node:path"; +import { EMBEDDED_DOC_FILENAMES, EMBEDDED_DOCS } from "./docs-index.generated"; +import type { InternalResource, InternalUrl, ProtocolHandler } from "./types"; + +/** + * Handler for docs:// URLs. + * + * Resolves documentation file names to their content, or lists available docs. + */ +export class DocsProtocolHandler implements ProtocolHandler { + readonly scheme = "docs"; + + async resolve(url: InternalUrl): Promise { + // Extract filename from host + path + const host = url.rawHost || url.hostname; + const pathname = url.rawPathname ?? url.pathname; + const filename = host ? (pathname && pathname !== "/" ? host + pathname : host) : ""; + + if (!filename) { + return this.#listDocs(url); + } + + return this.#readDoc(filename, url); + } + + async #listDocs(url: InternalUrl): Promise { + if (EMBEDDED_DOC_FILENAMES.length === 0) { + throw new Error("No documentation files found"); + } + + const listing = EMBEDDED_DOC_FILENAMES.map(f => `- [${f}](docs://${f})`).join("\n"); + const content = `# Documentation\n\n${EMBEDDED_DOC_FILENAMES.length} files available:\n\n${listing}\n`; + + return { + url: url.href, + content, + contentType: "text/markdown", + size: Buffer.byteLength(content, "utf-8"), + sourcePath: "docs://", + }; + } + + async #readDoc(filename: string, url: InternalUrl): Promise { + // Validate: no traversal, no absolute paths + if (path.isAbsolute(filename)) { + throw new Error("Absolute paths are not allowed in docs:// URLs"); + } + + const normalized = path.posix.normalize(filename.replaceAll("\\", "/")); + if (normalized === ".." || normalized.startsWith("../") || normalized.includes("/../")) { + throw new Error("Path traversal (..) is not allowed in docs:// URLs"); + } + + const content = EMBEDDED_DOCS[normalized]; + if (content === undefined) { + const lookup = normalized.replace(/\.md$/, ""); + const suggestions = EMBEDDED_DOC_FILENAMES.filter( + f => f.includes(lookup) || lookup.includes(f.replace(/\.md$/, "")), + ).slice(0, 5); + const suffix = + suggestions.length > 0 + ? `\nDid you mean: ${suggestions.join(", ")}` + : "\nUse docs:// to list available files."; + throw new Error(`Documentation file not found: ${filename}${suffix}`); + } + + return { + url: url.href, + content, + contentType: "text/markdown", + size: Buffer.byteLength(content, "utf-8"), + sourcePath: `docs://${normalized}`, + }; + } +} diff --git a/packages/coding-agent/src/internal-urls/index.ts b/packages/coding-agent/src/internal-urls/index.ts index 13fa9a2d6..256362d04 100644 --- a/packages/coding-agent/src/internal-urls/index.ts +++ b/packages/coding-agent/src/internal-urls/index.ts @@ -22,6 +22,7 @@ export { AgentProtocolHandler, type AgentProtocolOptions } from "./agent-protocol"; export { ArtifactProtocolHandler, type ArtifactProtocolOptions } from "./artifact-protocol"; +export { DocsProtocolHandler } from "./docs-protocol"; export { applyQuery, parseQuery, pathToQuery } from "./json-query"; export { MemoryProtocolHandler, type MemoryProtocolOptions, resolveMemoryUrlToPath } from "./memory-protocol"; export { PlanProtocolHandler, type PlanProtocolOptions, resolvePlanUrlToPath } from "./plan-protocol"; diff --git a/packages/coding-agent/src/internal-urls/router.ts b/packages/coding-agent/src/internal-urls/router.ts index 9dd439fc9..e4324e6b0 100644 --- a/packages/coding-agent/src/internal-urls/router.ts +++ b/packages/coding-agent/src/internal-urls/router.ts @@ -1,5 +1,5 @@ /** - * Internal URL router for internal protocols (agent://, artifact://, plan://, memory://, skill://, rule://). + * Internal URL router for internal protocols (agent://, artifact://, plan://, memory://, skill://, rule://, docs://). */ import type { InternalResource, InternalUrl, ProtocolHandler } from "./types"; diff --git a/packages/coding-agent/src/prompts/system/system-prompt.md b/packages/coding-agent/src/prompts/system/system-prompt.md index 8bfdf977a..b47533bae 100644 --- a/packages/coding-agent/src/prompts/system/system-prompt.md +++ b/packages/coding-agent/src/prompts/system/system-prompt.md @@ -1,5 +1,5 @@ -Distinguished Staff Engineer. +You are a distinguished staff engineer operating inside Oh My Pi, a Pi-based coding harness. High-agency. Principled. Decisive. Expertise: debugging, refactoring, system design. @@ -172,6 +172,17 @@ Main branch: {{git.mainBranch}} {{/if}} + +Oh My Pi ships internal documentation accessible via `docs://` URLs (resolved by tools like read/grep). +- Read `docs://` to list all available documentation files +- Read `docs://.md` to read a specific doc + + +- **ONLY** read docs when the user asks about omp/pi itself: its SDK, extensions, themes, skills, TUI, keybindings, or configuration. +- When working on omp/pi topics, read the relevant docs and follow .md cross-references before implementing. + + + {{#if skills.length}} Scan descriptions vs task domain. Skill covers output? Read `skill://` first. @@ -221,7 +232,7 @@ Notice the sequential habit: - Comfort in doing one thing at a time - Illusion that order = correctness - Assumption that B depends on A -**Use Task tool when:** + **Use Task tool when:** - Editing 4+ files with no dependencies between edits - Investigating 2+ independent subsystems - Work decomposes into pieces not needing each other's results diff --git a/packages/coding-agent/src/prompts/tools/bash.md b/packages/coding-agent/src/prompts/tools/bash.md index f608a3897..3336ab9c5 100644 --- a/packages/coding-agent/src/prompts/tools/bash.md +++ b/packages/coding-agent/src/prompts/tools/bash.md @@ -8,7 +8,7 @@ Executes bash command in shell session for terminal operations like git, bun, ca - `skill://` URIs are auto-resolved to filesystem paths before execution - `python skill://my-skill/scripts/init.py` runs the script from the skill directory - `skill:///` resolves within the skill's base directory -- `agent://`, `artifact://`, `plan://`, `memory://`, and `rule://` URIs are also auto-resolved to filesystem paths before execution +- `agent://`, `artifact://`, `plan://`, `memory://`, `rule://`, and `docs://` URIs are also auto-resolved to filesystem paths before execution diff --git a/packages/coding-agent/src/prompts/tools/read.md b/packages/coding-agent/src/prompts/tools/read.md index e55324ff2..b907f773a 100644 --- a/packages/coding-agent/src/prompts/tools/read.md +++ b/packages/coding-agent/src/prompts/tools/read.md @@ -23,6 +23,8 @@ Reads files from local filesystem or internal URLs. - `memory://root/` - read relative path within project memory root - `agent://` - read agent output artifact - `agent:///` or `agent://?q=` - extract JSON from agent output + - `docs://` - list available pi documentation files + - `docs://.md` - read a specific pi documentation file diff --git a/packages/coding-agent/src/sdk.ts b/packages/coding-agent/src/sdk.ts index b86666caf..78e4fb55c 100644 --- a/packages/coding-agent/src/sdk.ts +++ b/packages/coding-agent/src/sdk.ts @@ -19,6 +19,7 @@ import { type CustomCommandsLoadResult, loadCustomCommands as loadCustomCommandsInternal, } from "./extensibility/custom-commands"; +import { discoverAndLoadCustomTools } from "./extensibility/custom-tools"; import type { CustomTool, CustomToolContext, CustomToolSessionEvent } from "./extensibility/custom-tools/types"; import { CustomToolAdapter } from "./extensibility/custom-tools/wrapper"; import { @@ -39,6 +40,7 @@ import { type FileSlashCommand, loadSlashCommands as loadSlashCommandsInternal } import { AgentProtocolHandler, ArtifactProtocolHandler, + DocsProtocolHandler, InternalUrlRouter, MemoryProtocolHandler, PlanProtocolHandler, @@ -770,6 +772,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {} getRules: () => rulebookRules, }), ); + internalRouter.register(new DocsProtocolHandler()); toolSession.internalRouter = internalRouter; toolSession.getArtifactsDir = getArtifactsDir; toolSession.agentOutputManager = new AgentOutputManager( @@ -848,6 +851,19 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {} time("getSearchTools"); } + debugStartup("sdk:discoverCustomTools:start"); + // Discover and load custom tools from .omp/tools/, .claude/tools/, etc. + const builtInToolNames = builtinTools.map(t => t.name); + const discoveredCustomTools = await discoverAndLoadCustomTools([], cwd, builtInToolNames); + for (const { path, error } of discoveredCustomTools.errors) { + logger.error("Custom tool load failed", { path, error }); + } + if (discoveredCustomTools.tools.length > 0) { + customTools.push(...discoveredCustomTools.tools.map(loaded => loaded.tool)); + } + time("discoverAndLoadCustomTools"); + debugStartup("sdk:discoverCustomTools:done"); + const inlineExtensions: ExtensionFactory[] = options.extensions ? [...options.extensions] : []; if (customTools.length > 0) { inlineExtensions.push(createCustomToolsExtension(customTools)); diff --git a/packages/coding-agent/src/tools/fetch.ts b/packages/coding-agent/src/tools/fetch.ts index 845d20562..d2588fbf4 100644 --- a/packages/coding-agent/src/tools/fetch.ts +++ b/packages/coding-agent/src/tools/fetch.ts @@ -966,11 +966,12 @@ function countNonEmptyLines(text: string): number { /** Render fetch call (URL preview) */ export function renderFetchCall( - args: { url: string; timeout?: number; raw?: boolean }, + args: { url?: string; timeout?: number; raw?: boolean }, uiTheme: Theme = theme, ): Component { - const domain = getDomain(args.url); - const path = truncate(args.url.replace(/^https?:\/\/[^/]+/, ""), 50, "…"); + const url = args.url ?? ""; + const domain = getDomain(url); + const path = truncate(url.replace(/^https?:\/\/[^/]+/, ""), 50, "\u2026"); const description = `${domain}${path ? ` ${path}` : ""}`.trim(); const meta: string[] = []; if (args.raw) meta.push("raw"); diff --git a/packages/coding-agent/src/web/search/render.ts b/packages/coding-agent/src/web/search/render.ts index 456bce445..f58a5ceab 100644 --- a/packages/coding-agent/src/web/search/render.ts +++ b/packages/coding-agent/src/web/search/render.ts @@ -282,11 +282,11 @@ export function renderSearchResult( /** Render web search call (query preview) */ export function renderSearchCall( - args: { query: string; provider?: string; [key: string]: unknown }, + args: { query?: string; provider?: string; [key: string]: unknown }, theme: Theme, ): Component { const provider = args.provider ?? "auto"; - const query = truncateToWidth(args.query, 80); + const query = truncateToWidth(args.query ?? "", 80); const text = renderStatusLine({ icon: "pending", title: "Web Search", description: query, meta: [provider] }, theme); return new Text(text, 0, 0); }