# fundev **Repository Path**: ledao/fundev ## Basic Information - **Project Name**: fundev - **Description**: No description available - **Primary Language**: Unknown - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-07-11 - **Last Updated**: 2026-08-09 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # fundev 用 Go 写的终端编码 agent(对标 Claude Code)。除了作为 TUI 程序跑,它的核心是一套**可嵌入的 SDK**——`package fundev`:你在自己的 Go 程序里就能驱动一个完整的编码 agent(工具循环、权限、 上下文压缩、会话历史都在里面)。fundev 自己的 TUI 就是这套 SDK 的**参考客户端**,它维护的会话 状态一点没自留、全走 SDK;你照它接即可。 本文讲 **SDK 入门接入**。构建 / 运行 TUI 见 `CLAUDE.md`。 ## 心智模型:一个会话句柄 + 两个标配组件 **调用方持有历史**:`*Session` 不自留会话历史——你把本回合历史传进去,它把更新后的**完整历史**回传 给你,下一回合再传回来即可。历史权威在你手里(或落盘),故同一进程内多个会话可并发、状态零串扰。 - **`*Session`** — 会话句柄:持有后端 / 模型 / 推理档位 / 计划模式 / 工作目录 / shell 等 per-session 状态(随每回合经 ctx 注入,不落进程全局)。你用 `Run`(无头,直接回传更新后的历史)或 `RunTurn` (交互式,权限请求带稳定 ID)驱动一回合;它跑工具循环、发流式事件、处理权限。 - **`History`** — 「语义条目 → 良构 API 历史」。tool_use/result 配对、悬空调用合成、压缩摘要,全在这里。 - **`Transcript`** — 「agent 事件 → 可渲染时间线」(含进行中的流式/思考)。做 UI 时用它,你只写样式, 不用重造那套累积逻辑;它内部就持有一个 `History`。 你要写 UI,就用 `Transcript` + `RunTurn`;要做无头/服务端,用 `Run` 消费事件、拿回传历史即可。 ## 安装 ```sh go get gitee.com/ledao/fundev ``` 后端走 Claude 兼容协议,环境变量注入(也可按会话注入,见「配置」): ```sh export ANTHROPIC_BASE_URL=https://api.anthropic.com # 兼容后端地址 export ANTHROPIC_AUTH_TOKEN=sk-... # 或 ANTHROPIC_API_KEY export ANTHROPIC_MODEL=claude-opus-4-8 # 默认 claude-opus-4-8 ``` 工具里的 Glob/Grep 依赖系统 `rg`(ripgrep),缺失时装 `ripgrep` 包即可。 ## 30 秒最小示例(无头:调用方持有历史) `Run` 驱动一回合:传入本回合历史(含最新用户消息),阻塞到本回合工具循环跑完,**回传更新后的完整 历史**——直接追加下一条用户消息就能跑下一轮。事件里 `TextDelta` 是助手文本增量;写类工具执行前来 `PermissionRequest`,你往它的 `Reply` 通道回执裁决。 ```go package main import ( "context" "fmt" fundev "gitee.com/ledao/fundev" ) func main() { s := fundev.NewSession(fundev.Options{}) // 零值=满配编码 agent,后端读 ANTHROPIC_* 环境变量 ctx := context.Background() emit := func(ev fundev.Event) { switch e := ev.(type) { case fundev.TextDelta: fmt.Print(e.Text) case fundev.ToolUse: fmt.Printf("\n[工具 %s]\n", e.Name) case fundev.PermissionRequest: // 写类工具执行前的确认:经 Reply 通道由本端裁决 e.Reply <- fundev.Reply{Choice: fundev.AllowOnce} case fundev.Errored: fmt.Println("\n出错:", e.Err) } } // 第一回合:Run 返回即本回合结束,hist 是更新后的完整历史。 hist, _ := s.Run(ctx, fundev.NewTurn("列出当前目录的 Go 文件,数一下有多少个"), emit) // 多轮:把回传历史追加新用户消息,再 Run 一次即可(引擎不自持历史)。 hist = append(hist, fundev.UserText("再按修改时间排序")) _, _ = s.Run(ctx, hist, emit) } ``` > 要跨进程 / WebSocket 应答权限(带稳定 ID、可序列化),或做交互式 UI,改用 `RunTurn`——见下文 > 「做交互式 UI」。低层 `Run` 的权限走进程内 `Reply` 通道,适合同进程无头驱动。 ## 事件流 `Run` 的 emit 收到 `fundev.Event`;`RunTurn` 的 emit 收到 `any`(要么是 `fundev.Event`,要么是下面的 `InteractionRequest`)。都用类型 switch 分派。 **流式领域事件**(都实现 `fundev.Event`,样式无关): | 事件 | 关键字段 | 含义 | |------|----------|------| | `TextDelta` | `Text` | 助手正式回复的文本增量 | | `ThinkingDelta` | `Text` | 扩展思考增量 | | `ToolUse` | `ID, Name string; Input json.RawMessage` | 模型发起一次工具调用 | | `ToolResult` | `ToolUseID, Content string; Images []Block` | 一次工具结果 | | `Notice` | `Text` | 系统提示(仅显示,不进历史) | | `Compacted` | `Summary string` | 上下文被自动压缩(`Transcript` 据此持久化压缩边界) | | `Usage` | `Input, Output, Total…` | 本次请求 token 计量 | | `RetryScheduled` | `Attempt, Max int; Delay; Err` | 可重试错误退避中(供倒计时) | | `Errored` | `Err error` | 出错 | | `Done` | — | agent 一次运行结束(每轮一个) | **权限 / 提问请求** —— 形态取决于驱动: | 驱动 | 请求 | 应答 | |------|------|------| | 低层 `Run`(emit 收 `Event`) | `PermissionRequest{Title, Detail, Options, Reply}` / `QuestionRequest{Questions, Reply}` | 往其 `Reply` 通道发 `Reply{…}` / `[]string{…}`(进程内) | | 交互 `RunTurn`(emit 收 `any`) | `InteractionRequest{ID, Kind, Title, Detail, Options, Questions}` | `s.Respond(id, Reply{…})` / `s.RespondQuestion(id, []string{…})`(带稳定 ID,可序列化,跨进程/WebSocket) | 一回合何时结束:`Run` / `RunTurn` **返回**即本回合(工具循环)跑完;过程中也会收到一个 `Done` 事件。 ## 权限与提问 走 `RunTurn` 时,写类工具(Write/Edit/Bash…)执行前来 `InteractionRequest{Kind: InteractionPermission}`, 你按 `Title` 自定裁决(走低层 `Run` 则改用 `PermissionRequest`、往它的 `Reply` 通道回执): ```go case fundev.InteractionRequest: if e.Kind == fundev.InteractionPermission { choice := fundev.Deny if strings.HasPrefix(e.Title, "Read") { // 举例:只读类自动放行 choice = fundev.AllowOnce } s.Respond(e.ID, fundev.Reply{Choice: choice, Feedback: "只允许读操作"}) } ``` 三种选择:`AllowOnce`(仅本次)、`AllowAlways`(本会话此类不再问)、`Deny`(拒绝,`Feedback` 回给 模型)。不想逐个应答: ```go s := fundev.NewSession(fundev.Options{ BypassPermissions: true, // 跳过确认(deny 规则 / 敏感路径 / 灾难命令仍拦) // 或 DelegatePermissions: true —— 全交本端裁决,不读 settings.json 的 permissions 规则 }) ``` WebSocket 断线但会话续存时,用 `s.CancelInteractions()` 把悬着的请求全部按取消解阻。 ## 做交互式 UI:`Transcript` + `RunTurn` 要渲染一个活的 TUI/Web 界面,难点不是把历史条目画出来,而是**进行中的部分**:流式助手文本要 缓冲、逐帧显示半截,到工具边界才定稿;思考实时显示、边界丢弃;打断标记;tool 配对。这套累积逻辑 `Transcript` 都替你做了——你喂事件、读 `Items()` 逐帧渲染(`ViewItem` 只描述「显示什么」,样式你定)。 此路径下 **`Transcript` 是会话权威**,你把 `tr.History().Messages()` 交给 `RunTurn` 驱动(`RunTurn` 不自累积,历史全由 `Transcript` 持有——避免两份历史): ```go s := fundev.NewSession(fundev.Options{}) tr := fundev.NewTranscript() runTurn := func(userText string) { tr.Submit(userText, "") // 记进会话时间线(第二参 apiText:斜杠命令展开后的实际文本,空则同 userText) render(tr.Items()) // 你的渲染函数:逐个 ViewItem 上样式 _, _ = s.RunTurn(context.Background(), tr.History().Messages(), func(ev any) { if e, ok := ev.(fundev.Event); ok { tr.Observe(e) // 流式事件 → 更新时间线(含 Live 进行中项) } if ir, ok := ev.(fundev.InteractionRequest); ok && ir.Kind == fundev.InteractionPermission { s.Respond(ir.ID, fundev.Reply{Choice: fundev.AllowOnce}) } render(tr.Items()) // 每个事件后重画 }) } runTurn("重构 X 模块") ``` `Items()` 里每个 `ViewItem`: | 字段 | 说明 | |------|------| | `Kind` | `ViewUser` / `ViewAssistant` / `ViewToolUse` / `ViewToolResult` / `ViewInterrupt` / `ViewThinking` | | `Text` / `ToolID` / `ToolName` / `ToolInput` / `Images` | 按 Kind 取用 | | `Live` | 进行中(未定稿):流式助手文本 / 思考——你可给它加个光标/呼吸效果 | 打断当前回合:`tr.Interrupt()`(定稿半截文本 + 注入中断标记);配合取消传给 `RunTurn` 的 ctx。 notice、滚动、样式等纯 UI 状态由你自管——`Transcript` 只管会话时间线。 > 持久化,两条路: > - **自管**:存 `tr.History().Entries()` 与 `tr.History().Compaction()`(压缩边界+摘要),恢复用 > `tr.History().Reset(entries)` + `SetCompaction`——语义条目,跨端稳定,你决定存哪。 > - **用会话库门面**(磁盘单一权威、多端可见、可续接):`NewSessionTranscript(cwd, id)` 造一个写穿式 > Transcript,`Observe` 收到的定稿条目逐条落盘 JSONL(无需每回合全量保存);配套 > `SaveSession` / `LoadSession` / `ListSessions` / `MostRecentSession`,`/resume` 时用 > `NewSessionTranscriptSeeded(cwd, id, entries, boundary, summary)` 从已恢复的历史重建时间线。 > fundev 自己的 TUI 走的就是这条。 ## 配置:模型 / 推理档位 / 工作目录 / 多会话 配置都挂在会话句柄上,每回合注入、不落进程全局,故同进程多会话可并发、各跑各的: ```go s := fundev.NewSession(fundev.Options{ Config: fundev.Config{ BaseURL: "https://my-gateway/v1", // 留空用内置默认(SDK 不读 env) AuthToken: "sk-...", // 或 APIKey;两者都空则请求报「未注入鉴权」 Model: "claude-opus-4-8", }, WorkDir: "/path/to/repoA", // 文件工具路径基准 + Bash 初始 cwd;多会话各设各的即可跑在不同 worktree Effort: "high", // 推理档位:off/low/medium/high/xhigh }) s.SetModel("claude-sonnet-5") // 运行中调整,下一回合生效 s.SetEffort("low") s.SetPlanMode(true) // 计划模式:只调研不改动 s.Steer("先别动测试文件") // 回合进行中插话,下个工具边界并入 s.Interrupt() // 打断当前回合 ``` ## 自定义工具 零值 `Options` = 满配编码 agent(内置 Read/Write/Edit/Bash/Glob/Grep/…)。加你自己的工具: ```go weather := fundev.ToolDef{ Name: "get_weather", Description: "查询某城市的当前天气", InputSchema: json.RawMessage(`{"type":"object","properties":{"city":{"type":"string"}},"required":["city"]}`), ReadOnly: true, // 进只读并发白名单(与 Read/Grep 同批并发跑) Handler: func(ctx context.Context, input json.RawMessage, progress func(string)) (fundev.ToolOutput, error) { var in struct { City string `json:"city"` } _ = json.Unmarshal(input, &in) progress("查询中…") // 执行中回吐进度(本端收到 Notice 事件) return fundev.ToolOutput{Text: in.City + ":晴,26°C"}, nil }, } s := fundev.NewSession(fundev.Options{ ExtraTools: []fundev.ToolDef{weather}, DisableTools: []string{"WebFetch"}, // 摘掉不想要的内置工具(模型看不到) }) ``` `NeedsApproval: true` 让该工具执行前也走权限确认;`ToolOutput.Images`(用 `fundev.ImageBlock` 构造) 可在结果里附图。 ## 自定义系统提示 / 纯宿主接管 ```go s := fundev.NewSession(fundev.Options{ SystemPrompt: func(base string) string { // 拿内置 base,追加或整段替换 return base + "\n\n补充规则:回答用中文;不要主动 git push。" }, DisableAmbientConfig: true, // 不读 .claude/settings.json、.mcp.json、SKILL.md、.claude/agents DelegatePermissions: true, // 权限全交本端 }) ``` ## 两个标配组件也可单用 / 覆盖 `History` 与 `Transcript` 都可脱离 `Session` 单独用(如离线处理一段事件、或做纯投影): ```go h := fundev.NewHistory() h.Append(fundev.Entry{Kind: fundev.EntryUser, Text: "hi"}) msgs := h.Messages() // 良构 API 历史 tr := fundev.NewTranscript() tr.Observe(fundev.TextDelta{Text: "答"}) items := tr.Items() // 渲染时间线 ``` 宿主已有自己的会话模型时,可实现 `History` 接口,用 `NewTranscriptWith(h)` 传给 `Transcript`;或干脆 绕开这两个组件,自己拼 `[]Message` 直接喂 `Run` / `RunTurn`(历史本就由调用方持有)。 ## 进阶:低层 `Run` / `RunTurn` - `Session.RunTurn(ctx, history, emit)` —— **调用方持史**(如上「做 UI」);`emit` 收到 `any`。 - `Session.Run(ctx, history, emit)` —— 最低层,`emit` 收 `fundev.Event`,权限走进程内 chan(`PermissionRequest.Reply`)。同步一次性驱动用。 - 包级 `fundev.Run` / `fundev.RunWith` —— 不建会话的一次性用法(走环境变量 / 按调用指定后端)。 ## 许可 见仓库 LICENSE。