Harness · Coding Agent Book06 / MonkeyCode
研究总览
M06 · SOURCE-GROUNDED TUTORIAL

MonkeyCode
从源码学会它怎么工作

它是把多种 CLI 放入远程 VM 的控制平面;Agent 智能来自所选 CLI,平台负责隔离、凭据与协作。 我们不把 README 当结论,而是沿主循环、工具、上下文、权限、扩展、协作和状态一路读到实现。

Go/TS · Remote Agent Control PlaneAGPLddc3794fc31e21 个结论 · 61 处引用
这门课怎么读

先建立直觉,再沿一条任务链钻进代码

参考教程的做法不是把 API 名称罗列出来,而是从一个小白能理解的问题开始,先解释“为什么需要这个机制”,再用概念对比、执行链路和固定提交的源码回答“它究竟怎么做”。本页把 MonkeyCode 的 21 个源码结论重新编排成十节课;每个结论都保留证据等级、文件路径、行号和可点击源码。

你会得到一张可复述的架构地图一次完整任务的链路追踪能迁移到自研 Harness 的设计判断
你不会得到把 README 功能当成已验证事实把 prompt 约束说成 OS 沙箱把一次双模型调用夸成多 Agent 平台
M00 · MAP

先看全景:这个 Agent 的控制面在哪里

下面的图不是产品宣传图,而是把固定提交里最关键的入口、循环、模型、工具、安全、状态和协作节点放在一张地图上。

架构图全屏打开 ↗
一轮执行链路全屏打开 ↗
核心机制

DB 预登记 → VM → Redis 交接 → 选定 CLI

上下文

只知道窗口上限;压缩和 memory 交给内层 Agent

适用建设

多人远程 Coding Agent SaaS、隔离租户、统一审计

M00.5 · TRACE

跟踪一个任务:从输入到交付

把下面九步当成你读源码时的“地图坐标”。每到一个节点,都能回到后面的章节查具体实现。

读图提醒

箭头只表示控制面之间的关系,不代表每个实现都同步、串行或拥有 OS 隔离;真正的边界要以对应章节的源码摘录和 caveat 为准。

M01 · ORIENTATION

先把 Agent 看成一台会交付的机器

如果只看 README,你知道它能做什么;钻进源码后,我们要知道它为什么能做、什么时候会停、失败后谁负责收拾。

先用一个生活比喻

把 Agent 想成一间带传送带的工作室:入口收任务,主循环决定下一步,模型负责提出动作,工具负责动手,状态账本负责让下一班人接着干。

本节阅读法先问问题读事实看代码做迁移判断
读源码时先问本课的判断方式
这一层有没有独立证据?没有就不把其他层的能力冒充成默认行为;回到固定提交继续追调用链。
谁拥有最终控制权?区分模型输出、框架规则、用户审批和 OS 隔离四种不同力量。
这一章先建立通用概念

固定提交账本没有把该维度单独拆出,但它会在其他章节的源码路径中体现。先沿执行链路阅读,再回到报告页核对证据。

小练习 1

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M02 · LOOP

主循环:模型为什么会继续动

一次模型调用为什么会变成十几步?循环靠什么继续,靠什么停止?

先用一个生活比喻

像一个会看回执的快递员:模型先写行动单,工具返回回执,主循环把回执放回桌面,再让模型决定下一张行动单。

这套实现先回答了什么?

MonkeyCode 更像机场塔台:它决定哪架飞机、在哪个跑道、带什么配置起飞,但不会替 Codex 或 Claude 亲自驾驶。

本节阅读法先问问题读事实看代码做迁移判断
源码问题固定提交给出的线索白话结论
它是多 CLI 的任务控制平面,不是第四套 Agent loopbackend/pkg/taskflow/types.go:554MonkeyCode 更像机场塔台:它决定哪架飞机、在哪个跑道、带什么配置起飞,但不会替 Codex 或 Claude 亲自驾驶。
任务创建拆成数据库预登记、VM 创建、Redis 交接、运行态启动backend/biz/task/usecase/task.go:556先把工单和工作间登记好,再等工作间真的上线,最后才把任务交给里面的 Agent。
Taskflow Create 失败被记录但吞掉,任务可能滞留 processingbackend/pkg/lifecycle/taskhook.go:104工单已经盖了“处理中”,但真正开工失败后只记了一条日志,状态机可能还以为工作在继续。
01
L1 · fact · monkey-architecture-001

它是多 CLI 的任务控制平面,不是第四套 Agent loop

先看源码事实

Taskflow 协议把 CodingAgent 枚举为 Codex、Claude、MCAIReview、OpenCode;任务请求携带 system prompt、模型、配置文件、MCP 与资源,真正的推理—工具循环交给所选 CLI。

翻译成白话

MonkeyCode 更像机场塔台:它决定哪架飞机、在哪个跑道、带什么配置起飞,但不会替 Codex 或 Claude 亲自驾驶。

为什么这对自研重要

比较 Harness 时,应把平台编排能力与各 CLI 内核能力拆开计分。

固定提交源码摘录
  554  // ==================== CreateTask 类型 ====================
  555  
  556  // CodingAgent 编码代理类型
  557  type CodingAgent int
  558  
  559  const (
  560  	CodingAgentCodex CodingAgent = iota + 1
  561  	CodingAgentClaude
  562  	CodingAgentMCAIReview
  563  	CodingAgentOpenCode
  564  )
  565  
  566  // LLM 模型配置
  567  type LLM struct {
  568  	ApiKey      string   `json:"api_key"`
  569  	BaseURL     string   `json:"base_url"`
  570  	Model       string   `json:"model"`
  571  	ApiType     string   `json:"api_type,omitempty"` // 接口类型 anthropic | openai
  572  	Temperature *float32 `json:"temperature,omitempty"`
  573  }
  574  
  575  // ConfigFile 配置文件
  576  type ConfigFile struct {
  577  	Path    string  `json:"path"`
  578  	Content string  `json:"content"`
  579  	Mode    *uint32 `json:"mode,omitempty"`
  580  }
  581  
  582  // TaskExecutionConfig 任务运行配置
  583  type TaskExecutionConfig struct {
  584  	Envs           map[string]string `json:"envs,omitempty"`
  585  	ConfigFiles    []ConfigFile      `json:"config_files,omitempty"`
  586  	McpServers     []McpServerConfig `json:"mcp_servers,omitempty"`
  587  	AgentResources *AgentResources   `json:"agent_resources,omitempty"`
为什么相信这条结论?查看 3 处证据
02
L1 · fact · monkey-lifecycle-001

任务创建拆成数据库预登记、VM 创建、Redis 交接、运行态启动

先看源码事实

Create 先校验 host/model/concurrency 并预创建任务记录,再请求 2 核 8GiB VM,将完整 CreateTaskReq 以 TTL 写入 Redis,最后切到 pending;VM ready hook 取出请求并调用 TaskManager.Create。

翻译成白话

先把工单和工作间登记好,再等工作间真的上线,最后才把任务交给里面的 Agent。

为什么这对自研重要

分阶段便于恢复和审计,但 Redis 交接键成为启动链路的关键依赖。

固定提交源码摘录
  556  	limit, err := a.resolveTaskConcurrencyLimit(ctx, user.ID)
  557  	if err != nil {
  558  		return nil, err
  559  	}
  560  	ctx = entx.WithTaskConcurrencyLimit(ctx, limit)
  561  
  562  	vmID := fmt.Sprintf("agent_%s", uuid.NewString())
  563  	prepared, err := a.repo.PrepareCreate(ctx, user, req, token, vmID)
  564  	if err != nil {
  565  		a.logger.With("error", err, "req", req).ErrorContext(ctx, "failed to create task")
  566  		return nil, err
  567  	}
  568  	if prepared == nil || prepared.ProjectTask == nil || prepared.Model == nil || prepared.Image == nil {
  569  		return nil, fmt.Errorf("failed to prepare task")
  570  	}
  571  	pt := prepared.ProjectTask
  572  	m := prepared.Model
  573  	i := prepared.Image
  574  	t := pt.Edges.Task
  575  	if t == nil {
  576  		return nil, fmt.Errorf("task edge is nil")
  577  	}
  578  	if git.URL == "" {
  579  		git.URL = pt.RepoURL
  580  	}
      … 26 lines omitted; exact range 556–617 …
  607  		LLM: taskflow.LLMProviderReq{
  608  			Provider: taskflow.LlmProviderOpenAI,
  609  			ApiKey:   m.APIKey,
  610  			BaseURL:  m.BaseURL,
  611  			Model:    m.Model,
  612  		},
  613  		Cores:    "2",
  614  		Memory:   8 << 30,
  615  		Envs:     env,
  616  		LogStore: normalizeTaskLogStore(t.LogStore),
  617  	})
