# 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-07-20 ## 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`** — 会话引擎。你 `Submit` 用户消息、订阅事件;它跑工具循环、发流式事件、处理权限。 - **`History`** — 「agent 事件 → 良构 API 历史」。tool_use/result 配对、悬空调用合成、压缩摘要,全在这里。 - **`Transcript`** — 「agent 事件 → 可渲染时间线」(含进行中的流式/思考)。做 UI 时用它,你只写样式, 不用重造那套累积逻辑。 你要写 UI,就消费 `Transcript`;要做无头/服务端,直接消费事件即可。 ## 安装 ```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 秒最小示例(无头:引擎持有历史) `Subscribe` 拿事件流,`Submit` 发一轮。引擎自己持有会话历史,多轮就再 `Submit`。事件里 `TextDelta` 是助手文本增量;写类工具执行前来 `InteractionRequest`,你按 ID 用 `Respond` 应答;`StateChanged` 到 `StateIdle` 表示本轮(及排队)全部结束。 ```go package main import ( "fmt" fundev "gitee.com/ledao/fundev" ) func main() { s := fundev.NewSession(fundev.Options{}) // 零值=满配编码 agent,后端读 ANTHROPIC_* 环境变量 _, events, cancel := s.Subscribe() // 快照 + 后续事件流 + 退订 defer cancel() s.Submit(fundev.UserText("列出当前目录的 Go 文件,数一下有多少个")) for ev := range events { switch e := ev.(type) { case fundev.TextDelta: fmt.Print(e.Text) case fundev.ToolUse: fmt.Printf("\n[工具 %s]\n", e.Name) case fundev.InteractionRequest: // 权限/提问,带稳定 ID if e.Kind == fundev.InteractionPermission { s.Respond(e.ID, fundev.Reply{Choice: fundev.AllowOnce}) // 由本端裁决 } case fundev.Errored: fmt.Println("\n出错:", e.Err) case fundev.StateChanged: if e.State == fundev.StateIdle { return // 空闲=全部跑完 } } } } ``` > `Subscribe` 可被多次调用——多个前端订阅同一个会话,事件广播给每一个(迟到的用返回的快照追上)。 ## 事件流 订阅通道上流过的是 `any`,用类型 switch 分派。三类: **流式领域事件**(都实现 `fundev.Event`,样式无关): | 事件 | 关键字段 | 含义 | |------|----------|------| | `TextDelta` | `Text` | 助手正式回复的文本增量 | | `ThinkingDelta` | `Text` | 扩展思考增量 | | `ToolUse` | `ID, Name string; Input json.RawMessage` | 模型发起一次工具调用 | | `ToolResult` | `ToolUseID, Content string; Images []Block` | 一次工具结果 | | `Notice` | `Text` | 系统提示(仅显示,不进历史) | | `Usage` | `Input, Output, Total…` | 本次请求 token 计量 | | `RetryScheduled` | `Attempt, Max int; Delay; Err` | 可重试错误退避中(供倒计时) | | `Errored` | `Err error` | 出错 | | `Done` | — | agent 一次运行结束(每轮一个) | **交互请求**(可序列化,跨进程/WebSocket 也适用): | 事件 | 关键字段 | 应答 | |------|----------|------| | `InteractionRequest` | `ID; Kind(InteractionPermission/InteractionQuestion); Title, Detail; Options; Questions` | `s.Respond(id, Reply{…})` / `s.RespondQuestion(id, []string{…})` | **回合状态**: | 事件 | 含义 | |------|------| | `StateChanged{State}` | `StateRunning` / `StateIdle`——供 UI 启用/禁用输入框等 | ## 权限与提问 写类工具(Write/Edit/Bash…)执行前来 `InteractionRequest{Kind: InteractionPermission}`,你按 `Title` 自定裁决: ```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().Reset(entries)`——语义条目,跨端稳定。 ## 配置:模型 / 推理档位 / 工作目录 / 多会话 配置都挂在会话句柄上,每回合注入、不落进程全局,故同进程多会话可并发、各跑各的: ```go s := fundev.NewSession(fundev.Options{ Config: fundev.Config{ BaseURL: "https://my-gateway/v1", // 留空回落 ANTHROPIC_BASE_URL 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-4-6") // 运行中调整,下一回合生效 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` 接口经 `Options.History` 覆盖标配。 ## 进阶:低层 `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。