Skip to content

Session, agent, and job

Source: harness/docs/runtime/session-job.md
Status: current

Parents: ../canon/grain.md, control-effects.md, approval.md, ../client/transcript.md, ../packages/plexus-expectations/README.md. Not this file: license pipeline (approval.md); TUI paint (../client/transcript.md); grain spine (../canon/grain.md); compact fold ABI (control-effects.md). Do not rename Actor to Agent. Do not mint Jobs from the client.

History is head and each turn’s tail. Agent is a snapshotted spec plus that pointer. Job is a WorkContainer, not a WorkExpectation. Background is a flag on the node, not a type and not a second owning list. POLICY FOLLOWS HEAD. Leaves license; sequences project. Completions and judges stay PEW.


History is head and each turn’s tail; Agent is a snapshotted spec plus that pointer; Job is the orchestration graph (a Turn is a Job; a tool call is a host ToolCallJob; tool BODY is the leaf executor after license; completions stay PEW); background is a flag on the node, not a type and not a second lane.


Speech Meaning
SessionRoot The session plexus document. Workspace path, operator overlay, chrome. Not a message bag.
AgentSpec Reusable recipe. Identity of a kind of conversational worker. Snapshot onto the Agent at mint. Not cwd, not frozen tools, not cost.
Agent This instance: spec snapshot + head pointer, optional parent, optional nickname. Thin.
Head Live list tip (Agent.head; SessionRoot.head is an alias, not a second synced pointer). Attribution walks tail. findRunningTurn / liveTurns start here.
Tail “I continue X”, not parent. SessionTurn.tail is the previous SessionUnit (genesis SessionSystemPrompt). CompletionJob.tail / TurnCompactChunk.tail is the previous tape cell or the turn; remainder continues the tool. Isolated intern sits under the turn with no tail.
WorkContainer Work-graph class. Brand is on the class — do not duck-type .work. walkWork is a preorder of .work.
Job Orchestration graph node: class Job extends WorkContainer. children, background, walkBoundary, orchestrationResult, computed orchestrated, cancelled, completed. Collectors live here. Not a PEW kind. Package has only this class.
walkBoundary Contour flag on Job. false (default): collectors expand this node’s children. true: collectors yield the node and stop. walkWork still descends. SessionTurn stays false.
Collectors jobs / foregroundJobs / backgroundJobs on Job. Expand until walkBoundary. Split by the yielded node’s own background flag. Skip non-Job children. COLLECTORS ARE NOT .work.
Approval cursor approvalQueue / approvalQueueHead on sequence Jobs. Coverable Approvable leaves (needsHuman). Not collectors. Not .work. Law: approval.md.
background Job flag. This child is on the parent Job’s background plane. Not delivery. Not a second list. ReACT join releases at completed || (orchestrated && background).
SequenceJob Host: SessionTurn, ProgrammaticJob. Cursor over leaves. Not Approvable.
ProgrammaticJob Host sequence. Scheme bag (interpretSchemeRepl). Inner admits are nested ToolCallJobs. Guard returns true; inner leaves still license.
PEW World-settling obligation (../canon/grain.md). Completions, judges, licensed MCP tools/call, licensed web_search / web_extract. Not a Job kind. PEW does not parent PEW except a judge child of an ApprovableJob.
Actor One handler per PEW/Work. This Agent is the conversational worker, not a PEW Actor.
Honor Orchestrator autorun over leftover attach PEW. New completions / MCP / web dispatch from origin (complete / Job BODY).
Executor registerExecutor(Class, guard, execute). Guard writes orchestrationResult. Law: approval.md.

There is no JobHub. There is no JobNode brand. There is no TurnJob class. There is no InterpretationExpectation. There is no delivery field on Job. There is no WorkDemand. Background is the Job flag, not a second conversation lane.


Cohort harnesses store a message list on a Session object. Once the list is gone, that bag is three jobs:

Host Holds
AgentSpec Recipe: name, description, constitution, nature (session | project | global; empty until mint / foreground), keepAlive, voidLegal. Snapshot at mint. Upgrade is a new snapshot + emit, not a silent rewrite of sealed genesis. AskAgentSpec is one-shot (omitted-spec ask, or agent/one-shot/* stamped with a recipe) — class, not a catalog recipe, not a nature.
Agent spec + head + optional parent + optional nickname. Test for any field tempted onto Agent: can it be spec default, session overlay, or a walk from head? If yes, it does not belong on Agent.
SessionRoot Document: sessionId, path, kvs, mode (act/plan posture), standingPolicy, actions, queues, allow/block lists, readAllowlist, agent.

SessionRoot.head is an alias for agent.head. findRunningTurn / liveTurns start there. Head writes are the turn, compact, and eval RPCs. The document owns the verb; the agent owns the pointer.

Sorting test (resume / fork / spawn), before adding a field:

Test Host
Resume of this conversation must restore it SessionRoot if it survives minting a new head; head if it is true of this round (inherit via tail)
Reuse it to start a different conversation of the same kind AgentSpec
Every new conversation re-reads it from disk config / project
It only exists while the loop is alive runtime

Dual-homes model / cwd / mode were dual because those three jobs shared a spelling on one struct.

Name Spec Session overlay Head / walk
model Recipe default kvs.model Completion receipt
cwd OUT SessionRoot.path / env overlay env packet on the turn derivation
mode Persona if that exists act/plan operator posture —
permissions / tools Ceiling — RegisteredToolsSet frozen for the round; currentPolicy turn overlay; asks on ApprovableJob
constitution IN — snapshot on first tail

Agent.model, Agent.tools, Agent.cwd are traps. Do not add them.

Isolation default is own genesis, fail-closed. A child Agent gets a snapshotted spec plus its own env overlay. It does not inherit parent conversation prefix or tools unless the spawn says so.


History is Agent.head and each turn’s tail. Genesis is SessionSystemPrompt (snapshotted constitution + env overlay). Next turn = new SessionTurn linked as tail + advance head. Attribution is the live head, then the tail walk. Tape cells (CompletionJob, TurnCompactChunk) carry the same pointer: they continue the previous cell or the turn, not their parent. There is no tape as a store. doneWork + queue are the ReACT tape (refs); owning list is Job.children. Paint iterates that tape, never collectors (../client/transcript.md).

Control chunks on the queue (TurnPromptChunk, TurnCompactChunk, ReACTChunk, ProgrammaticJob) are WorkContainers, not PEW. Compact fold: control-effects.md.

TEXT-ONLY CONTINUE. After a successful completion with no work tools, if other loop-end guards do not already continue (open page, callbacks, steers, titled-only, pending notes), config.json textOnlyContinue runs intern A (on when the bag is omitted; enabled: false turns it off): isolated json_schema on spoken responseText only (inline thinking is stripped) (intent, awaiting_user), judge wire (judgeConnection / judgeModel, no actor fallback), one shot, fail-seal. Continue iff intent && !awaiting_user. Ask stamps SessionTurn.questionLine (glance unfinished question, not license ask). Two consecutive intern nudges, then stop; work tools reset the budget, and later text-only rounds are classified again. Actor overlay is connection:model (first colon). Isolated intern is a turn child, not ReACT tape. Thought without an answer is a structural continue. Empty speech plus a thinking channel — a responseThought, a <think> block that is the whole message, a thinking content part, encrypted reasoning, billed reasoning tokens, or a stamped thought kind with no visible CoT — has not finished. It continues with no judge, ignoring textOnlyContinue, earlier work tools, and the intern streak. The nudge is the thought-continue note. Eight consecutive thought-only rounds, then stop.

SessionTurn.closed is reducer-exited. Open background children do not block the turn.


currentPolicy is a turn overlay of the tri-state (manual | auto | yolo), same shape as toolsDefinition: attributed on the turn that changed it; null inherits tail; first turn attributes from standingPolicy. Unknown / empty stored strings coerce to manual. Effective policy is a computed inherit (SessionTurn.effectivePolicy). SessionRoot.currentPolicy displays that inherit; assignment writes standing and, when a head exists, the overlay.

A new session copies last-choice defaultPolicy once at mint (sessionMintPolicy: project wins when set, else kernel, else manual). Kernel persist is config.json defaultPolicy, not kv.json. Operator policy/set (Shift-Tab) write-throughs session + project + kernel. A local standing write does not. Attach does not re-seed.

The license pipeline reads SessionRoot.currentPolicy (approval.md). Head repoint restores that round’s policy. evalSpec is unattended admission (auto | yolo); observer overlay must not replace it. Unattended does not write last-choice. The policy program is {kernel}/policy.yaml (SessionRoot.policyProgram), not this field.


Package model: ../packages/plexus-expectations/src/shared/models/Job.ts. Host kinds live in harness-sdk.

  • Job is the orchestration graph node. A Turn is a Job. A workflow bag is a Job. Both spawn, both resolve, both can be moved off the parent’s await set.
  • A tool call is a host ToolCallJob (is-a ApprovableJob). Emission identity and votes on one object. After an allow-side guard, SystemToolJob (including FileMutateToolJob) runs as execute; MCPToolJob / web-search / web-extract BODY intern’s and calls interned methods. Outcome lives on ToolCallJob.outcomeJson. Completions remain PEW on Job.children without being Jobs.
  • Background is a flag, not a Job type. “Moved to background” is executor behavior: the row stays in the parent’s children; the parent ReACT no longer joins it (joinReleased: completed || (orchestrated && background); AgentCallJob may also wait durable acceptance). The row does not leave the turn.
  • walkBoundary is the contour. Nested background is a child Job with the flag, not a second session plane. Recursing collectors through a walkBoundary is session-global collectors under a new name.
  • Normalized form is a Scheme program that can spawn tool calls, completions, turns, agent runs. Host ProgrammaticJob is that face. A native FS observe is a SystemToolJob; write_file / edit_file are FileMutateToolJob. Inner tools parent onto .children via pushInvocation.
Parent Turn extends Job
child Job (still in children)
background = false → foregroundJobs → parent ReACT waits
background = true → backgroundJobs → parent ReACT proceeds once orchestrated
jobs = both
honor / watch walk .work (raw children), not collectors

ProgrammaticJob execute is interpretSchemeRepl. SessionTurn seals via closed.

COLLECTORS ARE NOT .work. Job.work is raw children. PEW leaves keep WorkExpectation.work = [this]. Honor (walkWork) picks instanceof WorkExpectation. If paint uses jobs by accident, background floods the tape. If honor walked only foregroundJobs, a background PEW never gets an Actor.

Document .work is the head, inbound predecessor turns still in the inbound chain, other unclosed liveTurns, plus leftover background ToolCallJob / ProgrammaticJob on closed turns. The Turn node is a direct item so registerExecutor(SessionTurn) still claims it. Tape is not work.

Relative, not global:

  1. Child contour detaches its Job → parent foregroundJobs still contains the child Job, not the inner; parent backgroundJobs does not gain the inner.
  2. Parent detaches the child Job → parent foregroundJobs drops the child and the child’s descendants; child foregroundJobs is unchanged.
  3. Attached child contour with an open inner Job ⇒ parent join stays open; detach the child ⇒ parent proceeds even if the inner is still open.

Flag is on the node. Plexus child.list is single-homed. background is not a per-edge annotation.


PEW is runner concern: ambient driver-driven resolution of expectations. It does not dictate the orchestration graph shape.

Row What Seal Collectors
Sequence Job SessionTurn, ProgrammaticJob closed on the turn; execute finally on the bag Yes. Also approvalQueue / approvalQueueHead
Licensed leaf ToolCallJob kinds (ApprovableJob) Allow-side execute; deny-side receipt + completed; cancel: cancelled + receipt + completed Yes for the Job. Judges are PEW (instanceof Job is false)
PEW CompletionExpectation, leftover intern McpToolExpectation / WebSearchExpectation / WebExtractExpectation, remainder classifier child Actor seals leftover PEW; new MCP/web seal on the Job No

Do not collapse PEW into Job. Do not make every Job a WorkExpectation. WorkExpectation does not extend Job. A licensed tool leaf does not mint tool PEW as identity — BODY is execute after an allow-side guard; MCP/web BODY calls interned methods. Never run a PEW leaf through an executor.

Subagent / background tool / workflow are tenants of work hosted on the producing turn, visibility by ancestry. Watch names hosted children (../client/transcript.md). Do not make a tool-in-background an Agent with an empty spec.


../packages/plexus-expectations/src/executor/orchestrator.ts:

  • Honor. Autorun over walkWork(session.root). Skip done, tree-orphan, unknown connection. No approve hook. Honor sees completions and judges — not tool Jobs. Unstarted MCP must not spin — deny and ask are not execute.
  • Executors. registerExecutor(Class, guard, execute) autoruns reachable unresolved Jobs of that class. Sequence guards typically return true (SessionTurn waits inbound predecessors, then true — never false, which would deny BODY). An Approvable guard is LICENSE WHEN. ProgrammaticJob execute is ../packages/harness-server/src/scheme-repl/interpret.ts: bindings mint inner ToolCallJobs; the trampoline joins those Jobs (Promises, not await in Scheme). ReACT join is queue.shift plus waitCompleted → joinReleased. Nested contour is reachable as Job.children and must run; liveTurns is tail-ancestry for chrome, not the executor gate.

../packages/harness-server/src/daemon/session/SessionOrchestrator.ts registers SessionTurn and the host leaf kinds. Interpret while the turn is in walkWork; abort when it leaves. Programs never await (control-effects.md).

Dormancy = origin unreachable from live head. Not a stored status. A job produced on an abandoned branch is invisible to the live branch.


Alternative Failure
Session-as-bag Dual-homes model/cwd/mode. Rollback of head does not restore round-attributed posture.
currentPolicy session-standing as SSoT A later head-repoint keeps a later policy on an earlier round.
Last-choice permission mode as a kv key sessionMintKvs would copy a string bag; kv-clear could reveal a parent. defaultPolicy is the sibling last-choice (config.json / project CRDT).
WorkDemand as PEW / a second conversation lane Deleted. ReACT background is the Job background flag.
Package job subclasses ONLY Job in plexus-expectations. Host kinds in harness.
License on PEW / ApprovableExpectation approval.md.
Fat Agent Session-as-bag renamed.
Tape / message list on SessionRoot Grain forbids journal as product substrate.
Tool-in-background as Agent with empty spec Homonym. No recipe, no spine.
Job.work = jobs or foregroundJobs Collectors and PEW share a list, or background PEW never honors.
Await in program bodies control-effects.md.
Paint / compact using collector jobs Background floods the tape.
Parallel synced SessionRoot.head and Agent.head Two tips. The accessor alias is the law.
Collectors recurse through walkBoundary Nested background collapses to one plane.
Client mint of Jobs / PEW Grain: daemon writes bodies.
Rename Actor → Agent Grain Actor is the PEW handler.

Doc Role
../canon/grain.md Causal spine, PEW nouns
control-effects.md Flat programs; compact fold
approval.md LICENSE WHEN; ToolCallJob is-a ApprovableJob
../client/transcript.md Watch / tape paint
../packages/plexus-expectations/src/shared/models/Job.ts Graph geometry
../packages/harness-sdk/src/models/session/SessionRoot.ts Document
../packages/harness-sdk/src/models/session/Agent.ts Thin agent
../packages/harness-sdk/src/models/session/turn/session-turn.ts Turn is a Job
../packages/harness-server/src/daemon/session/SessionOrchestrator.ts Honor + executors
../packages/harness-server/src/scheme-repl/interpret.ts Scheme bag BODY