Skip to main content

"Harness" is a lens we use to audit the bot's post-prompt-dispatch machinery — not a module. If you're looking for the harness, read ai/profile, ai/capabilities, ai/loop, ai/policy, and ai/tools together.

What the term means here

In agent-tooling discourse, a harness is everything after the first prompt dispatch: tool execution, safety gating, context compaction, state persistence, and observability. Four families exist — CLI harnesses (Claude Code), framework harnesses (Vercel AI SDK, LangGraph), IDE harnesses (Cursor), and eval harnesses (inspect-ai). v10r is a product-embedded bot, not a CLI, so only the subset of harness patterns that pay back in an end-user product ships here.

We use the term as a diagnostic. Asking "does v10r have all the harness primitives?" surfaces gaps. Once the gap is named and fixed, the term retires — the code lives under concrete slice names, not harness/.

Primitives — who owns what

Primitive Owning slice File
Tool dispatch & schema-level scope filtering ai/profile + ai/capabilities profile/profile.tscomposeTurn mounts each active capability's tools(); a desk capability activates only when its scope is granted (capabilities/desk-scope.ts)
Tool metadata (surface-split) ai/tools tools/_types.ts — surface-neutral ToolRisk/ToolMeta (chatbot retrieval, no scope) + DeskToolMeta (adds scope); collections chatbotToolMeta / deskbotToolMeta / allToolMeta in tools/index.ts
Desk-mutation SSOT (one-door rule) ai/tools tools/desk-execute.tsexecuteDeskToolCall: the proposal-approval replay routes every desk mutation through it; index.test.ts drift-guards the replay map against the live tool set
Approval gate (risk → proposal) ai/policy + ai/tools policy/governor.tsrequiresApproval(risk) (write/destructive gated); write/destructive tools return a requiresApproval sentinel carrying the reviewed baseline (tools/proposed-target.ts) instead of mutating
Approval boundary (sentinels → one proposal, loop stop) ai/proposals proposals/approval-boundary.tscollectApprovalRequests, toCardSteps, stoppedAtApproval
Plan validation ai/proposals + ai/tools proposals/plan-validation.tsvalidateProposedPlan over tools/desk-mutation-inputs.ts (one valibot schema per mutation tool, also the model-facing JSON Schema)
Proposal execution (receipts, receipt message) ai/proposals proposals/execute-proposal.tsexecuteProposal, proposalOutcome; receipts in db/ai/proposals.ts (recordProposalStep, markInterruptedIfStale)
Step loop & provider fallback ai chat-orchestrator.tsstreamText + stopWhen; provider rotation & cooldown in _shared/streaming-turn.ts
Per-request scope step caps ai/profile profile/deskbot.tsstepBudget (read-only incl. desk:ask = 3, mutation = 5); profile/chatbot.ts = 3
Context compaction (fixes AI SDK #9631) ai/loop + ai/capabilities loop/compact.tscompactToolResults; capabilities/compaction.tswrapToolsWithCompaction + the resolve_ref tool
System prompt assembly ai/profile profile/profile.tscomposeTurn: identity → guidance → stable grounding ‖ awareness → dynamic grounding → guides (cache order); see profiles.md
Retrieval integration ai/capabilities the chatbot's grounding lanes — project-docs.ts, project-map.ts, navigation.ts — run in parallel under composeTurn's shared query embedding
Conversation windowing ai/context context/history.tswindowMessages
Plan-gating predicate ai/capabilities capabilities/desk-plan.tsshouldRequirePlan decides the <planning> guide
Proposal state machine + step receipts db/ai + ai/policy db/schema/ai/proposal.ts, db/schema/ai/proposal-step.ts, db/ai/proposals.ts
Audit log (scaffolded stub) db/ai db/schema/ai/audit-log.ts

What ships as load-bearing vs. scaffold

Load-bearing (exercised on every request): tool dispatch, step loop, provider fallback, compaction, system-prompt assembly, proposals table, agent_proposal + agent_proposal_step row writes.

Scaffolded stub (seam visible, one write site, no query UI): agent_audit_log — retention policy is a product decision v10r should not make for adopters.

What we explicitly don't ship

  • SKILL.md files — developer-CLI pattern, category error for a product bot.
  • Visible subagent delegation — dev-tool concept, invisible in Linear/Notion.
  • Generator-evaluator auto-review — same-model evaluation "confidently praises regardless of quality" (Anthropic).
  • Full Mastra adoption — framework weight without matching payoff at this scope.
  • Per-tool needsApproval: true — approval fatigue is reproducible; risk-tiered gates are the working pattern.
  • A harness/ module — the seam is emergent across loop/context/policy/tools; naming it adds no value.

Approval gate + plan-before-execute

Two distinct mechanisms govern deskbot mutations.

The hard gate is at the tool layer. A deskbot write or destructive tool (desk_update_cells, desk_rename_file, desk_update_markdown, desk_delete_file) never mutates in the agent loop — its execute validates the target and returns a requiresApproval sentinel. requiresApproval(risk) in policy/governor.ts is the single rule: write/destructive gated, read/create not (creates are reversible via soft delete, so they mutate in-loop, auto-approved). The orchestrator turns the sentinel into a pending agent_proposal (PlanCard); the mutation runs only via the approve-route replay through executeDeskToolCall (the one-door SSOT), which records a real approvedBy/approvedAt. Even a single-target overwrite or delete is gated — the old self-serve confirmed: boolean two-phase handshake (which the model could satisfy itself) is gone.

Planning is soft guidance, not the gate. shouldRequirePlan({ mutatingScopeGranted, destructiveIntent }) (the desk-plan capability's rule) decides only whether to instruct the model to plan first — inject the <planning> guide so it batches work into one desk_propose_plan. It was widened from the old three-condition AND (≥2 tools + ≥2 targets), which let every single-target destructive op skip planning; now any granted-mutating-scope + destructive-intent turn gets the nudge. It no longer decides whether a mutation may run — the requiresApproval sentinel does.

The execute path closes the loop. Each desk_propose_plan step carries its exact args and its reviewed baseline (target: the file's version / updatedAt at proposal time), persisted on the proposal payload after validateProposedPlan accepted the plan; on approval executeProposal replays them step-by-step through executeDeskToolCall, writing a step receipt (agent_proposal_step) in each step's transaction, refusing a step whose file moved on since review as a conflict, and stopping at the first non-ok step with the earlier steps kept (no rollback). No model turn follows: the door persists a deterministic execution receipt message the model reads as history. Approval binds execution.

Overwrites and deletes are recoverable: db/desk snapshots a pre-image desk.file_revision before mutating (capture only — no restore UI yet).

Risk tiers — UI mapping

Scope UI treatment
desk:read Silent auto; I/O log only
desk:create Auto with notification; bot-originated writes inherit this tier
desk:write on an existing file Pending proposal → PlanCard; approve to run (pre-image desk.file_revision backs recovery)
desk:delete Pending proposal → PlanCard with target name; soft delete (trash, desk-trash retention window) + desk.file_revision pre-image — the card says so, never "permanent"
Multi-step destructive batch One PlanCard covering all steps — one read, one approval

Reading order for the curious

  1. tools/_types.ts — the risk vocabulary
  2. profile/profile.ts + profile/deskbot.ts — what a turn is composed from: scope-gated capabilities, the step budget (load-bearing seam)
  3. chat-orchestrator.ts — the step loop; _shared/streaming-turn.ts — provider rotation
  4. loop/compact.ts + capabilities/compaction.ts — the #9631 workaround
  5. policy/governor.ts — the approval gate (requiresApproval); capabilities/desk-plan.ts — the plan-gating predicate (shouldRequirePlan)
  6. db/schema/ai/proposal.ts — the proposal state machine
← Back to Blueprint

Think this pattern could be better? Tell us how.

Leave feedback