JsonPromptGenerator: object fields named 'id' or 'index' silently shadow the built-in {{id}} / {{index}} template variables

Open Beginner friendly
#63 0 comments 0 reactions 0 assignees View on GitHub

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

bug S3

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 as abc (via resolveId), but the prompt template renders {{id}} as xyz. 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 idField and element { id: 'human-readable' }, the loop tracks the prompt as the array index "0" while the template renders {{id}} as human-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:

  1. Apply the element fields first and the built-ins last (so the built-ins win and the docstring stays accurate), or
  2. 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:

  • idField differs from a field named id on the element: {{id}} should equal the resolved tracking id, not the element's id field.
  • Element has an index field: {{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

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from joewalker/loop-the-loop

All issues in joewalker/loop-the-loop

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.