Skip to content

Fixes and packaging - #9

Merged
vinniefalco merged 34 commits into
cppalliance:masterfrom
vinniefalco:master
Sep 2, 2026
Merged

Fixes and packaging#9
vinniefalco merged 34 commits into
cppalliance:masterfrom
vinniefalco:master

Conversation

@vinniefalco

Copy link
Copy Markdown
Member

Odds and ends

Refine the `promptforge-core-support` public API before the agentic harness lands. `GuardNonce` gains a `wrap` method, a `Display` impl, and equality derives; `detail` gains three model-catalog validation constants; `CancelHandle` moves onto `tokio_util::sync::CancellationToken` and grows `child()`. Both production call sites of the free `wrap` migrate to the method.

- `CancelHandle` replaces its hand-rolled `Arc<AtomicBool>` and `Arc<Notify>` pair with a single `CancellationToken`, which deletes the enable-check-await lost-wakeup machinery; `child()` delegates to `child_token()` so an orchestrator can cancel one subagent while a run-level cancel reaches every subagent.
- The free `wrap` stays as a deprecated one-line alias instead of a breaking delete, because the crate carries crates.io publish metadata.
- `GuardNonce` derives `PartialEq`, `Eq`, and `Hash`, and its `Display` renders the 32 lowercase hex digits so logs can correlate envelopes to runs.
- `detail` adds `MODEL_CATALOG_VALIDATION_STARTED`, `MODEL_CATALOG_VALIDATION_SUCCEEDED`, and `MODEL_CATALOG_VALIDATION_FAILED`, aliasing `Observation::ModelCatalogValidationStarted` and its siblings.
- No in-tree caller of the deprecated free `wrap` remains; the alias serves downstream users only.
A same-origin check replaces the any-loopback rule, so no other local server can load in the webview and inherit the IPC bridge, folder picker, or desktop flag. Also dispatch the maximized event only on state transitions instead of every resize event, and share one event-loop proxy via clones instead of three independent handles.
promptforge-workshop is now the Tauri app crate and promptforge-desktop-shell is deleted. The app builds on tauri::Builder with single-instance, dialog, opener, and window-state plugins; the setup hook boots the gateway, waits for health, and opens a programmatic WebviewUrl::External window (hidden while built, undecorated on Windows after construction, then shown); RunEvent::Exit shuts the gateway down exactly once. The same-origin navigation policy moves to navigation.rs, with denied URLs opened from Rust through tauri-plugin-opener. bridge.rs carries the Windows WebView2 web-message file-drop bridge (same workspace-drop contract, promptforge:file-drop dispatch) and a PermissionRequested handler granting the microphone only, with per-block SAFETY comments and the COM tests; off Windows, Tauri's DragDrop event feeds the same dispatch, Linux gets the WebKitGTK media-permission handler, and macOS gets the Info.plist usage string plus audio-input entitlement through a tauri.macos.conf.json overlay. The UI switches to @tauri-apps/api window controls and plugin-dialog's folder picker, with the Tauri modules mocked through esbuild aliases in tests. tauri.conf.json plus capabilities/default.json wire the loopback remote origin, and cargo tauri build produces the NSIS installer.

Field-report fixes ride along: downloads resume from a kept .part through a Range request guarded by a provenance marker (the ART-007 tests change meaning: a failed publication keeps the partial); the STT stop path names the dropped leading audio when a take exceeds the interim window with no final pass; the mic stays visible and names its blocker on click (no GPU, no provisioned speech models via the capability endpoint's new engine flag, chat not ready); the two tilde expansions are unified on one pure tested core; and a joining session's status line is recomputed from the current probe rather than replaying a stale heartbeat transition. The heartbeat.rs ceiling rises to 969 for the join_status helper and its tests.

Note: cargo tauri build force-enables tauri/custom-protocol, which pulls the WebView2 loader's C++ objects into the link; on a machine whose release-mode MSVC CRT libs are mismatched (the field report's VS 2026/CUDA 13.2 class) that link fails. The installer above was produced by building the exe with plain cargo and running cargo tauri bundle.
The repo mixed CRTs - whisper.cpp and the WebView2 loader compile /MT while Rust links /MD - and suppressed the conflict warning with /NODEFAULTLIB:LIBCMT. That held only while the dynamic import libs carried every symbol the /MT objects needed; CUDA 13.2's cudart.lib (built with /guard:cf) and the WebView2 loader's nothrow-new reference broke the release link on the current VS 2026 toolchain (LNK1120), and cargo tauri build with it. Now the Windows target builds with +crt-static, and two CMAKE_* env vars ride whisper-rs-sys's own build-script passthrough so cmake compiles the whisper core and the NVCC kernels /MT to match (CMP0091 NEW because whisper.cpp's cmake_minimum_required predates the runtime-library abstraction). Verified: cargo tauri build with CUDA produces the NSIS installer; clippy -D warnings and the full workspace test suite pass under the new config. Also folds in the formatting pass over the Phase 7 clippy fixes.
Add the standard windows_subsystem attribute: release builds are a GUI app (no console window when launched from the installer), debug builds keep the console for eprintln diagnostics.
…dow flicker

