proto

package
v0.3.9 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 21, 2026 License: Apache-2.0 Imports: 2 Imported by: 0

Documentation

Overview

本文件定义浏览器鉴权相关接口的线格式:ticket 签发响应与会话展示条目。

职责:

  • 作为 agentd 服务端与 internal/client 之间的单一契约来源

边界:

  • 只有线格式,不含任何行为;凭据明文永远不出现在这里 (AuthTicketResp 只回 URL,会话 cookie 只经 Set-Cookie 下发)

本文件定义桌面薄壳与控制台之间经 agentd 中转的数据类型。

职责:只声明线上的数据形状。 边界:

  • 不含任何指令类型。薄壳只上报、不接指令:让控制台点得动薄壳需要一条 反向通道,那条通道比它服务的动作还贵,设计上已排除(spec §5)。
  • 不含凭据字段。这条通道只走版本与同步结论。

discipline.go —— 控制台配置执行纪律的线格式(B157)。

职责:GET /api/discipline 与 PUT /api/discipline/mapping 的请求/响应结构。 边界:

  • 文件正文的读写复用 FileRead / FileWriteReq / FileWriteResp / FileConflictResp, 不另造一套——那与工作树在线编辑是同一件事的同一形状
  • 不含任何密钥字段:纪律块是纯文本指令

env.go —— 控制台配置 env 文件的线格式(B158)。

职责:GET /api/env、GET /api/env/file/keys、PUT /api/env/mapping 的请求/响应结构。

边界:

  • 文件正文的读写复用 FileRead / FileWriteReq / FileWriteResp / FileConflictResp, 不另造一套——那与工作树在线编辑是同一件事的同一形状
  • **本文件里没有任何字段承载 env 的值**:默认视图只交出 key 名与值长度, 全文只走 FileRead(且只在用户点「编辑正文」时)。这条是 spec §7 的凭据边界
  • 与 DisciplineResp 同构,少了 Builtins 一节——env 没有内置默认

executor_default.go —— 控制台配置机器级缺省执行者的线格式(B160)。

职责:GET / PUT /api/executor/default 的请求与响应结构。

边界:

  • 只覆盖 config 的 executor 段两个标量字段,不碰 approver、proc_fence 等 其它机器级配置(哪些不给写、为什么,见 spec §1.2)
  • 不含任何密钥字段

frames.go —— 结构化回合帧的线格式。

职责:

  • 定义 Frame 与 FrameType:frames.jsonl 每一行、以及 GET /api/tasks/{id}/frames 每一行的形状

边界:

  • 纯类型定义:不写文件、不做 I/O、不认识任何具体 executor
  • 不是事件:控制面事件是 Event(events 表),帧只用 RefSeq 指向它

为什么帧的 Seq 与 Event.Seq 是两套编号:帧 Seq 是**任务内**从 1 开始的行号, 由 FrameWriter 维护;Event.Seq 是 SQLite 的**库级**自增主键。混用会让 「第 5 帧」和「第 5 号事件」互相冒充。

projects.go —— 项目树、机器投影与跨机汇总的线格式类型(W3a)。

职责:

  • 定义 GET /api/projects/tree 的三层嵌套响应(project → location → workspace)
  • 定义 GET /api/machines 的机器投影
  • 定义 §5.3 的跨机汇总信封(machines 一栏让「谁没答上来」必须可见)

