JsonPromptGenerator: object fields named 'id' or 'index' silently shadow the built-in {{id}} / {{index}} template variables
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 82/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Quiet
- Tech stack
- typescript
- Domain
- tooling
Research direction
Start in src/prompt-generators/json.ts at buildVariables and read the JsonPromptGenerator prompt-template behavior. Add coverage for elements with id and index fields, checking that {{id}} uses the resolved tracking ID and {{index}} uses the iteration position. Done means element fields remain usable while both built-in placeholders retain their documented values.
Written by the indexing model from the issue text.
Description
Observed behavior
buildVariables in src/prompt-generators/json.ts seeds the variables map with the built-in placeholders first, then overlays every top-level field of the element:
const variables: Record<string, string> = {
id,
index: String(index),
};
if (
element !== null &&
typeof element === 'object' &&
!Array.isArray(element)
) {
for (const [key, val] of Object.entries(element as Record<string, unknown>)) {
variables[key] = String(val); // overrides id and index
}
}
The JsonTask.promptTemplate docstring states:
{{id}}- the resolved ID used for LoopState tracking{{index}}- the 0-based position in the iteration
But because the element's own fields are written into variables after the built-ins, any field named id or index silently replaces the documented placeholder values. Concretely:
- With
idField: 'uuid'and element{ uuid: 'abc', id: 'xyz' }, the loop tracks the prompt asabc(viaresolveId), but the prompt template renders{{id}}asxyz. The id in the report, in loop-state.json, and on stdout (Processing: abc) no longer matches the id in the prompt body sent to the agent. - With no
idFieldand element{ id: 'human-readable' }, the loop tracks the prompt as the array index"0"while the template renders{{id}}ashuman-readable. Same desync. - With element
{ index: 'whatever' },{{index}}no longer reflects the iteration position.
This is silent: there is no warning that a built-in was overridden, and the inconsistency only shows up if the user happens to compare loop-state contents against the prompt body.
Expected behavior
The documented contract should hold: {{id}} should always be the resolved tracking id, and {{index}} should always be the iteration position. Either:
- Apply the element fields first and the built-ins last (so the built-ins win and the docstring stays accurate), or
- Detect a collision and throw a clear error so the user picks a different placeholder name.
Option (1) preserves the user's ability to reference all element fields while keeping the documented {{id}} and {{index}} semantics. Option (2) is stricter but better matches the principle of failing fast on ambiguous config.
Minimal reproduction
import { JsonPromptGenerator } from 'loop-the-loop/prompt-generators/json';
import { LoopState } from 'loop-the-loop/util/loop-state';
const generator = await JsonPromptGenerator.create({
data: [{ uuid: 'abc', id: 'xyz', title: 'Hello' }],
idField: 'uuid',
promptTemplate: 'id={{id}} title={{title}}',
});
for await (const p of generator.generate(new LoopState('ignored.json'))) {
console.log('tracking id:', p.id);
console.log('prompt :', p.prompt);
}
// tracking id: abc
// prompt : id=xyz title=Hello (expected: id=abc)
Suggested fix
Swap the order in buildVariables so the built-ins are written last:
function buildVariables(
element: unknown,
id: string,
index: number,
): Record<string, string> {
const variables: Record<string, string> = {};
if (
element !== null &&
typeof element === 'object' &&
!Array.isArray(element)
) {
for (const [key, val] of Object.entries(element as Record<string, unknown>)) {
variables[key] = String(val);
}
} else {
variables['value'] = String(element);
}
// Built-ins win, matching the docstring.
variables['id'] = id;
variables['index'] = String(index);
return variables;
}
Add tests covering:
idFielddiffers from a field namedidon the element:{{id}}should equal the resolved tracking id, not the element'sidfield.- Element has an
indexfield:{{index}}should equal the iteration position.
- Dominant language
- TypeScript
- Stars
- 2
- Forks
- 1
- PR merge metrics
- No merged PRs in 30d
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from joewalker/loop-the-loop
-
bug S4
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
joewalker/loop-the-loop#88 ·
-
Git exec(): child killed by a signal rejects with new Error('') and loses the signal information Openbug S3
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
joewalker/loop-the-loop#84 ·
-
bug S3
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
joewalker/loop-the-loop#83 ·
-
bug S3
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
joewalker/loop-the-loop#82 ·
-
bug S4
Difficulty 1/5 Under an hour Newbie friendliness 90/100
joewalker/loop-the-loop#79 ·
All issues in joewalker/loop-the-loop
Similar issues
-
calcite-components needs triage refactor
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
Esri/calcite-design-system#15203 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 91/100
-
community first-timers-only good first issue hacktoberfest help wanted low hanging fruit up-for-grabs
Difficulty 1/5 Under an hour Newbie friendliness 95/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Automattic/studio#4908 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 90/100