fix(tasktool): use agent session key, persist plan on create, add docs
- Add WithToolSessionKey/ToolSessionKey context helpers to pkg/tools/base.go - Inject opts.SessionKey into ctx in runAgentLoop so tasktool uses the same agent-scoped session key as conversation history instead of the bare channel:chatID composite (fixes plan cross-agent/scope collision) - Call Save inside CreatePlan so plans persist after restart even on channels that never return a message ID (fixes data loss on non-Telegram) - Add tasktool section to config/config.example.json and docs/tools_configuration.md (P3 documentation gap)
This commit is contained in:
parent
e7585e8185
commit
8f40b0099f
6 changed files with 78 additions and 7 deletions
|
|
@ -431,6 +431,15 @@
|
||||||
},
|
},
|
||||||
"write_file": {
|
"write_file": {
|
||||||
"enabled": true
|
"enabled": true
|
||||||
|
},
|
||||||
|
"tasktool": {
|
||||||
|
"enabled": true,
|
||||||
|
"icons": {
|
||||||
|
"pending": "🔘",
|
||||||
|
"in_progress": "🟡",
|
||||||
|
"completed": "🟢",
|
||||||
|
"failed": "🔴"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"heartbeat": {
|
"heartbeat": {
|
||||||
|
|
|
||||||
|
|
@ -206,6 +206,48 @@ The skills tool configures skill discovery and installation via registries like
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Task Tool
|
||||||
|
|
||||||
|
The task tool lets the agent create and track a step-by-step execution plan visible to the user in real time.
|
||||||
|
|
||||||
|
### Config
|
||||||
|
|
||||||
|
| Config | Type | Default | Description |
|
||||||
|
| ------------------- | ------ | ------- | -------------------------------------- |
|
||||||
|
| `enabled` | bool | true | Enable the task planning tool |
|
||||||
|
| `icons.pending` | string | 🔘 | Icon shown for pending tasks |
|
||||||
|
| `icons.in_progress` | string | 🟡 | Icon shown for tasks in progress |
|
||||||
|
| `icons.completed` | string | 🟢 | Icon shown for completed tasks |
|
||||||
|
| `icons.failed` | string | 🔴 | Icon shown for failed tasks |
|
||||||
|
|
||||||
|
### Configuration Example
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tools": {
|
||||||
|
"tasktool": {
|
||||||
|
"enabled": true,
|
||||||
|
"icons": {
|
||||||
|
"pending": "🔘",
|
||||||
|
"in_progress": "🟡",
|
||||||
|
"completed": "🟢",
|
||||||
|
"failed": "🔴"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
| Variable | Description |
|
||||||
|
| -------------------------------------------- | ------------------------ |
|
||||||
|
| `PICOCLAW_TOOLS_TASK_TOOL_ENABLED` | Enable/disable the tool |
|
||||||
|
| `PICOCLAW_TOOLS_TASK_TOOL_ICONS_PENDING` | Pending task icon |
|
||||||
|
| `PICOCLAW_TOOLS_TASK_TOOL_ICONS_IN_PROGRESS` | In-progress task icon |
|
||||||
|
| `PICOCLAW_TOOLS_TASK_TOOL_ICONS_COMPLETED` | Completed task icon |
|
||||||
|
| `PICOCLAW_TOOLS_TASK_TOOL_ICONS_FAILED` | Failed task icon |
|
||||||
|
|
||||||
## Environment Variables
|
## Environment Variables
|
||||||
|
|
||||||
All configuration options can be overridden via environment variables with the format `PICOCLAW_TOOLS_<SECTION>_<KEY>`:
|
All configuration options can be overridden via environment variables with the format `PICOCLAW_TOOLS_<SECTION>_<KEY>`:
|
||||||
|
|
|
||||||
|
|
@ -798,6 +798,8 @@ func (al *AgentLoop) runAgentLoop(
|
||||||
agent.Sessions.AddMessage(opts.SessionKey, "user", opts.UserMessage)
|
agent.Sessions.AddMessage(opts.SessionKey, "user", opts.UserMessage)
|
||||||
|
|
||||||
// 3. Run LLM iteration loop
|
// 3. Run LLM iteration loop
|
||||||
|
// Inject session key so tools (e.g. tasktool) can look it up from context.
|
||||||
|
ctx = tools.WithToolSessionKey(ctx, opts.SessionKey)
|
||||||
finalContent, iteration, err := al.runLLMIteration(ctx, agent, messages, opts)
|
finalContent, iteration, err := al.runLLMIteration(ctx, agent, messages, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,9 @@ func (tm *TaskManager) CreatePlan(sessionKey string, tasks []Task) *SessionTasks
|
||||||
}
|
}
|
||||||
copy(st.Tasks, tasks)
|
copy(st.Tasks, tasks)
|
||||||
tm.tasks[sessionKey] = st
|
tm.tasks[sessionKey] = st
|
||||||
|
// Persist immediately so the plan survives a restart even when no
|
||||||
|
// message ID is available (e.g. channels that don't return a message ID).
|
||||||
|
go func() { _ = tm.Save(sessionKey) }()
|
||||||
return st
|
return st
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,9 @@ type Tool interface {
|
||||||
type toolCtxKey struct{ name string }
|
type toolCtxKey struct{ name string }
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ctxKeyChannel = &toolCtxKey{"channel"}
|
ctxKeyChannel = &toolCtxKey{"channel"}
|
||||||
ctxKeyChatID = &toolCtxKey{"chatID"}
|
ctxKeyChatID = &toolCtxKey{"chatID"}
|
||||||
|
ctxKeySessionKey = &toolCtxKey{"sessionKey"}
|
||||||
)
|
)
|
||||||
|
|
||||||
// WithToolContext returns a child context carrying channel and chatID.
|
// WithToolContext returns a child context carrying channel and chatID.
|
||||||
|
|
@ -32,6 +33,11 @@ func WithToolContext(ctx context.Context, channel, chatID string) context.Contex
|
||||||
return ctx
|
return ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithToolSessionKey returns a child context carrying the session key.
|
||||||
|
func WithToolSessionKey(ctx context.Context, sessionKey string) context.Context {
|
||||||
|
return context.WithValue(ctx, ctxKeySessionKey, sessionKey)
|
||||||
|
}
|
||||||
|
|
||||||
// ToolChannel extracts the channel from ctx, or "" if unset.
|
// ToolChannel extracts the channel from ctx, or "" if unset.
|
||||||
func ToolChannel(ctx context.Context) string {
|
func ToolChannel(ctx context.Context) string {
|
||||||
v, _ := ctx.Value(ctxKeyChannel).(string)
|
v, _ := ctx.Value(ctxKeyChannel).(string)
|
||||||
|
|
@ -44,6 +50,12 @@ func ToolChatID(ctx context.Context) string {
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ToolSessionKey extracts the session key from ctx, or "" if unset.
|
||||||
|
func ToolSessionKey(ctx context.Context) string {
|
||||||
|
v, _ := ctx.Value(ctxKeySessionKey).(string)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
// AsyncCallback is a function type that async tools use to notify completion.
|
// AsyncCallback is a function type that async tools use to notify completion.
|
||||||
// When an async tool finishes its work, it calls this callback with the result.
|
// When an async tool finishes its work, it calls this callback with the result.
|
||||||
//
|
//
|
||||||
|
|
|
||||||
|
|
@ -98,11 +98,14 @@ func (t *TaskTool) Execute(ctx context.Context, args map[string]any) *ToolResult
|
||||||
|
|
||||||
channel := ToolChannel(ctx)
|
channel := ToolChannel(ctx)
|
||||||
chatID := ToolChatID(ctx)
|
chatID := ToolChatID(ctx)
|
||||||
// We use the same combination for task state as session manager might.
|
// Use the agent-scoped session key injected by the agent loop so that task
|
||||||
// But note: AgentLoop uses scopes out of routes. We'll use channel:chatID as implicit for now
|
// plans are stored under the same key used for conversation history.
|
||||||
// To be perfectly aligned with SessionKey, we'd need to extract SessionKey from context.
|
// Fall back to channel:chatID only when running outside of an agent loop
|
||||||
// We'll add SessionKey to context later if needed, or just use channel:chatID for tasks since planning is chat-specific.
|
// (e.g. unit tests or direct tool invocations).
|
||||||
sessionKey := fmt.Sprintf("%s:%s", channel, chatID)
|
sessionKey := ToolSessionKey(ctx)
|
||||||
|
if sessionKey == "" {
|
||||||
|
sessionKey = fmt.Sprintf("%s:%s", channel, chatID)
|
||||||
|
}
|
||||||
|
|
||||||
action, ok := args["action"].(string)
|
action, ok := args["action"].(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue