Skip to content

Commit a65d518

Browse files
jitokimoh-my-graph
andauthored
fix(handoff): a quoted placeholder is resolved too — say so, and say how to quote one (#234)
* fix(handoff): a quoted placeholder is resolved too — say so, and say how to quote one Both unresolvable-placeholder reasons speak as if the graph had meant the token. `{{ inputs.demo }}` says "no such input was provided"; worse, `{{ artifacts.nosuch }}` says "artifact not available (its producing node has not completed)" — asserting a producing node that may not exist anywhere in the graph. The commonest way to reach either is a prompt that MENTIONS the syntax: quoting it from another graph, or explaining it to the model. That prompt gets a diagnostic about wiring it never wrote, and the artifacts form gets it late — `run --dry-run` deliberately skips artifact-side errors (cmd/oh-my-graph/dryrun.go:117), so the false claim surfaces only once the node has actually spawned and been paid for. One extra line on both reasons, stating the rule and the remedy: cannot resolve {{ inputs.demo }}: no such input was provided note: every {{ ... }} in a prompt is resolved, including one that is only being quoted or explained — to keep such text literal, break the two braces apart ("{ {"), or pass the text in as an input or artifact instead of writing it in the prompt Appended at exactly the two sites the report reproduced (handoff.go:159 and :171) and nowhere else: a filter on a feedback token and an unreadable artifact file are real wiring bugs, where this advice would mislead. Wording only. No escaping syntax, no flag, no change to what Interpolate resolves — placeholderPattern is untouched. The three tests fail without the change and print the old messages while doing so: --- FAIL: TestInterpolate_MissingInput cannot resolve {{ inputs.nope }}: no such input was provided --- FAIL: TestInterpolate_ArtifactNotYetAvailable cannot resolve {{ artifacts.pending }}: artifact not available (its producing node has not completed) They assert the sentence by its own words rather than against quotingHint, so an assertion cannot survive the sentence losing its rule or its remedy. go build ./... clean; make test green (19 packages, -race -count=1); make fmt-check clean. Signed-off-by: jitokim <pigberger70@gmail.com> Co-Authored-By: oh-my-graph <graphs@oh-my-graph.dev> * docs(changelog): quote both diagnostics — the old one, and the line it now carries The Unreleased entry for the placeholder fix described the new sentence but never showed either message, so a reader could not tell what actually changed in the output. It now quotes both forms verbatim, and names the command they were captured from: go run ./cmd/oh-my-graph run /tmp/omg-repro-placeholder.yaml --dry-run Old (209df64^, internal/handoff/handoff.go:146 and :170): cannot resolve {{ inputs.demo }}: no such input was provided cannot resolve {{ artifacts.nosuch }}: artifact not available (its producing node has not completed) New (handoff.go:159 and :183, the quotingHint constant at :56): cannot resolve {{ inputs.demo }}: no such input was provided note: every {{ ... }} in a prompt is resolved, including one that is only being quoted or explained — to keep such text literal, break the two braces apart ("{ {"), or pass the text in as an input or artifact instead of writing it in the prompt The quoted text was read back off the running binary, not copied from the implementation report. CHANGELOG.md only — no code, no release, no tag, no version bump. Signed-off-by: jitokim <pigberger70@gmail.com> Co-Authored-By: oh-my-graph <graphs@oh-my-graph.dev> --------- Signed-off-by: jitokim <pigberger70@gmail.com> Co-authored-by: oh-my-graph <graphs@oh-my-graph.dev>
1 parent 408b5d0 commit a65d518

3 files changed

Lines changed: 97 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,41 @@ oh-my-graph is **alpha software**. The graph YAML schema, the CLI, and the
1212

1313
### Changed
1414

15+
- **An unresolvable `{{ inputs.x }}` or `{{ artifacts.id }}` now says that a
16+
merely-quoted placeholder is resolved too, and how to quote one.** The two
17+
reasons were written as if the graph had meant the token — the artifact one
18+
asserts "its producing node has not completed" about a node that may not
19+
exist — so a prompt that only explains the syntax to the model got a
20+
diagnostic pointing at wiring that was never there.
21+
22+
Before, both errors said only what was missing:
23+
24+
```
25+
cannot resolve {{ inputs.demo }}: no such input was provided
26+
cannot resolve {{ artifacts.nosuch }}: artifact not available (its producing node has not completed)
27+
```
28+
29+
After, each carries a second line stating the rule and the way out:
30+
31+
```
32+
cannot resolve {{ inputs.demo }}: no such input was provided
33+
note: every {{ ... }} in a prompt is resolved, including one that is only being quoted or explained — to keep such text literal, break the two braces apart ("{ {"), or pass the text in as an input or artifact instead of writing it in the prompt
34+
```
35+
36+
The artifacts form gains the same single line after `artifact not available
37+
(its producing node has not completed)`. The command those two captures came
38+
from is the address for them — a graph whose prompts only *mention* the
39+
syntax:
40+
41+
```sh
42+
go run ./cmd/oh-my-graph run /tmp/omg-repro-placeholder.yaml --dry-run
43+
```
44+
45+
Appended at exactly those two sites and nowhere else: a filter on a feedback
46+
token and an unreadable artifact file are real wiring bugs, where this advice
47+
would mislead. Wording only — no escaping syntax, no flag, and what
48+
`Interpolate` resolves is unchanged.
49+
1550
- **`runs list` collapses the per-run skip warnings into one summary line, and
1651
the detail moves behind `--show-skipped`.** Option chosen: *collapse by
1752
default, restore on demand* — not silence, and not a filter. Every skipped

internal/handoff/handoff.go

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,19 @@ func (e *InterpolationError) Error() string {
4444
return fmt.Sprintf("cannot resolve {{ %s.%s }}: %s", e.Kind, e.Reference, e.Reason)
4545
}
4646

47+
// quotingHint is the second line of the two diagnostics a prompt earns when it
48+
// only MENTIONS a placeholder instead of using one. Both reasons below name a
49+
// wiring bug ("no such input", "its producing node has not completed"), which
50+
// presumes the graph meant the token — but the commonest way to reach them is a
51+
// prompt that quotes the syntax from another graph or explains it to the model.
52+
// The hint states the rule and the way out, so the reader is not left debugging
53+
// a node that does not exist. It is appended ONLY at those two sites: a filter
54+
// on a feedback token, or an unreadable artifact file, are real wiring bugs
55+
// where this advice would mislead.
56+
const quotingHint = "\nnote: every {{ ... }} in a prompt is resolved, including one that is only being quoted or explained — " +
57+
"to keep such text literal, break the two braces apart (\"{ {\"), or pass the text in as an input or artifact " +
58+
"instead of writing it in the prompt"
59+
4760
// placeholderPattern matches {{ inputs.name }} / {{ artifacts.id }} /
4861
// {{ feedback.id }} with an optional `| inline` filter. Group 1 = kind,
4962
// group 2 = reference, group 3 = filter (empty or "inline"). Whitespace
@@ -143,7 +156,7 @@ func (h *Handoff) resolveLocked(kind, ref, filter string) (string, error) {
143156
if kind == "inputs" {
144157
value, ok := h.inputs[ref]
145158
if !ok {
146-
return "", &InterpolationError{Kind: kind, Reference: ref, Reason: "no such input was provided"}
159+
return "", &InterpolationError{Kind: kind, Reference: ref, Reason: "no such input was provided" + quotingHint}
147160
}
148161
return value, nil
149162
}
@@ -167,7 +180,7 @@ func (h *Handoff) resolveLocked(kind, ref, filter string) (string, error) {
167180
return "", &InterpolationError{
168181
Kind: kind,
169182
Reference: ref,
170-
Reason: "artifact not available (its producing node has not completed)",
183+
Reason: "artifact not available (its producing node has not completed)" + quotingHint,
171184
}
172185
}
173186
if filter != "inline" {

internal/handoff/handoff_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ func TestInterpolate_MissingInput(t *testing.T) {
3333
if iErr.Reference != "nope" || iErr.Kind != "inputs" {
3434
t.Fatalf("error identified the wrong reference: %+v", iErr)
3535
}
36+
assertQuotingHint(t, err)
3637
}
3738

3839
// --- interpolation: artifacts ----------------------------------------------
@@ -103,6 +104,52 @@ func TestInterpolate_ArtifactNotYetAvailable(t *testing.T) {
103104
if iErr.Kind != "artifacts" || iErr.Reference != "pending" {
104105
t.Fatalf("wrong reference in error: %+v", iErr)
105106
}
107+
assertQuotingHint(t, err)
108+
}
109+
110+
// assertQuotingHint pins the second diagnostic line by its own words, not by
111+
// the constant that produces it: an assertion written against quotingHint
112+
// would keep passing if the sentence lost the rule or the remedy.
113+
func assertQuotingHint(t *testing.T, err error) {
114+
t.Helper()
115+
if err == nil {
116+
t.Fatal("expected an interpolation error, got nil")
117+
}
118+
for _, want := range []string{
119+
"every {{ ... }} in a prompt is resolved, including one that is only being quoted or explained",
120+
`break the two braces apart ("{ {")`,
121+
"pass the text in as an input or artifact",
122+
} {
123+
if !strings.Contains(err.Error(), want) {
124+
t.Fatalf("error is missing the quoting hint %q:\n%v", want, err)
125+
}
126+
}
127+
}
128+
129+
// TestInterpolate_MentionedPlaceholderExplainsItself is the reported defect: a
130+
// prompt that only TALKS ABOUT a placeholder is interpolated like any other,
131+
// and the bare reason then asserts a wiring bug — for artifacts, that a node
132+
// which does not exist "has not completed". Both forms must carry the hint,
133+
// because both reach the reader with nothing else to explain what happened.
134+
func TestInterpolate_MentionedPlaceholderExplainsItself(t *testing.T) {
135+
h := New(t.TempDir(), nil)
136+
137+
for _, tc := range []struct {
138+
name string
139+
tmpl string
140+
}{
141+
{"inputs", "In another graph you would write {{ inputs.demo }} to read an input."},
142+
{"artifacts", "In another graph you would write {{ artifacts.nosuch }} to read an artifact."},
143+
} {
144+
t.Run(tc.name, func(t *testing.T) {
145+
_, err := h.Interpolate(tc.tmpl)
146+
var iErr *InterpolationError
147+
if !errors.As(err, &iErr) {
148+
t.Fatalf("expected *InterpolationError, got %T: %v", err, err)
149+
}
150+
assertQuotingHint(t, err)
151+
})
152+
}
106153
}
107154

108155
func TestPersistOutput_WritesFile(t *testing.T) {

0 commit comments

Comments
 (0)