Summary
In an app scaffolded from blueprint-react-node, any operation runner that calls the trigger(type, payload, capability) helper throws before a task is queued:
Operation "find-online-copy" threw: app is not defined
This breaks the documented app→agent seam. Operation runners get (args, ctx, { store, trigger }), and trigger is the only way an author can enqueue agent work from a2app.schema.mjs.
Root cause
toolkits/blueprint-react-node/template/server.mjs:229:
trigger: (type, payload, capability) => app.trigger({ type, payload: payload ?? {}, ...(capability ? { capability } : {}) }),
The file never declares app. The adapter instance is created further down as:
const a2app = createA2App(binding, { ... }); // line 308
The comment above the wrapper says "app is assigned further down and read at call time", but the binding is actually named a2app, so the closure throws a ReferenceError on first call.
server.mjs is system-owned and hash-locked, so app authors can't patch it without failing the ownership gate. The only workaround is to stop using trigger, which is what the agent did in the report below.
Repro
- Scaffold a
blueprint-react-node app.
- In
a2app.schema.mjs, declare an event and an operation runner that triggers it:
events: [{ type: "book.find_online_copy" }],
operationRunners: {
"find-online-copy": (args, _ctx, { store, trigger }) => {
const book = store.get("books", args?.book);
if (!book) return { ok: false, reason: "no such book" };
const { taskId } = trigger("book.find_online_copy", { book: book.id }, "web-search");
book.onlineSearchTask = taskId;
store.put("books", book);
return { ok: true, book: book.id, queued: taskId };
},
},
- Declare the operation in
operations.json, then POST /api/ops/find-online-copy with { "book": "<id>" }.
Expected: { eventId, taskId } comes back, and the task appears in the A2App task APIs where agent-app <dir> bridge start can claim it.
Actual: Operation "find-online-copy" threw: app is not defined
Suggested fix
Point the wrapper at the real instance:
trigger: (type, payload, capability) => a2app.trigger({ type, payload: payload ?? {}, ...(capability ? { capability } : {}) }),
const a2app is in the temporal dead zone only until boot finishes, and runners execute after that, so the call-time read is safe. Also:
- Add a template test that runs an operation runner which calls
trigger(...) with a capability and asserts that a task is queued. No current test exercises this path.
- Check the other blueprints (
blueprint-go-react, blueprint-rust-react, Python) for the same wrapper.
- Ship this as a toolkit update so existing apps get it through
agent-app <dir> toolkit-sync.
Context
Found during Pi end-to-end testing of the framework (app: reading-list-full, operation find-online-copy, event book.find_online_copy).
Summary
In an app scaffolded from
blueprint-react-node, any operation runner that calls thetrigger(type, payload, capability)helper throws before a task is queued:This breaks the documented app→agent seam. Operation runners get
(args, ctx, { store, trigger }), andtriggeris the only way an author can enqueue agent work froma2app.schema.mjs.Root cause
toolkits/blueprint-react-node/template/server.mjs:229:The file never declares
app. The adapter instance is created further down as:The comment above the wrapper says "
appis assigned further down and read at call time", but the binding is actually nameda2app, so the closure throws aReferenceErroron first call.server.mjsis system-owned and hash-locked, so app authors can't patch it without failing the ownership gate. The only workaround is to stop usingtrigger, which is what the agent did in the report below.Repro
blueprint-react-nodeapp.a2app.schema.mjs, declare an event and an operation runner that triggers it:operations.json, thenPOST /api/ops/find-online-copywith{ "book": "<id>" }.Expected:
{ eventId, taskId }comes back, and the task appears in the A2App task APIs whereagent-app <dir> bridge startcan claim it.Actual:
Operation "find-online-copy" threw: app is not definedSuggested fix
Point the wrapper at the real instance:
const a2appis in the temporal dead zone only until boot finishes, and runners execute after that, so the call-time read is safe. Also:trigger(...)with a capability and asserts that a task is queued. No current test exercises this path.blueprint-go-react,blueprint-rust-react, Python) for the same wrapper.agent-app <dir> toolkit-sync.Context
Found during Pi end-to-end testing of the framework (app:
reading-list-full, operationfind-online-copy, eventbook.find_online_copy).