PerFilePromptGenerator: prompt template includes are re-read from disk once per file
まだ誰も着手していません。
評価
- 難易度
- 2/5
- 見積もり時間
- 1〜3時間
- 初心者へのやさしさ
- 75/100
- issue の種類
- リファクタリング
- 明瞭さ
- 明確に書かれている
- 活発さ
- 静か
- 技術スタック
- typescript
- 領域
- performance
調査の方向性
src/prompt-generators/per-file.ts と src/util/expand-prompt.ts から始め、提供されている dry-run の再現、または readFile の呼び出し回数を数えながらユニットレベルのジェネレーター再現を実行します。ファイルごとのファイル置換を維持したまま、include の展開をファイルごとのループの外に移動します。完了条件は、生成されるプロンプトが変わらず、include ファイルが generate() の呼び出しごとに1回だけ読み込まれることです。
索引モデルが issue の本文から書いたものです。
説明
Observed behavior
PerFilePromptGenerator.generate() calls expandPrompt(template, basePath, { file }) inside the per-file loop. expandPrompt runs expandIncludes first (which recursively readFiles every {{include:path}} target) and only then substitutes the variables.
Source: src/prompt-generators/per-file.ts:67-78
async *generate(loopState: LoopState): AsyncIterable<Prompt> {
const { filePattern, excludePatterns } = this.#task;
const allFiles = await resolveFiles(filePattern, excludePatterns);
for (const file of allFiles) {
if (loopState.isOutstanding(file)) {
const template = this.#task.promptTemplate;
const prompt = await expandPrompt(template, this.#basePath, { file });
yield { id: file, prompt };
}
}
}
And src/util/expand-prompt.ts:17-29:
export async function expandPrompt(
template: string,
basePath: string,
variables: Readonly<Record<string, string>>,
): Promise<string> {
let result = await expandIncludes(template, basePath);
for (const [key, value] of Object.entries(variables)) {
result = result.replaceAll(`{{${key}}}`, value);
}
return result;
}
The only per-file variable is file. Includes do not, and cannot, depend on it: expandIncludes runs strictly before variable substitution. So the resolved-includes form of the template is identical across every iteration of the loop, yet it is recomputed (and the include files re-read) for every file.
For the project's own src/examples/quality-check/quality-check.json config, which uses {{include:prompt.md}} against filePattern: src/**/*.ts, every prompt re-reads prompt.md even though its contents never change between iterations. Larger include trees and bigger file sets amplify the wasted I/O linearly. With recursive includes the multiplier compounds, since expandIncludes walks the include graph again from scratch on every call.
Expected behavior
Expand {{include:...}} macros once per generate() call, cache the result, and only run the {{file}} substitution per iteration. The include graph is fixed for the lifetime of a run, so this is a safe optimization with the same observable output.
Minimal reproduction
mkdir -p /tmp/llt-include-repro/src
for i in $(seq 1 200); do touch /tmp/llt-include-repro/src/f$i.ts; done
# A non-trivial include file
cat > /tmp/llt-include-repro/prompt.md <<'MD'
Review {{file}} for issues.
MD
cat > /tmp/llt-include-repro/config.json <<'JSON'
{
"name": "include-perf",
"agent": "claude-sdk",
"promptGenerator": [
"per-file",
{
"filePattern": "src/**/*.ts",
"promptTemplate": "{{include:prompt.md}}"
}
]
}
JSON
# Use --dry-run so we exercise only prompt generation, then strace/instrument the
# readFile calls to prompt.md. With 200 files there are 200 readFile calls for
# prompt.md; one is sufficient.
cd /tmp/llt-include-repro
pnpx loop-the-loop --dry-run config.json
Equivalently, a unit-level repro: wrap node:fs/promises.readFile with a counter, run PerFilePromptGenerator.generate() over N files where the template is {{include:prompt.md}}, and observe N reads of prompt.md instead of 1.
Suggested fix
Move the include expansion out of the loop:
async *generate(loopState: LoopState): AsyncIterable<Prompt> {
const { filePattern, excludePatterns, promptTemplate } = this.#task;
const allFiles = await resolveFiles(filePattern, excludePatterns);
const expandedTemplate = await expandIncludes(promptTemplate, this.#basePath);
for (const file of allFiles) {
if (loopState.isOutstanding(file)) {
const prompt = expandedTemplate.replaceAll('{{file}}', file);
yield { id: file, prompt };
}
}
}
This preserves the existing behavior for templates with no includes (one cheap expandIncludes call up front instead of N), and avoids re-reading any included files for every iteration. The same optimization applies to other generators that call expandPrompt in a loop (json, bugzilla, github, gitlab) and is worth considering uniformly, but per-file is the one this issue is about. A unified alternative would be to add an expandPromptWithCachedIncludes (or an opt-in precomputeIncludes) helper in src/util/expand-prompt.ts and use it from all looping generators.
Note this is purely a performance/maintainability fix: callers see the same expanded text either way, so no behavioral migration is required.
- 主要言語
- TypeScript
- スター
- 2
- フォーク
- 1
- PR マージ指標
- 30日以内にマージされた PR はありません
環境構築
このプロジェクトには開発コンテナ、Dockerfile、コントリビューションガイドがありません。まず README を読み、一般的な手順ははじめてのコントリビューションガイドを参照してください。
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
joewalker/loop-the-loop のほかの issue
-
bug S4
難易度 2/5 1〜3時間 初心者へのやさしさ 75/100
joewalker/loop-the-loop#88 ·
-
Git exec(): child killed by a signal rejects with new Error('') and loses the signal informationオープンbug S3
難易度 2/5 1〜3時間 初心者へのやさしさ 82/100
joewalker/loop-the-loop#84 ·
-
bug S3
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
joewalker/loop-the-loop#83 ·
-
bug S3
難易度 2/5 1〜3時間 初心者へのやさしさ 76/100
joewalker/loop-the-loop#82 ·
-
bug S4
難易度 1/5 1時間未満 初心者へのやさしさ 90/100
joewalker/loop-the-loop#79 ·
joewalker/loop-the-loop の issue をすべて見る
似ている issue
-
fix(worker): keep normal prepare waiting states failure-free対応中かも @LichKing-2234 が今日担当しました。 オープンagent:codex priority:P1
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
AgoraIO-Extensions/agent-infra#3745 ·
メンテナーはふだん 1 日以内に返信
-
The five-line work window can drop its oldest line: the per-line byte share ignores the JSON framing対応中かも @Eapp1e が今日担当しました。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 84/100
makecindy/cindy#7973 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
community first-timers-only good first issue hacktoberfest help wanted low hanging fruit up-for-grabs
難易度 1/5 1時間未満 初心者へのやさしさ 78/100
lingdojo/kana-dojo#33337 · コメント 1 件 · リアクション 5 件 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 76/100
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
code-yeongyu/senpi#4917 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信