为什么相信这条结论?查看 3 处证据
03
L1 · risk · monkey-lifecycle-002

Taskflow Create 失败被记录但吞掉,任务可能滞留 processing

先看源码事实

handleProcessing 在先写入 processing 后调用 TaskManager.Create;若调用失败只写 error log,不 return err,外层 withError 无法把任务转入 error。

翻译成白话

工单已经盖了“处理中”,但真正开工失败后只记了一条日志,状态机可能还以为工作在继续。

为什么这对自研重要

应把 Create 错误返回给生命周期管理器,并为 processing-without-session 增加 watchdog。

固定提交源码摘录
  104  		reqKey := fmt.Sprintf("task:create_req:%s", id.String())
  105  		val, err := h.redis.Get(ctx, reqKey).Result()
  106  		if err != nil {
  107  			h.logger.With("task_id", id, "error", err).ErrorContext(ctx, "failed to get CreateTaskReq from redis")
  108  			return fmt.Errorf("failed to get CreateTaskReq from Redis: %w", err)
  109  		}
  110  
  111  		defer h.redis.Del(ctx, reqKey)
  112  
  113  		if err := h.repo.Update(ctx, &domain.User{}, id, func(up *db.TaskUpdateOne) error {
  114  			up.SetStatus(consts.TaskStatusProcessing)
  115  			return nil
  116  		}); err != nil {
  117  			return fmt.Errorf("failed to update task status: %w", err)
  118  		}
  119  
  120  		var createReq taskflow.CreateTaskReq
  121  		if err := json.Unmarshal([]byte(val), &createReq); err != nil {
  122  			h.logger.With("task_id", id, "error", err).ErrorContext(ctx, "failed to unmarshal CreateTaskReq")
  123  			return fmt.Errorf("failed to unmarshal CreateTaskReq: %w", err)
为什么相信这条结论?查看 2 处证据
小练习 2

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M03 · MODEL

模型调用:流式输出如何变成可执行步骤

模型输出的文字、思考、工具调用和错误,经过哪些转换才进入 Agent 状态?

先用一个生活比喻

模型像电话另一端的同事:你听到的不是一整段录音,而是一串实时片段;Harness 要边听边拼装,还要能在电话断线时留下可恢复的记录。

这套实现先回答了什么?

工作 VM 拿的是代金券,不是模型厂商的保险柜钥匙;平台看到券后再替它换成真正凭据。

本节阅读法先问问题读事实看代码做迁移判断
源码问题固定提交给出的线索白话结论
LLM proxy 用 VM 绑定临时密钥隐藏真实上游凭据backend/biz/task/usecase/task.go:585工作 VM 拿的是代金券,不是模型厂商的保险柜钥匙;平台看到券后再替它换成真正凭据。
代理只放行三种 LLM 协议,并阻止任务偷换模型backend/biz/llmproxy/proxy.go:28这不是任意 HTTP 隧道;票上写的是哪个模型,就只能点那个模型。
运行中切模仅支持 OpenCode,并通过 restart 恢复 sessionbackend/biz/task/usecase/task.go:165换发动机不是所有车都支持:目前只有 OpenCode 能中途换模型,且本质是重启运行时再接回会话。
04
L1 · fact · monkey-provider-001

LLM proxy 用 VM 绑定临时密钥隐藏真实上游凭据

先看源码事实

任务拿到 ModelApiKey/runtime token 和平台 proxy URL;proxy 再从数据库解析 user、VM、provider、真实 BaseURL/API key,重写 Authorization/X-Api-Key 后访问上游。

翻译成白话

工作 VM 拿的是代金券,不是模型厂商的保险柜钥匙;平台看到券后再替它换成真正凭据。

为什么这对自研重要

可撤销密钥、统一用量和上游切换都集中在控制面。

固定提交源码摘录
  585  	var runtimeToken string
  586  	if keys := m.Edges.Apikeys; len(keys) > 0 {
  587  		m.APIKey = keys[0].APIKey
  588  		m.BaseURL = a.cfg.LLMProxy.BaseURL + "/v1"
  589  		runtimeToken = keys[0].APIKey
  590  	}
为什么相信这条结论?查看 3 处证据
05
L1 · fact · monkey-provider-002

代理只放行三种 LLM 协议,并阻止任务偷换模型

先看源码事实

入口只接受 chat/completions、responses、messages;请求必须有 bearer 或 X-api-key,body 中 model 若与 token 绑定模型不一致直接 403。

翻译成白话

这不是任意 HTTP 隧道;票上写的是哪个模型,就只能点那个模型。

为什么这对自研重要

减小了凭据代理被滥用成通用外网代理或越权消费的空间。

固定提交源码摘录
   28  const upstreamFailureMessage = "连接上游模型失败,请检查模型配置,或重试"
   29  
   30  var allowPaths = map[string]string{
   31  	"/v1/chat/completions": "/chat/completions",
   32  	"/v1/responses":        "/responses",
   33  	"/v1/messages":         "/messages",
   34  }
为什么相信这条结论?查看 2 处证据
06
L1 · limitation · monkey-provider-003

运行中切模仅支持 OpenCode,并通过 restart 恢复 session

先看源码事实

SwitchModel 要求 processing、owner/privileged、模型授权;重建配置后若 runtime 不是 OpenCode 就报错,随后调用 TaskManager.Restart 并可选择 LoadSession。

翻译成白话

换发动机不是所有车都支持:目前只有 OpenCode 能中途换模型,且本质是重启运行时再接回会话。

为什么这对自研重要

Codex/Claude 的长任务无法享受平台级模型升降档。

固定提交源码摘录
  165  // SwitchModel 切换运行中任务使用的模型
  166  func (a *TaskUsecase) SwitchModel(ctx context.Context, user *domain.User, taskID uuid.UUID, req domain.SwitchTaskModelReq) (*domain.SwitchTaskModelResp, error) {
  167  	t, owner, err := a.Info(ctx, user, taskID)
  168  	if err != nil {
  169  		return nil, err
  170  	}
  171  	if !owner && !a.isPrivileged(ctx, user.ID) {
  172  		return nil, errcode.ErrForbidden
  173  	}
  174  	if t.Status != consts.TaskStatusProcessing {
  175  		return nil, fmt.Errorf("task is not processing")
  176  	}
  177  	if t.VirtualMachine == nil {
  178  		return nil, fmt.Errorf("task virtual machine is nil")
  179  	}
  180  
  181  	taskOwnerID := t.UserID
  182  	if a.modelHook != nil {
  183  		if err := a.modelHook.ValidateAccess(ctx, taskOwnerID, req.ModelID.String()); err != nil {
  184  			return nil, err
  185  		}
  186  	}
  187  	model, err := a.modelRepo.Get(ctx, taskOwnerID, req.ModelID)
  188  	if err != nil {
  189  		return nil, err
      … 18 lines omitted; exact range 165–218 …
  208  	var skillIDs, pluginIDs []string
  209  	if t.Extra != nil {
  210  		skillIDs = t.Extra.SkillIDs
  211  		pluginIDs = t.Extra.PluginIDs
  212  	}
  213  	coding, configs, agentRes, err := a.getCodingConfigs(ctx, t.CliName, model, skillIDs, pluginIDs, a.userScope(ctx, user), false)
  214  	if err != nil {
  215  		return nil, err
  216  	}
  217  	if coding != taskflow.CodingAgentOpenCode {
  218  		return nil, fmt.Errorf("switch model only supports opencode runtime")
为什么相信这条结论?查看 2 处证据
小练习 3

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M04 · TOOLS

工具系统:Agent 的手脚怎样被注册和调度

模型看见的工具说明,和真正执行工具的代码,是不是同一个东西?并发、编辑和失败结果怎么处理?

先用一个生活比喻

工具系统像机场:模型提交登机牌,注册表确认航班,权限闸机检查证件,调度器决定跑道,最后才允许真正起飞。

这套实现先回答了什么?

公司给默认技能,团队可换一版,个人还能再覆盖同名版本;被禁用的资源不会因强制投放而复活。

本节阅读法先问问题读事实看代码做迁移判断
源码问题固定提交给出的线索白话结论
资源按 global/team/user 合并,同名 user 覆盖 team 覆盖 globalbackend/biz/agentresource/types.go:99公司给默认技能,团队可换一版,个人还能再覆盖同名版本;被禁用的资源不会因强制投放而复活。
MCP Hub 把工具身份绑定到具体 user、task 和 VMbackend/biz/task/usecase/task.go:741工具调用不是“拿到平台 token 就随便用”,而是能追到哪台 VM、哪个人、哪个任务。
工具调用具备有效集过滤、幂等 replay 和状态审计backend/biz/mcphub/runtime/registry/service.go:56同一个任务重试同一张工具工单不会重复扣动外部系统;每次调用都有完整状态轨迹。
MCP 计费接口已预留,但当前实现是 Noopbackend/biz/mcphub/runtime/gateway/handler.go:228水表接口和插座都装好了,但现在里面还没有真正的计费表芯。
07
L1 · fact · monkey-resource-002

资源按 global/team/user 合并,同名 user 覆盖 team 覆盖 global

先看源码事实

ScopeFilter 可同时包含三个层级,调用方按 user>team>global 做 name override;dispatch 选择用户勾选项与 force-delivery,并过滤 disabled。

翻译成白话

公司给默认技能,团队可换一版,个人还能再覆盖同名版本;被禁用的资源不会因强制投放而复活。

为什么这对自研重要

这已经接近企业 Agent 的策略分层,而不是单用户插件目录。

固定提交源码摘录
   99  // ScopeFilter constrains a listing or dispatch query to a subset of the
  100  // three scope tiers. Used by the *Scoped Repo methods.
  101  //
  102  // Semantic: rows whose scope_type is "global" are included when
  103  // IncludeGlobal is true; rows whose scope is "team" are included when
  104  // TeamID != nil and matches; same for user. Multiple flags may be set —
  105  // the result is the union, with name-based override (user > team > global)
  106  // applied by the call sites that need overrides (Listing / dispatch).
  107  type ScopeFilter struct {
  108  	IncludeGlobal bool
  109  	TeamID        *uuid.UUID
  110  	UserID        *uuid.UUID
  111  }
  112  
  113  // GlobalOnlyScope is the default scope for back-compat call sites that
  114  // haven't migrated to ScopeFilter yet. Matches the historical "global only"
  115  // behavior of the unsuffixed Repo methods.
  116  func GlobalOnlyScope() ScopeFilter {
  117  	return ScopeFilter{IncludeGlobal: true}
  118  }
  119  
  120  // SkillSelection bundles a ScopeFilter with the user-picked skill IDs for
  121  // the dispatch path. ListActiveSkillsScoped takes this so the same struct
  122  // flows from the task usecase down to the SQL.
  123  type SkillSelection struct {
  124  	Scope           ScopeFilter
  125  	UserSelectedIDs []uuid.UUID
  126  }
为什么相信这条结论?查看 3 处证据
08
L1 · fact · monkey-mcp-001

MCP Hub 把工具身份绑定到具体 user、task 和 VM

先看源码事实

任务拿到本地 builtin MCP 和带 runtime token 的平台 MCP;Hub 认证先由 ModelApiKey 找到 VirtualMachineID,再查唯一 TaskVirtualMachine,形成 user/task/model subject。

翻译成白话

工具调用不是“拿到平台 token 就随便用”,而是能追到哪台 VM、哪个人、哪个任务。

为什么这对自研重要

为审计、限额和撤销提供了细粒度主体。

固定提交源码摘录
  741  func (a *TaskUsecase) buildMCPConfigs(taskID uuid.UUID, token string) []taskflow.McpServerConfig {
  742  	mcps := []taskflow.McpServerConfig{
  743  		{
  744  			Type: "http",
  745  			Name: "mcaiBuiltin",
  746  			Url:  proto.String(fmt.Sprintf("http://127.0.0.1:65510/mcp?task_id=%s", taskID.String())),
  747  		},
  748  	}
  749  
  750  	if token != "" {
  751  		mcps = append(mcps, taskflow.McpServerConfig{
  752  			Type: "http",
  753  			Name: "monkeycode-ai",
  754  			Url:  proto.String(fmt.Sprintf("%s/mcp", strings.TrimRight(a.cfg.Server.BaseURL, "/"))),
  755  			Headers: []*taskflow.McpHttpHeader{
  756  				{
  757  					Name:  "Authorization",
  758  					Value: fmt.Sprintf("Bearer %s", token),
  759  				},
  760  			},
  761  			Command: new(string),
  762  			Args:    []string{},
  763  			Env:     map[string]string{},
  764  		})
  765  	}
  766  
  767  	return mcps
为什么相信这条结论?查看 2 处证据
09
L1 · fact · monkey-mcp-002

工具调用具备有效集过滤、幂等 replay 和状态审计

先看源码事实

Hub 合并 platform/user/team 工具和用户开关,过滤 disabled/deleted/missing upstream;调用 ID 由 task、JSON-RPC ID、tool、canonical args 哈希,重复请求复用结果;首次调用写 pending,再标 success/failed/unknown。

翻译成白话

同一个任务重试同一张工具工单不会重复扣动外部系统;每次调用都有完整状态轨迹。

为什么这对自研重要

这是比把 MCP URL 直接交给 Agent 更成熟的连接器治理层。

固定提交源码摘录
   56  func (s *Service) ListEffectiveTools(ctx context.Context, userID uuid.UUID) ([]repo.ToolSnapshot, error) {
   57  	platform, err := s.listPlatformTools(ctx)
   58  	if err != nil {
   59  		return nil, err
   60  	}
   61  	userTools, err := s.repo.ListUserPublishedTools(ctx, userID)
   62  	if err != nil {
   63  		return nil, err
   64  	}
   65  	teamTools, err := s.repo.ListTeamPublishedTools(ctx, userID)
   66  	if err != nil {
   67  		return nil, err
   68  	}
   69  
   70  	settings := map[uuid.UUID]bool{}
   71  	if s.userSettings != nil {
   72  		settings, err = s.userSettings.ListEnabledMap(ctx, userID)
   73  		if err != nil {
   74  			return nil, err
   75  		}
   76  	}
   77  
   78  	tools := append(platform, userTools...)
   79  	tools = append(tools, teamTools...)
   80  	return applyUserSettingsDefaultEnabled(tools, settings), nil
为什么相信这条结论?查看 4 处证据
10
L1 · limitation · monkey-mcp-003

MCP 计费接口已预留,但当前实现是 Noop

先看源码事实

gateway 在调用前后执行 CanConsume/Consume,但默认 billing.Noop 两个方法都直接返回 nil。

翻译成白话

水表接口和插座都装好了,但现在里面还没有真正的计费表芯。

为什么这对自研重要

不能仅凭调用链存在就宣称已实现工具预算或付费配额。

固定提交源码摘录
  228  	if h.billing != nil {
  229  		if err := h.billing.CanConsume(ctx, call); err != nil {
  230  			_ = h.calls.MarkFailed(ctx, call.ID, err.Error(), "")
  231  			return nil, err
  232  		}
  233  	}
  234  
  235  	result, upstreamRequestID, err := h.upstream.CallTool(ctx, upstream, tool, CallToolParams{
  236  		Name:      tool.Name,
  237  		Arguments: args,
  238  	})
  239  	switch classifyUpstreamError(err) {
  240  	case repo.ToolCallStatusSuccess:
  241  		if err := h.calls.MarkSuccess(ctx, call.ID, result, upstreamRequestID); err != nil {
  242  			return nil, err
  243  		}
  244  		if h.billing != nil {
  245  			completed := *call
  246  			completed.Status = repo.ToolCallStatusSuccess
  247  			completed.ResultJSON = result
  248  			completed.UpstreamRequestID = upstreamRequestID
  249  			if err := h.billing.Consume(ctx, &completed); err != nil {
  250  				return nil, err
  251  			}
为什么相信这条结论?查看 2 处证据
小练习 4

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M05 · CONTEXT

上下文:有限窗口怎样装下长任务

当对话、工具输出、计划和记忆越来越多,系统怎样决定留下什么、折叠什么、放到哪里?

先用一个生活比喻

上下文不是聊天记录,而是一张会整理的工作台:常用零件放桌面,旧材料装进档案盒,必要时只留下索引卡。

这套实现先回答了什么?

平台知道油箱标称多大,却不看里面还剩多少油;何时压缩历史由 OpenCode/Codex/Claude 自己决定。

本节阅读法先问问题读事实看代码做迁移判断
源码问题固定提交给出的线索白话结论
平台知道模型窗口上限,但不管理内层 compactionbackend/biz/task/usecase/task.go:788平台知道油箱标称多大,却不看里面还剩多少油;何时压缩历史由 OpenCode/Codex/Claude 自己决定。
任务摘要是异步 UI 元数据,不是 Agent 记忆回写backend/biz/task/service/tasksummary.go:32它会给长对话写一段“给人看的剧情简介”,但这段简介不会自动塞回 Agent 的脑子里。
11
L1 · limitation · monkey-context-001

平台知道模型窗口上限,但不管理内层 compaction

先看源码事实

OpenCode 配置缺省 context=200000、output=32000;Taskflow 请求只下发 prompt/config/session,MonkeyCode 仓库没有在推理循环中计算 live context 或触发 compaction 的代码。

翻译成白话

平台知道油箱标称多大,却不看里面还剩多少油;何时压缩历史由 OpenCode/Codex/Claude 自己决定。

为什么这对自研重要

跨 runtime 的长任务可靠性会随各 CLI 内核不同而波动。

固定提交源码摘录
  788  func modelRuntimeDefaults(m *db.Model) (thinking bool, contextLimit int, outputLimit int) {
  789  	thinking = m.ThinkingEnabled
  790  	contextLimit = cmp.Or(m.ContextLimit, 200000)
  791  	outputLimit = cmp.Or(m.OutputLimit, 32000)
  792  	return thinking, contextLimit, outputLimit
为什么相信这条结论?查看 3 处证据
12
L1 · fact · monkey-context-002

任务摘要是异步 UI 元数据,不是 Agent 记忆回写

先看源码事实

TaskSummaryService 从持久化 task log 取 conversation,用独立 LLM 延迟生成 summary 并写入 Task.summary;任务 Create/Continue 协议未读取该字段回灌内层会话。

翻译成白话

它会给长对话写一段“给人看的剧情简介”,但这段简介不会自动塞回 Agent 的脑子里。

为什么这对自研重要

摘要改善列表浏览和通知,不应被误计为上下文压缩或长期记忆。

固定提交源码摘录
   32  // TaskSummaryService 任务摘要生成服务
   33  type TaskSummaryService struct {
   34  	cfg                *config.Config
   35  	db                 *db.Client
   36  	llm                *llm.Client
   37  	summaryQueue       *delayqueue.TaskSummaryQueue
   38  	logger             *slog.Logger
   39  	conversationReader ConversationReader
   40  
   41  	// 生命周期管理
   42  	cancel context.CancelFunc
   43  	wg     sync.WaitGroup
   44  }
   45  
   46  type tasklogGateway interface {
   47  	QueryTurns(ctx context.Context, taskID uuid.UUID, taskCreatedAt time.Time, opts tasklog.QueryTurnsOpts, store consts.LogStore) (*tasklog.QueryTurnsResp, error)
   48  }
   49  
   50  type ConversationReader interface {
   51  	Fetch(ctx context.Context, taskID uuid.UUID, createdAt time.Time, store consts.LogStore, initialContent string, maxRounds int) ([]llm.Message, error)
   52  }
   53  
   54  type tasklogConversationReader struct {
   55  	gateway tasklogGateway
   56  	logger  *slog.Logger
      … 25 lines omitted; exact range 32–92 …
   82  		db:                 d,
   83  		llm:                llmClient,
   84  		summaryQueue:       sq,
   85  		logger:             logger,
   86  		conversationReader: newTasklogConversationReader(tlg, logger),
   87  	}
   88  
   89  	// 启动消费者
   90  	s.Start(context.Background())
   91  
   92  	return s, nil
为什么相信这条结论?查看 3 处证据
小练习 5

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M06 · SECURITY

权限与沙箱:能做什么,在哪里做

审批按钮、规则引擎、容器和操作系统沙箱分别解决什么问题?为什么“问过用户”不等于“隔离了风险”?

先用一个生活比喻

审批像门卫问你有没有预约,沙箱像把访客关在指定房间;前者决定是否放行,后者限制放行后能摸到什么。

这套实现先回答了什么?

Codex 在房间里面拿的是万能钥匙;安全取决于这个“房间”到底是不是一间真正隔离的 VM,而造房间的代码不在本仓库。

本节阅读法先问问题读事实看代码做迁移判断
源码问题固定提交给出的线索白话结论
隔离边界主要依赖仓库外 VM,Codex 内层 sandbox 明确关闭backend/pkg/taskflow/types.go:72Codex 在房间里面拿的是万能钥匙;安全取决于这个“房间”到底是不是一间真正隔离的 VM,而造房间的代码不在本仓库。
平台只转发 auto-approve,具体权限语义继承所选 CLIbackend/biz/task/usecase/task.go:157平台提供“自动点同意”的总开关,但什么动作需要同意、允许后能做多大,仍由里面那套 CLI 决定。
zip 资源有文件数、单文件、总量和 zip-slip 双重校验backend/biz/agentresource/unpack.go:12插件压缩包不能假装很小再突然炸开,也不能用 ../../ 偷写 VM 里的其他路径。
SSRF guard 能防 DNS rebinding,但私有化示例默认关闭backend/pkg/netguard/guard.go:53防护能力本身很认真,但自建用户照示例部署时默认不会打开这扇防火门。
13
L1 · limitation · monkey-sandbox-001

隔离边界主要依赖仓库外 VM,Codex 内层 sandbox 明确关闭

先看源码事实

平台协议申请带 host、image、CPU、memory、git 和 TTL 的 VirtualMachine;但 Codex 模板设置 sandbox_mode=danger-full-access,并将 /workspace 标为 trusted。仓库内只有 VM API 合同,没有底层 namespace/hypervisor enforcement。

翻译成白话

Codex 在房间里面拿的是万能钥匙;安全取决于这个“房间”到底是不是一间真正隔离的 VM,而造房间的代码不在本仓库。

为什么这对自研重要

部署评审必须联审 codingmatrix/Taskflow 执行器,不能因类型名叫 VirtualMachine 就自动认定强隔离。

固定提交源码摘录
   72  // VirtualMachine 虚拟机信息
   73  type VirtualMachine struct {
   74  	ID            string               `json:"id"`
   75  	AccessToken   string               `json:"access_token,omitempty"`
   76  	EnvironmentID string               `json:"environment_id"`
   77  	HostID        string               `json:"host_id"`
   78  	Hostname      string               `json:"hostname"`
   79  	Arch          string               `json:"arch"`
   80  	OS            string               `json:"os"`
   81  	Name          string               `json:"name"`
   82  	Repository    string               `json:"repository"`
   83  	Status        VirtualMachineStatus `json:"status"`
   84  	StatusMessage string               `json:"status_message"`
   85  	Cores         int32                `json:"cores"`
   86  	Memory        uint64               `json:"memory"`
   87  	Disk          uint64               `json:"disk"`
   88  	TTL           TTL                  `json:"ttl"`
   89  	ExternalIP    string               `json:"external_ip"`
   90  	CreatedAt     int64                `json:"created_at"`
   91  	Version       string               `json:"version"`
   92  }
为什么相信这条结论?查看 3 处证据
14
L1 · fact · monkey-permission-001

平台只转发 auto-approve,具体权限语义继承所选 CLI

先看源码事实

AutoApprove API 只把 task ID 和布尔值传给 Taskflow;Codex 模板使用 approval_policy=untrusted,OpenCode 模板却允许 doom_loop、外部目录和 .env 读取。

翻译成白话

平台提供“自动点同意”的总开关,但什么动作需要同意、允许后能做多大,仍由里面那套 CLI 决定。

为什么这对自研重要

统一 UI 不等于统一安全语义,需要为每个 runtime 建立可比较的 capability policy。

固定提交源码摘录
  157  // AutoApprove implements domain.TaskUsecase.
  158  func (a *TaskUsecase) AutoApprove(ctx context.Context, _ *domain.User, id uuid.UUID, approve bool) error {
  159  	return a.taskflow.TaskManager().AutoApprove(ctx, taskflow.TaskApproveReq{
  160  		ID:          id,
  161  		AutoApprove: &approve,
  162  	})
  163  }
为什么相信这条结论?查看 3 处证据
15
L1 · fact · monkey-resource-003

zip 资源有文件数、单文件、总量和 zip-slip 双重校验

先看源码事实

默认限制 1000 文件、单文件 32MiB、总解压 256MiB;先查声明尺寸,再用 LimitReader 验证实际尺寸,并拒绝绝对路径、反斜线和 .. 逃逸。

翻译成白话

插件压缩包不能假装很小再突然炸开,也不能用 ../../ 偷写 VM 里的其他路径。

为什么这对自研重要

插件供应链仍需签名/哈希治理,但基础解包攻击面处理得较完整。

固定提交源码摘录
   12  // UnzipLimits guards the in-memory unzipper against zip bombs and overly
   13  // large archives. All limits are inclusive of the value (size == limit is OK).
   14  type UnzipLimits struct {
   15  	MaxFileSize  int64 // per-entry uncompressed size
   16  	MaxTotalSize int64 // sum of all entries' uncompressed sizes
   17  	MaxFiles     int   // max number of file entries
   18  }
   19  
   20  // DefaultUnzipLimits is the policy used by the Resolver.
   21  var DefaultUnzipLimits = UnzipLimits{
   22  	MaxFileSize:  32 << 20,  // 32 MiB
   23  	MaxTotalSize: 256 << 20, // 256 MiB
   24  	MaxFiles:     1000,
   25  }
   26  
   27  // unzipToMemory reads a zip archive entirely from memory and returns its
   28  // regular file entries. Directories are skipped. Paths are validated to
   29  // reject zip-slip ("../..") and absolute paths. Entries that exceed the
   30  // limits cause the entire archive to be rejected so callers can fall back
   31  // to skipping the whole asset (matching the Resolver's per-skill policy).
为什么相信这条结论?查看 3 处证据
16
L1 · risk · monkey-security-001

SSRF guard 能防 DNS rebinding,但私有化示例默认关闭

先看源码事实

netguard 校验 http/https、特殊 IPv4 写法、localhost/private/metadata IP,并在 dial 时使用已验证 IP;测试覆盖十进制、八进制、十六进制、IPv6 和私有代理。然而示例配置明确为 SaaS 开启、私有化默认 false。

翻译成白话

防护能力本身很认真,但自建用户照示例部署时默认不会打开这扇防火门。

为什么这对自研重要

需要按部署信任边界决定默认值,并在关闭时明确告警。

固定提交源码摘录
   53  func (g *Guard) ValidateURL(ctx context.Context, rawURL string) error {
   54  	u, err := url.Parse(strings.TrimSpace(rawURL))
   55  	if err != nil {
   56  		return fmt.Errorf("parse url: %w", err)
   57  	}
   58  	if u.Scheme != "http" && u.Scheme != "https" {
   59  		return fmt.Errorf("unsupported scheme %q", u.Scheme)
   60  	}
   61  	if u.Hostname() == "" {
   62  		return errors.New("empty host")
   63  	}
   64  	if !g.Enabled() {
   65  		return nil
   66  	}
   67  	_, err = g.resolveHost(ctx, u.Hostname())
   68  	return err
   69  }
为什么相信这条结论?查看 4 处证据
小练习 6

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M07 · ECOSYSTEM

指令、MCP、Skills 与插件:能力如何接进来

一条系统指令、一个 Skill、一个 MCP server 和一个插件,分别在什么时候进入上下文和执行路径?

先用一个生活比喻

这像给工作室接设备:说明书不是设备,设备也不等于电源;成熟 Harness 会分别治理发现、信任、加载、调用和卸载。

这套实现先回答了什么?

规则是小纸条直接塞进去,技能是压缩包让 VM 自己下载,插件还要告诉 OpenCode 从哪个入口文件启动。

本节阅读法先问问题读事实看代码做迁移判断
源码问题固定提交给出的线索白话结论
rules、skills、plugins 是三条不同投放链backend/biz/task/usecase/task.go:795规则是小纸条直接塞进去,技能是压缩包让 VM 自己下载,插件还要告诉 OpenCode 从哪个入口文件启动。
17
L1 · fact · monkey-resource-001

rules、skills、plugins 是三条不同投放链

先看源码事实

rules 以内联 ConfigFile 写入 .ai-ready/rules;skills 对三种 CLI 都以 24h presigned zip URL 下发;plugins 只给 OpenCode,并将 entry 变成 file:// URL 注入 opencode.json。

翻译成白话

规则是小纸条直接塞进去,技能是压缩包让 VM 自己下载,插件还要告诉 OpenCode 从哪个入口文件启动。

为什么这对自研重要

资源协议兼顾小文本与大包,但各 runtime 的插件能力并不对齐。

固定提交源码摘录
  795  // agentRuleBaseDir / agentSkillBaseDir / agentPluginBaseDir 是 codingmatrix 在
  796  // VM 内部约定的 .ai-ready/ 投放路径。rule 走 .md 平铺;skill / plugin 解 zip
  797  // 后按目录结构展开;plugin 的 entry 字段再以 file:// 注入到 opencode.json 的
  798  // `plugin` 数组里。Claude / Codex 不消费 plugin(spec §6.3)。
  799  const (
  800  	agentRuleBaseDir   = "${HOME}/.codingmatrix/project-tpl/.ai-ready/rules/"
  801  	agentSkillBaseDir  = "${HOME}/.codingmatrix/project-tpl/.ai-ready/skills/"
  802  	agentPluginBaseDir = "${HOME}/.codingmatrix/project-tpl/.ai-ready/plugins/"
  803  )
为什么相信这条结论?查看 4 处证据
小练习 7

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M08 · COLLABORATION

子 Agent:把一个大任务拆成可治理的协作

什么时候是普通工具调用,什么时候才算子 Agent?子 Agent 的上下文、预算、取消和结果怎样回到父 Agent?

先用一个生活比喻

不是把同事叫来聊天就叫协作;真正的协作要有工单、权限、截止时间、交付物和回收机制。

这套实现先回答了什么?

旁观者能看直播,任务主人才能按按钮和继续对话;掉线重连后还能从历史接上。

本节阅读法先问问题读事实看代码做迁移判断
源码问题固定提交给出的线索白话结论
远程协作以 owner write gate、历史回放和实时流为核心backend/biz/task/handler/v1/task.go:323旁观者能看直播,任务主人才能按按钮和继续对话;掉线重连后还能从历史接上。
平台层没有可见的子 Agent 调度与独立治理实体backend/pkg/taskflow/types.go:626如果 Codex/OpenCode 内部自己再派子 Agent,MonkeyCode 目前只会把它们看成同一间 VM 里的同一项任务,无法逐个授权、暂停和计费。
18
L1 · fact · monkey-remote-001

远程协作以 owner write gate、历史回放和实时流为核心

先看源码事实

WebSocket stream 根据任务 owner 决定 writable;非 owner 的上行消息被丢弃。attach 模式先订阅并回放历史,再消费实时事件;用户可继续输入、停止、取消当前操作、切 auto-approve 和回答 Agent 问题。

翻译成白话

旁观者能看直播,任务主人才能按按钮和继续对话;掉线重连后还能从历史接上。

为什么这对自研重要

它实现了人—Agent 远程接管,而不是多 Agent 共享计划或黑板。

固定提交源码摘录
  323  // Stream 任务数据流 WebSocket
  324  //
  325  //	@Summary		任务数据流 WebSocket
  326  //	@Description	功能定位:该接口通过 WebSocket 转发任务运行数据。任务对话继续输入使用 `type=user-input`。
  327  //	@Description	数据格式约定:当前仅支持文本帧透传。服务端将 Agent 的原始文本数据包装为如下结构返回给前端(对应 domain.TaskStream):
  328  //	@Description	```json
  329  //	@Description	{ "type": "string", "data": "string", "kind": "string", "timestamp": 0 }
  330  //	@Description	```
  331  //	@Description	user-input 上行新格式:
  332  //	@Description	```json
  333  //	@Description	{ "type": "user-input", "data": "{\"content\":\"57un57ut5aSE55CG6L+Z5Liq6Zeu6aKY\",\"attachments\":[{\"url\":\"https://example-bucket.oss-cn-hangzhou.aliyuncs.com/temp/a.txt\",\"filename\":\"a.txt\"}]}" }
  334  //	@Description	```
  335  //	@Description	user-input 上行旧格式仍兼容:
  336  //	@Description	```json
  337  //	@Description	{ "type": "user-input", "data": "继续处理这个问题" }
  338  //	@Description	```
  339  //	@Description	user-input 下行和历史返回统一使用新 JSON payload 字符串:
  340  //	@Description	```json
  341  //	@Description	{ "type": "user-input", "data": "{\"content\":\"57un57ut5aSE55CG6L+Z5Liq6Zeu6aKY\",\"attachments\":[]}", "timestamp": 0 }
  342  //	@Description	```
  343  //	@Description	`attachments` 为可选附件列表,最多 10 个;每项包含 `url` 和 `filename`,URL 需要匹配后端配置的附件白名单前缀。
  344  //	@Description	type 字段说明:
  345  //	@Description	- task-started: 本轮任务启动
  346  //	@Description	- task-ended: 本轮任务结束
  347  //	@Description	- task-error: 本轮任务发生错误
      … 26 lines omitted; exact range 323–384 …
  374  func (h *TaskHandler) Stream(c *web.Context, req domain.TaskStreamReq) error {
  375  	user := middleware.GetUser(c)
  376  	task, owner, err := h.usecase.Info(c.Request().Context(), user, req.ID)
  377  	if err != nil {
  378  		return err
  379  	}
  380  
  381  	if req.Mode == "" {
  382  		req.Mode = "new"
  383  	}
  384  	return h.stream(c, user, task, owner, req.Mode)
为什么相信这条结论?查看 3 处证据
19
L2 · limitation · monkey-subagent-001

平台层没有可见的子 Agent 调度与独立治理实体

先看源码事实

Taskflow CreateTaskReq 只有单一 CodingAgent、单 VM、单 task/session;WebSocket 和 MCP 审计也都按同一 task/VM 归因。仓库未定义 child task、parent task、handoff 或 child capability 合同。

翻译成白话

如果 Codex/OpenCode 内部自己再派子 Agent,MonkeyCode 目前只会把它们看成同一间 VM 里的同一项任务,无法逐个授权、暂停和计费。

为什么这对自研重要

多 Agent 能力最多继承 runtime,本平台尚未形成可观测、可治理的协作控制面。

固定提交源码摘录
  626  // CreateTaskReq 创建任务请求
  627  type CreateTaskReq struct {
  628  	ID             uuid.UUID         `json:"id"`
  629  	VMID           string            `json:"vm_id"`
  630  	SystemPrompt   string            `json:"system_prompt,omitempty"`
  631  	Text           string            `json:"text,omitempty"`
  632  	Attachments    []Attachment      `json:"attachments,omitempty"`
  633  	LLM            LLM               `json:"llm,omitzero"`
  634  	CodingAgent    CodingAgent       `json:"coding_agent,omitempty"`
  635  	Configs        []ConfigFile      `json:"configs,omitzero"`
  636  	McpConfigs     []McpServerConfig `json:"mcp_configs,omitzero"`
  637  	Env            map[string]string `json:"env,omitempty"`
  638  	LogStore       string            `json:"log_store,omitempty"`
  639  	AgentResources *AgentResources   `json:"agent_resources,omitempty"` // skill/plugin presigned URLs + rule content forwarded to codingmatrix agent
  640  }
为什么相信这条结论?查看 3 处证据
小练习 8

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M09 · STATE

会话、持久化与观测:让一次运行变成可追溯事实

如果进程崩了、用户刷新了、任务跑了一夜,系统凭什么恢复并解释“刚才究竟发生了什么”?

先用一个生活比喻

内存像白板,数据库像目录,append-only journal 像监控录像;可靠 Harness 不只保存最后答案,还保存每次转弯。

这套实现先回答了什么?

回答照常流给 Agent,同时平台在旁边读水表,不必让每个 CLI 各写一套计费代码。

本节阅读法先问问题读事实看代码做迁移判断
源码问题固定提交给出的线索白话结论
模型流被旁路解析,用量归因到 task/user/VMbackend/biz/llmproxy/proxy.go:246回答照常流给 Agent,同时平台在旁边读水表,不必让每个 CLI 各写一套计费代码。
遥测采用 OTLP,但输出前做严格 allowlist 消毒backend/pkg/telemetry/telemetry.go:29它不是把请求体、URL 和异常详情整包发给观测平台,而是先过一遍“只准这些字段出门”的白名单。
20
L1 · fact · monkey-observe-001

模型流被旁路解析,用量归因到 task/user/VM

先看源码事实

成功响应 body 被 UsageCapture 包装,兼容流式与非流式;token 结果被组装为含 task、user、provider、model、input/output/cache/total、request ID 的 usage event。

翻译成白话

回答照常流给 Agent,同时平台在旁边读水表,不必让每个 CLI 各写一套计费代码。

为什么这对自研重要

统一代理是跨 Harness 成本观测的高价值控制点。

固定提交源码摘录
  246  func (p *Proxy) modifyResponse(resp *http.Response) error {
  247  	if resp == nil || resp.Body == nil {
  248  		return nil
  249  	}
  250  	if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
  251  		return nil
  252  	}
  253  	ctx, ok := resp.Request.Context().Value(contextKey{}).(*proxyContext)
  254  	if !ok || ctx == nil || ctx.model == nil {
  255  		return nil
  256  	}
  257  	resp.Body = NewUsageCapture(p.logger, resp.Body, &UsageCaptureContext{
  258  		ctx:      resp.Request.Context(),
  259  		path:     normalizeUsageCapturePath(resp.Request.URL.Path),
  260  		stream:   ctx.stream,
  261  		proxyCtx: ctx,
  262  		proxy:    p,
  263  	})
  264  	return nil
为什么相信这条结论?查看 2 处证据
21
L1 · fact · monkey-observe-002

遥测采用 OTLP,但输出前做严格 allowlist 消毒

先看源码事实

OTLP exporter 用 batch processor;导出前过滤 span attributes、events、links 和 status,仅保留 task/session/request/VM、HTTP/RPC/DB 等允许字段,exception 只留类型并清空 status 描述。

翻译成白话

它不是把请求体、URL 和异常详情整包发给观测平台,而是先过一遍“只准这些字段出门”的白名单。

为什么这对自研重要

降低 prompt、token、密钥进入第三方 trace backend 的风险,但也牺牲部分排障细节。

固定提交源码摘录
   29  func Setup(ctx context.Context, cfg Config) (Shutdown, error) {
   30  	otel.SetTextMapPropagator(propagation.TraceContext{})
   31  	if !enabled(cfg) {
   32  		return func(context.Context) error { return nil }, nil
   33  	}
   34  
   35  	opts := []otlptracegrpc.Option{}
   36  	if endpoint := strings.TrimSpace(cfg.Endpoint); endpoint != "" {
   37  		if strings.Contains(endpoint, "://") {
   38  			opts = append(opts, otlptracegrpc.WithEndpointURL(endpoint))
   39  		} else {
   40  			opts = append(opts, otlptracegrpc.WithEndpoint(endpoint))
   41  		}
   42  	}
   43  	if cfg.Insecure {
   44  		opts = append(opts, otlptracegrpc.WithInsecure())
   45  	}
   46  
   47  	exporter, err := otlptracegrpc.New(ctx, opts...)
   48  	if err != nil {
   49  		return nil, err
   50  	}
   51  	tp, ok := otel.GetTracerProvider().(*sdktrace.TracerProvider)
   52  	if !ok {
   53  		_ = exporter.Shutdown(ctx)
      … 1 lines omitted; exact range 29–65 …
   55  	}
   56  
   57  	processor := sdktrace.NewBatchSpanProcessor(
   58  		&resourceExporter{SpanExporter: exporter, resource: buildResource(cfg)},
   59  		sdktrace.WithMaxQueueSize(2048),
   60  		sdktrace.WithMaxExportBatchSize(512),
   61  		sdktrace.WithBatchTimeout(5*time.Second),
   62  		sdktrace.WithExportTimeout(5*time.Second),
   63  	)
   64  	tp.RegisterSpanProcessor(processor)
   65  	return processor.Shutdown, nil
为什么相信这条结论?查看 4 处证据
小练习 9

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M10 · ENGINEERING

测试、恢复与工程取舍:把漂亮机制变成可靠产品

哪些行为有测试证明?哪些只是配置或 prompt 约定?当安全、性能、可恢复性冲突时,源码选择了什么?

先用一个生活比喻

这像验收一座桥:图纸说明结构,测试证明承重,故障演练证明断电后还能不能让人安全回来。

本节阅读法先问问题读事实看代码做迁移判断
读源码时先问本课的判断方式
这一层有没有独立证据?没有就不把其他层的能力冒充成默认行为;回到固定提交继续追调用链。
谁拥有最终控制权?区分模型输出、框架规则、用户审批和 OS 隔离四种不同力量。
这一章先建立通用概念

固定提交账本没有把该维度单独拆出,但它会在其他章节的源码路径中体现。先沿执行链路阅读,再回到报告页核对证据。

小练习 10

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M11 · PRACTICE

把读懂变成会判断

下面的练习不要求你先写一个完整 Agent,而是训练你检查设计边界:事实是什么、推断是什么、如果换成自研产品要补哪一层。

Q1

它是多 CLI 的任务控制平面,不是第四套 Agent loop

问题:如果把这段机制移植到你的 Agent,最先要确认哪个输入、状态或安全边界?

参考答案

比较 Harness 时,应把平台编排能力与各 CLI 内核能力拆开计分。

证据:backend/pkg/taskflow/types.go:554
Q2

任务创建拆成数据库预登记、VM 创建、Redis 交接、运行态启动

问题:如果把这段机制移植到你的 Agent,最先要确认哪个输入、状态或安全边界?

参考答案

分阶段便于恢复和审计,但 Redis 交接键成为启动链路的关键依赖。

证据:backend/biz/task/usecase/task.go:556
Q3

Taskflow Create 失败被记录但吞掉,任务可能滞留 processing

问题:如果把这段机制移植到你的 Agent,最先要确认哪个输入、状态或安全边界?

参考答案

应把 Create 错误返回给生命周期管理器,并为 processing-without-session 增加 watchdog。

证据:backend/pkg/lifecycle/taskhook.go:104
Q4

隔离边界主要依赖仓库外 VM,Codex 内层 sandbox 明确关闭

问题:如果把这段机制移植到你的 Agent,最先要确认哪个输入、状态或安全边界?

参考答案

部署评审必须联审 codingmatrix/Taskflow 执行器,不能因类型名叫 VirtualMachine 就自动认定强隔离。

证据:backend/pkg/taskflow/types.go:72
Q5

远程协作以 owner write gate、历史回放和实时流为核心

问题:如果把这段机制移植到你的 Agent,最先要确认哪个输入、状态或安全边界?

参考答案

它实现了人—Agent 远程接管,而不是多 Agent 共享计划或黑板。

证据:backend/biz/task/handler/v1/task.go:323
APPENDIX · SOURCE INDEX

本课读过的实现文件

文件索引帮助你在课程外继续追踪调用链;每一条路径来自固定提交的证据账本。

  1. 01backend/pkg/taskflow/types.goL554–587, L626–640, L72–92, L122–139, L626–640, L626–640
  2. 02backend/biz/task/usecase/task.goL875–928, L556–617, L622–687, L157–163, L585–590, L165–218, L253–299, L788–792, L795–803, L955–1002, L1004–1041, L982–1010, L741–767
  3. 03backend/pkg/lifecycle/taskhook.goL92–130, L104–123, L129–136
  4. 04backend/templates/codex.tmplL1–13, L1–6
  5. 05backend/biz/task/handler/v1/task.goL323–384, L387–423, L640–721, L585–610
  6. 06backend/templates/opencode.tmplL40–50, L27–40
  7. 07backend/biz/llmproxy/proxy.goL160–185, L203–236, L28–34, L116–150, L246–264, L280–317
  8. 08backend/biz/task/service/tasksummary.goL32–92, L121–145, L148–186
  9. 09backend/biz/agentresource/resolver.goL13–29, L49–54
  10. 10backend/biz/agentresource/types.goL99–126
  11. 11backend/biz/agentresource/unpack.goL12–31, L38–98, L101–126
  12. 12backend/biz/mcphub/auth/service.goL45–80, L22–28
  13. 13backend/biz/mcphub/runtime/registry/service.goL56–80
  14. 14backend/biz/mcphub/runtime/gateway/handler.goL144–185, L187–260, L263–267, L228–251
  15. 15backend/biz/mcphub/billing/noop.goL9–20
  16. 16backend/pkg/netguard/guard.goL53–69, L143–179
  17. 17backend/pkg/netguard/guard_test.goL22–59
  18. 18backend/config/server/config.yaml.exampleL7–10
  19. 19backend/pkg/telemetry/telemetry.goL29–65, L95–107
  20. 20backend/pkg/telemetry/sanitize.goL10–78, L102–127
下一步

从课程回到报告,做一次反向核验

教程负责让你读懂,报告负责让你查证。打开报告页,任选一个章节,尝试只靠源码摘录复述它的边界。

查看 MonkeyCode 报告 ↗