Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
311 lines
17 KiB
Markdown
311 lines
17 KiB
Markdown
# Agent Daemon
|
|
|
|
`packages/agent/daemon` provides the reusable daemon-side agent runtime kit. Host
|
|
daemons use it to run agent sessions and emit agent activity while keeping their
|
|
own HTTP API, persistence, workspace/runtime lifecycle, and product integration.
|
|
|
|
## Minimal Host Wiring
|
|
|
|
```go
|
|
runtime, err := agentdaemon.NewRuntime(agentdaemon.Config{
|
|
Reporter: activityReporter,
|
|
ProcessTransport: agentdaemon.NewLocalProcessTransport(),
|
|
HostMetadata: agentdaemon.HostMetadata{
|
|
ClientInfo: agentdaemon.ClientInfo{
|
|
Name: "my-desktop",
|
|
Title: "My Desktop",
|
|
Version: "1.0.0",
|
|
},
|
|
WorkspaceEnvName: "MY_WORKSPACE_ID",
|
|
OpenClawSessionKeyPrefix: "agent:main:my-desktop-",
|
|
},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
controller := runtime.Controller()
|
|
```
|
|
|
|
Controller startup coordination is scoped to one Session. The legacy `Start`
|
|
form that omits `AgentSessionID` is scoped by room and provider while it
|
|
allocates and deduplicates the Session ID. Provider-specific credential
|
|
coordination belongs in the Host startup gate; provider I/O must not run under
|
|
a process-wide Controller startup lock.
|
|
|
|
`Controller.SubscribeWhenAvailable` is the observation-only stream attachment
|
|
for consumers that already have durable session identity but may race runtime
|
|
resume after a daemon restart. It waits for `Start`, `Resume`, or
|
|
`Host.EnsureRuntimeSession` to repopulate the runtime registry and then
|
|
subscribes with the current state snapshot. It never starts or resumes a
|
|
provider itself; lifecycle authority stays with Agent Host.
|
|
|
|
Hosts that need to prepare a provider launch immediately before process spawn
|
|
can set `ProviderLaunchPreparer`. The hook receives the provider, session,
|
|
command, environment, cwd, and direct-start mode; it returns the command,
|
|
environment, cwd, and optional cleanup function to use for `ProcessTransport`
|
|
startup.
|
|
|
|
Prepare errors fail session start before spawning a process. When prepare
|
|
succeeds, cleanup runs after the provider process is closed, including process
|
|
start or initialize failure, live-session close, idle release, and live process
|
|
replacement. Cleanup failures are logged and do not replace the original close
|
|
or start error.
|
|
|
|
Hosts that need unrestricted command networking while retaining a
|
|
Codex-compatible provider's permission-mode filesystem sandbox and approval UX
|
|
can set `Config.CommandNetworkAccessPolicy` when using the default adapters.
|
|
The host policy receives canonical provider IDs and should explicitly allow
|
|
only providers that require command networking. A nil policy denies command
|
|
network access. Hosts that construct adapters directly can instead use
|
|
`CodexAppServerAdapterOptions{CommandNetworkAccess: true}`.
|
|
|
|
Both forms set `sandboxPolicy.networkAccess` on read-only and workspace-write
|
|
turns. They do not change `approvalPolicy`, `approvalsReviewer`, writable roots,
|
|
or network-proxy policy. Full-access turns remain unrestricted by definition.
|
|
|
|
## Process Cassette Transport
|
|
|
|
`NewRecordingProcessTransport` decorates an existing `ProcessTransport` and
|
|
writes one versioned cassette manifest plus a JSONL stream of successful
|
|
outbound writes and observed stdout, stderr, and exit frames.
|
|
`NewReplayProcessTransport` loads a complete cassette and exposes the same
|
|
`ProcessConnection` contract without starting a provider process. Replay waits
|
|
for each expected outbound write before releasing later inbound frames and
|
|
waits for already-recorded inbound frames to be consumed before validating the
|
|
next outbound write. This preserves the recorded stream order without treating
|
|
normal reader/writer goroutine scheduling as a mismatch. Replay
|
|
fails closed on missing, additional, reordered, or different outbound JSON.
|
|
Its playback controller can pause before the next inbound frame, resume from
|
|
the same virtual time, or fast-forward recorded waits. Fast-forward never skips
|
|
frames or outbound assertions. It may temporarily pass a paused barrier for
|
|
checkpoint seeking; disabling fast-forward restores the requested paused state.
|
|
|
|
`NewSessionReplayProcessTransport` constructs one fixed replay router from
|
|
Cassette, root Session, and Cassette-directory registrations. It keeps one
|
|
`ReplayProcessTransport` per Cassette, routes process launches by
|
|
`RootAgentSessionID`, and exposes Cassette-scoped playback and verification. Duplicate
|
|
Cassette or root Session registrations fail construction. An unregistered root
|
|
Session launch fails closed.
|
|
|
|
`SessionRecordingProcessTransport` keeps lightweight wrappers around live
|
|
provider connections, so `continue-session` capture can attach after a process
|
|
has started. It also captures later root, parallel child, and nested child
|
|
connections in the same SessionGraph. Each connection is keyed by recorded
|
|
Session identity, provider, and Session-local launch ordinal; global sequence is
|
|
diagnostic only. `Arm` also freezes the owning Recording ID into every captured
|
|
frame and decoded Provider input unit, so a delayed callback cannot be
|
|
reattributed to a later capture generation.
|
|
Provider tape schema v4 marks every connection as
|
|
`process-start` or `attached-live-connection`. Provider adapters cold-bootstrap
|
|
the former and restore their initialized protocol checkpoint for the latter;
|
|
the checkpoint crosses the portable Host historical-state boundary as
|
|
`providerResumeCheckpoint`, while unrelated private runtime context stays out
|
|
of the Cassette. Missing or unknown origins fail closed. Provider probes and
|
|
setup commands use the normal local transport. A complete manifest records the exact frame count,
|
|
decoded payload bytes, stored bytes, largest frame, per-kind byte distribution,
|
|
and SHA-256 of `frames.jsonl`, so deletion or mutation fails before replay
|
|
starts. Recording fails before writing a decoded payload above 8 MiB or a tape
|
|
above 256 MiB; provider traffic is a protocol stream, not a bulk-file archive.
|
|
`Runtime.Close` closes live
|
|
provider sessions first and then finalizes a transport that exposes
|
|
`Finalize() error`, ensuring recording manifests are marked complete only after
|
|
connection shutdown and replay verifies every recorded connection and chunk
|
|
was consumed.
|
|
|
|
Schema v4 also requires `projectionVersion: 1`. Recording sends original bytes
|
|
to the live adapter and persists a protocol-aware projected copy: correlated
|
|
account email and recognized CWD path fields become portable, while
|
|
credential-bearing protocol methods and residual structured secrets or
|
|
unclassified absolute paths fail closed. Recognized HOME paths use
|
|
`${REPLAY_HOME}` and resolve against the isolated replay HOME. Projection
|
|
preserves the frame that completes a split protocol message. It does not scan
|
|
or rewrite ordinary prompt or Provider text.
|
|
|
|
## Package Ownership
|
|
|
|
This package owns:
|
|
|
|
- agent session controller mechanics
|
|
- built-in provider adapters and ACP protocol handling
|
|
- process transport abstractions
|
|
- runtime-to-activity report emission
|
|
- provider descriptors for update capability, trusted source, and execution strategy
|
|
|
|
The host daemon owns:
|
|
|
|
- HTTP, IPC, or CLI APIs
|
|
- durable persistence and event publishing
|
|
- provider availability and install status
|
|
- update metadata caching, source-ownership verification, and update actions
|
|
- workspace attachment, runtime VM lifecycle, and product auth
|
|
|
|
## Provider Authentication Status
|
|
|
|
`providerregistry` owns each provider's auth status command, runner kind, and
|
|
parser kind. Hosts execute the descriptor-owned runner in their provider
|
|
runtime, then use `providerstatus.ParseAuthStatusOutput` for text-command
|
|
results. Codex uses the dedicated `codex_app_server_account` runner: it
|
|
initializes `codex app-server` and calls `account/read`, matching session
|
|
startup instead of trusting the shallower `codex login status` output.
|
|
The same package exposes narrow helpers for explicit Codex and Claude API
|
|
billing configuration. Credential-file or token presence must not be used as
|
|
proof that an OAuth session is still authenticated.
|
|
|
|
`providerstatus` also owns the provider-neutral evidence reduction contract.
|
|
Local status commands and credential files produce `configured`; a successful
|
|
provider request produces `authenticated`; an explicit remote auth failure
|
|
produces `required`. Remote evidence outranks stale local files until the host
|
|
crosses an explicit account, credential, or runtime boundary. Transient probe
|
|
failures preserve a settled observation.
|
|
|
|
`tuttid` consumes this reducer in its provider-status service. It combines the
|
|
local probe with real agent-run outcomes, owns freshness and credential-change
|
|
reset, and projects the same public status to Tutti Desktop and AgentGUI.
|
|
External hosts consume the same reducer only when their runtime cannot use the
|
|
`tuttid` status service. `DesktopIntegrationDescriptor` declares when such a
|
|
host must wait for credential projection before probing; the host maps that
|
|
semantic barrier to its own synchronization mechanism.
|
|
|
|
`StatusDescriptor.RemoteAuthProbe` owns the provider-backed strategy without
|
|
owning credential storage. Claude Code declares its OAuth usage request to
|
|
`https://api.anthropic.com/api/oauth/usage`; hosts resolve the OAuth access
|
|
token from their own credential authority and never send an API key or
|
|
`ANTHROPIC_AUTH_TOKEN` to that endpoint. Codex declares the provider-usage
|
|
strategy, which invokes `account/rateLimits/read` through the provider runtime
|
|
without publishing credential bytes. A successful request authenticates the
|
|
session, an explicit authentication rejection requires login, and throttling,
|
|
server, or transport failures preserve the local `configured` state. `tuttid`
|
|
expires this remote evidence after 15 minutes; Desktop asks again only while
|
|
its window is visible and focused. OAuth refresh-token rotation remains
|
|
credential-owner policy and is not performed by this status probe.
|
|
|
|
## Live Session Recycling
|
|
|
|
Agent sessions are durable controller records. For providers that support live
|
|
session release, the runtime reaper may close an idle provider process without
|
|
closing the Tutti agent session. The provider session id remains attached to the
|
|
session, and the next `Exec` resumes the provider live session before starting a
|
|
new turn.
|
|
|
|
User-initiated `Close` is still destructive for the controller session: it
|
|
completes the session, publishes completion activity, and removes the in-memory
|
|
record. Idle live-session release must not emit completion activity, clear the
|
|
provider session id, remove runtime directories, or interrupt active turns and
|
|
pending interactive requests.
|
|
|
|
Standard ACP Agent Extensions participate in the default idle reaper only when
|
|
their live `initialize` handshake advertises `session/load` or
|
|
`session/resume`. After 30 minutes of inactivity, release closes only the ACP
|
|
transport and its CLI process; it does not send `session/close`. The next
|
|
`Exec` launches a replacement CLI process and restores the preserved provider
|
|
session. Extensions that do not advertise a restore method remain live, and a
|
|
pending interactive request makes the session busy rather than releasable.
|
|
Replacement processes continue the durable Session's lifecycle snapshot
|
|
sequence so restored activity cannot be rejected as stale. Release, resume,
|
|
and live settings RPCs (including permission changes) share the per-Session
|
|
lifecycle fence; a failed transport close keeps the live handle registered so
|
|
a later release can retry.
|
|
The local process transport serializes concurrent close attempts and retries
|
|
termination after a failed attempt instead of caching that failure forever.
|
|
An ACP client whose release failed remains owned as a physical handle but is
|
|
not considered usable. One replacement may reconnect the durable provider
|
|
session, but the old client's inbound handler is quarantined before close so
|
|
late output cannot enter the replacement Turn. If that old handle still cannot
|
|
close, later Start/Resume attempts spend one bounded cleanup attempt and return
|
|
`agent.process_cleanup_pending` without spawning another process while the
|
|
retired handle remains.
|
|
The Controller also sweeps adapter-owned retired handles independently of its
|
|
canonical Session registry, so provisional or preserve-state removal cannot
|
|
orphan cleanup ownership. Each sweep gives every cleanup-capable adapter at
|
|
most one failed Close budget across canonical and detached handles, and reports
|
|
resource counters separately from canonical idle-session release results.
|
|
|
|
Claude Code SDK sessions keep the SDK `session_id` in `ProviderSessionID` and
|
|
mirror the opaque SDK resume cursor in `runtimeContext.resumeCursor`. The sidecar
|
|
owns SDK stream ordering, turn cancellation, orphan result draining, and cursor
|
|
updates; the Go adapter forwards requests, persists session state patches, and
|
|
restores the last cursor on resume.
|
|
|
|
## Cloud Projection Extension Points
|
|
|
|
External daemons (for example tsh desktopd) can project local agent activity to
|
|
a remote controlplane without forking any `activity/` code.
|
|
|
|
**Scope ID semantics (RFC hard constraint):** the scope identifier in these
|
|
shared contracts is opaque — on the tutti side it is the **workspace ID**, for
|
|
external daemons such as tsh it is the **control-plane room ID**. workspace ≡
|
|
room, one-to-one, with no implicit translation anywhere: `roomID` in the store
|
|
interfaces is exactly the `WorkspaceID` on report inputs and is sent on the
|
|
wire as `roomId`. External daemons must pass the control-plane room ID directly
|
|
and must not introduce a second mapping in between.
|
|
|
|
- **`SyncStateStore`** — inject persistence for per-session sync states
|
|
(pending counts, failure counters, last error) via
|
|
`agentsessionstore.WithSyncStateStore`. `FileAgentSyncStateStore` is a
|
|
ready-made file-backed implementation.
|
|
- **`SessionActivityReporterAdapter`** — wraps any `SessionActivityReporter`
|
|
(such as `agentsessionstore.Client` configured with the controlplane
|
|
`BaseURL`) into an `ActivityReporter`. It converts `ReportActivityInput`
|
|
into per-session state and message reports and tracks/persists sync state
|
|
through a `SyncStateStore`.
|
|
- **Syncer backoff and cursor persistence** — `WithSyncBackoff`
|
|
(`DefaultSyncBackoffConfig`: 10s initial, 5min cap, 2.0 multiplier) enables
|
|
per-session exponential backoff for failed message syncs, and
|
|
`WithMessageCursorStore` persists message sync cursors so pulls resume after
|
|
a restart. Both are opt-in; without them behavior is unchanged.
|
|
|
|
`ActivityReporter` and `SessionActivityReporterAdapter` are compatibility
|
|
projection contracts; they are not sufficient to host a runtime controller
|
|
because their state and message writes may use separate transactions.
|
|
`agentdaemon.Config.Reporter` requires `DurableActivityReporter`, whose
|
|
`ReportSubmitProvenance` method must atomically persist the canonical
|
|
client-submit message and the submitted Turn admission before provider
|
|
dispatch, and return only after that message can be queried by
|
|
`clientSubmitId`. A host decorator embeds or otherwise preserves this required
|
|
interface; there is no optional capability probe to forward manually.
|
|
|
|
The daemon service passes `ClientSubmitID` through typed create/send and runtime
|
|
inputs. `Exec` uses `ReportSubmitProvenance` as the pre-dispatch admission
|
|
barrier. After `Exec` returns, the service explicitly calls the required
|
|
`RuntimeController.DurablyReportSubmitProvenance` method as an idempotent
|
|
reconciliation barrier before it accepts any submit claim; this second call
|
|
does not represent a second provider admission. The runtime adapter delegates
|
|
that call to the controller after `Exec` has released the session lifecycle
|
|
lock; the controller places the uncoalesced barrier behind earlier reports in
|
|
the same FIFO. A barrier failure is delivery-unknown, and provider work is
|
|
never blindly replayed. Host-owned user submissions, including follow-up
|
|
submissions, carry a stable `ClientSubmitID`; standalone internal runtime
|
|
calls may retain their generated prompt-message identity.
|
|
|
|
```go
|
|
client := agentsessionstore.NewClient(agentsessionstore.Config{
|
|
BaseURL: "https://controlplane.example.com",
|
|
Token: token,
|
|
})
|
|
fileStore := agentsessionstore.NewFileAgentSyncStateStore(stateDir)
|
|
reporter := agentsessionstore.NewSessionActivityReporterAdapter(
|
|
client,
|
|
agentsessionstore.WithReporterSyncStateStore(fileStore),
|
|
)
|
|
store := agentsessionstore.New(
|
|
client,
|
|
agentsessionstore.WithSyncStateStore(fileStore),
|
|
agentsessionstore.WithMessageCursorStore(fileStore),
|
|
agentsessionstore.WithSyncBackoff(agentsessionstore.DefaultSyncBackoffConfig()),
|
|
)
|
|
```
|
|
|
|
## Legacy Defaults
|
|
|
|
The legacy runtime constructors still default to `TUTTI_WORKSPACE_ID`,
|
|
`tsh-desktop` ACP client metadata, and `agent:main:tsh-` OpenClaw session keys
|
|
for compatibility. New host integrations must use `agentdaemon.NewRuntime` with
|
|
explicit `HostMetadata`; the root facade does not apply legacy host identity
|
|
defaults. `ProcessTransport` is also required when using the built-in provider
|
|
adapters; hosts that pass custom `Adapters` own that transport setup themselves.
|
|
|
|
State directory defaults still follow the historical `TUTTI_STATE_DIR` /
|
|
`.tutti` behavior. State-dir injection is intentionally left for a later
|
|
host-boundary pass.
|