package builtin // scratchpad_tool.go 实现 Scratchpad 三件套工具. // // 工具列表: // - scratchpad_write:写入键值对(可选 TTL) // - scratchpad_read:读取指定键的值 // - scratchpad_list:列出所有未过期的键 // // 设计决策: // // 三个工具共享同一个 *Scratchpad 实例(由 Engine.New() 创建后注入). // 工具自身无状态--Scratchpad 是状态,工具是操作接口. // 这和 TaskCreate/TaskList/TaskUpdate 共享 *TaskStore 的模式完全一致. // // 升华改进(ELEVATED): Scratchpad 工具是"Agent 工作内存"的外化接口-- // // 允许 Agent 在不污染对话历史的情况下存储推理中间状态. // 跨行业扩展:数据分析 Agent 存储"当前处理到第几行", // 供后续轮次恢复而无需重新计算;物流 Agent 存储"已分配的路线 ID 集合". // 替代方案:<让 Agent 把临时结果放进对话历史> // - 否决:临时数据污染对话历史,撑大上下文,降低模型推理质量. import ( "context" "encoding/json" "fmt" "strings" "time" "git.flytoex.net/yuanwei/flyto-agent/pkg/permission" "git.flytoex.net/yuanwei/flyto-agent/pkg/tools" ) // ScratchpadStore 是 Scratchpad 存储接口. // // 精妙之处(CLEVER): 定义接口而非直接依赖 *engine.Scratchpad-- // builtin 包不能 import engine 包(循环导入). // 接口声明在 builtin 包中,engine 包的 *Scratchpad 实现此接口, // 在 engine.registerBuiltinTools() 中注入--依赖反转,解循环. // 替代方案:<把 Scratchpad 移到独立包> - 可行,但增加包层级; // 接口注入方案代价最低,与 TaskStore 模式一致. type ScratchpadStore interface { Set(key, value string, ttl time.Duration) Get(key string) (string, bool) Delete(key string) Keys() []string } // ========== ScratchpadWriteTool ========== // ScratchpadWriteTool 向 Scratchpad 写入键值对. type ScratchpadWriteTool struct { store ScratchpadStore } // NewScratchpadWriteTool 创建 scratchpad_write 工具实例. func NewScratchpadWriteTool(store ScratchpadStore) *ScratchpadWriteTool { return &ScratchpadWriteTool{store: store} } func (t *ScratchpadWriteTool) Name() string { return "scratchpad_write" } func (t *ScratchpadWriteTool) Description(_ context.Context) string { return "Writes a key-value pair to the scratchpad (in-memory temporary storage). " + "Use this to store intermediate results, planning notes, or state flags " + "that you need to access in later conversation turns without polluting the chat history. " + "Optionally set ttl_seconds to auto-expire the entry." } func (t *ScratchpadWriteTool) InputSchema() json.RawMessage { return json.RawMessage(`{ "type": "object", "properties": { "key": { "type": "string", "description": "The key to store the value under. Use descriptive names like 'step_count' or 'pending_items'." }, "value": { "type": "string", "description": "The value to store. Can be plain text, JSON, or any string representation." }, "ttl_seconds": { "type": "number", "description": "Optional time-to-live in seconds. 0 or omitted means never expire." } }, "required": ["key", "value"] }`) } func (t *ScratchpadWriteTool) Metadata() tools.Metadata { return tools.Metadata{ ConcurrencySafe: true, ReadOnly: false, Destructive: false, SearchHint: "scratchpad write store save temporary memory", PermissionClass: permission.PermClassGeneric, AuditOperation: "write", } } // scratchpadWriteInput 是 scratchpad_write 的输入参数. type scratchpadWriteInput struct { Key string `json:"key"` Value string `json:"value"` TTLSeconds float64 `json:"ttl_seconds"` // 用 float64 接收,避免整数/浮点歧义 } func (t *ScratchpadWriteTool) Execute(_ context.Context, input json.RawMessage, _ tools.ProgressFunc) (*tools.Result, error) { var params scratchpadWriteInput if err := json.Unmarshal(input, ¶ms); err != nil { return nil, fmt.Errorf("scratchpad_write: invalid input: %w", err) } if params.Key == "" { return &tools.Result{Output: "error: key is required", IsError: true}, nil } // 计算 TTL var ttl time.Duration if params.TTLSeconds > 0 { // 精妙之处(CLEVER): float64 → time.Duration 路径-- // 用户可能传 3.5(3.5 秒),直接 time.Duration(params.TTLSeconds) 会把它当纳秒, // 必须乘以 time.Second.float64 乘法保留小数精度,再转 Duration 截断到纳秒. ttl = time.Duration(params.TTLSeconds * float64(time.Second)) } t.store.Set(params.Key, params.Value, ttl) msg := fmt.Sprintf("scratchpad: wrote key=%q (%d bytes)", params.Key, len(params.Value)) if ttl > 0 { msg += fmt.Sprintf(", expires in %.1fs", params.TTLSeconds) } return &tools.Result{Output: msg}, nil } // ========== ScratchpadReadTool ========== // ScratchpadReadTool 从 Scratchpad 读取指定键的值. type ScratchpadReadTool struct { store ScratchpadStore } // NewScratchpadReadTool 创建 scratchpad_read 工具实例. func NewScratchpadReadTool(store ScratchpadStore) *ScratchpadReadTool { return &ScratchpadReadTool{store: store} } func (t *ScratchpadReadTool) Name() string { return "scratchpad_read" } func (t *ScratchpadReadTool) Description(_ context.Context) string { return "Reads a value from the scratchpad by key. " + "Returns the stored value and whether the key was found." } func (t *ScratchpadReadTool) InputSchema() json.RawMessage { return json.RawMessage(`{ "type": "object", "properties": { "key": { "type": "string", "description": "The key to read." } }, "required": ["key"] }`) } func (t *ScratchpadReadTool) Metadata() tools.Metadata { return tools.Metadata{ ConcurrencySafe: true, ReadOnly: true, Destructive: false, SearchHint: "scratchpad read get retrieve temporary memory", PermissionClass: permission.PermClassReadOnly, AuditOperation: "read", } } // scratchpadReadInput 是 scratchpad_read 的输入参数. type scratchpadReadInput struct { Key string `json:"key"` } // scratchpadReadOutput 是 scratchpad_read 的结构化输出. type scratchpadReadOutput struct { Value string `json:"value"` Found bool `json:"found"` } func (t *ScratchpadReadTool) Execute(_ context.Context, input json.RawMessage, _ tools.ProgressFunc) (*tools.Result, error) { var params scratchpadReadInput if err := json.Unmarshal(input, ¶ms); err != nil { return nil, fmt.Errorf("scratchpad_read: invalid input: %w", err) } if params.Key == "" { return &tools.Result{Output: "error: key is required", IsError: true}, nil } value, found := t.store.Get(params.Key) out := scratchpadReadOutput{Value: value, Found: found} data, _ := json.MarshalIndent(out, "", " ") return &tools.Result{Output: string(data), Data: out}, nil } // ========== ScratchpadListTool ========== // ScratchpadListTool 列出 Scratchpad 中所有未过期的键. type ScratchpadListTool struct { store ScratchpadStore } // NewScratchpadListTool 创建 scratchpad_list 工具实例. func NewScratchpadListTool(store ScratchpadStore) *ScratchpadListTool { return &ScratchpadListTool{store: store} } func (t *ScratchpadListTool) Name() string { return "scratchpad_list" } func (t *ScratchpadListTool) Description(_ context.Context) string { return "Lists all unexpired keys currently stored in the scratchpad. " + "Use this to discover what intermediate results are available before reading them." } func (t *ScratchpadListTool) InputSchema() json.RawMessage { // 精妙之处(CLEVER): 空 object schema(无必填参数)-- // 工具调用不需要任何输入,但 JSON Schema 仍然要求 type:object, // 否则某些 SDK 端会拒绝空 schema. return json.RawMessage(`{"type": "object", "properties": {}}`) } func (t *ScratchpadListTool) Metadata() tools.Metadata { return tools.Metadata{ ConcurrencySafe: true, ReadOnly: true, Destructive: false, SearchHint: "scratchpad list keys enumerate temporary memory", PermissionClass: permission.PermClassReadOnly, AuditOperation: "read", } } func (t *ScratchpadListTool) Execute(_ context.Context, _ json.RawMessage, _ tools.ProgressFunc) (*tools.Result, error) { keys := t.store.Keys() if len(keys) == 0 { return &tools.Result{Output: "scratchpad is empty"}, nil } output := fmt.Sprintf("scratchpad keys (%d):\n%s", len(keys), strings.Join(keys, "\n")) return &tools.Result{Output: output, Data: keys}, nil }