The Gateway Config panel loaded its iframe from the gateway's own port (a cross-origin iframe), which made Chromium spawn renderer processes that flashed a console window on Windows. Now the workshop server proxies the config SPA's assets at /gateway/config/, so the iframe loads same-origin. The async gateway-origin probe is gone from the panel; the iframe mounts synchronously.
llama-server and icacls are console-subsystem executables; spawning them from a GUI-subsystem parent (the workshop desktop app) without CREATE_NO_WINDOW makes Windows briefly flash a console for each one. Add the flag to every Command::new in the gateway-local crate's spawn and DACL paths, through a crate-level constant matching Unsloth's pattern.
The crate is the model client that executors use to reach the gateway, not a part of the gateway binary, and the new name states that role. The change moves the directory to `crates/promptforge-model-client` and renames the package in the root `Cargo.toml`, in `Cargo.lock`, and in the manifests of `promptforge-core`, `promptforge-lua`, and `promptforge-workshop-server`. Every `promptforge_gateway_client` use path, re-export, and doc example becomes `promptforge_model_client`, and `README.md` and `AGENTS.md` prose changes to match.

- The rename stops at the crate name: the `GatewayClient` and `GatewayEndpoint` types, the `GatewayClientError` import alias, and the `PROMPTFORGE_GATEWAY_URL` and `PROMPTFORGE_GATEWAY_API_KEY` variables keep the gateway vocabulary.
- Test edits are path substitutions only; no test is added and no assertion changes.
At the parent commit, `routes/gateway_config.rs` exceeds its recorded
ceiling and the ratchet test
`ratchet::every_module_stays_at_or_below_its_ceiling_plus_slack` in
`crates/promptforge-workshop-server/tests/it` fails. This commit fixes
that pre-existing failure: it raises the module's entry in
`module-ceilings.toml` from 334 to 390, the module's measured size. An
added comment records the growth source: the same-origin config SPA
proxy routes, the shared `proxy_config_asset` relay, and the
dot-segment refusal.

- The raise lands in its own commit, apart from the growth it records;
 the added comment states the growth commit missed the re-record. The
 file's own rule asks for the raise in the same commit as the growth.
- The new ceiling equals the module's measured size, so the remaining
 headroom is the test's fixed 30-line slack.
The runtime gets one canonical vocabulary for completed run events and model-call metrics. The new `events` module in `promptforge-core-support` defines `RuntimeEvent`, `RuntimeEventKind`, `ToolCallEvent`, `CallMetrics` with its `Usage`, `LlamaTimings`, `VllmMetrics`, and `ClientTiming` sections, and the read-side `EventLog` history trait. The `Observer` trait gains five default-body content methods: `on_assistant_reply`, `on_assistant_tool_calls`, `on_tool_result`, `on_thinking`, and `on_user_input`.

- The write side and the read side are different types on purpose: `Observer` reports are write-only and never read back, while an `EventLog` is an explicit run input a host supplies. The crate `AGENTS.md` restates its report-only rule in these terms.
- `RuntimeEventKind` labels follow the Agent Client Protocol `sessionUpdate` names where an equivalent exists (`agent_message`, `tool_call`, `tool_call_update`, `agent_thought`, `user_message`), so persisted logs stay ACP-conversant. The enum is `#[non_exhaustive]`, and reserved future kinds are documented, not declared.
- The crate adds `serde` and `serde_json` dependencies.
- One `RuntimeEvent` serializes to one JSONL line; absent optional fields are omitted and deserialize back as `None`. The test `runtime_event_jsonl_line_shape_is_stable` pins the exact line as the persisted-log schema, so a field rename or reorder fails it.
- The content methods have default bodies that discard the report; `null_observer_inherits_content_method_defaults` pins that `NullObserver` compiles without overriding them and that the widened trait stays dyn-compatible.
- Nothing in this change produces or consumes the new vocabulary: the content methods have no caller, and `EventLog` has no implementation outside tests and doc examples.
Callers can now attribute a completion to the serving model and read
token and timing data. A new `response_metadata` function in
`normalize.rs` parses `model`, `usage`, llama.cpp `timings`, and vLLM
`metrics` into the `promptforge-core-support` types, which the crate
re-exports at its root. `Completion` carries the parsed values in
`#[doc(hidden)]` fields with public accessors.

- Metadata parsing is infallible and separate from the strict turn
 parse: each section parses independently and degrades to `None`, so
 a malformed metrics extension never fails a call whose turn was
 usable.
- The metrics types are not defined here: the crate takes a new
 dependency on `promptforge-core-support` and re-exports `Usage`,
 `LlamaTimings`, `VllmMetrics`, `ClientTiming`, and `CallMetrics`;
 the crate `AGENTS.md` records this vocabulary rule.
- Absent or null sections record `None` silently; a present malformed
 section records `None` and emits one `tracing` warning. Tests count
 warnings through a `with_warn_count` subscriber to pin silence,
 one-warning-per-section, and independent degrade.
- Absent llama.cpp draft counters (`draft_n`, `draft_n_accepted`)
 default to zero through `#[serde(default)]`: a non-speculative body
 means zero drafted tokens, not an unknown.
- `client_timing` is dormant: the only `Completion` construction site
 in `transport.rs` sets `None`, and nothing constructs the
 re-exported `CallMetrics`.
- The transport wiring is untested: new tests drive
 `response_metadata` over `serde_json` bodies directly; no test reads
 the metadata fields off a `Completion`.
The client now streams every completion, so a caller can consume deltas live and only one transport path remains. Every request sends `stream: true` with `stream_options.include_usage`, and the client reassembles the SSE chunks into the buffered body shape before normalization. The `gemma3_tool_code` emulation needs the whole reply to parse its fences, so the gateway now serves `stream: true` callers from one buffered upstream round trip, re-emitted as synthetic chunks.