边界:

  • 纯类型:不含探测逻辑、不含转发逻辑,实现都在 internal/agentd
  • 这三层里只有 location 有持久化真相(B62 的 project_locations 表); project 是 origin_url 的纯函数,workspace 是现场探测的产物,都不落库
  • 本文件是前后端契约的 Go 侧真相:改动必须同步 web/src/api/types.ts 与 web/src/api/testdata/*.json(后者由 TestContractFixtures -update 生成,不手写)

Package proto 是 handoff 协议类型的唯一定义处。

职责:

  • 定义任务状态(TaskState)、事件类型(EventType)及任务/事件/工单(Ticket)数据结构
  • 提供任务状态机迁移合法性校验(CanTransit)

边界:

  • 纯类型包:无 I/O、无业务逻辑、无外部依赖
  • 不负责持久化、事件派发、状态变更执行等行为

PTY 终端会话的线格式类型(W4 PTY 终端 spec §3.1、§5)。

职责:定义 REST 与 /ws/pty 的请求/响应/控制帧形状。 边界:不含任何行为;会话真相在 internal/ptyhost,这里只是它的线格式投影。

reclaim.go —— handoff reclaim 的传输契约类型。

职责:

  • 定义 GET /api/reclaim 的列表响应与 POST /api/tasks/{id}/reclaim 的动作响应
  • 定义四种 409 拒绝理由的机器码,供 CLI 分派渲染

边界:

  • 只描述线上格式,不含任何判定逻辑(判定在 internal/agentd/reclaim.go)
  • 不描述进程残留(那是 FootprintRow 的事,两者互不覆盖)

status.go —— handoff status 的响应线格式。

职责:

  • 定义 BuildInfo / ActiveTask / StatusResp 三个结构与 Live* 取值常量

边界:

  • 只有数据,无行为、无 I/O(与本包其余部分同规格)
  • 不定义「怎么展示」:文本渲染归 cmd/status.go

update.go —— POST /api/update 的线格式。

职责:

  • 定义换版接口的成功响应、拒绝响应与可判别的拒绝原因常量

边界:

  • 只有数据,无行为、无 I/O(与本包其余部分同规格)
  • 请求参数不在这里:tag / sha256 / force 走 query,body 是 tar.gz 原文, 没有 JSON 请求体可定义

工作台状态同步的线格式类型(2026-08-20 状态同步 spec §4.2)。

职责:定义 /api/workbench/state 四个端点的请求/响应形状。 边界:

  • 不含任何行为
  • **Payload 一律是字符串**,内容是前端序列化好的 JSON。agentd 不解析它, 所以也不该让 JSON 解码器替它解析一遍(spec §4.2)

Index

Constants

View Source
const (
	DisciplineModeDefault = "default"
	DisciplineModeFile    = "file"
	DisciplineModeOff     = "off"
)

纪律档位取值。与 config 的三档语义一一对应(键不存在 / 值为文件名 / 值为空串)。

View Source
const (
	EnvModeFile = "file"
	EnvModeOff  = "off"
)

env 档位取值。与 config 的两档语义一一对应(键不存在 / 值为文件名)。

View Source
const (
	PtyCtrlAttached = "attached" // 服务端 → 客户端,建连首帧
	PtyCtrlExit     = "exit"     // 服务端 → 客户端,shell 已退出
	PtyCtrlError    = "error"    // 服务端 → 客户端
	PtyCtrlResize   = "resize"   // 客户端 → 服务端
)

/ws/pty 的 text 帧类型。binary 帧恒为 PTY 原始字节,不走 JSON。

View Source
const (
	LiveAlive   = "alive"
	LiveDead    = "dead"
	LiveUnknown = "unknown"
)

ActiveTask.Live 的三个取值。

为什么必须有 unknown:探不出结论时猜一个值就是在制造假阳性,而一条会说谎的 诊断命令比没有更糟——因为你会信它。

View Source
const (
	PullStageDownloading = "downloading"
	PullStageInstalling  = "installing"
	PullStageFailed      = "failed"
)

自拉的阶段取值。

只有三个:没有 "done"(见 UpdateStatus.PullState 的注释——成功的终点是重启), 也没有单独的 "verifying"(sha256 比对与解包后自检都发生在 installing 内部)。 **不要为了让阶段看起来更完整而加一个实现从不产出的取值**——消费方会写死 代码去处理它,而那段代码永远不会被执行,也永远不会被测到。

View Source
const (
	// UpdateModePull: 只下发 tag + sha256,由 agentd 自己去下载(body 必须为空)
	UpdateModePull = "pull"
	// UpdateModePush: 协调者推 tar.gz 原文(body 必须非空)。省略 mode 且 body
	// 非空等价于本模式,这是为了让老 CLI 的请求继续被正确处理
	UpdateModePush = "push"
)

换版模式,走 query 参数 mode。

为什么要显式 mode 而不靠「tag 有没有」隐式判别:现有判别已经压在 「body 空不空」这一个维度上,再叠一层「tag 有没有」,三种模式的判据就散在 两个维度上,加第四种时必然出错。显式 mode 还让新旧 agentd 的分歧点变成 一个可测的单点。

View Source
const (
	// UpdateReasonBusy: 有 running / waiting_answer 任务,且未带 force
	UpdateReasonBusy = "busy"
	// UpdateReasonUnmanaged: agentd 非托管启动,换完没人拉起。force 不越过
	UpdateReasonUnmanaged = "unmanaged"

	// UpdateReasonPullInProgress: 已有一个自拉在跑。force 不越过——两个自拉
	// 会往同一个临时文件路径(release.TempName(tag) 是确定性的)写,
	// 互相截断出一个坏二进制,而 Activate 会把它装上去
	UpdateReasonPullInProgress = "pull_in_progress"
)

换版被拒的可判别原因。

为什么要机器可读而不只给一句人话:两种拒绝的处置**完全不同**——活跃任务 可以 --force 越过,非托管不行。CLI 要据此选处置建议,而给一条注定失败的 命令比不给更糟(spec §4.6)。

View Source
const MaxDoneNoteBytes = 4096

MaxDoneNoteBytes 是归档说明的字节上限。

为什么超限要报错而不是截断:B6 的教训正是「静默截断让协调者盲信自己看到的是 全文」。协调者写了 6KB 说明、系统悄悄存 4KB,比直接拒绝糟糕得多。 取值 4096:比一句话说明宽出两个数量级,同时挡住「把整个 diff 粘进来」的误用。

Variables

TerminalStates 是任务的两个终态:到此不再有 executor 持有工作区。 存储层按它生成「非终态」查询条件,避免与状态机定义漂移。

Functions

func CanTransit

func CanTransit(from, to TaskState) bool

CanTransit 校验状态迁移是否合法(如 completed 不可回 running)。

参数:

  • from: 来源状态
  • to: 目标状态

返回:

  • true 表示迁移合法,false 表示不允许该迁移

注意:

  • 未在迁移表中登记的状态一律视为不可迁移
  • 本函数仅做静态校验,不实际变更状态

Types

type ActiveTask

type ActiveTask struct {
	ID       string `json:"id"`
	Name     string `json:"name"`
	State    string `json:"state"`
	Executor string `json:"executor"`
	RepoPath string `json:"repo_path"`
	Live     string `json:"live"` // LiveAlive / LiveDead / LiveUnknown
	Note     string `json:"note"` // 判死或判不出的一句话理由;alive 时为空

	// Watchers 是当前订阅该任务事件流的连接数(几个协调者在听)。
	//
	// 为什么是指针:nil 表示**对端没给这个字段**(老 agentd),与「确实是 0」
	// 是两回事。猜一个 0 就是在制造假阳性——与 Live 三态用 unknown 而不猜死
	// 是同一条纪律:一条会说谎的诊断命令比没有更糟,因为你会信它。
	Watchers *int `json:"watchers,omitempty"`

	// Procs 是该任务当前占用的进程数。
	//
	// 为什么是指针:nil 表示**取不到这个信息**(老 agentd、adapter 不支持、
	// 平台不支持、或 pgid 判定为复用/凭据不全),与「确实是 0 个进程」是两回事。
	// 猜一个 0 就是制造假阳性——与 Watchers、Live 三态同一条纪律。
	Procs *int `json:"procs,omitempty"`
}

ActiveTask 是一个非终结任务及其 executor 存活结论。

注意:ID 始终是完整 UUID。文本渲染可以只显示前 8 位(与执行者进程的短 id 展示 handoff-<id8> 一致,便于人肉对照),但任何拿去当参数的地方都必须用完整 UUID ——store.GetTask 是精确匹配,不做前缀查找。

type AddMachineReq added in v0.3.0

type AddMachineReq struct {
	Name  string `json:"name"`
	Addr  string `json:"addr"`
	Token string `json:"token"`
	User  string `json:"user"`
	Force bool   `json:"force"`
}

AddMachineReq 是 POST /api/machines 的请求体。

**Token 只进不出**:本结构仅用于反序列化请求。任何响应体、任何日志 都不得包含它——proto.Machine 从设计之初就没有 Token 字段,这条性质 必须保持。

Force=true 跳过可达性探测直接落库,用于「对端临时离线但确认地址无误」 的场景;默认 false,让粘错的地址或令牌当场暴露。

type ArchivedPayload

type ArchivedPayload struct {
	Note string `json:"note"`
}

ArchivedPayload 是 EventTypeArchived 的事件负载。

为什么定义在 proto 而不是 agentd:CLI 侧(B67 与任何解析事件流的脚本)要读 Note,放在 agentd 包里会逼两边各写一份结构体,形态一漂就是解析不出来。 这与只在 agentd 内部使用的 progressPayload 情况不同。

type Attachment added in v0.3.9

type Attachment struct {
	Kind string `json:"kind"`
	Path string `json:"path"`
}

Attachment 是账本卡片附件的 wire DTO。

type AuthTicketResp added in v0.3.0

type AuthTicketResp struct {
	URL       string    `json:"url"`
	ExpiresAt time.Time `json:"expires_at"`
}

AuthTicketResp 是 POST /api/auth/tickets 的响应。

URL 是可直接打开的兑换地址(含一次性 ticket);ExpiresAt 是该 ticket 的过期时刻。

type BuildInfo

type BuildInfo struct {
	Version  string `json:"version,omitempty"`
	Revision string `json:"revision"`
	Time     string `json:"time"`
	Modified bool   `json:"modified"`
	Go       string `json:"go"`

	// Platform 是构建目标平台,形如 "linux/amd64",在 buildinfo.Read() 里用
	// runtime.GOOS + "/" + runtime.GOARCH 现算填入(CLI 与 agentd 同一条路径,
	// 不会出现只有一端填的情况)。
	//
	// **空串表示对端没给这个字段**(老 agentd)。此时远程升级必须明确拒绝而不是
	// 猜一个默认值——猜错就是给一台 linux 机器推一个 darwin 二进制,自检会拦下,
	// 但那是白跑一次 15MB 上传换来的一条晦涩错误。
	Platform string `json:"platform,omitempty"`
}

BuildInfo 是一个 handoff 二进制的构建标识。

字段说明:

  • Version: release 版本号(形如 v0.1.0),构建时由 ldflags 注入; **空串表示不是 release 构建**(本地 go build / go run / 测试二进制), 此时调用方应退回 Revision 展示
  • Revision: vcs.revision;**空串表示不是 go build 产物**(go run / 测试 二进制没有 vcs 戳),调用方应显示「版本未知」而不是空
  • Time: vcs.time
  • Modified: vcs.modified——true 表示这个二进制是带未提交改动编出来的, 它对不上任何一个提交,排障时这是关键信息
  • Go: 编译所用 Go 版本

为什么 Version 与 Revision 并存而不是二选一:它们回答不同的问题。 Version 回答「该不该更新」(自动更新比的是它),Revision 回答「出问题的 是哪个提交」(排障比的是它)。release 构建两者都有。

type Card added in v0.3.9

type Card struct {
	ID                 string       `json:"id"`
	Title              string       `json:"title"`
	Status             string       `json:"status"`
	TerminateReason    string       `json:"terminate_reason,omitempty"`
	Priority           string       `json:"priority"`
	Project            string       `json:"project"`
	ParentID           string       `json:"parent"`
	WorkflowName       string       `json:"workflow"`
	WorkflowVersion    int          `json:"workflow_version"`
	Attachments        []Attachment `json:"attachments,omitempty"`
	AcceptanceCriteria string       `json:"acceptance_criteria,omitempty"`
	BaseBranch         string       `json:"base_branch,omitempty"`
	DriverSession      string       `json:"driver_session,omitempty"`
	DriverHeartbeatAt  time.Time    `json:"driver_heartbeat_at,omitempty"`
	CreatedAt          time.Time    `json:"created_at"`
	UpdatedAt          time.Time    `json:"updated_at"`
}

Card 是账本卡片的 wire DTO;字段名与现有账本 HTTP 响应保持一致。

type CardBrief added in v0.3.9

type CardBrief struct {
	ID     string `json:"id"`
	Title  string `json:"title"`
	Status string `json:"status"`
}

CardBrief 是详情中直接子卡的最小摘要 wire DTO。

type CardCreateResp added in v0.3.9

type CardCreateResp struct {
	ID string `json:"id"`
}

CardCreateResp 是建卡响应。

type CardDetail added in v0.3.9

type CardDetail struct {
	Card                Card           `json:"card"`
	Relations           []Relation     `json:"relations"`
	Events              []LedgerEvent  `json:"events"`
	TaskStates          []TaskStateRow `json:"task_states"`
	EffectiveBaseBranch string         `json:"effective_base_branch"`
	Decisions           []Decision     `json:"decisions"`
	Needs               string         `json:"needs"`
	Children            []CardBrief    `json:"children"`
}

CardDetail 是卡片详情的 wire DTO。

type CardView added in v0.3.9

type CardView struct {
	ID            string       `json:"id"`
	Title         string       `json:"title"`
	Status        string       `json:"status"`
	Priority      string       `json:"priority"`
	Project       string       `json:"project"`
	Workflow      string       `json:"workflow"`
	Parent        string       `json:"parent"`
	BaseBranch    string       `json:"base_branch"`
	Attachments   []Attachment `json:"attachments"`
	Following     string       `json:"following"`
	Blocked       bool         `json:"blocked"`
	BlockedBy     []string     `json:"blocked_by"`
	MergedCount   int          `json:"merged_count"`
	Needs         string       `json:"needs"`
	OpenDecisions int          `json:"open_decisions"`
	ChildrenTotal int          `json:"children_total"`
	ChildrenDone  int          `json:"children_done"`
	Conflict      bool         `json:"conflict"`
	OpenTickets   int          `json:"open_tickets"`
}

CardView 是列表卡片及查询期派生标记的 wire DTO。

type CardWorkflowLocation added in v0.3.9

type CardWorkflowLocation struct {
	ID              string `json:"id"`
	Workflow        string `json:"workflow"`
	WorkflowVersion int    `json:"workflow_version"`
	Status          string `json:"status"`
}

CardWorkflowLocation 是迁移响应中的卡位置。

type Cost added in v0.3.0

type Cost struct {
	// Ticks 是花费,单位 1 USD = 10^10 ticks。
	//
	// 为什么用整数 ticks 而不是浮点美元:grok 原生就给 ticks,且它的文档明说
	// 浮点求和对不上服务端的账。统一整数累加,只在展示的最后一步转美元。
	Ticks int64 `json:"ticks"`
	// State 见 CostState 的注释。CostUnknown 时 Ticks 恒为 0。
	State CostState `json:"state"`
}

Cost 是累计花费及其可信度。

注意:State 为 CostPartial 时,Ticks 只是**已知部分**的和,是下界不是总额。

type CostState added in v0.3.0

type CostState string

CostState 是花费的可信度。

取值范围**分两级**:单条账目(SpendEntry / ledger 行)只可能是 CostReported / CostEstimated / CostUnknown;CostPartial 只在**求和之后** 产生(部分行有花费、部分行没有),任何 adapter 都不会产出它。 别去找「哪个 adapter 报 partial」——没有。

const (
	// CostReported:执行器自报了花费且完整。
	CostReported CostState = "reported"
	// CostEstimated:执行器不报花费,由 handoff 按 API 牌价估算(只有 codex)。
	CostEstimated CostState = "estimated"
	// CostPartial:**仅聚合级**。有已知部分,但有调用没拿到花费——所以它是
	// **下界**,真实值只会更高。展示时必须能读出这一点。
	CostPartial CostState = "partial"
	// CostUnknown:一次都没拿到。展示成「—」,**绝不是 $0.00**:
	// 花费的缺席意味着 "unreported or incomplete, never free"。
	CostUnknown CostState = "unknown"
)

type CreatePtySessionReq added in v0.3.0

type CreatePtySessionReq struct {
	BasePath string `json:"base_path"`
	BaseKind string `json:"base_kind"`
	// Rel 是相对 BasePath 的子目录,空串=工作树根;BaseKind=home 时忽略。
	Rel  string `json:"rel"`
	Cols int    `json:"cols"`
	Rows int    `json:"rows"`
}

CreatePtySessionReq 是 POST /api/pty/sessions 的请求体。 BaseKind="home" 时 BasePath 被忽略(服务端用它自己的 $HOME,见 spec §5.2)。

type CreateWorkspaceEntryReq added in v0.3.0

type CreateWorkspaceEntryReq struct {
	Name string `json:"name"`
	Kind string `json:"kind"` // "file" 或 "dir"
}

CreateWorkspaceEntryReq 是 POST /api/workspaces/entry 的请求体。

type CreateWorktreeReq added in v0.3.0

type CreateWorktreeReq struct {
	// Mode 二选一:"new_branch"(建新分支并开树)/ "existing_branch"(把已有分支开成一棵树)。
	Mode string `json:"mode"`
	// Branch 是要新建或要检出的分支名,必填。
	Branch string `json:"branch"`
	// Base 是新分支的起点,仅 new_branch 模式有意义;空串时由服务端推导。
	Base string `json:"base"`
}

CreateWorktreeReq 是 POST /api/projects/{name}/worktrees 的请求体。

type Cumulative added in v0.3.0

type Cumulative struct {
	// InputTokens 是未命中缓存的输入(口径见 Store.UpsertSpend 的注释)。
	InputTokens int `json:"input_tokens"`
	// CachedTokens 是命中缓存的输入(读缓存 + 写缓存)。
	CachedTokens int `json:"cached_tokens"`
	// OutputTokens 是模型产出,含 reasoning。
	OutputTokens int `json:"output_tokens"`
	// TotalTokens 是上面三项之和,由 store 算好,前端不再自己加。
	TotalTokens int `json:"total_tokens"`
	// Cost 是累计花费;nil = 还没有任何一条账目带花费信息。
	Cost *Cost `json:"cost,omitempty"`
}

Cumulative 是任务的累计消耗快照。

与 Usage 的区别(**改错了不会报错,只会显示错的数**):Usage 描述 「现在占用多少 context」(最后一次模型调用的输入侧),本结构描述 「这个任务一共烧了多少」(跨全部调用累加)。两者数量级差几倍到几十倍, 不要因为字段名像就互相赋值。

边界:本结构由 Store.TaskCumulative 对 task_usage_ledger 求和产出, 只在**单任务读取**时填充;列表接口不填(见 Store.ListTasks 的注释)。

type Decision added in v0.3.9

type Decision struct {
	ID         int64     `json:"id"`
	CardID     string    `json:"card_id,omitempty"`
	Body       string    `json:"body"`
	Options    []string  `json:"options,omitempty"`
	Status     string    `json:"status"`
	CreatedBy  string    `json:"created_by"`
	Answer     string    `json:"answer,omitempty"`
	AnsweredBy string    `json:"answered_by,omitempty"`
	CreatedAt  time.Time `json:"created_at"`
	AnsweredAt time.Time `json:"answered_at,omitempty"`
}

Decision 是卡上裁决项的 wire DTO。

type DesktopState added in v0.3.3

type DesktopState struct {
	// AppVersion 是薄壳自身版本(desktop 侧的 embedbin.Version)。空串=判不出
	// (开发构建未注入版本),此时控制台一律不提示。
	AppVersion string `json:"app_version"`
	// SyncPlan 是本次开机同步的结论:skip / blocked / failed / done。
	SyncPlan string `json:"sync_plan"`
	// SyncBusy 是 blocked 时的活跃任务数;-1 表示探测失败,不要当 0 用。
	SyncBusy int `json:"sync_busy"`
	// SyncError 是 failed 时的原文,供控制台原样展示。
	SyncError string `json:"sync_error,omitempty"`
}

DesktopState 是薄壳向控制台公开的自身状态。

字段全部由薄壳填,agentd 只做带 TTL 的转发。控制台据此判断「有没有新版」—— 必须用薄壳的版本比,不能用 agentd 自己的版本:同步被拦或失败时两者恰好不等, 用 agentd 的版本会去劝用户下载一个他已经装好了的版本。

type DirEntry added in v0.3.0

type DirEntry struct {
	Name  string `json:"name"`
	IsDir bool   `json:"is_dir"`
	Size  int64  `json:"size,omitempty"`
	// Ignored 表示该条目被 .gitignore 排除(判据是 git check-ignore,不是前端
	// 猜后缀)。false 会被 omitempty 省略——**缺键 = 未被忽略**,不代表「没查过」:
	// 查不出来时(git 不可用、目录不是仓库)服务端一律按未忽略返回并打日志,
	// 宁可少标一个,也不把源码标成垃圾。
	Ignored bool `json:"ignored,omitempty"`
}

DirEntry 是工作树目录列举里的一项(GET /api/workspaces/dir)。

字段是刻意克制的:文件浏览需要的是「这一层有什么、哪些能展开、多大、哪些 不归 git 管」,而 mtime / mode / owner 都会诱导前端做它不该做的判断(比如按 mtime 猜改动,那是 diff 的活)。Size 只对普通文件有意义,目录恒 0 并被 omitempty 省略。

type DirListResult added in v0.3.0

type DirListResult struct {
	Entries []DirEntry `json:"entries"`
}

DirListResult 是 GET /api/workspaces/dir 的响应体。

Entries 永不为 nil:空目录返回 [],前端 `.map` 不需要判空。

type DirtyFile

type DirtyFile struct {
	// Status 是 porcelain 的两字符状态码,如 "M " / "??" / "R "。
	Status string `json:"status"`
	Path   string `json:"path"`
}

DirtyFile 是脏工作树里的一个条目,来自 git status --porcelain。

type DisciplineBinding added in v0.3.0

type DisciplineBinding struct {
	Executor    string `json:"executor"`
	Mode        string `json:"mode"`
	File        string `json:"file,omitempty"`
	DefaultTier string `json:"default_tier"`
}

DisciplineBinding 是一个 executor 的当前档位。

Mode 三值:

  • "default":配置里没有这个键,用内置默认(DefaultTier 指出是哪版)
  • "file":用 File 指定的文件
  • "off":显式关闭注入

DefaultTier 恒有值:Mode 为 default 时界面要显示「内置默认(single-context)」; 其余两档它是「改回默认会变成什么」的预告,同样要显示。

type DisciplineBuiltin added in v0.3.0

type DisciplineBuiltin struct {
	Tier    string `json:"tier"`
	Content string `json:"content"`
}

DisciplineBuiltin 是一份内置纪律块。Tier 取 "subagent" / "single-context"。

type DisciplineFile added in v0.3.0

type DisciplineFile struct {
	Name   string `json:"name"`
	Size   int64  `json:"size"`
	SHA256 string `json:"sha256"`
}

DisciplineFile 是纪律块目录下的一个文件。Size 是磁盘真实大小。

type DisciplineMappingReq added in v0.3.0

type DisciplineMappingReq struct {
	Bindings []DisciplineBinding `json:"bindings"`
}

DisciplineMappingReq 是 PUT /api/discipline/mapping 的请求体:**整段替换**。

为什么整段替换而不是逐项 patch:界面就是整块保存,整段替换让「界面所见 = 落盘所得」无需推理;逐项 patch 还要额外定义「没出现的键是保持还是删除」。 这条成立的前提是 GET 返回的 Bindings 是全集(注册的 adapter ∪ 配置里的键), 若日后有只送部分键的写入方,本语义必须重新审视。

type DisciplineResp added in v0.3.0

type DisciplineResp struct {
	Dir      string              `json:"dir"`      // <DataDir>/discipline 绝对路径,界面照原样显示
	Builtins []DisciplineBuiltin `json:"builtins"` // 内置两版全文,随二进制走,只读
	Files    []DisciplineFile    `json:"files"`    // 该机纪律块目录下的文件(不含正文)
	Bindings []DisciplineBinding `json:"bindings"` // 该机每个 executor 的当前档位
}

DisciplineResp 是 GET /api/discipline 的响应:一次给全配置面要用的四样东西。

为什么一次给全:纪律分区要文件列表 + 内置全文,开发机详情要 executor 档位 + 可选文件名,同一份数据喂两处界面,不做两套接口。用户文件的**正文不在这里** (按需单读),内置全文只有两份、几 KB,随列表带走。

type DownloadState added in v0.3.3

type DownloadState struct {
	// Stage:idle / downloading / verifying / done / failed。
	Stage string `json:"stage"`
	Tag   string `json:"tag,omitempty"`
	// Percent 为 -1 表示不可知(服务端没给 Content-Length)。
	Percent int    `json:"percent"`
	Path    string `json:"path,omitempty"` // done 时的绝对路径。
	Opened  bool   `json:"opened"`         // 是否成功唤起文件管理器。
	Error   string `json:"error,omitempty"`
}

DownloadState 是桌面端安装包下载的进度与结果。

type EnvBinding added in v0.3.0

type EnvBinding struct {
	Executor string `json:"executor"`
	Mode     string `json:"mode"`
	File     string `json:"file,omitempty"`
}

EnvBinding 是一个 executor 的当前档位。**只有两档**:

  • "off":配置里**没有这个键** → 启动时不注入任何环境变量
  • "file":用 File 指定的文件

注意与 DisciplineBinding 的**错位**:discipline 的「键不存在」是「用内置默认」、 「空串」才是关闭;env 没有内置默认,「键不存在」就是唯一的关闭表达。落盘时 **绝不写空串**——空串会让 Resolver 走到「读 <dir>/」这种无意义路径。

type EnvFile added in v0.3.0

type EnvFile struct {
	Name   string `json:"name"`
	Size   int64  `json:"size"`
	SHA256 string `json:"sha256"`
}

EnvFile 是 env 目录下的一个文件。Size 是磁盘真实大小。

type EnvKey added in v0.3.0

type EnvKey struct {
	Key        string `json:"key"`
	ValueBytes int    `json:"value_bytes"`
	Duplicate  bool   `json:"duplicate,omitempty"`
}

EnvKey 是解析出的一个变量。**永不含值**——这是本设计的凭据边界所在。

ValueBytes 是值的字节长度,口径是**展开后**(Parse 的产物):它让「这个变量 是不是空的」可判断,而不泄露内容。注意展开用 lookup=nil,所以引用了外部变量 的值在这里会显示为更短甚至 0——这不是 bug,是刻意不查 agentd 自己的环境, 否则同一个文件在不同机器上会显示出不同的长度,既误导又多泄露一层信息。

Duplicate 为真表示该键在文件里出现过多次(Resolver 的既有行为是 WARN 不拒, 界面照此标注、不拦保存)。

**刻意没有「是否单引号字面量」这一项**:Parse 只回 Key/Value,不暴露引号风格, 要标它就得在 handler 里重扫一遍原始行、再造一套与 Parse 可能漂移的解析。

type EnvKeysResp added in v0.3.0

type EnvKeysResp struct {
	Keys []EnvKey `json:"keys"`
}

EnvKeysResp 是 GET /api/env/file/keys 的响应。

type EnvMappingReq added in v0.3.0

type EnvMappingReq struct {
	Bindings []EnvBinding `json:"bindings"`
}

EnvMappingReq 是 PUT /api/env/mapping 的请求体:**整段替换**。

为什么整段替换而不是逐项 patch:界面就是整块保存,整段替换让「界面所见 = 落盘所得」无需推理。这条成立的前提是 GET 返回的 Bindings 是全集(注册的 adapter ∪ 配置里的键),若日后有只送部分键的写入方,本语义必须重新审视。

type EnvResp added in v0.3.0

type EnvResp struct {
	Dir      string       `json:"dir"`      // <DataDir>/env 绝对路径,界面照原样显示
	Files    []EnvFile    `json:"files"`    // 该机 env 目录下的文件(不含正文)
	Bindings []EnvBinding `json:"bindings"` // 该机每个 executor 的当前档位
}

EnvResp 是 GET /api/env 的响应:一次给全配置面要用的三样东西。

为什么一次给全:Env 分区要文件列表,开发机详情要 executor 档位 + 可选文件名, 同一份数据喂两处界面,不做两套接口。文件正文与变量清单都**不在这里**(按需单读)。

type Event

type Event struct {
	Seq       int64           `json:"seq"`
	TaskID    string          `json:"task_id"`
	Type      EventType       `json:"type"`
	Payload   json.RawMessage `json:"payload"`
	CreatedAt time.Time       `json:"created_at"`
}

Event 表示任务生命周期中产生的一条事件记录。

JSON 线格式契约(wait 命令输出与 WS 推送共用此结构):{"seq":..,"task_id":.., "type":..,"payload":{..},"created_at":..},key 必须小写(上层脚本按此解析)。

type EventType

type EventType string

EventType 表示任务产生的事件类型。

const (
	EventTypePermissionRequest EventType = "permission_request"
	EventTypeQuestion          EventType = "question"
	EventTypeProgress          EventType = "progress"
	EventTypeCompleted         EventType = "completed"
	EventTypeFailed            EventType = "failed"
	// EventTypeTurnFailed 表示**一个回合**失败了,而任务**仍然活着**——
	// handleResult 在发这条之前已经把任务迁到 waiting_review,协调者一个
	// continue 就能接着干。
	//
	// 为什么必须与 EventTypeFailed 分开而不是共用一个类型加个字段:
	// 客户端要据此决定「要不要收流、要不要报任务终结」,而这是一个封闭取值的
	// 判断,不能靠 fail_reason 的散文去猜(那是十来处各自措辞、改一句文案就能
	// 静默改掉客户端行为的东西)。分成两个类型还有一个好处:**旧客户端遇到未知
	// 类型会当普通事件继续跟随**,于是它不再假终态退出——bug 对旧 CLI 自动消失。
	//
	// 与 EventTypeCompleted 的关系:两者是**同一个状态迁移**(都进 waiting_review),
	// 所以消费端对它俩的行为必须一致,只是一个成功一个失败。
	EventTypeTurnFailed EventType = "turn_failed"
	EventTypeStalled    EventType = "stalled"
	// EventTypeDeliveryFailed 表示协调者的应答已落库但没能送达 executor。
	//
	// 为什么必须是一类事件而不只是日志:应答未送达时 executor 仍原地阻塞,
	// 而工单已被消耗、不再出现在挂起项里——若只写日志,协调者这边完全无感,
	// 任务会一直挂到看门狗超时。作为事件产出才能唤醒协调者去执行 handoff resume。
	EventTypeDeliveryFailed EventType = "delivery_failed"
	// EventTypeApproverDecision 表示分级审批链中廉价模型审批者对权限请求的裁决
	// 结果(approve/escalate/error)。只入库做审计(show 可见),不唤醒协调者——
	// approve 路径已自动放行、escalate 路径由紧随其后的 permission_request 唤醒。
	EventTypeApproverDecision EventType = "approver_decision"
	// EventTypeApproverDisabled 表示本任务连续多次裁决失败(fail-closed),审批链
	// 已停用,后续权限请求一律直接升级人工协调者,不再浪费一次注定失败的裁决调用。
	EventTypeApproverDisabled EventType = "approver_disabled"
	// EventTypePermissionReuse 表示一次权限请求命中了本任务内**同一权限描述**的
	// 既有人工批准,被自动放行而没有再次叫醒协调者(B57②)。
	// 复用必须留痕,否则「我明明没批过这个」将无从对质。
	EventTypePermissionReuse EventType = "permission_reuse"
	// EventTypeDenyGuidanceRelayed 表示协调者拒绝时给出的原因已作为一条消息
	// 下发给 executor(B50)。
	EventTypeDenyGuidanceRelayed EventType = "deny_guidance_relayed"
	// EventTypeDenyGuidanceDropped 表示拒绝原因没能下发——回合在下一条提问到达前
	// 就终结了。协调者据此知道要用 continue 自己把话带上。
	EventTypeDenyGuidanceDropped EventType = "deny_guidance_dropped"
	// EventTypeApprovalDropped 表示审批者的裁决没能下发给 executor——裁决回来时
	// 回合已经结束(任务离开 running/waiting_answer)。agentd 已代为回了一个
	// 干净的 reject,本事件说明「那条裁决去哪了」。
	//
	// 与 deny_guidance_dropped 是同一根因(回合结束即无下发通道)的 approve 方向,
	// 但后果更重:拒绝原因丢了只是少一段指导,批准丢了会让 executor 那条请求
	// 悬到自行 abort,**打断的是下一个回合**(08-17 实测两次同型)。
	EventTypeApprovalDropped EventType = "approval_dropped"
	// EventTypeTicketsVoided 表示任务终结时把剩余挂起工单一并作废了(B63)。
	//
	// 为什么必须留痕:pending_tickets 是协调者接管陌生会话时「我还欠哪些没答」
	// 的权威清单,工单凭空消失与工单凭空挂着一样难排查——show 里要能回答
	// 「那张单是何时、因为什么被作废的」。
	//
	// **只入库不 Publish**,且在客户端不可交付(见 client.isDeliverable):它与
	// completed/failed 同时刻产生,可交付就会抢走一次性 wait 的收手权。
	EventTypeTicketsVoided EventType = "tickets_voided"
	// EventTypeTicketAnswered 是 reply/审批者自动批准消耗工单后的审计事件。
	// 它供账本镜像回放清除对应的未决工单,不唤醒 wait(应答回程另有 hub)。
	EventTypeTicketAnswered EventType = "ticket_answered"
	// EventTypeArchived 是任务被 done 归档时追加的终态事件,payload 为 ArchivedPayload。
	//
	// 为什么归档需要一条自己的事件:在此之前 Done 只做状态迁移、不追加任何事件,
	// 跟随中的 wait --follow 只能从「订阅被关掉了」间接推断任务结束。等待方要判断
	// 「这个任务做完没有」时,事件流里根本没有可等的东西(B68)。
	//
	// 注意:本事件**唤醒 wait**(与 progress / approver_decision 那类只入库的事件不同),
	// README 与 handoff skill 的事件表必须同步列出它。
	EventTypeArchived EventType = "archived"
	// EventTypeResourcePressure 表示执行机的进程余量已达高水位(参考上限的九成)。
	//
	// 为什么必须是一类**唤醒**事件而不只是日志:日志在执行机上,协调者手边
	// 没有;而这条告警的全部价值就在于「在第一条 fork 失败出现之前」让协调者
	// 知道要收敛。2026-08-12 事故里没有任何前兆,第一个信号就是整机瘫痪。
	EventTypeResourcePressure EventType = "resource_pressure"
	// EventTypeTaskProcPressure 是**单个任务**的进程数越线告警。
	//
	// 与 EventTypeResourcePressure 的分工:后者说「这台机器快满了」(uid 级),
	// 前者说「是这个任务在吃」(任务级)。事故复盘时前者才能定位到人。
	EventTypeTaskProcPressure EventType = "task_proc_pressure"
)

type ExecutorDefaultReq added in v0.3.0

type ExecutorDefaultReq struct {
	Default string `json:"default"`
	Model   string `json:"model"`
}

ExecutorDefaultReq 是 PUT /api/executor/default 的请求体。

两个字段都是**整体替换**语义:缺席与空串一视同仁。Model 为空串是一个有意义 的取值(= 不设默认模型),不是「这一项不改」——本接口没有「不改」这个表达。

type ExecutorDefaultResp added in v0.3.0

type ExecutorDefaultResp struct {
	Default   string   `json:"default"`   // 当前缺省执行者名
	Model     string   `json:"model"`     // 缺省执行者的默认模型;空串 = 用执行器自身默认
	Available []string `json:"available"` // 该机已注册的 adapter 名,按名字升序
}

ExecutorDefaultResp 是 GET /api/executor/default 的响应。

Model 的语义是「**Default 的**默认模型」,不是全局默认——agentd 的 resolveModel 只在 execName == Default 时才套用它,派别的执行器返回空串。 界面文案必须照这个语义写,不要写成「不分执行器」(那是修过的旧行为)。

type FileConflictResp added in v0.3.0

type FileConflictResp struct {
	Error   string   `json:"error"`
	Current FileRead `json:"current"`
}

FileConflictResp 是 409 的响应体。

带上 Current(磁盘现状的完整读取结论)是为了让冲突界面一次成型:用户要在 「放弃我的改动」和「用我的内容覆盖」之间选,两个动作都需要磁盘现状——前者 要它的正文,后者要它的哈希当新基线。分两次请求会在两次之间再开一个窗口。

type FileRead added in v0.3.0

type FileRead struct {
	Content   string `json:"content"`
	Size      int64  `json:"size"`
	Truncated bool   `json:"truncated,omitempty"` // 超过 1 MiB,只返回开头
	Binary    bool   `json:"binary,omitempty"`    // 前 8 KiB 出现 NUL 字节
	SHA256    string `json:"sha256,omitempty"`
}

FileRead 是一次文件读取的完整结论(GET /api/workspaces/file 的响应体)。

为什么是结构体而不是继续返回一个 content 字符串:写回需要知道「这份内容完不 完整、是不是文本、基线哈希是多少」,而这三件事只有读的那一刻知道。让调用方 二次判断(比如按扩展名猜二进制)必然与服务端分叉成「前端说能编辑、后端说不能」。

SHA256 只在**完整且是文本**时才有值。它唯一的用途是当写入前置条件,而 Binary / Truncated 两种情况本来就不许写——**空值即「这文件不可编辑」**, 前端不必再判一次,后端也不必为一个注定被拒的写入算哈希。

Size 是磁盘真实大小,不是 len(Content):截断时两者不同,而用户要看到的是真实大小。

type FileWriteReq added in v0.3.0

type FileWriteReq struct {
	Content    string `json:"content"`
	BaseSHA256 string `json:"base_sha256"`
}

FileWriteReq 是 PUT /api/workspaces/file 的请求体。

BaseSHA256 必填:它是调用方**读到那一版**的哈希,服务端拿它与磁盘现状比对, 不一致就 409。空串一律判为不匹配——没读过就想写,正是覆盖别人改动的场景。

type FileWriteResp added in v0.3.0

type FileWriteResp struct {
	SHA256 string `json:"sha256"`
	Size   int64  `json:"size"`
}

FileWriteResp 是写入成功后的响应。

SHA256 是**新内容**的哈希,调用方直接拿它当下一次写入的 base_sha256, 不需要为了拿新基线再读一次。

type FlowDetail added in v0.3.9

type FlowDetail struct {
	Name    string    `json:"name"`
	Version int       `json:"version"`
	Nodes   []NodeDef `json:"nodes"`
	States  []string  `json:"states"`
}

FlowDetail 是工作流详情的 wire DTO。

type FootprintResp

type FootprintResp struct {
	Rows  []FootprintRow `json:"rows"`
	Usage *ProcUsage     `json:"usage,omitempty"`

	// Pty 是终端会话的足迹。会话只在内存里,所以这一段与 Rows 不同——
	// 它不含历史,列出的都是此刻活着的会话。
	Pty []PtyFootprintRow `json:"pty,omitempty"`
}

FootprintResp 是 GET /api/footprint 的响应:全部任务(含已归档)的足迹体检。

type FootprintRow

type FootprintRow struct {
	TaskID  string `json:"task_id"`
	Name    string `json:"name"`
	State   string `json:"state"`
	Procs   int    `json:"procs"`
	Verdict string `json:"verdict"`
}

FootprintRow 是一个任务的进程足迹体检结果。

注意:Verdict 恒非空。判不出结论时给 leader_reuse / no_credential,而不是 把 Procs 抹成 0 了事——「没有残留」与「我们不敢下结论」是两回事,后者需要 人工看一眼,前者不需要。

type Frame added in v0.3.0

type Frame struct {
	// Seq 是任务内单调递增的帧号,从 1 开始。与 Event.Seq 无关(见文件头)。
	Seq int64 `json:"seq"`
	// TS 是帧产生时刻。
	TS time.Time `json:"ts"`
	// Turn 是回合序号,从 1 开始。
	Turn int `json:"turn"`
	// Type 决定下面哪些字段有意义。
	Type FrameType `json:"type"`

	// Part 标识帧所属的片段:text/reasoning 靠它拼接,tool_call/tool_result
	// 靠它配对。只需在**同一回合内**唯一,跨回合可以重复。
	Part string `json:"part,omitempty"`

	// Delta 是 text / reasoning 的文本增量(不是快照)。
	Delta string `json:"delta,omitempty"`

	// Tool 是 tool_call 的工具名。
	Tool string `json:"tool,omitempty"`
	// Input 是 tool_call 的入参,可能被头尾截断。
	Input string `json:"input,omitempty"`
	// Output 是 tool_result 的输出,可能被头尾截断。
	Output string `json:"output,omitempty"`
	// Status 是 tool_result 的结局(ok / error / 上游原文)。
	Status string `json:"status,omitempty"`

	// Truncated 报告 Input/Output 是否被截断。
	Truncated bool `json:"truncated,omitempty"`
	// Bytes 是截断前的原始字节数(未截断时为 0)。
	Bytes int64 `json:"bytes,omitempty"`

	// RefSeq 是 event 帧指向的 events 表 seq。
	RefSeq int64 `json:"ref_seq,omitempty"`
	// Event 是 event 帧的事件类型名。刻意的小冗余:让前端不查 events 表
	// 也知道该画什么形状的卡片,类型名是稳定的,不会漂移。
	Event string `json:"event,omitempty"`

	// Reason 是 turn_start 的起因:"dispatch"(Adapter.Start)或
	// "send"(Adapter.Send)。不细分"续接"与"回答提问"——Send 是单一方法,
	// adapter 分不出来,编出来的区分是假的。
	Reason string `json:"reason,omitempty"`
	// Instructions 是 turn_start(reason=send)携带的审核者指令原文——
	// continue 的修改指令或 reply 的应答文本。前端靠它渲染「审核者气泡」。
	// dispatch 回合恒为空;旧帧无此字段(前端按缺席处理,向后兼容)。
	// 不截断:这是人写的指令,长度天然有限;截了反而丢审阅依据。
	Instructions string `json:"instructions,omitempty"`
}

Frame 是一条结构化回合帧,对应 frames.jsonl 的一行。

字段按 Type 取用,无关字段一律 omitempty 缺席:

  • text / reasoning: Part + Delta
  • tool_call: Part + Tool + Input(可能 Truncated,Bytes 为原始长度)
  • tool_result: Part + Status + Output(同上)
  • event: RefSeq + Event
  • turn_start: Reason + Instructions(send 时)

type FrameType added in v0.3.0

type FrameType string

FrameType 是帧的类型。

const (
	// FrameText 是模型正文增量(按 Part 拼接)。
	FrameText FrameType = "text"
	// FrameReasoning 是思维链增量(按 Part 拼接)。绝不进回合正文。
	FrameReasoning FrameType = "reasoning"
	// FrameToolCall 是一次工具调用,一次性完整帧。
	FrameToolCall FrameType = "tool_call"
	// FrameToolResult 是一次工具结果,与 tool_call 靠同一个 Part 配对。
	FrameToolResult FrameType = "tool_result"
	// FrameEvent 是控制面事件的引用(只存指针与类型名,不复制 payload)。
	FrameEvent FrameType = "event"
	// FrameTurnStart 是回合边界。
	FrameTurnStart FrameType = "turn_start"
)

type Gate added in v0.3.9

type Gate struct {
	RequireAttachment   string `json:"require_attachment,omitempty"`
	RequireAcceptance   bool   `json:"require_acceptance,omitempty"`
	RequireChildrenDone bool   `json:"require_children_done,omitempty"`
}

Gate 是工作流节点进入条件的 wire DTO。

type LatestResp added in v0.3.3

type LatestResp struct {
	Tag       string `json:"tag"`
	CheckedAt string `json:"checked_at,omitempty"` // RFC3339;空=从未查过。
}

LatestResp 是 GET /api/update/latest 的响应。

Tag 是最新发布的版本号。空串表示查不出(限流、断网、缓存为空),消费方一律 按「没有新版」处理;通知是锦上添花,绝不能自己变成故障源。

type LedgerEvent added in v0.3.9

type LedgerEvent struct {
	Seq       int64           `json:"seq"`
	CardID    string          `json:"card_id"`
	Type      string          `json:"type"`
	Actor     string          `json:"actor"`
	Payload   json.RawMessage `json:"payload"`
	CreatedAt time.Time       `json:"created_at"`
}

LedgerEvent 是账本事件的 wire DTO。

type Machine added in v0.3.0

type Machine struct {
	// Name 为 ""=本机(与 tasks.target 的空串语义一致;UI 显示「本机」)。
	Name string `json:"name"`
	Addr string `json:"addr"`
	// Relay 是这台机器的 relay 节点名;空=直连形态。
	//
	// 为什么需要它:relay 形态与 addr 互斥,中继机器的 Addr 恒为空,界面上
	// 那张卡片会一个身份标识都没有。前端在 Addr 为空时用它显示「中继 · <node>」。
	Relay string `json:"relay,omitempty"`
	// Reachable=false 时 Error 必非空。
	Reachable bool   `json:"reachable"`
	Version   string `json:"version"`
	// Executors / DefaultExecutor 取自探活时读到的 GET /api/status,只读投影,
	// 不构成「机器配置面」——执行者开关等写操作不在 W3a 范围内。
	Executors       []string `json:"executors"`
	DefaultExecutor string   `json:"default_executor"`
	// ProbeMs 是本次探活实测往返毫秒;本机恒 0(进程内直查,不自拨 HTTP)。
	ProbeMs     int64  `json:"probe_ms"`
	ActiveTasks int    `json:"active_tasks"`
	Error       string `json:"error"`

	// PtySupported 是这台机器的 PTY 能力位,探活时从它的 StatusResp 投影而来。
	//
	// 三态,与 StatusResp.PtySupported 同一纪律:
	//   nil   = 没上报(对端版本过旧,或这台机器压根没探到)
	//   false = 平台明确不支持
	//   true  = 支持
	// 消费方(控制台)据此决定终端入口画什么。**nil 不许当 false 用**:
	// 那会让老版本 agentd 上的终端入口凭空消失,而它其实可能是能用的。
	PtySupported *bool `json:"pty_supported,omitempty"`

	// RevealSupported 是这台机器的「在访达中显示」能力位,探活时从它的
	// StatusResp 投影而来。三态与 PtySupported 同一纪律。
	RevealSupported *bool `json:"reveal_supported,omitempty"`

	// Upgrade 是这台机器最近一次升级的状态(本机恒缺席:本机版本走薄壳同步路)。
	// 缺席=本 agentd 进程内没发起过升级;读法见 MachineUpgrade 的三态说明。
	Upgrade *MachineUpgrade `json:"upgrade,omitempty"`

	// ScratchRoot 是这台机器的草稿区路径,探活时从它的 StatusResp 投影而来。
	// 空串(omitempty 后为缺席)= 这台机器不支持临时文件,前端不渲染入口。
	ScratchRoot string `json:"scratch_root,omitempty"`
}

Machine 是 GET /api/machines 的单台投影。

列表 = 本机 + cfg.Targets 全部条目;运行数据现场探活(并发、共 3s 预算)。 **不可达是数据不是错误**:单台超时/拒连不影响整个响应 200。

type MachineStatus added in v0.3.0

type MachineStatus struct {
	Name string `json:"name"`
	Ok   bool   `json:"ok"`
	// FetchedAt 是本机拿到该机数据的时刻(快照读则为快照时刻),UI 据此显示数据新旧。
	FetchedAt time.Time `json:"fetched_at"`
	// Error 是不可达原因原文,Ok=true 时为空串。
	Error string `json:"error"`
}

MachineStatus 是跨机汇总信封里每台机器的应答情况。

硬约束(W3a §5.3):任何一台机器没答上来,都必须出现在汇总响应的 machines 里且 Ok=false 带原因——静默少几行是本设计的头号失败模式。

type MachineUpgrade added in v0.3.4

type MachineUpgrade struct {
	Running bool `json:"running"`
	// Status 是终态:ok / skip / fail;从未跑完时为空。
	Status  string `json:"status,omitempty"`
	Verdict string `json:"verdict,omitempty"`
	// Reason / Remedy 原样透传 internal/upgrade 的结论,不在这里重新措辞。
	Reason string `json:"reason,omitempty"`
	Remedy string `json:"remedy,omitempty"`
	// From / To 仅在 Status==ok 时有值。
	From string `json:"from,omitempty"`
	To   string `json:"to,omitempty"`
}

MachineUpgrade 是一台执行机最近一次升级的状态,随 GET /api/machines 一起返回。

为什么需要它:升级**没有进度流**,这是刻意的——完成的判据就是这台机器的 version 变成了最新。但那只覆盖成功路径:失败时版本压根不会变,控制台按钮上的 「升级中」就永远清不掉,而后端其实早已放弃(真机实测:agentd 三分钟前就记下 「下载 checksums.txt 超时」,界面还在转)。这一段把**终态**交回控制台, 补的是出口,不是进度流。

三态读法:

nil            = 这台机器本进程内从未发起过升级(agentd 重启即回到 nil)
Running=true   = 正在升级;其余字段是上一轮的结果,可能全空
Running=false  = 已结束,Status 为终态

**nil 不许当「没失败」用**:它只说明这个 agentd 不知道,不说明没发生过。

type MachineUpgradeResp added in v0.3.3

type MachineUpgradeResp struct {
	// Accepted=true 时升级已在后台开始,进度靠 GET /api/machines 的 version 变化观察。
	Accepted bool   `json:"accepted"`
	Verdict  string `json:"verdict"`
	Reason   string `json:"reason,omitempty"`
	Remedy   string `json:"remedy,omitempty"`
	// Forcible 表示这次拒绝能不能被 ?force=1 越过。非托管永远 false。
	Forcible bool `json:"forcible"`
	Busy     int  `json:"busy,omitempty"`
}

MachineUpgradeResp 是 POST /api/machines/{name}/upgrade 的响应。

type MachinesResp added in v0.3.0

type MachinesResp struct {
	Machines []Machine `json:"machines"`
}

MachinesResp 是 GET /api/machines 的响应信封。

type MigrateCardReq added in v0.3.9

type MigrateCardReq struct {
	Workflow string `json:"workflow"`
	Status   string `json:"status"`
	Version  int    `json:"version,omitempty"`
}

MigrateCardReq 是显式目标工作流、落点列和可选版本的迁移请求。

type MigrateCardResp added in v0.3.9

type MigrateCardResp struct {
	OK    bool                 `json:"ok"`
	ID    string               `json:"id"`
	From  CardWorkflowLocation `json:"from"`
	To    CardWorkflowLocation `json:"to"`
	Event LedgerEvent          `json:"event"`
}

MigrateCardResp 是跨流迁移响应。

type NewCardReq added in v0.3.9

type NewCardReq struct {
	Title      string `json:"title"`
	Project    string `json:"project"`
	Workflow   string `json:"workflow,omitempty"`
	Priority   string `json:"priority,omitempty"`
	Parent     string `json:"parent,omitempty"`
	BaseBranch string `json:"base_branch,omitempty"`
}

NewCardReq 是建卡请求。workflow 缺席或为空表示尚未定性,由账本解析为 triage。

type NodeDef added in v0.3.9

type NodeDef struct {
	Name             string       `json:"name"`
	Template         string       `json:"template,omitempty"`
	Override         NodeOverride `json:"override,omitempty"`
	Dispatch         bool         `json:"dispatch,omitempty"`
	Verdict          bool         `json:"verdict,omitempty"`
	CarryCardContext bool         `json:"carry_card_context,omitempty"`
	MaxRounds        int          `json:"max_rounds,omitempty"`
	Next             string       `json:"next,omitempty"`
	OnFail           string       `json:"on_fail,omitempty"`
	Gate             Gate         `json:"gate,omitempty"`
	HumanBases       []string     `json:"human_bases,omitempty"`
}

NodeDef 是工作流节点的 wire DTO。

type NodeOverride added in v0.3.9

type NodeOverride struct {
	Executor   string `json:"executor,omitempty"`
	Discipline string `json:"discipline,omitempty"`
	Target     string `json:"target,omitempty"`
	Model      string `json:"model,omitempty"`
}

NodeOverride 是工作流节点模板覆盖的 wire DTO。

type ProcUsage

type ProcUsage struct {
	Used  int `json:"used"`
	Limit int `json:"limit"`
}

ProcUsage 是本机当前 uid 的进程占用与上限。

为什么两个数必须一起给:只看 Used 不知道离墙还有多远,只看 Limit 没有意义。 2026-08-12 devbox 整机 fork 瘫痪时,346/2666 这两个数并排才说明得了问题。

type ProjectBranch added in v0.3.0

type ProjectBranch struct {
	Name     string `json:"name"`
	Worktree string `json:"worktree"`
}

ProjectBranch 是一个本地分支,带「是否已被工作树占用」。

Worktree 为已检出该分支的工作树路径;空串 = 没有任何工作树占用它。 git 不允许同一分支被两个工作树同时检出,所以占用者就是「这个分支现在 不能再开树」的全部原因——界面据此把选项置灰并说清是谁占着。

type ProjectBranchesResp added in v0.3.0

type ProjectBranchesResp struct {
	// Branches 永不为 nil(空仓库返回空数组)。
	Branches []ProjectBranch `json:"branches"`
	// Default 是推导出的基准分支;推导不出为空串。
	Default string `json:"default"`
	// WorktreeRoot 是手工新建工作树的落点根目录,供界面如实回显「会建在哪」。
	// 界面只回显这个根,不自己拼完整路径——目录名的生成规则只有服务端一份。
	WorktreeRoot string `json:"worktree_root"`
}

ProjectBranchesResp 是 GET /api/projects/{name}/branches 的响应。

顶层形状(branches + default)与 /api/tasks/{id}/branches 一致,但 branches 是对象数组而非字符串数组——多了占用信息,两者刻意不共用类型。

type ProjectLocation

type ProjectLocation struct {
	ProjectID string    `json:"project_id"`
	Name      string    `json:"name"`
	Path      string    `json:"path"`
	OriginURL string    `json:"origin_url"`
	CreatedAt time.Time `json:"created_at"`
	Status    string    `json:"status,omitempty"`
}

ProjectLocation 是一条「项目 × 机器」位置记录:项目在**这一台**机器上的 那一个工作副本。

模型(B62):

  • 项目(project):一份代码的逻辑身份,与机器无关,由 ProjectID 标识
  • 位置(location):项目在某一台机器上的工作副本,由 Path 标识
  • ADR-0008:一台机器上一个项目**最多一个位置**,由 ProjectID 做主键强制

字段:

  • ProjectID: sha256(归一化 origin) 前 16 位;**纯函数派生**,每台机器各算 各的,同一个 origin 必然得到同一个值——跨机引用因此不需要任何协调
  • Name: 人可读引用(每台机器内唯一),由 origin 末段派生,冲突时补 -2; 只用于 --project <名字> 与 project rm <名字>,**不参与身份判定**
  • Path: 该机器上的绝对路径(登记时 Abs+Clean,且已归并到主工作树)
  • OriginURL: agentd 在该机器上**现读**的权威值,不采信调用方上送的字符串
  • CreatedAt: 登记时间
  • Status: project ls 时**现场探得**的实际状态("有效"/"路径不存在"/ "不是 git 仓库"),不落库,仅列表响应携带——它是登记与文件系统漂移的 可见化手段

type ProjectLocationNode added in v0.3.0

type ProjectLocationNode struct {
	// Machine 是该位置所在机器:""=本机;否则为本机 cfg.Targets 的键。
	Machine string `json:"machine"`
	// Name 是登记名(project_locations.name),每台机器内唯一,不参与身份判定。
	Name string `json:"name"`
	Path string `json:"path"`
	// Workspaces 是该位置下的全部工作树;探测失败时为空数组而非 null。
	Workspaces []Workspace `json:"workspaces"`
	// ProbeError 是探测失败的人话说明,空串=正常。
	//
	// 为什么失败不返回错误码:项目树必须能展示「登记还在、目录已失效」这种
	// 真实状态,整棵树 500 会让用户连哪个项目坏了都看不见。
	ProbeError string `json:"probe_error"`
}

ProjectLocationNode 是一个项目在**一台**机器上的位置(项目树的中间层)。

不变式(ADR-0008 / W3a §1.1):单机响应里每个项目的 locations 恒为 0 或 1 条; 长度 >1 只可能出现在 ?scope=all 的汇总结果里,此时每条的 Machine 互不相同。

type ProjectNode added in v0.3.0

type ProjectNode struct {
	// ProjectID 由 projectid.FromOrigin(origin_url) 派生,跨机天然相等。
	ProjectID string `json:"project_id"`
	OriginURL string `json:"origin_url"`
	// Name 取该项目下首条登记的 name(各机登记名可能不同,展示取其一)。
	Name      string                `json:"name"`
	Locations []ProjectLocationNode `json:"locations"`
}

ProjectNode 是项目树的顶层:一个跨机器同一的项目。

type ProjectTreeResp added in v0.3.0

type ProjectTreeResp struct {
	Projects []ProjectNode `json:"projects"`
	// Unowned 是算不出 project_id 的脏行(列出登记名),诚实列出不吞。
	Unowned  []string        `json:"unowned"`
	Machines []MachineStatus `json:"machines,omitempty"`
}

ProjectTreeResp 是 GET /api/projects/tree 的响应。

Machines 仅在 ?scope=all 时出现(单机请求不带这一栏,omitempty)。

type PtyControl added in v0.3.0

type PtyControl struct {
	Type      string `json:"type"`
	Since     uint64 `json:"since"`
	Truncated bool   `json:"truncated"`
	ExitCode  *int   `json:"exit_code,omitempty"`
	Message   string `json:"message,omitempty"`
	Cols      int    `json:"cols,omitempty"`
	Rows      int    `json:"rows,omitempty"`
}

PtyControl 是 /ws/pty 上双向共用的控制帧。

为什么一个结构体走两个方向:控制帧是低频路径(建连、退出、改尺寸), 拆成四个类型只会让两端各多三个分支。高频的数据路径**不经过它**—— PTY 字节走 binary 帧,零解析零 base64 膨胀(spec §5.3)。

Since / Truncated 刻意不带 omitempty:attached 帧里「从 0 开始」与 「没有截断」都是有意义的结论,缺键会让前端分不清「服务端说了 false」 和「服务端这版还不认识这个字段」。

type PtyFootprintRow added in v0.3.0

type PtyFootprintRow struct {
	ID         string `json:"id"`
	BasePath   string `json:"base_path"`
	PID        int    `json:"pid"`
	Procs      *int   `json:"procs,omitempty"`
	Foreground bool   `json:"foreground"`
}

PtyFootprintRow 是一个终端会话的足迹体检结果。

Procs 为指针:数不出来(平台不支持枚举)时是 nil,**不是 0**——与 ProcUsage 同一条理由,0 看起来像结论。

type PtySession added in v0.3.0

type PtySession struct {
	ID string `json:"id"`
	// Machine 是**线注解,不入库**:""=本机,否则为汇总方 cfg.Targets 的键,
	// 由汇总方盖章。与 Task.Machine 同款。
	Machine   string    `json:"machine"`
	BasePath  string    `json:"base_path"`
	BaseKind  string    `json:"base_kind"` // "workspace" | "home"
	Shell     string    `json:"shell"`
	CreatedAt time.Time `json:"created_at"`
	Cols      int       `json:"cols"`
	Rows      int       `json:"rows"`
	Attached  int       `json:"attached"`
	// Foreground 表示会话里有命令跑在前台。控制台据此决定关 tab 时要不要先确认
	//(spec §6.2)。**不带 omitempty**:false 是一个有意义的结论(「空闲,随便关」),
	// 缺键会让前端分不清它和「这版服务端还不认识这个字段」。
	Foreground bool `json:"foreground"`
	PID        int  `json:"pid"`
	ExitCode   *int `json:"exit_code,omitempty"`
	// Incompatible 表示会话由协议不兼容的旧版本托管:进程还活着,但本版接不进去,
	// 前端只能给出「重开一个终端」的出口。不带 omitempty,false 也是明确结论。
	Incompatible bool `json:"incompatible"`
	// BytesOut 是该会话累计输出的字节数,也是 /ws/pty 的 since 水位。
	BytesOut uint64 `json:"bytes_out"`
}

PtySession 是一个终端会话的线格式投影。

ExitCode 用指针表达三态里的两态:**缺席 = 还活着**,出现 = 已退出且这是退出码。 与 StatusResp.Update / StatusResp.Proc 同一纪律——不用 0 或 -1 冒充「不知道」。

type PtySessionsResp added in v0.3.0

type PtySessionsResp struct {
	Sessions []PtySession    `json:"sessions"`
	Machines []MachineStatus `json:"machines,omitempty"`
}

PtySessionsResp 是 GET /api/pty/sessions 的响应。 Machines 仅在 ?scope=all 时出现,形状与 ProjectTreeResp 一致。

type PullState added in v0.3.0

type PullState struct {
	Tag   string `json:"tag"`
	Stage string `json:"stage"` // PullStage* 之一
	// Error 是 Stage=failed 时的原文。**必须带原文**:调用方拿到它才能
	// 直接看到 "proxyconnect tcp: dial tcp 127.0.0.1:1080: connection refused"
	// 这种一眼定位的信息,而不是一句「版本仍是 X」
	Error     string    `json:"error,omitempty"`
	StartedAt time.Time `json:"started_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

PullState 是一次自拉换版的进度与结局。

type ReclaimAction

type ReclaimAction string

ReclaimAction 是一次回收实际做了什么。

const (
	// ReclaimRemoved 走 git worktree remove 删掉了。
	ReclaimRemoved ReclaimAction = "removed"
	// ReclaimPruned 走 git worktree prune 清掉了在册条目(remove 失败后的兜底)。
	ReclaimPruned ReclaimAction = "pruned"
	// ReclaimAlreadyAbsent 本来就不在册,无动作。幂等成功走这条。
	ReclaimAlreadyAbsent ReclaimAction = "already_absent"
)

type ReclaimError

type ReclaimError struct {
	Error  string        `json:"error"`
	Reason ReclaimReason `json:"reason"`
	// Dirty 仅在 Reason=dirty 时非空,是结构化清单而非预渲染文本——
	// 渲染是 CLI 的事。
	Dirty []DirtyFile `json:"dirty,omitempty"`
}

ReclaimError 是 409 的响应体。

type ReclaimListResp

type ReclaimListResp struct {
	// Rows 只含「仍有残留或判不出」的行;干净收场的任务不入表。
	Rows []ReclaimRow `json:"rows"`
	// Scanned 是本次体检过的终态任务总数,供 CLI 打「共体检 N 个」。
	Scanned int `json:"scanned"`
}

ReclaimListResp 是 GET /api/reclaim 的响应。

type ReclaimReason

type ReclaimReason string

ReclaimReason 是 409 拒绝的机器码。

为什么必须有:四种拒绝共用 409 一个状态码,CLI 要分派渲染就只能靠它。 靠解析中文文案是不行的——文案是给人看的、会改,机器码是契约、不改。

const (
	ReasonNotTerminal     ReclaimReason = "not_terminal"
	ReasonDirty           ReclaimReason = "dirty"
	ReasonRepoUnreachable ReclaimReason = "repo_unreachable"
	ReasonNotManaged      ReclaimReason = "not_managed"
)

type ReclaimResp

type ReclaimResp struct {
	Removed bool          `json:"removed"`
	Action  ReclaimAction `json:"action"`
	WorkDir string        `json:"work_dir"`
	Branch  string        `json:"branch"`
	// Discarded 是 force 强删时被丢弃的条目。留痕用:强删不能悄悄发生。
	Discarded []DirtyFile `json:"discarded,omitempty"`
}

ReclaimResp 是 POST /api/tasks/{id}/reclaim 成功时的响应。

type ReclaimRow

type ReclaimRow struct {
	TaskID string `json:"task_id"`
	Name   string `json:"name"`
	// State 是任务状态(completed / failed),不是工作树状态。
	State   string `json:"state"`
	Branch  string `json:"branch"`
	WorkDir string `json:"work_dir"`
	// Worktree 是工作树状态,取值见 WorktreeState。
	Worktree WorktreeState `json:"worktree"`
	// DirtyCount 仅在 Worktree=dirty 时有意义。列表只给条数不给清单——
	// 清单可能很长,要看细节走单任务回收的 409 响应。
	DirtyCount int `json:"dirty_count"`
	// Note 是 Worktree=unknown / prunable 时的真因,供人读。
	Note string `json:"note,omitempty"`
}

ReclaimRow 是 GET /api/reclaim 列表里的一行。

type Relation added in v0.3.9

type Relation struct {
	From, To, Type string
	CreatedAt      time.Time
}

Relation 是账本关系边的 wire DTO。 PascalCase 键是现有线格式,刻意不加 JSON tag 以保持兼容。

type RenameWorkspaceEntryReq added in v0.3.0

type RenameWorkspaceEntryReq struct {
	NewName string `json:"new_name"`
}

RenameWorkspaceEntryReq 是 PATCH /api/workspaces/entry 的请求体。

type SearchHit added in v0.3.0

type SearchHit struct {
	Rel  string `json:"rel"`
	Line int    `json:"line"`
	Text string `json:"text"`
}

SearchHit 是搜索命中的一行(GET /api/workspaces/search)。

type SearchResult added in v0.3.0

type SearchResult struct {
	Hits      []SearchHit `json:"hits"`
	Truncated bool        `json:"truncated"`
}

SearchResult 是搜索的完整结论。

type SessionInfo added in v0.3.0

type SessionInfo struct {
	ID         string     `json:"id"`
	DeviceName string     `json:"device_name"`
	CreatedAt  time.Time  `json:"created_at"`
	ExpiresAt  time.Time  `json:"expires_at"`
	LastSeenAt time.Time  `json:"last_seen_at"`
	RevokedAt  *time.Time `json:"revoked_at"`
}

SessionInfo 是 GET /api/auth/sessions 的单条会话。

注意:不含任何凭据字段——cookie 哈希都不给,展示与吊销只需要 id

type SpendEntry added in v0.3.0

type SpendEntry struct {
	Key          string
	InputTokens  int
	CachedTokens int
	OutputTokens int
	CostTicks    int64
	// CostState 只能是 CostReported / CostEstimated / CostUnknown 三者之一。
	CostState CostState
}

SpendEntry 是一条待入账的消耗(adapter 产出,store 消费)。

Key 必须在同一个任务内**稳定且唯一**——它是幂等的全部依据。同 Key 重复上报 按**覆盖**处理(不是累加),所以流式增长的值可以放心重复报: opencode 对同一条 message 会随生成推很多次、id 相同而 tokens 在涨, 覆盖天然取到最终值;重复推同值则是无操作。

type StatusResp

type StatusResp struct {
	Version BuildInfo `json:"version"`
	Listen  string    `json:"listen"`

	// ListenAux 是 loopback 辅助监听地址(B85):Listen 为单网卡 IP 时 agentd
	// 额外监听 "127.0.0.1:<同端口>",本机 CLI 的确定性改写拨的就是它。
	// 空 = 无辅助监听(Listen 为 loopback/通配,或对端是老 agentd)。
	ListenAux string `json:"listen_aux,omitempty"`

	DataDir string `json:"data_dir"`
	// ScratchRoot 是草稿区的绝对路径(<DataDir>/scratch),控制台浮窗的临时文件
	// 落在这里。**缺席 = 这台机器不支持临时文件**(老 agentd,或目录建不出来)。
	//
	// 与 PtySupported 那种能力位的三态纪律不同:那里 nil 要按「不知道,放行」处理,
	// 而这里缺的是一个**路径**——没有路径就没法发请求,放行只会换来一次必然 400。
	ScratchRoot     string         `json:"scratch_root,omitempty"`
	StartedAt       time.Time      `json:"started_at"`
	Executors       []string       `json:"executors"`
	DefaultExecutor string         `json:"default_executor"`
	TaskCounts      map[string]int `json:"task_counts"`
	Active          []ActiveTask   `json:"active"`

	// StallTimeout 是 agentd 看门狗判定「卡住」的空闲阈值,形如 "2h0m0s"
	//(time.Duration.String())。空串 = 对端未提供。
	//
	// 为什么要外露:wait --follow 的 --timeout 若不大于它,两个计时器同时到点时
	// 客户端的 124 会抢在 agentd 的 stalled 前面退出进程,把一次带 last_seq 和
	// idle 时长的**诊断**降级成一句「我没收到东西」——协调者拿到的信息严格更少。
	StallTimeout string `json:"stall_timeout,omitempty"`

	// Update 是自动更新状态。**指针 + omitempty**:老版本 agentd 不发这个字段,
	// 消费方拿到 nil 就该什么都不显示,而不是显示一个「未托管、无待命」的假状态
	Update *UpdateStatus `json:"update,omitempty"`

	// Proc 是本机 uid 级的进程占用与上限。指针 + omitempty:老 agentd 不发这个
	// 字段,消费方拿到 nil 应当什么都不显示,而不是显示一个「0/0」的假状态。
	Proc *ProcUsage `json:"proc,omitempty"`

	// PtySupported 报告本机 agentd 是否支持 PTY 终端。
	//
	// 三态,与 Update / Proc 同一纪律:
	//   缺席(nil) = 对端 agentd 太老,没上报这个字段——**不许当成 false**
	//   false     = 平台不支持(Windows:ConPTY 是另一套 API,本轮不假装支持)
	//   true      = 支持
	// 前端据此决定画真终端、画「这台机器不支持」还是画「对端版本过旧,未上报」。
	PtySupported *bool `json:"pty_supported,omitempty"`

	// RevealSupported 报告本机 agentd 是否支持「在访达中显示」(B108)。
	//
	// 三态与 PtySupported 逐字相同:
	//   缺席(nil) = 对端 agentd 太老,没上报这个字段——**不许当成 false**
	//   false     = 平台不支持(只有 macOS 有 `open -R` 这个语义)
	//   true      = 支持
	//
	// 注意:这只是**平台**支持度。真能不能揭示还要看调用方是不是从回环来的
	//(远程浏览器点了会在 agentd 那台机器的桌面上弹窗,没人看得见),那一层
	// 由端点自己判,不进能力位——它是每请求的属性,不是机器的属性。
	RevealSupported *bool `json:"reveal_supported,omitempty"`

	// PtySessions 是当前活着的终端会话数。指针 + omitempty,与 Proc 同一纪律:
	// nil = 对端没上报,渲染时整行不打印;0 = 确实一个都没有。
	//
	// 为什么 status 只给个数、不给每个会话占多少进程:数进程要枚举全机进程,
	// 而 status 有「不能变成慢命令」的硬纪律。进程数在 /api/footprint 里给。
	PtySessions *int `json:"pty_sessions,omitempty"`
}

StatusResp 是 GET /api/status 的响应。

注意:TaskCounts 的六个状态键恒存在,计数为零也出现——缺键与零值对消费方 是两回事。

type Task

type Task struct {
	ID              string    `json:"id"`
	Target          string    `json:"target"`
	RepoPath        string    `json:"repo_path"`
	Branch          string    `json:"branch"`
	PlanPath        string    `json:"plan_path"`
	PlanSummary     string    `json:"plan_summary"`
	ExecutorSession string    `json:"executor_session"`
	State           TaskState `json:"state"`
	CreatedAt       time.Time `json:"created_at"`
	UpdatedAt       time.Time `json:"updated_at"`
	// Name 是任务的展示名(dispatch --name 或从 plan/prompt 派生)。
	Name string `json:"name"`
	// Executor 是任务选择的执行者(dispatch --executor);空=缺省执行者(老任务兼容)。
	Executor string `json:"executor"`
	// Model 是任务级模型覆盖(dispatch --model);空=executor 自身默认。
	Model string `json:"model"`
	// Discipline 是本任务实际注入的纪律块来源标注(如「内置:single-context」)。
	// **不落盘**:它只在派发响应里回显给协调者,agentd 重启后为空。
	//
	// 为什么要回显:配置化把纪律块从 plan 文件里拿走后,写 plan 的人再也看不见它,
	// dispatch 必须当场把「这次注入的是哪块」说出来;CLI 拿到的就是这个对象。
	Discipline string `json:"discipline,omitempty"`
	// DisciplineName 是派发时点名的纪律块角色名(如 review);空=按 executor 兜底。
	// 该列后加,老任务为空——空是有意义的取值(走兜底),不回填、不编造。
	//
	// 为什么必须落盘:resumeForContinue 与 ResumeTask 只拿得到 executor 名,
	// 不落盘的话一次 continue 或一次 agentd 重启就会让点名的任务静默退回兜底块,
	// 而且首回合是对的,事后极难查。
	DisciplineName string `json:"discipline_name,omitempty"`
	// WorkDir 是任务工作区目录。空=原地模式(工作区即 RepoPath,由 Workdir() 统一回退)。
	// 审阅命令(diff/fetch/run)与 executor 的 cwd 都从这里取值,不得直接读 RepoPath。
	WorkDir string `json:"work_dir"`
	// WorktreeManaged 表示 WorkDir 是 agentd 创建的 worktree,任务完成(done)时由
	// agentd 负责删除;用户自带 worktree(Worktree=false)或原地模式均不受管理。
	WorktreeManaged bool `json:"worktree_managed"`
	// BaseCommit 是本任务新分支的**实际起点**(40 位 sha);空=切已存在分支
	// (没有起点这回事)或老任务(该列后加,不回填、不编造)。
	// 它回答的是「这个任务建在哪个提交上」——B35 之前这个问题无处可问。
	BaseCommit string `json:"base_commit"`
	// BaseAhead 是派发当时任务仓库 HEAD 领先 BaseCommit 的提交数:这些提交
	// 不在任务分支里。0 表示起点就是仓库 HEAD,或该数字当时没能算出来。
	BaseAhead int `json:"base_ahead"`
	// RepoDirtyCount 是派发当时任务仓库未提交改动的**总数**(含未跟踪文件);
	// 0=干净,或本次不是 managed(--new-worktree)模式。这些改动不在新工作树
	// 里,executor 看不到它们。
	RepoDirtyCount int `json:"repo_dirty_count"`
	// RepoDirtyFiles 是上述改动的文件名展示串(逗号分隔,封顶 5 个,超出补
	// 「等 N 处」);服务端截断后的展示用字段,与 PlanSummary 同形,不供程序消费
	//(要精确条数请读 RepoDirtyCount)。
	RepoDirtyFiles string `json:"repo_dirty_files"`
	// DoneNote 是归档时协调者留下的完成说明(handoff done --note);空串=未留说明
	// 或该任务归档于本功能上线之前。它回答的是「这次到底做完了什么、为什么放行」——
	// 归档之后除了一个 completed 状态位,此前没有任何地方记录这件事。
	DoneNote string `json:"done_note"`
	// ActualModel 是 executor 报回的**实际**模型名;空=执行者还没报(回合未
	// 开始)或该任务跑在不报模型名的旧版执行者上。
	//
	// 它与 Model 是两件事:Model 是 dispatch --model 发下去的**入参**(常为空,
	// 意思是「用执行者自己的默认」),ActualModel 是执行者实际在用的那个。
	// 二者不一致时以 ActualModel 为准,界面不并列显示。
	ActualModel string `json:"actual_model,omitempty"`
	// Usage 是当前 context 占用;nil=还没有任何一次模型调用完成。
	Usage *Usage `json:"usage,omitempty"`
	// Cumulative 是任务的累计消耗;nil = 没有任何账目(或本次是列表读取,
	// 列表不填充——见 Store.ListTasks)。与 Usage 是两个口径,别混。
	Cumulative *Cumulative `json:"cumulative,omitempty"`
	// Machine 是这条任务所在的机器:""=本机;否则为**本机** cfg.Targets 的键。
	//
	// 线注解,不入库(存储层不读不写这一列):它由汇总方在响应时盖章,
	// 语义是「我从哪个 target 拉来的」。
	//
	// 为什么不复用 Target:`target` 存的是「当年派发它的那个 CLI 管这台机器叫
	// 什么」——换一台笔记本、换一份配置派发,同一台机器可以叫不同名字,它是
	// 历史记录不是路由键。透明路由与 UI 的机器筛选必须锚在本机配置上。
	Machine string `json:"machine"`
	// ProjectID 是任务的项目归属:读时按 repo_path 与 project_locations.path
	// 等值 join 得到(W3a §1.3),未归属为 ""。
	//
	// 线注解,不入库:tasks 表不加这一列——历史任务或已注销项目的任务应当
	// 诚实显示「未归属」,而不是一列陈旧数据说谎。
	ProjectID string `json:"project_id"`
}

Task 表示一个 handoff 任务。

JSON 线格式契约(CLI wait/tasks/attach 输出与 server WS/REST 共用此结构, key 必须小写——上层脚本按 {"id":..,"state":..,"created_at":..} 解析)。

func (*Task) Workdir

func (t *Task) Workdir() string

Workdir 返回 executor cwd 与审阅命令的统一取值点:WorkDir 非空返回它 (worktree 模式),否则返回 RepoPath(原地模式)。

注意:所有需要「任务在哪个目录工作」的代码必须走本方法,直接读 RepoPath 会在 worktree 模式下拿到错误的工作目录。

type TaskPlan added in v0.3.0

type TaskPlan struct {
	Name      string `json:"name"`
	Content   string `json:"content"`
	Size      int64  `json:"size"`
	Truncated bool   `json:"truncated,omitempty"`
}

TaskPlan 是 GET /api/tasks/{id}/plan 的响应:**派发当刻交给 executor 的指令原文**。

它就是 agentd 归档在任务目录里的那份 plan/prompt(dispatch 的 plan 文件, prompt-only 派发时是 prompt.md;两者都有时是拼好的那一份)。控制台把它当 「第一条审核者消息」展示——在此之前,界面上唯一能看到的只有一个截断的 plan_summary,「这个任务当初到底被要求做什么」无处可查。

Size 是磁盘真实大小,不是 len(Content):截断时两者不同,用户要看到的是真实大小。

type TaskState

type TaskState string

TaskState 表示任务所处状态。

const (
	TaskStatePending       TaskState = "pending"
	TaskStateRunning       TaskState = "running"
	TaskStateWaitingAnswer TaskState = "waiting_answer"
	TaskStateWaitingReview TaskState = "waiting_review"
	TaskStateCompleted     TaskState = "completed"
	TaskStateFailed        TaskState = "failed"
)

func (TaskState) IsTerminal

func (s TaskState) IsTerminal() bool

IsTerminal 报告该状态是否为终态(completed / failed)。

type TaskStateRow added in v0.3.9

type TaskStateRow struct {
	Target, TaskID, Purpose, LastType string
	LastSeq                           int64
}

TaskStateRow 是卡上挂账任务的镜像状态摘要 wire DTO。 PascalCase 键是现有线格式,刻意不加 JSON tag 以保持兼容。

type TaskView

type TaskView struct {
	Task
	// Watchers 是当前订阅该任务事件流的连接数(几个协调者在听)。
	// 0 不一定是异常:waiting_review 与终态本来就不需要有人盯,判据见
	// handoff status 的 unattended。
	Watchers int `json:"watchers"`
}

TaskView 是 Task 的 API 视图:任务本体 + 不落库的运行态。

为什么用嵌入而不是给 Task 加字段:Watchers 是 agentd 内 Hub 的瞬时状态, 与任务的持久身份无关。加进 Task 会让存储层背一个它不该知道的概念,迟早有人 把它写进 SQLite;嵌入则让存储结构保持纯粹,同时 JSON 字段提升后线格式与旧版 逐字节兼容——只多一个 watchers 键,老客户端解码不受影响。

注意:Watchers 是服务端应答那一刻的快照,不做任何时效承诺。

type TasksResp added in v0.3.0

type TasksResp struct {
	Machines []MachineStatus `json:"machines"`
	Tasks    []TaskView      `json:"tasks"`
}

TasksResp 是 GET /api/tasks?scope=all 的跨机汇总响应(W3a §5.3)。

注意:不带 scope=all 的 GET /api/tasks 仍返回**裸数组** []TaskView, 与 W2 契约逐字节不变——汇总是另一种形状,不能改写既有端点的响应形态。

Tasks 里的远端条目取自 mirror_tasks 快照(不现场扇出),其 Machine 字段 由本机 agentd 盖章;本机条目 Machine 为空串。

type Ticket

type Ticket struct {
	ID         string          `json:"id"`
	TaskID     string          `json:"task_id"`
	Kind       string          `json:"kind"`
	Request    json.RawMessage `json:"request"`
	Answer     *string         `json:"answer"`
	CreatedAt  time.Time       `json:"created_at"`
	AnsweredAt *time.Time      `json:"answered_at"`
	// DeliveredAt 是应答送达 executor 的时刻;非 nil 才代表 executor 真的收到了。
	// 与 AnsweredAt 分开记录:「协调者已裁决」与「裁决已送达」是两件事实,
	// 合并会让中继失败后无从判断该不该重投(见 Manager.RecoverStuck)。
	DeliveredAt *time.Time `json:"delivered_at"`
	// Fingerprint 是 gate 工单的裁决指纹:权限描述全文的 sha256 十六进制串。
	// 它让「协调者是不是已经就同一件事表过态」成为一次索引查询而不是全表扫文本。
	// ask 工单不参与复用,留空。
	Fingerprint string `json:"fingerprint"`
}

Ticket 表示一次需人工介入的请求,Kind 取 "gate"(许可门)或 "ask"(提问)。

JSON 线格式契约(attach 输出 pending_tickets 与 REST 响应共用此结构, key 必须小写):{"id":..,"task_id":..,"kind":..,"request":{..},"answer":..,..}。

type UpdateError

type UpdateError struct {
	Error  string `json:"error"`
	Reason string `json:"reason,omitempty"`
}

UpdateError 是换版被拒的响应体。

Reason 为空表示这次失败不属于上面两道闸(参数错、校验不过、自检不过等), 此时消费方**不该编处置建议**,只报原始错误原文。

type UpdateResp

type UpdateResp struct {
	OK      bool   `json:"ok"`
	Version string `json:"version,omitempty"` // 换上的版本;纯重启模式为空
	Prev    string `json:"prev,omitempty"`    // 旧二进制留存路径,回滚要用

	// Accepted 表示这次请求只是被**受理**,换版还没发生(自拉模式,202)。
	// 与 Restarted 的区别是时态:Restarted 说"我这就重启",Accepted 说
	// "我开始下载了,结果去 status 里看"。调用方据此决定是直接等版本号变,
	// 还是要一路盯着 pull_state
	Accepted bool `json:"accepted,omitempty"`

	Restarted bool `json:"restarted"`
}

UpdateResp 是换版成功的响应。

Restarted 恒为 true——接口返回 200 就意味着 agentd 随后会触发优雅关停。 保留这个字段是为了让消费方读代码时不必去猜「返回之后还会发生什么」。

type UpdateStatus

type UpdateStatus struct {
	Managed bool `json:"managed"`

	// Pull 表示对端支持自拉换版。
	//
	// 为什么是指针:nil 表示**对端没给这个字段**(老 agentd 不上报),与
	// 「对端说 false」是两回事。这条区分是选路判据——老 agentd 收到
	// mode=pull + 空 body 会掉进「纯重启」分支并回 200,CLI 若据此以为
	// 受理了,就会干等到超时报「已换版但新进程未上线」,一次纯误导。
	// 与同结构族里 BuildInfo.Platform 空串、ActiveTask.Watchers *int 同款纪律。
	Pull *bool `json:"pull,omitempty"`

	// PullState 是最近一次自拉换版的状态,仅存内存、不落盘。
	//
	// 为什么没有 done 态:成功路径的终点是**进程重启**,状态自然消失——
	// 而那时 status 报的版本号已经变了,调用方靠版本号就能确认。一个落盘的
	// done 会在下次启动时变成误导性的陈旧数据。失败时进程不重启,状态留在
	// 内存里可查,这正是需要它的场合。
	PullState *PullState `json:"pull_state,omitempty"`
}

UpdateStatus 是这台 agentd 与「换版」有关的状态。

字段说明:

  • Managed: 当前 agentd 进程是不是被进程管理器(systemd / launchd)拉起的。 **false 时换版被硬拒绝**——换完 exit(0) 之后没人拉起,这台机器上就此 没有 agentd 在跑,且没有任何信号告诉任何人。`--force` 也不越过这一条
  • Pull: 本 agentd 支持「自拉换版」(POST /api/update?mode=pull)
  • PullState: 最近一次自拉的状态;nil = 本进程还没自拉过,或上一次已成功 (成功的终点是重启,状态随进程一起消失)

为什么没有「待命版本」了:B59 取消了「下载完等空闲窗口再换」的自主决策, 换版由操作者一条命令触发并当场完成,中间不存在待命态(见 B59 spec D1)。

type Usage added in v0.3.0

type Usage struct {
	// ContextTokens 是当前 context 占用的 token 数。永远 > 0——取不到时整个
	// Usage 为 nil,不用 0 冒充「没用 token」(B69/B70 纪律)。
	ContextTokens int `json:"context_tokens"`
	// ContextWindow 是该模型的上下文窗口上限(百分比的分母)。
	// nil = 该 executor 不在协议里报窗口(claudecode / opencode),此时界面
	// 只显绝对值。**绝不由 handoff 猜**:猜错是静默错误,百分比照常显示只是错的。
	ContextWindow *int `json:"context_window,omitempty"`
}

Usage 是任务当前的 context 占用快照。

「当前占用」= 最后一次模型调用的输入侧(含缓存命中),**不是**回合或会话的 累加。两者差别巨大:实测一个 4 次模型调用的 grok 回合,累加值是真实占用的 4 倍,且工具调用越多越离谱,长回合会超过 100%(探针笔记 §4.2)。

边界:本结构只描述「占用」,不描述「消耗」。累计 token 与花费是另一个口径, 将来以新增字段的形式加进来,形状不变、不需要重新设计。

type WorkbenchBase added in v0.3.5

type WorkbenchBase struct {
	BaseKey string `json:"base_key"`
	// Payload 是前端序列化好的 JSON 字符串(布局 + 基准目录元数据)。
	Payload string `json:"payload"`
	// UpdatedAt 是毫秒时间戳。毫秒而非秒:淘汰按它排序,秒级精度下同秒写入的
	// 多行并列,裁掉哪一条就成了随机的。
	UpdatedAt int64 `json:"updated_at"`
}

WorkbenchBase 是一个基准目录的持久化状态行。

type WorkbenchBaseReq added in v0.3.5

type WorkbenchBaseReq struct {
	BaseKey string  `json:"base_key"`
	Payload *string `json:"payload"`
}

WorkbenchBaseReq 是 PUT /api/workbench/state/base 的请求体。

Payload 用指针表达三态里的两态:**取 null = 删除该行**,否则是要写入的内容。 为什么不用空串当删除信号:空串是一个合法但无意义的 payload,用它当信号会让 「前端 bug 发了个空串」静默变成「删掉用户的布局」。

type WorkbenchDockReq added in v0.3.5

type WorkbenchDockReq struct {
	Payload *string `json:"payload"`
}

WorkbenchDockReq 是 PUT /api/workbench/state/dock 的请求体。 Payload 取 null = 清空悬浮窗现场,语义同 WorkbenchBaseReq.Payload。

type WorkbenchSelectedReq added in v0.3.5

type WorkbenchSelectedReq struct {
	BaseKey string `json:"base_key"`
}

WorkbenchSelectedReq 是 PUT /api/workbench/state/selected 的请求体。 BaseKey 为空串表示「当前没有选中任何目录」,这是合法状态,会落库成空串。

type WorkbenchStateResp added in v0.3.5

type WorkbenchStateResp struct {
	Selected string          `json:"selected"`
	Dock     string          `json:"dock"`
	Bases    []WorkbenchBase `json:"bases"`
}

WorkbenchStateResp 是 GET /api/workbench/state 的响应。

Selected / Dock 没有内容时是**空串**而不是缺键:两者都是「当前没有」这个 明确结论,缺键会让前端分不清它和「这版服务端还不认识这个字段」。

type Workspace added in v0.3.0

type Workspace struct {
	Path string `json:"path"`
	// Branch 是该工作树当前所在分支;detached HEAD 时为空串,此时看 Head。
	Branch string `json:"branch"`
	// Head 是短 sha。
	Head   string `json:"head"`
	IsMain bool   `json:"is_main"`
	// Managed 表示该工作树落在 agentd 的数据区(<DataDir>/worktrees)下——
	// 既包括任务自建树(worktrees/<id8>),也包括手工新建树(worktrees/manual/<名>)。
	// 判据只看路径前缀,不区分二者:本字段没有任何行为消费者(回收只认终态任务
	// 的记录、从不扫目录),为它加特例只会留下一个要读三处代码才懂的例外。
	Managed bool `json:"managed"`
	// CreatedAt 是这个工作树被建出来的时间;零值 = 取不到。
	//
	// 取法:stat <git 公共目录>/worktrees/<名>/gitdir。那个文件由
	// git worktree add 写一次之后就不再动,是唯一稳定的创建时间证据。
	// 刻意不 stat 工作树目录本身——它的 mtime 会随着往里写代码变化,
	// 排出来的是「最近动过」而不是「什么时候建的」。
	//
	// **主工作树恒为零值**:它没有 worktrees/<名>/gitdir,而 .git 目录的 mtime
	// 是「最后一次在里面增删条目」不是创建时间(实测一个 08-07 建的仓库报出
	// 08-18)。准确的答案要文件系统 birthtime,Go 标准库不给。消费方(控制台
	// 排序)把主工作树钉在第一位、不参与比较,这个值没有消费者——如实留零值,
	// 好过报一个自信的错值。
	//
	// 为什么取不到时留零值而不是报错:整棵项目树不该因为一个 stat 失败就 500。
	// 消费方把零值当「最旧」处理。
	CreatedAt time.Time `json:"created_at"`
}

Workspace 是一个 git 工作树(含主工作树自身)。

来源:`git -C <location.path> worktree list --porcelain` 现场探测,不落库—— worktree 会在 agentd 背后被 add/remove,落表必然产生说谎的行。

type WorktreeState

type WorktreeState string

WorktreeState 是一个终态任务的 managed worktree 当前所处的态。

注意:Unknown 与 Absent 必须分开。「仓库不可达所以判不出」与「确实没有残留」 是两回事,把前者渲染成后者等于用一个假结论把该看的东西藏起来(同 B70 的 「不猜 0」纪律)。

const (
	// WorktreeClean 在册且 git status 为空,可直接回收。
	WorktreeClean WorktreeState = "clean"
	// WorktreeDirty 在册但有未提交改动或未跟踪文件,默认拒绝回收。
	WorktreeDirty WorktreeState = "dirty"
	// WorktreePrunable 在册但目录已不存在。它不占磁盘,占的是分支——
	// 照样能让 git push --delete 被拒,因此必须能被看见与回收。
	WorktreePrunable WorktreeState = "prunable"
	// WorktreeAbsent 不在册,无残留。
	WorktreeAbsent WorktreeState = "absent"
	// WorktreeUnknown 仓库不可达或不是 git 仓库,判不出。
	WorktreeUnknown WorktreeState = "unknown"
)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL