* fix: reduce context duplication from ~22k to ~11k tokens Remove redundant env info and root AGENTS.md injection that OpenCode already provides, addressing significant token waste on startup. Changes: - src/agents/utils.ts: Remove duplicated env fields (working dir, platform, date) from createEnvContext(), keep only OmO-specific fields (time, timezone, locale) - src/hooks/directory-agents-injector/index.ts: Skip root AGENTS.md injection since OpenCode's system.ts already loads it via custom() Fixes #379 * refactor: remove unused _directory parameter from createEnvContext() --------- Co-authored-by: sisyphus-dev-ai <sisyphus-dev-ai@users.noreply.github.com>
183 lines
5.1 KiB
TypeScript
183 lines
5.1 KiB
TypeScript
import type { PluginInput } from "@opencode-ai/plugin";
|
|
import { existsSync, readFileSync } from "node:fs";
|
|
import { dirname, join, resolve } from "node:path";
|
|
import {
|
|
loadInjectedPaths,
|
|
saveInjectedPaths,
|
|
clearInjectedPaths,
|
|
} from "./storage";
|
|
import { AGENTS_FILENAME } from "./constants";
|
|
import { createDynamicTruncator } from "../../shared/dynamic-truncator";
|
|
|
|
interface ToolExecuteInput {
|
|
tool: string;
|
|
sessionID: string;
|
|
callID: string;
|
|
}
|
|
|
|
interface ToolExecuteOutput {
|
|
title: string;
|
|
output: string;
|
|
metadata: unknown;
|
|
}
|
|
|
|
interface ToolExecuteBeforeOutput {
|
|
args: unknown;
|
|
}
|
|
|
|
interface BatchToolCall {
|
|
tool: string;
|
|
parameters: Record<string, unknown>;
|
|
}
|
|
|
|
interface EventInput {
|
|
event: {
|
|
type: string;
|
|
properties?: unknown;
|
|
};
|
|
}
|
|
|
|
export function createDirectoryAgentsInjectorHook(ctx: PluginInput) {
|
|
const sessionCaches = new Map<string, Set<string>>();
|
|
const pendingBatchReads = new Map<string, string[]>();
|
|
const truncator = createDynamicTruncator(ctx);
|
|
|
|
function getSessionCache(sessionID: string): Set<string> {
|
|
if (!sessionCaches.has(sessionID)) {
|
|
sessionCaches.set(sessionID, loadInjectedPaths(sessionID));
|
|
}
|
|
return sessionCaches.get(sessionID)!;
|
|
}
|
|
|
|
function resolveFilePath(path: string): string | null {
|
|
if (!path) return null;
|
|
if (path.startsWith("/")) return path;
|
|
return resolve(ctx.directory, path);
|
|
}
|
|
|
|
function findAgentsMdUp(startDir: string): string[] {
|
|
const found: string[] = [];
|
|
let current = startDir;
|
|
|
|
while (true) {
|
|
// Skip root AGENTS.md - OpenCode's system.ts already loads it via custom()
|
|
// See: https://github.com/code-yeongyu/oh-my-opencode/issues/379
|
|
const isRootDir = current === ctx.directory;
|
|
if (!isRootDir) {
|
|
const agentsPath = join(current, AGENTS_FILENAME);
|
|
if (existsSync(agentsPath)) {
|
|
found.push(agentsPath);
|
|
}
|
|
}
|
|
|
|
if (isRootDir) break;
|
|
const parent = dirname(current);
|
|
if (parent === current) break;
|
|
if (!parent.startsWith(ctx.directory)) break;
|
|
current = parent;
|
|
}
|
|
|
|
return found.reverse();
|
|
}
|
|
|
|
async function processFilePathForInjection(
|
|
filePath: string,
|
|
sessionID: string,
|
|
output: ToolExecuteOutput,
|
|
): Promise<void> {
|
|
const resolved = resolveFilePath(filePath);
|
|
if (!resolved) return;
|
|
|
|
const dir = dirname(resolved);
|
|
const cache = getSessionCache(sessionID);
|
|
const agentsPaths = findAgentsMdUp(dir);
|
|
|
|
for (const agentsPath of agentsPaths) {
|
|
const agentsDir = dirname(agentsPath);
|
|
if (cache.has(agentsDir)) continue;
|
|
|
|
try {
|
|
const content = readFileSync(agentsPath, "utf-8");
|
|
const { result, truncated } = await truncator.truncate(sessionID, content);
|
|
const truncationNotice = truncated
|
|
? `\n\n[Note: Content was truncated to save context window space. For full context, please read the file directly: ${agentsPath}]`
|
|
: "";
|
|
output.output += `\n\n[Directory Context: ${agentsPath}]\n${result}${truncationNotice}`;
|
|
cache.add(agentsDir);
|
|
} catch {}
|
|
}
|
|
|
|
saveInjectedPaths(sessionID, cache);
|
|
}
|
|
|
|
const toolExecuteBefore = async (
|
|
input: ToolExecuteInput,
|
|
output: ToolExecuteBeforeOutput,
|
|
) => {
|
|
if (input.tool.toLowerCase() !== "batch") return;
|
|
|
|
const args = output.args as { tool_calls?: BatchToolCall[] } | undefined;
|
|
if (!args?.tool_calls) return;
|
|
|
|
const readFilePaths: string[] = [];
|
|
for (const call of args.tool_calls) {
|
|
if (call.tool.toLowerCase() === "read" && call.parameters?.filePath) {
|
|
readFilePaths.push(call.parameters.filePath as string);
|
|
}
|
|
}
|
|
|
|
if (readFilePaths.length > 0) {
|
|
pendingBatchReads.set(input.callID, readFilePaths);
|
|
}
|
|
};
|
|
|
|
const toolExecuteAfter = async (
|
|
input: ToolExecuteInput,
|
|
output: ToolExecuteOutput,
|
|
) => {
|
|
const toolName = input.tool.toLowerCase();
|
|
|
|
if (toolName === "read") {
|
|
await processFilePathForInjection(output.title, input.sessionID, output);
|
|
return;
|
|
}
|
|
|
|
if (toolName === "batch") {
|
|
const filePaths = pendingBatchReads.get(input.callID);
|
|
if (filePaths) {
|
|
for (const filePath of filePaths) {
|
|
await processFilePathForInjection(filePath, input.sessionID, output);
|
|
}
|
|
pendingBatchReads.delete(input.callID);
|
|
}
|
|
}
|
|
};
|
|
|
|
const eventHandler = async ({ event }: EventInput) => {
|
|
const props = event.properties as Record<string, unknown> | undefined;
|
|
|
|
if (event.type === "session.deleted") {
|
|
const sessionInfo = props?.info as { id?: string } | undefined;
|
|
if (sessionInfo?.id) {
|
|
sessionCaches.delete(sessionInfo.id);
|
|
clearInjectedPaths(sessionInfo.id);
|
|
}
|
|
}
|
|
|
|
if (event.type === "session.compacted") {
|
|
const sessionID = (props?.sessionID ??
|
|
(props?.info as { id?: string } | undefined)?.id) as string | undefined;
|
|
if (sessionID) {
|
|
sessionCaches.delete(sessionID);
|
|
clearInjectedPaths(sessionID);
|
|
}
|
|
}
|
|
};
|
|
|
|
return {
|
|
"tool.execute.before": toolExecuteBefore,
|
|
"tool.execute.after": toolExecuteAfter,
|
|
event: eventHandler,
|
|
};
|
|
}
|