Files
agent-radar 2ed3415fb7
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
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00
..

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

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.

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.