{"slug": "a-substring-is-not-a-speech-act-my-ai-agent-executed-questions-and-quotes", "title": "A Substring Is Not a Speech Act: My AI Agent Executed Questions and Quotes", "summary": "A developer built an interactive piece in which recorded model replies drive subsequent actions, but found that a substring-matching parser treated questions, negations, and quotations as executable commands. The fix replaces fragment matching with a closed map of authored clauses plus a passive set, requiring every sentence in a response to be registered before any operation fires, so an unregistered clause disables the response's operations entirely.", "body_md": "*Originally published on [hexisteme notes](https://hexisteme.github.io/notes/a-substring-is-not-a-speech-act.html).*\n\nI built a small interactive piece where recorded model replies control what happens next. A reply can join the work, reserve a direction, undertake a task, or delegate the final choice. The mapping is intentionally narrow: these are authored clauses in a saved record, not a general conversation engine.\n\nThen a question joined the work.\n\nThe first language binding looked for a few useful fragments. If a response contained `함께 결정` or `같이 결정`, the parser treated it as an offer to decide together. That seemed convenient because the recorded replies used those words. It also meant that these unrelated sentences became actions:\n\n`같이 결정해볼까?` is a question, but it executed `join`.`같이 결정하지 않을래.` is a negated proposal, but it executed `delegate`.\nThe bug was not that the model used an unusual phrase. The bug was that the executor confused a substring with a speech act. Seeing characters is not proof that a speaker made an affirmative offer, and an offer is not permission to mutate state.\n\nThere were two separate facts in every record entry:\n\nThe old matcher used the first fact as if it had established the second. It also inspected fragments before it had decided whether the surrounding sentence was a question, a negation, or a quotation. Once a fragment had fired, later context could not take the action back.\n\nThis is a familiar shape in application code. A feature flag checks whether a comment contains a word. A webhook accepts a payload because a nested string resembles a command. A moderation rule looks for a token and silently treats a quotation as the speaker's own statement. Each one has the same type error: character presence is being used as authorization.\n\nThe repair is deliberately finite. The saved record contains the exact clauses that the maker has approved, and a closed map gives each clause one operation. Text that is allowed to appear without an operation lives in a separate passive set.\n\n``` js\nconst scored = new Map([\n  ['같이 결정해보자.', 'join'],\n  ['내가 쓴 부분을 토대로 이야기의 방향을 정해볼 수 있어.', 'reserve'],\n  ['좋아, 내가 초안을 해줄게.', 'undertake'],\n  ['마지막 방향은 네가 정해도 돼.', 'delegate']\n]);\n\nif (sentences.some(sentence => !scored.has(sentence) && !passive.has(sentence))) {\n  return { operations: [], evidence: [] };\n}\n```\n\nThe important line is the whole-response check. The parser first proves that every sentence belongs to the closed score or the passive set. Only then does it collect operations. An unregistered clause anywhere in the response disables the response's operations, so a recognized fragment cannot launder a quotation, explanation, question, or negation into an action.\n\nThe source kind is checked too. An unknown kind is an input error, not an invitation to guess. The implementation does not claim to understand Korean, infer a model's hidden intention, or classify arbitrary conversation. A new phrase becomes executable only after it is entered into the record and the authored score.\n\nThe saved browser check exercises the same function that drives the visible piece. At the first checkpoint, the recorded response has produced joint and reserve while labor and delegation remain zero. At the later checkpoint, the registered responses produce all four intended operations, and the browser reports no WebGL error. The check also keeps keyboard behavior outside the buttons and confirms that finished playback does not silently start a new game.\n\nThe negative controls are the useful part. A question containing the right words must remain inert. A negated proposal must remain inert. A quotation with an affirmative sentence inside it must remain inert unless the complete quoted form is itself an authored clause. An unregistered paraphrase must remain inert even when a human reader thinks it means the same thing.\n\nThis also gives the interface a clearer failure mode. The record can show that a response was received while the operation list stays empty, so an operator can distinguish “text arrived” from “the text had permission.” That distinction is useful in logs and review tools: retain the original clause, its source kind, and the rejection reason instead of replacing the text with a guessed intent. A future author can then extend the score deliberately, and a reviewer can see which negative control would have changed if the extension were unsafe.\n\nThe same discipline closed a few neighboring holes. Marks are validated before time filtering so a non-finite value cannot disappear as if it were outside the sample. Coordinates and ranges are rejected at creation. Preview and result share one hinge function. An explicit new-game action is the only operation that clears a finished playback. None of these checks attempts to make the parser clever; they make its allowed surface smaller and observable.\n\nWhen text controls a side effect, a lexical hit is evidence about characters. It is not permission to act. Keep the source kind, preserve the complete authored clause, validate the whole response before executing anything, and make unknown text fail closed.\n\nThis approach trades coverage for an honest contract. A finite grammar can tell you exactly which phrases are executable and exactly which near misses are rejected. A broad language classifier may accept more natural wording, but it also moves the decision into a probabilistic layer that is harder to audit and easier to confuse with intent.\n\nThe falsifier is simple: if a newly recorded question, negation, quotation, or unregistered paraphrase produces a non-empty operation, the boundary has failed. The next useful test is therefore a growing corpus of negative controls, not a larger pile of positive examples.\n\nWhere does an AI agent in your system still treat a substring as permission when it needs an authored clause?\n\n*Email list for these notes: [hexisteme.beehiiv.com](https://hexisteme.beehiiv.com/?modal=signup&utm_source=devto&utm_campaign=notes-engineering&utm_content=a-substring-is-not-a-speech-act) — no issue has gone out yet, so you would be on it before the first one. No welcome sequence, no course, no upsell.*\n\n*More notes at [hexisteme.github.io/notes](https://hexisteme.github.io/notes/).*", "url": "https://wpnews.pro/news/a-substring-is-not-a-speech-act-my-ai-agent-executed-questions-and-quotes", "canonical_source": "https://dev.to/hexisteme/a-substring-is-not-a-speech-act-my-ai-agent-executed-questions-and-quotes-27n5", "published_at": "2026-09-20 00:00:06+00:00", "updated_at": "2026-09-20 00:24:38.921840+00:00", "lang": "en", "topics": ["ai-agents", "natural-language-processing", "ai-tools", "developer-tools"], "entities": ["hexisteme notes"], "alternates": {"html": "https://wpnews.pro/news/a-substring-is-not-a-speech-act-my-ai-agent-executed-questions-and-quotes", "markdown": "https://wpnews.pro/news/a-substring-is-not-a-speech-act-my-ai-agent-executed-questions-and-quotes.md", "text": "https://wpnews.pro/news/a-substring-is-not-a-speech-act-my-ai-agent-executed-questions-and-quotes.txt", "jsonld": "https://wpnews.pro/news/a-substring-is-not-a-speech-act-my-ai-agent-executed-questions-and-quotes.jsonld"}}