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
- Variables
- func CanTransit(from, to TaskState) bool
- type ActiveTask
- type AddMachineReq
- type ArchivedPayload
- type Attachment
- type AuthTicketResp
- type BuildInfo
- type Card
- type CardBrief
- type CardCreateResp
- type CardDetail
- type CardView
- type CardWorkflowLocation
- type Cost
- type CostState
- type CreatePtySessionReq
- type CreateWorkspaceEntryReq
- type CreateWorktreeReq
- type Cumulative
- type Decision
- type DesktopState
- type DirEntry
- type DirListResult
- type DirtyFile
- type DisciplineBinding
- type DisciplineBuiltin
- type DisciplineFile
- type DisciplineMappingReq
- type DisciplineResp
- type DownloadState
- type EnvBinding
- type EnvFile
- type EnvKey
- type EnvKeysResp
- type EnvMappingReq
- type EnvResp
- type Event
- type EventType
- type ExecutorDefaultReq
- type ExecutorDefaultResp
- type FileConflictResp
- type FileRead
- type FileWriteReq
- type FileWriteResp
- type FlowDetail
- type FootprintResp
- type FootprintRow
- type Frame
- type FrameType
- type Gate
- type LatestResp
- type LedgerEvent
- type Machine
- type MachineStatus
- type MachineUpgrade
- type MachineUpgradeResp
- type MachinesResp
- type MigrateCardReq
- type MigrateCardResp
- type NewCardReq
- type NodeDef
- type NodeOverride
- type ProcUsage
- type ProjectBranch
- type ProjectBranchesResp
- type ProjectLocation
- type ProjectLocationNode
- type ProjectNode
- type ProjectTreeResp
- type PtyControl
- type PtyFootprintRow
- type PtySession
- type PtySessionsResp
- type PullState
- type ReclaimAction
- type ReclaimError
- type ReclaimListResp
- type ReclaimReason
- type ReclaimResp
- type ReclaimRow
- type Relation
- type RenameWorkspaceEntryReq
- type SearchHit
- type SearchResult
- type SessionInfo
- type SpendEntry
- type StatusResp
- type Task
- type TaskPlan
- type TaskState
- type TaskStateRow
- type TaskView
- type TasksResp
- type Ticket
- type UpdateError
- type UpdateResp
- type UpdateStatus
- type Usage
- type WorkbenchBase
- type WorkbenchBaseReq
- type WorkbenchDockReq
- type WorkbenchSelectedReq
- type WorkbenchStateResp
- type Workspace
- type WorktreeState
Constants ¶
const ( DisciplineModeDefault = "default" DisciplineModeFile = "file" DisciplineModeOff = "off" )
纪律档位取值。与 config 的三档语义一一对应(键不存在 / 值为文件名 / 值为空串)。
const ( EnvModeFile = "file" EnvModeOff = "off" )
env 档位取值。与 config 的两档语义一一对应(键不存在 / 值为文件名)。
const ( PtyCtrlAttached = "attached" // 服务端 → 客户端,建连首帧 PtyCtrlExit = "exit" // 服务端 → 客户端,shell 已退出 PtyCtrlError = "error" // 服务端 → 客户端 PtyCtrlResize = "resize" // 客户端 → 服务端 )
/ws/pty 的 text 帧类型。binary 帧恒为 PTY 原始字节,不走 JSON。
const ( LiveAlive = "alive" LiveDead = "dead" LiveUnknown = "unknown" )
ActiveTask.Live 的三个取值。
为什么必须有 unknown:探不出结论时猜一个值就是在制造假阳性,而一条会说谎的 诊断命令比没有更糟——因为你会信它。
const ( PullStageDownloading = "downloading" PullStageInstalling = "installing" PullStageFailed = "failed" )
自拉的阶段取值。
只有三个:没有 "done"(见 UpdateStatus.PullState 的注释——成功的终点是重启), 也没有单独的 "verifying"(sha256 比对与解包后自检都发生在 installing 内部)。 **不要为了让阶段看起来更完整而加一个实现从不产出的取值**——消费方会写死 代码去处理它,而那段代码永远不会被执行,也永远不会被测到。
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 的分歧点变成 一个可测的单点。
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)。
const MaxDoneNoteBytes = 4096
MaxDoneNoteBytes 是归档说明的字节上限。
为什么超限要报错而不是截断:B6 的教训正是「静默截断让协调者盲信自己看到的是 全文」。协调者写了 6KB 说明、系统悄悄存 4KB,比直接拒绝糟糕得多。 取值 4096:比一句话说明宽出两个数量级,同时挡住「把整个 diff 粘进来」的误用。
Variables ¶
var TerminalStates = []TaskState{TaskStateCompleted, TaskStateFailed}
TerminalStates 是任务的两个终态:到此不再有 executor 持有工作区。 存储层按它生成「非终态」查询条件,避免与状态机定义漂移。
Functions ¶
func CanTransit ¶
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
Attachment 是账本卡片附件的 wire DTO。
type AuthTicketResp ¶ added in v0.3.0
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
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
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
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
FileWriteReq 是 PUT /api/workspaces/file 的请求体。
BaseSHA256 必填:它是调用方**读到那一版**的哈希,服务端拿它与磁盘现状比对, 不一致就 409。空串一律判为不匹配——没读过就想写,正是覆盖别人改动的场景。
type FileWriteResp ¶ added in v0.3.0
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 ¶
ProcUsage 是本机当前 uid 的进程占用与上限。
为什么两个数必须一起给:只看 Used 不知道离墙还有多远,只看 Limit 没有意义。 2026-08-12 devbox 整机 fork 瘫痪时,346/2666 这两个数并排才说明得了问题。
type ProjectBranch ¶ added in v0.3.0
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 RenameWorkspaceEntryReq ¶ added in v0.3.0
type RenameWorkspaceEntryReq struct {
NewName string `json:"new_name"`
}
RenameWorkspaceEntryReq 是 PATCH /api/workspaces/entry 的请求体。
type SearchResult ¶ added in v0.3.0
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":..} 解析)。
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 表示任务所处状态。
func (TaskState) IsTerminal ¶
IsTerminal 报告该状态是否为终态(completed / failed)。
type TaskStateRow ¶ added in v0.3.9
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 ¶
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
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" )