- The new `crates/promptforge-model-client/src/client/stream.rs` holds `SseScanner`, which splits the byte stream into `data:` payloads, and `StreamAccumulator`, which folds them back into the buffered chat-completion shape, so streamed and buffered turns are judged by one rule set.
- `complete` takes a new required `on_delta` callback, and the new public enum `StreamDelta` carries `Text` and `Reasoning` fragments. Tool-call fragments buffer internally keyed by `index` and never surface as deltas.
- In the gateway, `response_chunks` converts the fence-rewritten reply into synthetic chunks; the handler removes `stream_options` from the buffered upstream call, and a trailing empty-choices summary chunk restores the usage.
- The response size cap now applies as the stream bytes arrive; a stream that ends without the `[DONE]` sentinel fails as malformed, and a mid-stream `error` envelope fails as a transport error.
- A tool-call batch finished by `length` or `content_filter` fails whole, so partial arguments never execute; truncated text still returns with its `finish_reason`.
- `client_timing` is now populated with TTFT, mean inter-token latency, and end-to-end time measured on the client's own clock.
- The test backends `ScriptedGateway` and `fake_backend` now answer with SSE, so the executor and gateway suites run through the streaming path.
- The executor call sites in `tool_loop.rs` and `tools.rs` pass no-op `on_delta` closures; no consumer of live deltas is wired in this change.
Section scripts can now call a bound tool directly, without a model turn. A new `tool_call` shim yields `Request::ToolCall`; the scheduler resolves the alias against the section's effective scope, dispatches the tool on a spawned task, and resumes the outcome into the coroutine. The dispatch body (cancel race, call counts, untrusted wrap, observer events) moves from `tool_loop.rs` into `dispatch_tool` in `promptforge-lua`, and the prose tool loop and the new scheduler arm both call it.

- `Answer::ToolCallResult` carries a classified `ToolCallOutcome`, not the dispatch text: the envelope renderer is the one resume site into Lua, so it applies the shape rule there, with `Plain` resumed as a string and `Structured` resumed as a table through the serde boundary. `dispatch_tool` itself keeps `Result<String>`.
- The one-time section install (tool-call counts, model resolution) moves to a shared `install_section_scope`; the first prose block or the first script dispatch runs it, whichever comes first, and a test pins that prose after a script dispatch still resolves the model.
- The untrusted nonce wrap runs before structured classification, so an untrusted binding's valid JSON still fails the parse; this ordering restricts structured output to trusted tools, and a test pins it.
- Every dispatch failure resumes as the call's error, so an author `pcall` catches it at the call site; an out-of-scope alias error names the in-scope set.
- `on_tool_result` fires only for script-initiated dispatches, carried by `ScriptReport`; a model-loop dispatch passes no report and its results ride the conversation echo.
- No production code constructs a `Structured` binding: `install_live_tools` pins author-bound tools to `ToolOutputKind::Plain`, and only test fixtures set `Structured`.
A Lua block may now run a long or infinite loop: no instruction
ceiling aborts it, and the run's `CancelHandle` is the kill switch
for a runaway block. `HOOK_BUDGET` rises from `1_000` hook firings
to `u64::MAX`; the hook still fires every `HOOK_INTERVAL`
instructions, and its job is now the cancellation poll. Tests, doc
comments, and both user guides now describe cancellation, not an
instruction quota, as the bound on a runaway block.

- The ceiling is disabled by value, not removed: `budget_hook`
 keeps its quota branch and the typed
 `crate::error::lua_quota::INSTRUCTION` error, now reachable only
 when the firing counter reaches exactly `u64::MAX` (`>=` becomes
 `==`).
- `HOOK_BUDGET` stays a compile-time constant in
 `crates/promptforge-lua/src/lib.rs`; the change adds no
 configuration path for it.
- A pre-cancelled run aborts a tight `while true do end` with
 `Error::Interrupted` on the main state and on every block
 coroutine, inside a bounded wall-clock;
 `a_pre_cancelled_run_aborts_a_tight_loop_promptly` and
 `every_block_coroutine_carries_the_cancellation_hook` pin this.
- The removal is scoped to the instruction quota:
 `a_loop_exceeding_the_old_instruction_budget_completes` proves a
 loop far past the old ceiling returns its result, and
 `the_memory_budget_error_stays_reachable` proves the heap ceiling
 still refuses over-allocation.
- No new test covers the log budget; the existing
 `shared_replay_consumes_the_configured_log_budget` test is
 unchanged.
An earlier commit changed the STT voice-capability source documentation but did not regenerate the assembled guide. This commit fixes that stale generated file. It runs the `make-user-guide` generator again, and the regenerated `guide/promptforge-user-guide.md` now documents the `engine` flag next to `gpu` in the `voice/capability` response.

- The diff carries only the regenerated output; the source documentation that feeds the generator is not part of this diff.
- No tests change; the commit touches documentation only.
The Workshop gains an executor for `.lua` agent programs: one long-running Lua chunk that drives its own model turns, run as a sibling of `promptforge-core` over the same substrate with no dependency between the two. `run_agent` compiles the source, builds a `SectionVm` without the section control surface, and drives the chunk as one coroutine: resume, validate the yield, await the one in-flight request, resume with the answer. `AgentConfig` carries the slim agent-side run configuration, and core's `RunConfig` gains no fields.

- The crate builds on the substrate crates (`promptforge-lua`, `promptforge-model-client`, `promptforge-tools`, `promptforge-store`, `promptforge-core-support`) and never on `promptforge-core`, `promptforge-parser`, or `promptforge-tool-picker`. `crates/promptforge-lua/AGENTS.md` now names both executors over its `#[doc(hidden)]` seam.
- `execute` and `fanout` are removed from the agent VM before author code runs; `jump` is never installed. An agent that touches them fails as an undefined global, and the driver keeps `Request::Execute`, `Request::Fanout`, and `Request::Mcp` as unreachable `AgentError::Internal` guards.
- Tool dispatch goes through the shared `promptforge_lua::dispatch_tool` body. Tools register by wire name with every alias in scope; on a wire-name collision the first binding wins alias lookup.
- Models register with no default: a bare `models.infer` fails until the program selects a model with `models.use`. The gateway client resolves once from the environment (`GatewayClient::from_env`) when the caller injects none.
- `run_agent` installs `AgentConfig::cancel` as the task's cancel scope. A test holds an accepted socket unanswered and shows a fired cancel interrupts a suspended `models.infer` with `AgentError::Interrupted`.
- A new scheduler test, `models_chat_is_nil_in_a_section_vm`, pins the mirror rule: a document prompt that calls `models.chat` fails with Lua's plain undefined-value error, with no typed error for the absence.
- `AgentConfig` carries `event_log`, `on_delta`, and `ui` slots that nothing in this change consumes; the agent-only host calls (`models.chat`, `runtime.events()`, `ui()`) are not installed in the agent VM.
- Nothing in this change calls `run_agent`: the workspace `Cargo.toml` entry and the `README.md` row register the crate, and it stays dormant.
Agent programs gain one stateless tool-capable model round that returns the reply or the model's unexecuted tool calls. A new `chat` shim yields `Request::Chat`, the protocol parse validates the author's messages and opts, and `dispatch_chat` in `workshop-agent` calls the model client's `complete` and resumes a `ChatResult` table. `install_agent_chat_shim` installs the shim on agent VMs only, so `models.chat` stays an undefined global in a section VM.

- The whole messages and opts validation lives once in `parse_chat` at the protocol boundary. The driver's `wire_messages` converts without re-checking and turns any violation into `AgentError::Internal`, so no second validator exists to drift.
- Wire `Message` `content` widens from `String` to `Value` so a validated content-parts array serializes verbatim; the inherent constructors wrap strings, and `from_validated_parts_serializes_role_and_content_verbatim` pins their wire shape byte-identical. The new constructor `Message::from_validated_parts` is `#[doc(hidden)]`, a cross-crate seam, and `content()` reports `""` for a parts array.
- The scheduler in `promptforge-core` gains only the `Request::Chat` arm its exhaustive match forces; the arm returns `Error::Internal` and is unreachable because no section VM installs the shim.
- `crates/workshop-agent/src/tests.rs` adds an owned SSE fixture gateway that converts scripted completion bodies to chunk streams and records every request body; `axum` and `async-trait` enter as dev-dependencies only.
- Requested tool calls resume unexecuted; dispatch is the program's decision. Absent result fields resume as true Lua nil, so a program branches on the presence of `tool_calls`, never on `finish_reason` - backends routinely finish tool-call rounds with `stop`.
- The round's binding is `opts.model` or the `models.use` selection, and the advertised tools are exactly `opts.tools`, default none. An unknown alias, an invalid message table, or a missing selection fails at the call site before any request is sent, and a program `pcall` catches it.
- Stream deltas go live to the host's `on_delta` callback and never ride the observer. A completed round fires `on_thinking`, then `on_assistant_reply` or `on_assistant_tool_calls`, each with model attribution and metrics, and the awaited call races cancellation.
- A `length` or `content_filter` finish on a tool-call round fails the whole call, so partial JSON arguments never execute; `length_and_content_filter_with_tool_calls_fail_the_batch` pins both paths.
- Message validation is shallow by design: content-part internals and `tool_calls` internals pass to the wire unread, and entry fields beyond `role`, `content`, `tool_call_id`, and `tool_calls` are dropped at the wire conversion.
The agent program needs read access to the host's event log to build its context. A new `runtime_events.rs` module in `promptforge-lua` serves `runtime.events()` as lazy userdata over the `EventLog`: `__len` reads a snapshot length bound, and `__index` converts one entry per access, so the log is never copied in bulk. The agent driver installs the view in `setup_agent_vm` and refreshes the bound before every coroutine resume, so appends become visible only at host-call resumes, never mid-chunk.

- The driver's `EventsSnapshot` and every Lua view share one `AtomicU64` length bound. All bound loads and stores use `Ordering::Relaxed`: the bound stays on the driver's task, and the log orders its own appends.
- `setup_agent_vm` now installs the read-only `tools.calls` counter surface through `install_tool_call_counts` and returns the counts, replacing the direct `ToolCallCounts::new` construction. An agent program can read its per-alias dispatch counts.
- The driver calls `refresh_events` on both resume paths, including validation-failure answers in the `YieldParse::Call` arm. The test `chat_turn_events_become_visible_after_the_next_resume_not_before` pins the rule: a mid-chunk `log()` append stays invisible until the next host-call resume.
- Every miss reads nil and never raises: index 0, negative, non-integer, and past-bound keys, and an in-bound entry a shrunk log no longer holds. Assignment raises `runtime.events() is read-only`, and a converted entry is a fresh table, so Lua mutation cannot reach the log.
- `install_runtime_events` refreshes once at install, so the bound starts at the log's current length and a relaunched agent sees its persisted history from its first instruction. With no log, `runtime.events()` returns a fresh empty table and no refresh handle.
- The `tool_call` dispatch body is unchanged; the new tests pin its existing behavior: untrusted output arrives nonce-wrapped, an unknown alias fails the call, and cancel during a suspended dispatch ends the run with `AgentError::Interrupted`.
The section scope gated script dispatch, so an author had to
advertise a tool to the model just to call it from Lua. A script
`tool_call` now resolves its alias against the run's full bound
tool catalog, and the scope only shapes what the model is offered.
`prepare_tool_call` reads the binding from the run's tool set and
fails with the new `UnboundToolCall` error, which names every bound
alias, when the alias has no binding.

- `OutOfScopeToolCall` is now the model tool loop's error alone.
 The script path raises the new `UnboundToolCall`, and `RunError`
 maps it to `RunErrorKind::Tool`.
- The counts map still seeds from the section's effective scope,
 and a dispatch of a bound alias outside it seeds that count
 through the existing `ensure`, because `increment` fails on an
 unseeded alias. `a_script_tool_call_reaches_a_bound_tool_outside_the_section_scope`
 pins the dispatch and the count read-back through `tools.calls`.
- The widened resolution does not reach the model:
 `a_script_tool_call_outside_the_scope_never_widens_the_advertised_set`
 shows the prose round still advertises only the section-scoped set.
- The `tools.calls` unknown-key error now says `has no seeded count`
 and lists the seeded aliases; docs in `scope.rs`, `tools_bridge.rs`,
 and `vm.rs` adopt the seeded vocabulary.
- No test asserts the reworded declared-but-unseeded hint in
 `tools_bridge.rs`; the updated typo test in `debug_and_counts.rs`
 covers the undeclared branch only.
The workshop gains an append-only record of its run events. `WorkshopObserver`, in the new `observer.rs` module, appends through the `Observer` content hooks, serves indexed reads through `EventLog`, broadcasts each entry through `subscribe()`, and with a persist path writes each event as one JSONL line behind a versioned header line. `load_from` replays such a file and continues the append; the version header exists because a format defined implicitly by a struct breaks old logs silently when the struct changes.

- One write guard covers the memory push, the file append, and the broadcast send, so entry order, file order, and broadcast order agree. Reports hold that lock across the file write; async callers go through `spawn_blocking`.
- The version-1 vocabulary is closed: `load_from` refuses an alien header, a torn line, or an event kind it does not know, and its error names the offending line. The module doc reserves the `plan` kind and the five-status tool state for a future version.
- A persistence failure is degradation, not loss: a warning names the path, the in-memory entry still lands, and later appends retry. A lock poisoned by a panicking peer is recovered, and both postures are tested.
- With a persist path `new` truncates whatever the file held and writes a fresh header; only `load_from` resumes an existing file.
- The integration suite commits a `version1.jsonl` fixture that every future build must load; `.gitattributes` pins the fixture to `eol=lf`, and replay also accepts CRLF endings.
- Nothing in the crate constructs a `WorkshopObserver`: the type is exported from `lib.rs` and otherwise dormant.
- The log drops data on purpose: `observe` lifecycle reports, the `_alias` on tool results, and the `_trusted` flag have no field in the persisted vocabulary.
An agent program asks its operator for input through a tool call that suspends until the answer arrives. The new `input.rs` module in `promptforge-workshop-server` holds the `WaitRegistry` of single-use wait tokens, the `UserInputTool`, and the `deliver_input_response` producer that fires `on_user_input` and completes a wait. `protocol.rs` gains the pushed `InputFrame` and the inbound `InputResponse`.

- Wait tokens are 128 bits from the OS-seeded cryptographic RNG (`rand::rng`), hex-encoded and single-use. The `WaitRegistry` `Debug` output shows the unresolved count and never a token, so a log cannot let its reader answer a prompt.
- A `WaitGuard` drop guard makes every dying wait an outcome: a dropped tool future or a registry cancel removes the wait and pushes a durable `input_cancelled` frame, so no path leaks a wait or leaves a stale prompt.
- The tool output is trusted, structured JSON: `text` holds the operator's input byte-exact, and `images` is present and always empty. Operator input is first-party, so no nonce envelope wraps it.
- Unresolved waits survive socket loss; `resend_unresolved` re-announces them as `input_required` frames on reconnect, in creation order. Both push sites ignore a failed frame send, because the retained wait is resent.
- `deliver_input_response` fires `on_user_input` byte-exact before it completes the wait, and exactly once per response, even when the token names a dead wait.
- This change wires no caller: `lib.rs` exports the module, and only the new tests and doctests construct `UserInputTool` or route an `input_response`.
The workshop server now runs `.lua` agent programs as sessions that outlive any one socket. The new `session_agents` module holds the `AgentSessions` registry, the per-session supervisor, and the `/agents/ws` endpoint; `protocol.rs` gains the agent frame family, and `config.rs` gains the `state_dir` and `agents.path` keys, each anchored beside the config file when absent. In support, `promptforge-tools` adds `structured_output` to `Tool`, and `workshop-agent` exports `run_agent_with_client`.

- `AGENTS.md` records a carve-out: agent sessions keep a session registry because sessions survive socket disconnect by design. The no-session-registry rule stands for every other endpoint.
- Durable `agent_event` frames drain from the persisted log by per-client cursor and carry their log `index`; ephemeral `agent_delta` frames ride a bounded broadcast and carry the `reply` id of the durable event that supersedes them, so the superseding event repairs dropped chunks. `SessionObserver` advances the round count live and `reply_stamp` derives the same count on replay, so delta stamps and event stamps agree.
- `structured_output` on `Tool` defaults to `false`; `UserInputTool` opts in, and `agent_tool_set` binds an opted-in tool as `ToolOutputKind::Structured`, so its JSON output resumes into Lua as a table now - a later string-to-table change would break every calling program. The honor is for trusted output only: an untrusted result is nonce-wrapped before any parse, so it fails instead of smuggling attacker-shaped data.
- `run_agent_with_client` becomes public as the host-injection seam, and the supervisor always passes the workshop's own gateway client. A `launch` without a usable client is refused as `LaunchRefusal::GatewayUnusable`, so the environment fallback never fires.
- Cancellation is a stop reason, never an error: `cancel` fires the session's retained `CancelHandle`, pending waits die as durable `input_cancelled` frames, and the supervisor relaunches the program over the retained event log. `AgentSessions::close` ends a session for good and leaves its JSONL on disk.
- Sessions persist their event log at `state_dir/sessions/<session-id>.jsonl`; an `attach` replays it from index zero and re-announces every unresolved wait.
- A client-sent launch name resolves through the discovered list before any filesystem read, so path-shaped names are refused; session ids are 128 bits from the cryptographic RNG, and the upgrade refuses a foreign `Origin` with 403 as the chat socket does.
- The session tool catalog holds `user_input` alone; no other tool is configured.
- No consumer of the new socket ships beyond the end-to-end suite in `tests/it/agents.rs`; the UI is untouched.
The SPA receives the client half of the agent-session wire contract. `ui/src/services/protocol.ts` gains the agent-session frame family, and the new `agent-socket.ts` owns one `/agents/ws` socket: it launches, attaches, reconnects, and delivers typed events. The new shared fixture `tests/fixtures/agent-frames.json` is asserted as the same JSON by fixture tests in `src/protocol.rs` and by the new `ui/test/agent-wire-fixtures.mjs`, so wire drift on either side fails that side's tests.

- Both fixture suites pin the identical twelve-case list, so a case added on one side fails the other side's test.
- `module-ceilings.toml` raises the `src/protocol.rs` ceiling from `958` to `1157` to admit the fixture tests.
- `AgentSocket` drops replayed `agent_event` frames with a per-session cursor over `index`. The cursor resets when a different session is acknowledged, so a launch after a refused reattach delivers the new log's head; `test/agent-socket.mjs` pins both paths.
- A dropped socket reconnects with backoff that doubles from one second to a 30 second cap, then reattaches to the acknowledged session by itself. Disposal detaches `onclose` before closing, so teardown fires no disconnect and schedules no reconnect.
- `launch`, `attach`, `respond`, and `cancelTurn` return false when the socket is down; non-JSON and unknown frames are skipped without a throw. An `error` frame with an empty message falls back to `the agent session failed`.
- No module in this diff constructs `AgentSocket` outside its two test files; no renderer consumes the new frames here. This diff changes no built file under `ui/dist`; `manifest.json` records only a new `inputHash`.
- The backoff timer itself is untested; `test/agent-socket.mjs` calls `connect()` by hand in place of the timer.
The workshop UI gets a surface where the operator launches a discovered agent, watches the transcript, and answers the agent's input requests. The new `AgentSessionService` folds socket frames into transcript snapshots, the new views render the snapshots, and the rebuilt `dist/` bundle ships the result. The `Agent Session` entry of the Window menu opens the composed `AgentPanel` as the dockview panel type `agent`.

- `AgentSessionService` is DOM-free and consumes the `AgentSessionWire` interface; `AgentSocket` satisfies it structurally, and the tests script a fake wire.
- Deltas coalesce into pending transcript items keyed by the reply id, one item per channel; the durable event that carries the id replaces them. A `settled` watermark drops late chunks whose round has settled.
- `AgentSessionView` repaints by a prefix diff over item identity, so settled rows are never rebuilt or re-announced.
- The panel is modal: the first session acknowledgment hides the `AgentMenu` for good. A new session id resets the transcript, and every acknowledgment unpins the wait; the server resends unresolved waits right after.
- Every content string lands through `textContent`, never markup.
- The input enables only while a wait token is pinned, and a response leaves byte-exact, untrimmed. An Enter during IME composition does not submit, and a failed send keeps the pin and folds a local error item.
- A malformed `tool_call` batch degrades to its raw text, and unknown event kinds render nothing.
- `agent-panel.ts` has no test of its own; the new suites drive `AgentMenu`, `AgentSessionService`, and `AgentSessionView` separately, and `window-menu.mjs` covers the menu entry.
A fresh install gets a working chat with no agents directory, and a green gate proves the built-in agent replaces the direct-to-gateway chat relay. The embedded `agents/chat.lua` loops on `user_input`, rebuilds its message list from `runtime.events()` every turn, and `pcall`s `models.chat` with `ui().selected_model`. The agent VM now installs `ui()` from the host's provider, and session failures reach the SPA as id-less `error` frames.

- `BUILTIN_CHAT_SOURCE` embeds `agents/chat.lua` with `include_str!`; discovery always offers `chat`, and a directory `chat.lua` shadows the embedded source. Only a `NotFound` read of the `chat` name falls back to the built-in: an unreadable `chat.lua` surfaces its error instead of silently serving the embedded copy.
- Session errors ride a new bounded broadcast beside the deltas and never enter the event log; a lagged receiver loses only what the transcript already shows as a turn without a reply. Both `Observation::ModelTurnFailed` and the supervisor's terminal run failure feed it, and the socket's biased `select!` sends the `error` frame ahead of the wait that follows a failed round.
- `ui()` invokes the provider afresh on every call and converts the snapshot with `UI_SNAPSHOT_OPTIONS`, so a JSON null field reads as nil; with no provider the `ui` global is not installed at all.
- The six tests in `chat_gate.rs` drive the embedded agent over an SSE mock gateway and pin byte-exact multi-turn history, live text and reasoning deltas, next-turn model switch, restart from the persisted JSONL, mid-generation cancel, and error survival.
- The built-in agent advertises no tools and no system prompt, and the relay it replaces does not change here; the relay is removed only after this gate is green.
Chat runs through agent sessions on `/agents/ws`, so the direct gateway chat path and the session tape have no consumer left. The server drops `POST /chat`, the `/ws` chat multiplexing (`gateway_chat`), `tape.rs`, and `TapeConfig`, and keeps `GET /v1/models`, the menu events, and the boot snapshots. The SPA drops the murm-ui chat surface (`WorkshopProvider`, `AgentController`, `ChatPanel`, the voice plugin wiring in `main.ts`) and rebuilds `ui/dist/`.

- The state anchor moves to `config.server.state_dir`: the per-profile model memory and the boot orphan sweep read it directly, and `AppState::new` loses the `StateError::Tape` failure path. The gateway stops forwarding a tape section, and the docs mark `[workshop.tape]` accepted and ignored.
- The public surface shrinks: `Tape`, `TapeError`, `TapeEvent`, `TapeConfig`, `ChatStream`, and `ChatRequest` leave the `lib.rs` re-exports, and the `promptforge-gateway-protocol` and `time` dependencies leave `Cargo.toml`.
- The agent-session panel becomes the singleton chat surface: the `chat` panel type and `openAgentPanel` are deleted, and File > New Agent now opens or focuses the `agent` panel.
- `POST /chat` answers 404, and the session refuses an inbound `chat` frame on `/ws` with an error frame that names `select_model` and `switch_profile`; new tests pin both.
- `GatewayClient` keeps SSE decoding for profile switches and cache downloads, loses `chat_completion`, `chat_completion_stream`, and `GatewayError::Serialize`, and the stalled-gateway header-bound test retargets to `switch_profile`.
- The hunk in `crates/promptforge-stt/tests/common/mod.rs` also repairs a compile break that predates this change: an earlier config change added the `state_dir` and `agents` fields without updating this helper, so the crate's tests did not build; the repair is inseparable from this config-shape migration.
- The vendored murm-ui tree in `ui/src/chat/` stays: the README records it as a utility dependency (the workshop tree's dropdown and icons), and this change deletes only its chat-surface wiring.
- The deleted UI suites (chat streaming, abort, reasoning, voice takes) get no replacement here; the smoke test now pins workbench boot and exactly one `/ws` plus one `/agents/ws` socket, and voice capture keeps no jsdom coverage in this diff.
An earlier change converted the model client to always-streaming SSE but missed this crate's test harness. Its buffered JSON completion bodies now fail as malformed streams and flip a run's status to failed; the miss surfaced only when the full workspace suite ran. This commit fixes that miss: a new `sse_text_completion` helper renders each completion as an SSE stream, and the `spawn_text_gateway` and `spawn_gated_gateway` fixtures return it in place of buffered `Json` bodies.

- The rendered stream contains no usage chunk; the events carry only `delta` content and a stop `finish_reason`.
- No test assertions change; only the two gateway fixtures change their response format.
This fixes a pre-existing break from commit `d8ad9e9`, which proxied the config iframe same-origin through the workshop server while the bridge kept its origin pin on the gateway's own port, so the panel-mode handshake dropped and the panel hung on its pending banner. `setupGatewayConfigBridge` now accepts messages only from `window.location.origin` and pins every reply to the same origin. The gateway-origin probe and the panel's unused failure path are removed.

- The change removes the probe path end to end: `fetchGatewayOrigin` from `gateway-config-api.ts`, and the `/gateway/origin` fetch stubs from `gateway-config-bridge.mjs` and `helpers/boot.mjs`.
- `GatewayConfigPanel` loses its load-failure surface: the change removes `showError`, the `disposed` flag, and the `.gateway-config-panel__error` style. No error rendering replaces them; the panel only mounts the iframe.
- `PANEL_CONTEXT` now sends the iframe an initial route of `#/local` instead of `#/models`.
- The bridge suite's `dispatch` helper defaulted synthetic events to `GATEWAY_ORIGIN`, which matched the old pin and hid the break. It now defaults to `WORKSHOP_ORIGIN`, and a new check refuses a `pf-bridge-ready` sent from the gateway origin.
The chat surface that the vendored murm-ui tree served is gone, so the tree leaves whole. The two pieces the workshop tree panel still used become local components: `DropdownMenu` in `src/ui/workshop/dropdown.ts` with a colocated `dropdown.css`, and `ICON_FOLDER_PLUS` and `ICON_TRASH_2` in `src/ui/workshop/icons.ts`. `ui/src/chat/` is deleted, `marked` leaves `package.json`, and `dist/` is rebuilt.

- The MIT attribution for the ported code moves with it: `ui/src/chat/LICENSE` becomes `ui/THIRD_PARTY_NOTICES.md`, which names the murm-ui 0.2.0 source files and commit and carries the license text, because the tree's own license file leaves with the tree.
- `DropdownMenu` is an instance owned by `WorkshopTreePanel` and closed in its `dispose()`; the module-level open-menu state of the old `showDropdown` is gone. `DropdownItem` drops the `id` field; the port has no disabled items and no alignment or width options.
- The layer rule loses the `chat` layer: `LAYERS` in `check-layers.mjs` shrinks to `base`, `services`, `ui`; `build.mjs` drops the `chatDir` exemption; a re-grown `chat/` directory is in no layer and is flagged. `src/css.d.ts` declares `*.css` modules for the colocated-CSS import. The "vendored code is never edited" section leaves `ui/AGENTS.md`, and the murm-ui note leaves `README.md`.
- Menu class names change from `mur-dropdown-menu`, `mur-dropdown-item`, and `mur-danger` to `workshop-dropdown`, `workshop-dropdown__item`, and `workshop-dropdown__item--danger`; `test/workshop-panel-menu.mjs` queries the new names.
- New `test/workshop-dropdown.mjs` pins role and aria wiring, item activation, Escape with focus return, outside pointer close, same-trigger toggle, trigger swap, arrow and Home/End focus movement, and `dispose()`. It also asserts `.workshop-dropdown{` is present in `dist/app.css`; the test's header records that the vendored stylesheet had dropped out of the bundle.
- `test/markdown-blocks.mjs`, `test/thinking-block.mjs`, `test/tool-activity.mjs`, and `test/turn-footer.mjs` are deleted with the chat code they covered and have no replacement. `test/icons.mjs` now checks two exports instead of fourteen.
vinniefalco and others added 2 commits September 1, 2026 13:35
The composer that carried the microphone was removed, and voice dictation went with it; this restores dictation on the agent session input. `AgentSessionView` mounts a `Push to talk` button beside `Send` and runs `setupVoice` over the button and the textarea. New `agent-voice.mjs` and `agent-voice-boot.mjs` suites pin the gating, interim splice, cursor insert, readOnly take, and discard behaviors.

- `voice.ts` now depends on a `VoiceStatus` interface (`showLocal`, `setRecording`) instead of the `StatusBar` class. `PanelServices` types `statusBar` as `TreeStatusSink & VoiceStatus`, and `AgentPanel` falls back to a `SILENT_STATUS` sink when it is built without services.
- `voice.css` drops the `.mur-form-icon-btn`, `.mur-action-btn`, and `.mur-app-embedded` rules. The recording class on the input is renamed from `mur-chat-input--recording` to `voice-input--recording`.
- The mic stays enabled in every state. The `VoiceBlocker` refuses a click and names the reason on the status bar when the `voiceCapability` probe is in flight or failed, when `gpu` or `engine` is false, or when `pendingInputToken` is null. A click that beats the probe is refused, not let through.
- A take dies with its wait. `onDidChangePendingInput` with a null token and `submit` both call `discardIfRecording` first; the send carries the text showing in the box, interim included, and a late final writes nothing.
- `setupVoice` keeps a stopped take's socket in `pendingFinal` until the final lands, so a discard or a socket drop in the stop window closes the socket, lifts `readOnly`, and restores the pre-take text.
- `StatusBar` is not in the diff; the wiring relies on it satisfying `VoiceStatus` structurally. The stop deadline timer that closes a socket whose final never arrives has no test; the suites reach that path only by closing the socket directly.
close cppalliance#10

The Discover UI writes model size estimates in GiB with two
decimals, and the `u32` schema rejected every non-whole-GiB value.
Change the `LocalModelConfig` field `vram_gb` from `Option<u32>` to
`Option<f64>` and validate that a set value is finite and positive.

- The new type matches the `[[stt_model]]` estimate, so the
  co-residency total in `validate.rs` adds it without a `f64::from`
  conversion.
- Validation rejects a non-finite or non-positive value with the
  error `vram_gb must be finite and greater than zero`; a test
  covers `0.0`, `-1.0`, `nan`, and `inf`.
- Integer TOML input still parses: a test pins `vram_gb = 14` to
  `Some(14.0)`.
- The `vram_gb` field on the dominion table stays `u32`; only the
  local model estimate accepts fractions.
@vinniefalco
vinniefalco force-pushed the master branch 8 times, most recently from a0bc576 to 335bef2 Compare September 2, 2026 01:39
…aller releases

The repository is never dirty after a build, no developer machine needs the
CUDA toolkit or a GPU, and a fresh clone builds on macOS and Linux.

- Both web UIs build through the new shared ui-build crate: esbuild bundles
  ui/src into $OUT_DIR/ui-dist (minified in release) and rust-embed serves
  it from there (disk in debug, embedded in release). The checked-in
  ui/dist trees, the manifest verifier, and the --package path are gone;
  Node 22 plus one npm ci per ui/ folder is now a hard build requirement,
  and CI fails any build that writes into the repository.
- The CUDA llama-server is compiled on GitHub by the llama-cuda-build tool
  (the former promptforge-gateway-build, now a CLI with --source/--tag/
  --arch/--out/--no-smoke) whose zip ships the CUDA runtime DLLs, and
  downloaded at run time like the Vulkan and Metal archives. On Windows
  x86-64 the gateway picks the backend from nvidia-smi (compute capability
  12.x gets the PromptForge Blackwell build, other NVIDIA GPUs the upstream
  CUDA 13 build, anything else Vulkan), overridable with the new
  [local] llama_backend key; the executable resolves [local]
  llama_server_path, then PROMPTFORGE_LLAMA_SERVER, then the managed
  download, falling back to a cached install when a download fails. The
  llama-cuda feature, the embedded bundle, the build-time CMake compile,
  and the llama.cpp submodule are removed; workshop-cuda now means only the
  whisper CUDA backend.
- Releases: cargo-dist publishes the gateway for Linux x86_64 and aarch64
  on promptforge-gateway-v* tags (shell installer, checksums, sample
  systemd unit, gated by a clean-machine install-and-serve test);
  release-workshop.yml builds the NSIS, two DMG, deb, and AppImage
  installers on promptforge-workshop-v* tags and gates the release on
  per-platform install, --version, and served-page tests.
- A bare cargo build builds only the gateway (workspace default-members),
  rust-toolchain.toml pins the MSRV channel, the README documents per-OS
  setup, and CI builds the desktop app on Linux.
@vinniefalco
vinniefalco merged commit 2786ea3 into cppalliance:master Sep 2, 2026
11 of 12 checks passed
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