018 · Workspace
Workspace and the UI-agnostic application layer (spec_workspace_018)
| Status | Implemented. Reverse-engineered from 53f8c53 (2026-09-24) and kept with the code since; its paths and names checked 2026-09-27 (//tools/specs). |
| Source | pkg/engine/*.go |
| Tests | pkg/engine/*_test.go (notably multi_test.go, turn_test.go, workspace_test.go) |
| Related | spec_engine_016, spec_sessions_017, spec_client_022, spec_service_021 |
pkg/engine is Blitz without a user interface. engine.Open builds a Workspace (agents, skills, tools, sessions, audit, model, engine) for one project directory, and the workspace exposes typed operations that return data and typed errors and never print. The terminal UI, the service and the desktop app all drive the same operations through the api.Backend interface, so a front end behaves identically whether the workspace is in-process or held by the service.
- WS-01
Open(ctx, cfg, Options)resolvescfg.Tools.WorkspaceDir(expand~, absolute) and writes the absolute path back intocfg. Everything relative resolves against the workspace, never the process CWD: one process may hold several workspaces. - WS-02 The workspace owns
cfgafterOpenand mutates it (model, pins, settings), so every workspace needs its own copy. - WS-03 One owner per workspace across processes: an exclusive, non-blocking OS file lock (
flockon Unix) on~/.blitz/locks/<hex(sha256(realpath))[:24]>.lock. The lock file records the directory and PID for humans. A held lock yieldsErrWorkspaceBusy. The lock is released onCloseor any failure duringOpen, and by the OS if the process dies. - WS-04 Build order: locale → agent registry (+ external agents from trusted paths; errors warn) → skills provider (+ discovery when enabled; policy problems warn) → tool registry → image store prune (
retain_days) → audit log (failure warns and disables) → session storage scoped to the workspace → persistent ADK event store → model → project memory → per-agent models → engine. - WS-05 If the configured model cannot be built, the workspace still opens, on a stand-in (
runtime.NewUnavailableModel) named after the configured model whose every call fails withErrModelUnavailableand the reason (ModelErrorSummary, secrets removed), so turns and worker runs fail with it instead of being answered;ModelErr()reports why. The same stand-in replaces the model when a settings change leaves it unbuildable (not the previous model, which the settings no longer describe), untilRetryModelbuilds it. - WS-06 Per-agent models: an agent’s frontmatter
default_modelapplies, and an[agent_models]pin wins over it. Pins naming unknown agents warn and are skipped; pins whose model fails to build warn and are skipped. - WS-07
Options:Streaming(partial text),Warn(non-fatal problems),Model(test override),Workers(shared worker store),NewModel(model factory override). - WS-08 Extra system instructions = rendered project memory + (for non-English reply locales) a reply-language instruction.
- WS-09
Closekills background processes and MCP servers, drains post-tool hooks, flushes the audit log and releases the lock.
Front ends use only api.Backend (implemented by *Workspace and by the service client). Groups:
| Group | Operations |
|---|---|
| Lifecycle / UI | Dir, ModelErr, SetUI(approver, prompter), Processes (nil when remote), AuditShell, ImagesEnabled, Close |
| Turns | Run, Steer |
| Sessions | OpenSession, ActiveSession, ListSessions(all), NewSession, LoadSession, SaveSnapshot, RenameSession |
| Agents & models | ListAgents, ActiveAgent, SetAgent, Model, SetModel, PinModel, Unpin, AllModelSettings, ModelSettings, UpdateModelSettings, Settings, Set |
| Extensions | ListSkills, SearchSkills, Skill, ListEnvs, RemoveEnv, PruneEnvs, ListMCPServers, ActiveAgentTools |
| Context | SessionUsage, Context, Compact, MemoryFiles, ReloadMemory, Reload, AddMemory, AvailableLocales, SetLocale, SandboxSummary |
| Changes | ListCheckpoints, Undo, SessionDiff, GitDiff, ListApprovals, RevokeApprovals, ClearApprovals |
| Images & search | LoadImage, AddImage, LoadAttachments, SearchProvider, SearchWeb, SearchSession |
Turn fields: Text, Prompt (sent instead of Text), Plan, ReadOnly (mode name), Aside (/btw), Accepted, Images, MaxTurns, MaxCostUSD, Timeout, FetchGrants, OnAccepted, OnFinished.
-
WS-29 Limits:
MaxTurns(model calls;ErrMaxTurns),MaxCostUSD(checked after every event against the session’s cost since the turn began; needs a priced model;ErrCostLimit, message “the turn reached its cost limit ($x)”),Timeout(ErrTimeLimit). The cost and time limits cancel the turn’s context with the limit as the cause, and the turn returns that cause.IsLimit(err)matches all three; workers use the same fields for theirlimits. -
WS-20 Unless
Accepted, the text passesprompt_submithooks first; a refusal returns*BlockedError{Reason}with nothing recorded or sent. Accepted prompts are audited (prompt). -
WS-21 The sent prompt is
Promptif set, elseText. WithPlan, the prompt is wrapped byruntime.PlanPromptand the transcript records/plan <Text>. -
WS-21a Mentions: every
@pathin the text as typed (IMG-01’s grammar,images.MentionedPaths) that names a file or folder the agent may read (resolved through the workspace: inside a root, not blocked; an image is the front ends’, IMG-01) adds a block to the prompt sent, after the hook contexts, in<mentioned-files>: a file as<file path="…">with its text (at most 64 KiB, else its start cut at a line and a note to read the rest withread_file; a binary file as a note of its size), a folder as<folder path="…">with its entries (folders with a slash, hidden and blocked ones left out, at most 200). At most 20 mentions and 256 KiB a prompt; paths that aren’t there are left as written. The transcript records the text as typed. -
WS-22 For non-aside turns: a checkpoint begins, labelled with the recorded text collapsed to single spaces and ellipsized to 60 chars; unless
Accepted, the user message is recorded in the transcript with an attachment note\n[images: a.png, b.png]. -
WS-23
OnAcceptedruns after recording and before sending, so steer messages taken from then on follow the prompt in the transcript. -
WS-24
FetchGrantspre-approve exactly those URLs for this turn (tools.WithFetchGrants). -
WS-25 Aside turns go to
Engine.Aside(no images, no checkpoint, no transcript, steer queue untouched). Other turns go toEngine.ExecutewithWithMaxTurns, one attachment per image,WithPlanOnlyorWithReadOnly(mode). -
WS-26 After the engine stops:
OnFinishedruns; usageBefore/Afterare captured; the concatenated final (non-partial, non-thought) model text isOutputand is recorded as the model message if non-empty; unread steer messages are returned inLeftover. A turn that fails part-way still returns what it produced. -
WS-27
Steer(text)runsprompt_submithooks (refusal →*BlockedError), records the text as a user message, and queues it on the engine. -
WS-28 Transcript write failures warn but never fail the turn. Messages are written to the turn’s session by ID (SES-16). A failed write is logged and, during a turn, sent to the client as a
Noticeevent (error): the chat won’t show that message when reopened, and the user knows.
- WS-30 Each ADK event becomes one
api.Eventper part, in order, withAuthor; exactly one ofText{Text, Partial, Repeat, Thought},ToolCall{ID, Name, Args, Partial},ToolResult{ID, Name, Result}. - WS-31 With streaming, partial text chunks arrive first; the final text event then repeats them with
Repeat=true. Front ends show partial chunks and non-Repeatfinal text, so text appears once.streamedresets after each non-partial event.
- WS-40
SetModel(ref)builds the model (may switch provider), swaps it for all unpinned agents, setsblitz.default_model, and returns the active agent’s pin if any (it still decides what the active agent runs on). Not persisted to the config file. - WS-41
PinModel(agent, ref)requires a known agent (*UnknownAgentError), builds the model, pins it, and saves[agent_models]in the workspace’s own settings file (never the global one), so pins are per workspace and win over global ones.Unpinremoves the pin, restores the agent’s owndefault_modelif declared, and removes the workspace’s line, or, when the global settings pin the agent, savesagent = ""there to mask it. An empty pin means “not pinned”. Results carrySaved{Path, Err}. - WS-42
ModelSettings(ref)rejects refs containing=or with no name (ErrBadModelRef); returns settings plus global temperature/max_tokens.UpdateModelSettings(ref, reset, changes)validates (*InvalidSettingError), applies from the next model call, lists keys the provider ignores asUnsupported, and saves under the key the file already uses for that model (e.g. a hand-writtenopenai/gpt-5) else the bare name. - WS-43
Set(key, value)supports onlyagency/agency_level(low|medium|high|extreme, elseErrInvalidAgency; unknown keys*UnknownSettingError); agents’ instructions embed agency, so the engine rebuilds. Session-only. - WS-44
Settings()returns agency, model, active agent, the reply locale and the permission mode.SetPermissionMode(mode)changes the workspace’s mode (ErrUnknownMode,ErrBypassNeedsSandbox); in plan modeRunplans every prompt (spec_approvals_005 APR-14). - WS-45 Plans for approval: each non-aside turn gets a
tools.PlanGate, planning when the turn isPlan, the session is in plan mode, or[blitz] plan_review = always(every prompt that isn’t read-only or an aside). When the planning run ends with an approved plan, the workspace leaves plan mode for the chosen mode (outside plan mode onlyaccept-editschanges it), records(plan approved) <go-ahead>(kindplan, not a rewind point) and runsCarryOutPromptin the same turn without plan-only — at most 3 approved plans per prompt; stop hooks then apply to the carried-out run. The output of successive runs in a turn is separated by a blank line, in the result and as a text event. Commands withmode: planrun read-only (ReadOnly = "/<name>") with their own prompt, not as plans.
- WS-50
Context()returns the latest prompt size (Usage.LastPrompt), whether auto-compaction is active (compaction && token_threshold > 0), the threshold and retained events. - WS-51
Compact(focus)summarises all but the latest user turn (keep = 1) and returns events compacted, summary length and usage before/after;ErrNothingToCompactwhen too short. - WS-52
ReloadMemoryre-reads memory files and rebuilds instructions;AddMemory(text)appends to the last configured memory file (defaultBLITZ.md) in the workspace and reloads. - WS-53
SetLocale(input)resolves a tag or language name (ErrUnknownLocale), changes the reply language for this workspace, rebuilds instructions, and saves[ui] locale. The interface language belongs to the client, which switches its own catalog with the returned tag. - WS-54
SearchWeb(terms)requires fetching enabled (ErrNoFetch) and a provider (ErrNoSearch); requests 10 results and keeps up to 5 viable links: http(s) with host, not a binary/document extension (pdf, archives, images, media, office), not an unresolved/grounding-api-redirect/, deduplicated by host+request URI (fragment ignored). Returns the links and a prompt (runtime.WebSearchPrompt); the caller runs it as a read-only turn withFetchGrants= the links. - WS-55
SearchSession(terms)searches the active transcript (up to 12 passages) and returns the match count and a prompt (runtime.SessionSearchPrompt).
- WS-60
ListCheckpoints(newest first),Undo(force)(audits restored files asundo; partial restores return both restored files and the error),SessionDiff()(unified diff of every file changed this session vs. its pre-change state). - WS-61
GitDiff(color)runsgit diff --no-ext-diff --no-textconv --stat --patch HEAD(before the first commit, against the empty tree), so staged and unstaged changes both show, then each untracked file as a new file (git diff --no-index /dev/null <file>, at most 50, then a count; 2026-10-04: it was plaingit diff, which hid staged changes and new files), withcolor.ui,core.fsmonitor=false, and every configuredfilter.*.{clean,smudge,process}blanked (requiredset false), because the agent can write.git/configand git would otherwise run commands from it outside the sandbox. - WS-62
ListApprovalsreturns session rules then saved rules, parsed from keyskind:subject(cmd:<dir>\x00<command>,write:<dir>,delete:<dir>,web:<host>,mcp:<server>:<tool>,uc-run:…).RevokeApprovals(keys…)removes from both;ClearApprovalsremoves all. 2026-10-03: both return the count and an error: saved approvals that couldn’t be removed from the file areErrApprovalsStillSaved, naming them (revoked for this session, back at the next start); over the service,RevokeApprovalsResponse.error(APPROVALS_STILL_SAVED), shown in red by/approvalsand the desktop.
- WS-70
SkillInfocombines a skill’s metadata with the policy evaluation (hash, tier, bypass, network, env passed/withheld, blocked reasons, per-script verdicts). See spec_skills_013. - WS-71
PruneEnvsremoves script environments that are incomplete or not needed by any script the policy lets run;ErrScriptsDisabledwhen skills are off. - WS-72
ListMCPServersreturns nothing if no server could be started; otherwise name, target (URL or command line), auto-approve. - WS-73
ActiveAgentToolslists the active agent’s tools sorted by name withPlanAllowed, plus MCP servers offered to it (noagentslist = primary only;*= all).
ErrWorkspaceBusy, ErrNoActiveSession, ErrSnapshotNameTaken, *ResumeError, *BlockedError, *UnknownAgentError, ErrBadModelRef, *InvalidSettingError, *UnknownSettingError, ErrInvalidAgency, ErrNothingToCompact, ErrUnknownLocale, ErrImagesDisabled, ErrNoFetch, ErrNoSearch, ErrUndoConflict, ErrScriptsDisabled, worker errors in spec_workers_023.
- WS-80
ModelErrorSummarytruncates SDK errors atClientConfig:and at the first newline and masks secrets, because some SDK errors embed the API key. - WS-81
SecretRedactormasks configured API keys, the web search key, every MCPenvvalue and the values of scrubbed environment variables in audit, logs and telemetry.