Conversation
Grow the placeholder Tutorial class framework into the API a Playground Course is written against: Course presentation and completion, the Editor namespaces (Project, Runtime, CodeEditor, Ruler), Copilot text and structured generation, and Spotlight reveal. Course callbacks now run on the Course program's own goroutine, one at a time, in the order the host dispatched the events, instead of each event starting its own goroutine — Course code is written as ordinary sequential code, and runtime logs are a judging channel whose order carries meaning. The framework registers every host event up front, so dispatching an event the Course did not subscribe to is accepted and dropped rather than reported as an unknown event, and it bounds the pending queue so a Course blocked in a slow capability tells the host it is not keeping up instead of silently losing events. complete and completeWith end the program from within: the current callback runs to its end, queued events are abandoned and Start returns, so the executor reports the run as completed. Repeated completions are ignored. generateJSON derives its JSON Schema from the struct the Course passed and decodes the response back into it. A struct with no exported fields is rejected, since encoding/json could never fill it and the Course would silently read zero values. A failed capability panics: the Course asked the editor, Copilot or the learner's UI for something that did not happen, and continuing would judge the learner on a false premise. Validation: unit tests drive the framework through a fake host, and a bundle-level test builds a Course program that uses the whole API against the real class-framework binding, which also fails if the ixgo exports were not regenerated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ivation - ShowVideo takes a declared video resource name, matching the contract merged in goplus#3437: videos are declared under assets/videos/<name>/, and the Course writes showVideo "step-to" rather than a file path - Reject recursive result types in generateJSON: a self-referential or mutually referential struct made schema derivation recurse without bound, which crashes the interpreter instead of reporting a mistake - Raise the event queue bound: presentation capabilities block until the learner acts, which can take minutes, and the runtime-log contract promises one event per appended entry, so the bound has to sit beyond what a running project can produce meanwhile - Say "serializable exported fields" where json:"-" also excludes a field Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The namespaces reach their course's run state through this field, and "program" sat one letter away from the unrelated Project namespace. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Tutorial lifecycle prototype (goplus#3445) reads the completion feedback from a feedback field, and the name also says what the value is. Align the wire shape before either side ships. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
createTutorialFramework wraps the host's typed methods into the capability table the executor consumes, mirroring the Go half's wire names and request shapes. It lives in tools/tutorial next to the Go package so the framework stays one self-contained unit, and spx-gui consumes it through a symlink the same way it consumes the spxls type declarations. The adapter deliberately imports nothing from spx-gui: the executor's framework shape is declared structurally, which keeps the whole directory extractable as its own repository. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The example course used a parenthesis-free command-style call as an expression (feedback := Copilot.generateText "..."), which XGo only allows at statement level, so the example never compiled. Fix the call and add a bundle test that compiles the example through the real classfile pipeline so the docs example cannot drift from the framework again. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reveal the API References panel right after filterAPIs narrows it to stepTo, using the panel's real Radar node name. The exact selector syntax is pending goplus#3441; a TODO marks the line for revisiting. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the hand-rolled single-capability framework stub with createTutorialFramework wired to a full mock TutorialFrameworkHost that logs every capability call and answers with canned values. The course source is the docs example imported via ?raw, so the debug page always exercises the exact example we document. Verified in the browser against the freshly built wasm: filterAPIs, showPrelude, showVideo and spotlight_reveal (with Go-side defaults mask=true/duration=0) run on start; dispatching the reached-target log drives getCode -> generateText -> completeWith and the executor exits with reason 'completed'. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Extract the mock TutorialFrameworkHost into tutorial-mock-host.ts so both debug pages share it; it logs every capability call and answers with canned values. - xgoexec debug page: the course source is now an editable textarea (prefilled with the docs example, with a button to load it back), and the status line turns green on 'exited: completed' / red on errors. - New /debug/tutorial-courses page: scripted course cases run one by one against the real executor wasm; each case declares a course source and the events to dispatch, and passes (green) when the run exits as 'completed'. Seeded with four cases: the docs example, a full-API course adapted from the bundle integration test, completion during onStart, and unsubscribed-event tolerance. Verified in the browser: all four cases pass, including the reflection-derived generateJSON schema round-tripping the bridge. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The contract (tutorial-class-framework.go / module_TutorialFramework.ts)
declares completeWith(message); the earlier switch to a {feedback} wire
field (b3784e9) followed the goplus#3445 prototype instead of the contract.
The contract is the source of truth, so revert to the {content} shape
shared by the other presentation capabilities and name the parameter
message; the prototype side should align with the contract instead.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
When a case exits before the deadline the 15s setTimeout kept running until it fired into the already-settled Promise.race; clear it once the case is over. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add a tools-tutorial-test job following the tools-ai-test precedent: it verifies the committed qexp export file matches the tutorial package's public API (build-wasm.sh silently regenerates it, so staleness never failed a build before — it only broke course execution inside the wasm interpreter), then runs the tutorial unit tests with -race and the executor bundle's classfile-binding tests. Also let the spx-gui job's Go cache cover the executor bundle modules. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Restructure the host interface from flat underscore-joined methods into course / editor / copilot / spotlight namespaces mirroring the author-facing API tree, per review feedback on goplus#3442 — friendlier for the host implementer. The wire stays flat: createTutorialFramework still maps each capability name onto the nested host method. The contract (module_TutorialFramework.ts) changes together with the code. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Per review feedback on goplus#3442: inline a copy of the docs example course instead of importing it from outside the frontend directory (the docs copy stays compile-guarded by tools/xgoexec-bundle), and fold the former /debug/xgoexec page into /debug/tutorial-courses as a free-form runner card (editable course source prefilled with the example, plain-XGo run, runtime-log dispatch), removing the old page and route. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Per review feedback on goplus#3442: drop spx-gui's tutorial-framework.test.ts (the adapter is maintained in tools/tutorial, so spx-gui testing it was misplaced, and its hand-written wire-name list stayed green when both sides forgot an update). The guard moves into the framework's own directory as a Go test that extracts capability names from the Go sources (mustCallCapability call sites) and from client.ts's capabilities table, then asserts the two sets match — renaming either side now fails tools-tutorial-test in CI without any manual list. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the single run-to-completion event loop with a token-scheduled execution model, following the review discussion on the event model: - Callbacks run as frames holding a cap-1 execution token, so exactly one piece of course code executes at any instant and shared variables in course code still never race (the token's release/acquire chain provides the happens-before edges a single loop used to). - Capabilities are classified in capabilityKinds by what they wait on. Calls waiting on an external party — the learner (course_show*) or the LLM (copilot_generate*) — release the token for the duration, so events keep being handled while a dialog is open or a generation is pending, and state observed after such a call is fresh. Calls that only wait on host computation keep the token; an unregistered name degrades to non-yielding, the conservative direction. - Each event gets its own lane (FIFO queue + single worker): triggers of one event stay strictly ordered and its handlers never interleave or re-enter, so check-then-act judgment code needs no guards. Only different events interleave, at waiting points. - Presentation calls from different frames are serialized on a dedicated lane, entered only after releasing the token (lock order matters: waiting for it while holding the token would freeze all frames), so the host never sees concurrent dialogs. - Completion closes intake; running and suspended frames finish their remaining statements (the host no-ops presentation after completion) and the program then exits. Frame panics are recorded and re-thrown on the main goroutine so the executor still reports exit 'error'. Contract docs gain the author-visible semantics and the host-visible note that non-presentation calls may now be pending concurrently. New tests cover yielding during a held dialog (logs processed meanwhile, fresh state observed after), same-event serialization, presentation serialization, and two generations pending concurrently; the wire-name guard now also checks capabilityKinds keys are real capabilities. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Change the serial unit from one lane per event to one lane per registered callback, per design discussion: when an author registers several callbacks on one event, expecting them to run independently is the natural reading. Each callback now owns its queue and worker — its triggers stay strictly ordered and it never re-enters, while different callbacks (including several on the same event) may interleave at waiting points. Callbacks registered after the program started get their worker immediately. Contract wording updated accordingly; tests drop cross-callback ordering assumptions and add coverage for two same-event callbacks running independently while one waits. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Address the external review of the execution model (4 High, 2 Medium, 1 nit — all confirmed): - Decode waiting-capability responses under the token: the bridge used to write straight into the caller's result (an author-shared struct for generateJSON) while the token was released — a race outside the serialization guarantee. Responses now land in a private buffer and are unmarshalled after the token is reacquired. - Admission moved to after token acquisition: a dequeued event's pre-check could go stale while its frame waited for the token, letting a callback start after completion or a fatal error. runFrame now re-checks the terminal state once it holds the token. - awaitEnd re-checks the fatal slot after the completion-path Wait: a failing course_complete (or a late frame panic) used to be swallowed and reported as completed. - Event delivery is all-or-none: deliverAll preflights every lane's capacity under a delivery lock before sending, so one full queue no longer leaves the event partially delivered; delivery after a fatal error now returns an error instead of feeding dead queues. - Worker startup checks the terminal state under the same lock as workers.Add, closing an Add-vs-Wait window for callbacks registered while the program is winding down. - The token comment no longer claims FIFO wakeup (Go does not promise it), and a classification test forces every capability into either capabilityKinds or an explicit fast list — the safe runtime default can no longer hide an unclassified capability. The host contract gains the matching obligation: on completion the host must promptly settle every still-pending call, since the program exits only after pending callbacks finish. Four regression tests pin the fixed behaviors. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The rules that keep the yielding model deadlock-free lived only in comments; this turns them into checks that fail in CI: - An AST-based test asserts: the token moves only through acquire/release, and only runFrame and mustCallCapability may operate it; presentationMu and the capability bridge are touched only inside mustCallCapability (whose fixed order releases the token before any long wait); mu- and deliverMu-held regions contain no blocking operation (channel ops, select, Wait, token ops, bridge calls), with deliverMu -> mu as the only permitted nesting and deliverAll's preflighted queue send as the one documented exemption. - release() gains a dynamic assertion: a non-blocking send that panics on a release without a matching acquire, so a pairing bug surfaces immediately instead of silently minting a second token. - init fills the token before entering the mu region, so the invariant holds without exceptions. Injecting violations (token op inside a mu region, presentationMu use outside its home) was verified to turn the check red. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rename the synchronization primitives so each name carries its domain and its object, per design discussion: - token -> execToken, acquire/release -> acquireExec/releaseExec: the execution-right token, with the object visible at every call site. - mu -> schedulerMu: it guards the scheduler's own bookkeeping (handler registry, lifecycle flags, lane starters). 'state'/'program' were rejected as ambiguous — 'program' collides with the learner project's running program; spx's scheduler uses the same name for the same role. - deliverMu -> eventDeliveryMu: says what is being delivered. - ending/endOnce/signalEnd/awaitEnd -> shutdown/shutdownOnce/ beginShutdown/awaitShutdown: the channel signals that wind-down has begun, not that everything has finished. Pure rename, no semantic change; the invariant checker's name-based rules are updated in lockstep. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
encoding/json promotes an untagged anonymous field's exported members into the outer object, but the derived schema nested them under the type name — a host result conforming to that schema was then ignored during unmarshalling, leaving the embedded fields at zero values (external review finding). Mirroring the full promotion/conflict rules is not worth it for an LLM-facing schema, so such fields are rejected with a clear error, consistent with how recursive types are handled; an anonymous field with an explicit json name is treated by encoding/json as a regular named field and keeps working, now pinned by a test that round-trips against encoding/json itself. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
'worker' collides with the browser Web Worker that hosts the whole wasm instance one container level up; the WaitGroup counts lane worker goroutines inside it. The new name carries its qualifier, and the field comment now states the distinction explicitly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Capture the discipline that keeps the yielding execution model correct as directory-scoped agent guidance, following the tools/ai precedent: locking rules (and their machine-checked enforcement), capability classification and contract-sync obligations, package testing pitfalls (process-global event registry, token-holding blocks), and the qexp/symlink build-chain constraints. Rules only — design rationale stays in program.go's comments. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Course.OnStart used to append to a plain slice that Start ran one after another on the main goroutine, so a second onStart could not begin until the first returned — including its waits on the learner. Nothing in the design asked for that; it was left over from the fully serial model and was never revisited when event callbacks moved to per-callback lanes. Course start now goes through the same path as every host event: each onStart registers a lane, and Start delivers the single course-start trigger through deliverAll. Several onStart callbacks run independently and may interleave at waiting points, exactly like several callbacks on one event; there is no ordering promise between them. Author callbacks now have exactly two execution paths: a lane worker's frame and the one MainEntry frame. A rejected delivery can only mean MainEntry already recorded a fatal error, which recordFatal keeps as the first one. Contract wording and AGENTS.md updated. TestHandlersAccumulate drops its start-order assumption and completes only once all four callbacks ran; TestCourseStartHandlersRunIndependently pins that a second onStart does not wait for the first one's showPrelude to return. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
XGot_ is the current prefix for a template receiver method; Gopt_ is the legacy spelling that gogen still accepts (isXGoCommon takes both "XGo" and "Gop"), which is why the old name kept working. spx v3 already uses XGot_Game_Main, so stop relying on the compatibility path. The generated qexp export follows. tools/xgoexec-bundle's build test covers this: with an unrecognized prefix the classfile binding fails with "Course has no method Main". Requested by review (go-wyvern).
tools/tutorial/README.md and README.zh.md describe the framework from a course author's point of view: the project layout, calling conventions, the execution model (one callback at a time, waiting calls yield, no re-entry, independent callbacks, serialized presentations, no timers), the per-callback queue limit, completion semantics, an API reference by namespace, failure behavior and the local debug page. The contracts in docs/develop/tutorial-v2 stay the source of truth; the design index links to the guide. Every go code block in both files compiles through the real classfile pipeline, the two files carry identical code blocks, and the first block is the example course verbatim. Suggested in review (go-wyvern).
This branch had an error being deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #3417. Adds a course-author guide for the Tutorial Class Framework, as suggested by @go-wyvern in the #3442 review.
Important
Stacked on #3442. This branch is #3442's branch plus one commit, so until #3442 merges the diff also shows its changes (a goplus/builder PR cannot be based on a fork branch). Review only the last commit,
docs(tutorial): add a course-author guide for the class framework. The guide documents the API that #3442 implements, so it should merge after #3442.What it adds
tools/tutorial/README.mdandtools/tutorial/README.zh.md, next to the implementation, following thetools/xgoexec/README.mdprecedent.docs/develop/tutorial-v2/index.mdlinking to the guide.The contracts in
docs/develop/tutorial-v2(tutorial-class-framework.go,module_TutorialFramework.ts) stay the source of truth. The guide says so up front: where they disagree, the guide is out of date.Contents
index.jsonfields,main_course.gox,project/, declared videos underassets/videos/<name>/onLog" patterncomplete/completeWithstop, what still runs, idempotencegenerateJSONfield rules and Spotlight selector status, #3441)/debug/tutorial-coursespageVerification
gocode block in both files (11 each) was compiled through the real classfile pipeline (xgobuild+ ixgo load, the same path astools/xgoexec-bundle's build tests). The two files carry identical code blocks, and the first block is the example course'smain_course.goxverbatim.generateJSONtype rules.