Skip to content

feat(harness): send the model found in the agent's code, and ask once for the prompt's lines (TH-8953) - #268

Merged
khushalsonawat merged 13 commits into
feat/TH-8804-source-subfolderfrom
feat/TH-8953-agent-prompt-model
Nov 11, 2026
Merged

khushalsonawat merged 13 commits into
feat/TH-8804-source-subfolderfrom
feat/TH-8953-agent-prompt-model

Conversation

@khushalsonawat

@khushalsonawat khushalsonawat commented Nov 10, 2026 •

Copy link
Copy Markdown
Member

Summary

The Agents tab shows an agent's prompt and model. Until now the agent document always sent "model": null, and the prompt was only the reading model's short quote whenever it didn't name the lines the prompt is written on. This fixes both, for builds from a repository or an upload (TH-8953).

  • The model. The agent description gets a model entry: provider, name, and the file and line where the name is written. The save keeps it only if that exact name is on that line, as a whole name (gpt-4o isn't found inside gpt-4o-mini). Otherwise it tells the reading model what to fix, or to leave the model out. With no source to check against, the model is dropped. build_authored_agent copies the provider and name into the document's existing model field. Temperature and token limit stay null. No field changes name or shape.
  • The whole prompt. When the reading model quotes the prompt but names no lines, the save asks once for the lines, so the existing copy fills in the whole prompt. If it can't name them (a prompt built at run time), the second save keeps the quote. The prompt stays in system_prompt_excerpt, as I decided; there's no new field.
  • Nothing moves for agents without a model. Their contract.json and the document's setup_hash are byte-identical to the base. A test pins the exact sha-256 and hash. This matters because the platform refuses a second, different document for a stored version.
  • Bounded on crafted repositories. The cited path is resolved within feat(harness): build an agent from a repo's subfolder #225's shared lookup budget, and the file is read as UTF-8, at most 1 MiB, ignoring any coding line. The name search is one pass. The slowest crafted save measured is 0.32 s, against the 1.6 s ceiling on 3.12.

Stacked on #225 (feat/TH-8804-source-subfolder), with its head 22bcbb8 merged in. It moves to dev once #225 merges.

Known limits:

  • When lines can't be named, the quote goes out and may be partial.
  • A name with a space or +, or one built across several lines, can't be recorded and stays null.
  • The provider isn't checked against the code, only the name.
  • Where the code gives an environment variable a default, the default is what gets recorded.
  • A value shaped like a key, cited as the model name, isn't redacted. The copied prompt lines already carry the source text as written, so this isn't a new exposure.
  • The first build after this lands has a new fingerprint wherever a model is found, so the platform keeps the older document for versions it has already stored.

After review (10 Nov)

Changes for the review comments:

  • The model check lives in src/fi/alk/harness/model_citation.py (source_model.py was already taken). _copy_prompt now resolves and reads through the same bounded helpers, so an unreadable prompt file is refused instead of raising.
  • The name must be the value written on the line. It has to sit between matching quotes and not be a key ("x":) or a lookup (after a subscript [, or the first argument of get(/getenv(). In configuration files (YAML, TOML, JSON, INI, .env, Dockerfile, shell), it can also be written bare after : or =. Nothing inside a comment counts. A name shaped like a setting is refused: OPENAI_MODEL, openai_model, model.
  • Named prompt lines must hold the quote. I compare letters and digits only, with \n and \uXXXX escapes decoded, so string pieces and JSON fit. The lines can't be shorter than the quote. A second mismatch in one reading keeps the quote and drops the lines.
  • A refused model citation is refused once per reading. The second time, the model is dropped. The two prompt nudges now come one at a time.

Fixes that no reviewer has seen:

  • The quote refusal now names the file and lines.
  • A value after ? (a ternary) isn't read as a key.
  • The --, ; and * line markers are no longer treated as comments.
  • Lookup keys are refused.
  • \u/\x escapes are decoded.
  • /* and <!-- comments are recognised.

These went in after the last of my two review rounds cleared (nothing above Medium), with tests and a planted bug for each.

Known limits added in that round, still open:

  • When two quoted values are on one line (FALLBACK="gpt-4o"; MODEL="gpt-4o-mini"), either is accepted.
  • gpt-4o is only found inside openai/gpt-4o when it's cited whole.

After the second review (11 Nov)

Dileep's 8 new comments are answered in each thread. Each change went through cold review rounds of three fresh reviewers (quote, rules, flows) until a round had nothing above Medium. The last round's Mediums and Lows were then fixed with tests and planted bugs, and are listed below.

The quote check (4525093427, 4525093451):

  • Every word of the quote must be in the named lines, in order, each within 24 letters of the one before; any gap is allowed at ... or ….
  • Both sides are compared after decoding escapes and HTML entities, with NFC, full-width forms and case folding. Escaped surrogate pairs (letters past U+FFFF in JSON) count as one character.
  • In Chinese, Japanese, Thai, Lao, Burmese and Khmer, each letter is a word.
  • A quote of 6 to 15 letters must appear whole, piece by piece at its .... A quote under 6 letters says nothing.
  • Lines are numbered at each \n, as Claude Code's Read numbers them: a form feed, a lone \r or U+2028 no longer moves a cited line.

The quote is held, and lines sent alone are checked (4525093437):

  • The reading's latest quote is held from the moment it's sent, read the way the contract reads it (aliases, lists, nulls dropped).
  • Lines sent alone that don't hold it are refused once more, naming the lines.
  • After that the lines are recorded in a new contract field, unconfirmed_prompt_from. A long quote is kept as the prompt. A quote under 16 letters keeps the lines instead (Khushal's call).
  • Both outcomes are said before they happen (the refusal) and after (the save's reply).
  • A call's own short quote, found word for word in its own lines, passes. A one-word name or a short quote after a confirmed one does not.
  • The field is left out of contract.json when empty, so other contracts keep their bytes and setup_hash, and it reaches the document under extension.contract.

The line rules (4525093459, 4525093467, 4525093475, 4525093482):

  • A dotted name with no digit reads as a setting; lookups cover getOrThrow, getProperty, ?.[ and more.
  • Capitals count as an env var only with an underscore. deployment is no setting word.
  • One scanner per line, skipping strings, configured per language: # where it is a comment (after a space in YAML, shell, Makefile, Dockerfile, .env); // and /* */ in languages that have block comments and in JSON (JSONC); -- in SQL, Lua and Haskell; INI ; lines. Triple-quoted strings are tracked in the languages that have them, and Python string prefixes are read.
  • Prose files and prompt templates hold no model. A template that renders configuration by its name (config.yaml.j2, .env.j2, Dockerfile.j2) is read as that file.
  • Makefiles, .flaskenv, Dotprompt frontmatter and ${MODEL:-gpt-4o} are read.

The reader (4525093490): read_source opens with O_NONBLOCK | O_NOFOLLOW | O_CLOEXEC, checks S_ISREG, reads in a loop, and logs only the error's kind.

Fixed after the last round, with tests and planted bugs, not reviewed again (per the rule above):

  • a short elided quote is matched piece by piece;
  • a one-word name or a sentence repeated in a comment no longer replaces a verified quote;
  • prompt templates named like configuration (dockerfile_writer.j2, system.prompt.j2) are text;
  • the messages say "fewer than 16 letters";
  • six pins.

Known limits (each listed in the ticket's ARCHITECTURE.md):

  • When lines can't be named, the quote goes out and may be partial. A first quoteless submission with lines is copied unchecked.
  • A range that starts early but holds the whole quote is accepted, with the extra lines.
  • More than 24 letters of code between two quoted words (a long comment or variable, a Jinja loop, .format(...)) refuses honest lines once. A quote with filled-in placeholders doesn't match.
  • Two calls in one message run one after another on the Claude backend, so the second sees the first's refusal.
  • The other backends' file tools number lines with splitlines(); they disagree with the save only when a form feed or similar sits above a cited line.
  • Unknown comment syntaxes (OCaml, VB, Clojure, Haskell and Lua block comments, Erlang, batch, Jinja {# #}), HOCON //, Go raw strings' middle lines, some C# and Swift raw strings, an unclosed " or backtick, a Rust lifetime, YAML 'tis, shell foo_'b'.
  • A model named inside prompt text that isn't triple-quoted (a JS template literal, a YAML | block) reads as a value. f"{prov}/gpt-4o" and YAML flow lists leave the model empty. GPT_4O and chat-model read as settings.

Verification

  • uv run ruff check .: not run on the whole repo; ruff check on the changed files passes.
  • Focused pytest target: 19 files with CI's Python 3.12.14 (the new tests/harness/test_contract_model.py, tests/providers/test_providers_agent_authored.py, tests/test_harness.py, and the save, nudge, refusal, scenario-source and setup-hash files). Result on the pushed head: 9 failed, 2426 passed, 4 errors. The base gives the same 9 failures and 4 errors by name, so none is new.
  • Full uv run pytest -q: not run locally (laptop limits); CI runs it.
  • TypeScript build/test: not touched.
  • agent-learn release-proof: not touched.

Each new check was proven by planting a bug in it and watching a test fail: 240 of 240 caught after the second review's fixes. The timing probes (worst crafted model check 0.23 s, whole save 0.32 s) covered slow coding lines, files over 1 MiB, FIFOs, links to /dev/zero, link fan-outs and unwinding chains, on Python 3.11, 3.12.3 and 3.12.14. The plan went through three cold review rounds, and the change through one; it cleared with 1 Medium and 9 Lows. Dileep's second review went through five cold rounds of three reviewers: the first four each found a High, and after the third and the fourth Khushal decided how to go on; the fifth cleared with 2 Mediums and 11 Lows, all fixed or listed above.

Checklist

  • Public API names stay under agent_learning.*, agent-learn, or @future-agi/agent-learning-kit
  • New user-facing workflow has docs or an example: the understand-agent skill describes what to record
  • New release claim has executable gate coverage: no release claim
  • No hosted-service dependency was added to release-gated examples
  • Security-sensitive output is redacted or justified: the document reads the model from the redacted copy (a URL-like name loses its userinfo and query); a test pins it

🤖 Generated with Claude Code

… for the prompt's lines (TH-8953)

The agent document always sent "model": null, and the prompt was only the
model's quote whenever it didn't name the prompt's lines.

The description now records the model the agent talks to the person with:
provider, name, and the file and line the name is written on. The save keeps
it only when that exact name is on that line as a whole name; otherwise it
says what to fix. The cited path is resolved within the save's shared lookup
budget and the file is read as UTF-8, at most 1 MiB, so a crafted repository
can't stall the check. With no source to check against, the model is dropped.
build_authored_agent copies provider and name into the document's model.

A description without a model saves and builds to the same bytes and setup
hash as before, so stored agent versions are unaffected.

When the model quotes the prompt without naming its lines, the save asks
once for the lines; a second identical save keeps the quote.

Signed-off-by: khushalsonawat <khushalsonawat@gmail.com>
Signed-off-by: khushalsonawat <khushalsonawat@gmail.com>

# Conflicts:
#	src/fi/alk/harness/tools.py

@cdileep23 cdileep23 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please check the comments.

Comment thread src/fi/alk/harness/tools.py Outdated
return raw.decode("utf-8", errors="replace").splitlines()


def _named_on(line: str, name: str) -> bool:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: the whole-name check passes for things that aren't the model's name.

This only checks the characters on either side of the match, never that the match is the value the model is set from. Run through accept_contract and build_authored_agent, all of these are saved:

  • MODEL = os.getenv("OPENAI_MODEL") with name OPENAI_MODEL: the document says {"provider": "openai", "name": "OPENAI_MODEL"}
  • llm = ChatOpenAI(model=settings.llm_model) with name settings.llm_model
  • model = "gpt-4o" # was gpt-4o-mini with name gpt-4o-mini (a comment)
  • FALLBACK="gpt-4o"; MODEL="gpt-4o-mini" with name gpt-4o (another variable)

The Agents page would show these as the model. The intended cases work (gpt-4o vs gpt-4o-mini, getenv defaults, YAML, ChatOpenAI(model="gpt-4o")); only SKILL.md stops the model citing an identifier, and the test says the save is the only gate.

Suggestion: in code files require the match to sit between matching quotes; in .yaml/.toml/.json accept an unquoted value after : or =.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in eadebfc. The name now has to be the value written on that line:

  • In code: between matching quotes, and not a key ("x":) or a lookup (after a subscript [, or the first argument of get(/getenv().
  • In config files (YAML, TOML, JSON, INI, .env, Dockerfile, shell): it can also be written bare after : or =.
  • Comments: nothing inside one counts (#, //, /* */, <!-- -->).
  • Setting names: a name shaped like a setting is refused too (OPENAI_MODEL, openai_model, model). That catches your getenv case, which the quote rule alone would still accept, because the name is quoted there.

Your four examples:

  • OPENAI_MODEL: refused.
  • settings.llm_model: refused.
  • the comment: refused.
  • FALLBACK="gpt-4o"; MODEL="gpt-4o-mini": still accepted. Telling which of two quoted values a line assigns needs parsing, so I've listed it under known limits in the PR.

Tests:

  • test_a_name_that_is_not_the_model_is_never_sent (through accept_contract and build_authored_agent)
  • test_a_setting_name_is_not_a_model
  • test_a_lookup_key_is_not_the_model
  • test_a_value_in_a_comment_is_not_the_model
  • test_model_whole_name_is_found

(
# A quote may stop short of the whole prompt; lines are copied whole. Asked once,
# since a prompt built at run time has no lines to name, and its quote is kept.
"prompt-lines",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: the named lines replace a correct quote, and nothing checks them against it.

With a quote and system_prompt_from given, _copy_prompt (line 397) overwrites system_prompt_excerpt with whatever lines are named. Example: import os / DB = "x" / PROMPT = """You are pair, a helpful assistant...""", quote "You are pair, a helpful assistant.", lines 1-2: saved, and the agent's instructions become import os\nDB = "x".

The copy gap was already there, but this nudge now asks every quote-only prompt for lines, so a model under pressure guesses a range far more often.

Fix: when both are given, refuse if the whitespace-normalised copied text doesn't contain the first ~80 characters of the quote, naming the file and range.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in eadebfc. When a quote and lines are both given, the lines must hold the quote, or the save refuses and names the file and range. I compare letters and digits only, with \n and \uXXXX escapes decoded, so prompts written as string pieces or stored in JSON still fit. The check uses the first 80 characters, and the copied text can't be shorter than the quote. If the same lines come back a second time, the quote is kept and the lines are dropped. That way a reading never sends unchecked code and never gets stuck. Your example is refused.

Tests:

  • test_named_lines_must_contain_the_quote
  • test_the_quote_window
  • test_honest_prompt_forms_hold_the_quote
  • test_a_prompt_with_escaped_characters_holds_the_quote
  • test_lines_refused_twice_leave_the_quote

Comment thread src/fi/alk/harness/tools.py Outdated
_MODEL_NAME_CHARS = frozenset(string.ascii_letters + string.digits + "._:/@-")


def _resolved_in_source(root: Path, path: str) -> Path | None:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: about 115 lines of new functions in a 1,712-line file.

_resolved_in_source, _source_lines, _named_on and _model_problems (410-513) are a real seam: _checked_against_source already imports source_tool_evidence lazily (line 245). A source_model.py beside it would keep tools.py from growing, or the body could say why it stays (standard 13).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved out to src/fi/alk/harness/model_citation.py in eadebfc; tools.py only calls it now. I didn't use source_model.py because that file already exists in the repo for the source facts.

Comment thread src/fi/alk/harness/tools.py Outdated
return None


def _source_lines(path: Path, limit: int) -> list[str] | None:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: this copies _copy_prompt's reader, and the copies already differ.

The docstring says "the way _copy_prompt reads them", and _resolved_in_source repeats its charged_path lookup (345-374). They already drift: this catches OSError on open, _copy_prompt doesn't (a chmod-000 prompt file makes the save raise PermissionError, while the same file cited for the model is refused cleanly). Have _copy_prompt call these two instead.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in eadebfc. _copy_prompt now resolves and reads through the same helpers as the model check: resolved_in_source, read_source and source_lines. A chmod-000 prompt file is now refused as "not a file in the source" instead of raising PermissionError. Its limits and messages are unchanged. Test: test_an_unreadable_prompt_file_is_refused_not_raised.

"copied whole.",
),
]
# All of them together, and each only once. Nudging in sequence would cost a turn per

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: the prompt nudges do arrive one after another now.

This says nudging in sequence never happens, but a contract with no excerpt gets the "prompt" nudge, the next one with only a quote gets "prompt-lines", and only the third is accepted: two extra submissions. Skip "prompt-lines" when "prompt" is already in nudged, or put the ask for lines into the "prompt" message.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in eadebfc. Once the "prompt" nudge has been sent, "prompt-lines" isn't sent as well, and the "prompt" message now asks for the lines itself. The "prompt" nudge also stays quiet when lines are named without a quote. Otherwise following the quote advice would set it off. Tests: test_one_prompt_nudge_at_a_time, test_leaving_the_quote_out_is_not_asked_about.

Comment thread src/fi/alk/harness/tools.py Outdated
return [f"model: line {model.line} is outside {shown_path}, which has {len(lines)} lines"]
if not _named_on(lines[model.line - 1], name):
return [
f"model: the name {shown_name} is not on line {model.line} of {shown_path} as a "

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: a wrong model citation is refused on every save.

The nudges beside it refuse once and then accept, and the no-source branch drops the model (line 255), but this refuses each time. The loop does end (the message says "or leave model out", and understand stops at max_turns), but an optional display field can now cost turns or fail a reading whose tools and schema were fine. Track "model" in nudged and drop it on the second mismatch.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in eadebfc. Through submit_contract, a refused model citation is refused once, with a line saying it'll be left out if it's refused again. The second time, the model is dropped and the save goes through. Tests: test_a_wrong_citation_is_refused_once_then_dropped, and test_accept_contract_refuses_a_wrong_citation_every_time for direct calls.

Comment thread tests/harness/test_contract_model.py Outdated
return lambda arguments: asyncio.run(tool.handler(dict(arguments)))


# P1, P8

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: # P1, P8 here (and # P3a-P3d, # P7 below) point to a plan that isn't in the repo (standard 11). Name the behaviour instead, or drop them.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in eadebfc: the comments name the behaviour now, not the plan.

… line, and check named prompt lines against the quote (TH-8953)

Answers the review of #268.

- The model check moves to model_citation.py. A name counts only when it
  is the value written on the line: between matching quotes and not a key
  or a lookup (a subscript, get(), getenv()), or bare after : or = in a
  configuration file, and never inside a comment. A name shaped like a
  setting (OPENAI_MODEL, openai_model, model) is refused.
- When a quote and lines are both given, the lines must hold the quote,
  compared by letters and digits with escapes decoded. A second mismatch
  in one reading keeps the quote and drops the lines.
- A refused model citation is refused once per reading, then dropped, and
  the prompt nudges come one at a time.
- _copy_prompt reads through the same bounded helpers, so an unreadable
  prompt file is refused instead of raising.

Signed-off-by: khushalsonawat <khushalsonawat@gmail.com>
Signed-off-by: khushalsonawat <khushalsonawat@gmail.com>

@cdileep23 cdileep23 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please check the comments.

Comment thread src/fi/alk/harness/model_citation.py Resolved
Comment thread src/fi/alk/harness/model_citation.py Resolved
Comment thread src/fi/alk/harness/tools.py Resolved
Comment thread src/fi/alk/harness/model_citation.py Resolved
Comment thread src/fi/alk/harness/model_citation.py Resolved
Comment thread src/fi/alk/harness/model_citation.py Resolved
Comment thread src/fi/alk/harness/model_citation.py Resolved
Comment thread src/fi/alk/harness/model_citation.py Resolved
…hecks (TH-8953)

- The named prompt lines must hold every piece of the quote, in order; the quote
  is split at ... and compared by letters and digits after escapes, HTML
  entities, NFKC and case folding. The 80-letter window and the length check go.
- A refused quote is held for the reading, so lines named later without a quote
  are still checked against it.
- Settings: capitals need an underscore to read as an environment variable; a
  setting word among the name's parts counts when it has an underscore or no
  digit; a dotted name with no digit is a setting. GPT4O, O1, mistral_large and
  gpt4o-deployment are models again.
- Lookups: more lookup calls, spaces before the bracket, a generic type.
- Comments: one scan that skips quoted strings; markers without spaces, a lone
  --, and block-comment middle lines count.
- Prose files: backticks are not quotes. Makefile, .mk, .flaskenv and
  ${VAR:-default} are configuration.
- read_source opens with O_NONBLOCK, O_NOFOLLOW and O_CLOEXEC, checks S_ISREG
  on the descriptor, reads to the limit in a loop and logs a failure by path and
  error kind. #225's two prompt-reader tests now watch os.open and os.read.

Signed-off-by: khushalsonawat <khushalsonawat@gmail.com>
Signed-off-by: khushalsonawat <khushalsonawat@gmail.com>
…, and close two known limits (TH-8953)

Local, not pushed: review round 3 did not clear (two Highs, reports in the
ticket folder); the push waits on Khushal's call.

- The quote's words are matched in order within 24 letters of each other, any
  gap at "...", with a charged budget; the reading's latest quote is held at
  the top of submit_contract; lines sent alone are refused once more, then the
  quote is kept and the lines recorded in unconfirmed_prompt_from.
- One line scanner for strings, comments and triple-quoted strings, per
  language; prose and prompt templates hold no model; more lookups and
  configuration forms; the reader logs the error kind only.

Signed-off-by: khushalsonawat <khushalsonawat@gmail.com>
Local, not pushed: review round 4 runs before the push (Khushal: go with A).

- Unspaced scripts: only a letter is a word.
- The quote is read as the contract reads it for the hold and the nudges; short
  quotes are held while nothing longer is; a quote under 6 letters says nothing;
  at the second mismatch a short quote keeps the lines, recorded unconfirmed.
- Each call decides from what was asked before it was sent.
- // and /* only in languages with block comments; # per language; config
  templates read as their inner kind; env.py is code.
- SKILL item 8 records the call-direction line in notes.

Signed-off-by: khushalsonawat <khushalsonawat@gmail.com>
…that can't fail (TH-8953)

Plants for round 3's fixes left eleven survivors. Four were caught by other
tests. Six get pins: URLs, globs, ${#x} and $# in a script without a suffix;
rt/tr string prefixes; a triple quote after floor division in Python; a quote
sent alone after lines alone were refused is not asked for lines.

_opens_a_string's 'whole' check could not fail: a prefix is one or two
letters and the scan looks back three, so it is removed.

Signed-off-by: khushalsonawat <khushalsonawat@gmail.com>
…-8953)

Lines are numbered at each newline, as the reading's Read and Grep number
them, so a form feed, a lone CR or U+2028 no longer moves a cited line. A
JSON-escaped surrogate pair reads as one character. JSON keeps // and /* */
comments (JSONC). TypeScript modules and Scala scripts have no # comments;
.env files read # after a space by suffix too; Python files named like a
Dockerfile or .env read # anywhere. An underscore counts in the apostrophe
look-back. A template named for configuration (.env.j2) is configuration. A
dead clause is removed. Pins for the 6- and 16-letter boundaries, the
not-a-regular-file log line, an escape past U+10FFFF, and eight rule parts.
SKILL item 6 says what happens with short quotes.

Signed-off-by: khushalsonawat <khushalsonawat@gmail.com>
A call's own short quote, found letter for letter in its own lines, passes
and becomes the held quote, so a verbatim short quote sent after a refused
paraphrase keeps the right lines. The quote is read after nulls are dropped,
as the contract reads it, and a blank quote is none. The once-only nudges
decide from what was asked before the call, like the refusals. The refusals
say what the fallback will do (a quote of a few words keeps the lines), and
a saved call that took the fallback says so.

Signed-off-by: khushalsonawat <khushalsonawat@gmail.com>
… update (TH-8953)

Plants on 4396675: a list quote with a null and the wrong lines is refused as
the call's own quote; asked_before's update in the nudges did nothing, since a
call either returns with the nudge or had nothing new to add.

Signed-off-by: khushalsonawat <khushalsonawat@gmail.com>
A short quote of 6-15 letters is looked for piece by piece at its ..., so
"Be warm ... brief." holds its lines. A call's own short quote on its own
lines replaces the held quote only when it is two words or more and the held
quote isn't one that lines already held, so a name like PROMPT or a sentence
repeated in a comment no longer overrides a verified quote. The messages say
"fewer than 16 letters" and "refused again against it"; SKILL item 6 says
when the lines are asked about once more. A template is configuration only
when its stem is a configuration name (.env.j2, Dockerfile.j2), and
system.prompt.j2 is a prompt. Pins for .conf, config.json.j2, quote
precedence, padded short quotes, case and punctuation in a correction.

Signed-off-by: khushalsonawat <khushalsonawat@gmail.com>
khushalsonawat added a commit that referenced this pull request Nov 10, 2026
Signed-off-by: khushalsonawat <khushalsonawat@gmail.com>

@cdileep23 cdileep23 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please check the comments.

at = close + 3
else:
stripped = line.lstrip()
if (block_comments and stripped.startswith(("* ", "*\t"))) or (

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3. A commented-out model inside a multi-line /* */ block is accepted when its line doesn't start with *. Only * middle lines count as comments here, and block_open_before tracks triple quotes only, so the scan doesn't know a block comment is open.

Repro: a.js with

/*
model: "gpt-4o"
*/

and the model cited as gpt-4o, a.js, line 2: model_problems returns no problem and the model is kept.

Fix: carry an open /* across lines in block_open_before, as it already does for """, and treat the cited line as text until */.

)
elif asked & {"prompt-quote", "prompt-lines-alone"}:
# Either way the lines are kept on record as unconfirmed.
contract.unconfirmed_prompt_from = list(contract.system_prompt_from)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3. Nothing on the platform reads unconfirmed_prompt_from yet, so what this branch saves is shown as the agent's prompt with no flag.

  • Quote of 16 letters or more: correct lines sent twice with a paraphrase save the paraphrase as system_prompt_excerpt, and the lines go only into unconfirmed_prompt_from. That outcome is your call; the platform will just show the paraphrase.
  • Shorter quote: the lines are kept as the prompt. Repro with the _BOOKING file: system_prompt_excerpt="Be warm and brief." with lines 1-4, sent twice. The second call saves the four import lines as the prompt.

Fix: for the short-quote case, don't send lines that failed twice as the prompt (leave it empty and keep them only as unconfirmed). The platform side needs a ticket to surface unconfirmed_prompt_from.

@khushalsonawat
khushalsonawat merged commit 658bc17 into feat/TH-8804-source-subfolder Nov 11, 2026
11 checks passed
@khushalsonawat
khushalsonawat deleted the feat/TH-8953-agent-prompt-model branch November 11, 2026 05:47
khushalsonawat added a commit that referenced this pull request Nov 11, 2026
… into TH-9491

- tools.py: _save_contract keeps the single writer (write_contract) and takes
  #268's new reply text, which names unconfirmed prompt lines.
- contract.py: #268's new fields, unconfirmed_prompt_from and model (ModelSource),
  are marked as kept as written: they hold source lines and the cited model name,
  read by the build. The test that fails on an unmarked field caught them.

Signed-off-by: khushalsonawat <khushalsonawat@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants