Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions docs/cron.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ This is the default for the cron tool.

When the job fires, PicoClaw sends the saved message back through the agent loop as a new agent turn. Use this for scheduled work that may need reasoning, tools, or a generated reply.

Because the saved `message` is replayed as a new user-style input, write it from the user's perspective or as direct instructions to the agent. Prefer wording such as `check the repo every hour and tell me if there is a new release` over third-person wording such as `check the repo and notify the user`.

### `deliver: true`

When the job fires, PicoClaw publishes the saved message directly to the target channel and recipient without agent processing.
Expand All @@ -50,6 +52,23 @@ For command jobs, `deliver` is forced to `false` when the job is created. The sa

The current CLI `picoclaw cron add` command does not expose a `command` flag.

## Writing Job Messages

For normal cron jobs without `command`, the saved `payload.message` becomes the next input sent to the agent when the job fires. In practice, that means the job message should read like something the user would say to the agent.

Recommended style:

- Use first-person or direct-address wording such as `tell me`, `remind me`, `reply in Chinese`, `do not reply if nothing changed`
- Be explicit about the quiet case if needed, for example `If there is no update, do not reply`
- Avoid third-person wording such as `notify the user`, because the model may continue replying in third person

Examples:

```text
Good: Check gdsfactory/gdsfactory every hour. If there is a new release, tell me in Chinese and summarize the changes. If nothing changed, do not reply.
Bad: Check gdsfactory/gdsfactory every hour. If there is a new release, notify the user and summarize the changes.
```

## Config and Security Gates

### `tools.cron`
Expand Down
2 changes: 2 additions & 0 deletions docs/tools_configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,8 @@ The cron tool is used for scheduling periodic tasks.
| `allow_command` | bool | true | Allow command jobs without extra confirmation |
| `exec_timeout_minutes` | int | 5 | Execution timeout in minutes, 0 means no limit |

For normal cron jobs without `command`, the saved `message` is later replayed into the agent loop as a new user-style message. Write it from the user's perspective, for example `check the repo every hour and tell me if there is a new release`, not `notify the user`.

For schedule types, execution modes (`deliver`, agent turn, and command jobs), persistence, and the current command-security gates, see [Scheduled Tasks and Cron Jobs](cron.md).

## MCP Tool
Expand Down
2 changes: 2 additions & 0 deletions docs/zh/tools_configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,8 @@ Cron 工具用于调度周期性任务。
| `exec_timeout_minutes` | int | 5 | 执行超时时间(分钟),0 表示无限制 |
| `allow_command` | bool | false | 允许 cron 任务执行 shell 命令 |

对于不带 `command` 的普通 cron 任务,保存下来的 `message` 会在任务触发时重新作为一条新的“用户消息”送回 agent。编写时应使用用户视角/直接对 agent 说话的口吻,例如 `每小时检查仓库更新,如果有新版本告诉我`,而不是 `如果有更新就通知用户`。

## MCP 工具

MCP 工具支持与外部 Model Context Protocol 服务器集成。
Expand Down
58 changes: 57 additions & 1 deletion pkg/agent/loop.go
Original file line number Diff line number Diff line change
Expand Up @@ -1033,6 +1033,47 @@ func (al *AgentLoop) ReloadProviderAndConfig(
// Ensure shared tools are re-registered on the new registry
registerSharedTools(al, cfg, al.bus, registry, provider)

var (
newMCPManager mcpController
mcpSummary mcpRegistrationSummary
)
if cfg.Tools.IsToolEnabled("mcp") && countEnabledMCPServers(cfg.Tools.MCP.Servers) > 0 {
newMCPManager = newMCPController()

workspacePath := cfg.WorkspacePath()
if defaultAgent := registry.GetDefaultAgent(); defaultAgent != nil && defaultAgent.Workspace != "" {
workspacePath = defaultAgent.Workspace
}

if err := newMCPManager.LoadFromMCPConfig(ctx, cfg.Tools.MCP, workspacePath); err != nil {
if closeErr := newMCPManager.Close(); closeErr != nil {
logger.ErrorCF("agent", "Failed to close MCP manager",
map[string]any{
"error": closeErr.Error(),
})
}
return fmt.Errorf("failed to initialize MCP during reload: %w", err)
}

mcpSummary = registerMCPToolsOnRegistry(registry, cfg, newMCPManager, newMCPManager.GetServers())
if err := registerMCPDiscoveryToolsOnRegistry(registry, cfg); err != nil {
if closeErr := newMCPManager.Close(); closeErr != nil {
logger.ErrorCF("agent", "Failed to close MCP manager",
map[string]any{
"error": closeErr.Error(),
})
}
return fmt.Errorf("failed to restore MCP discovery tools during reload: %w", err)
}
}
if al.mediaStore != nil {
for _, agentID := range registry.ListAgentIDs() {
if agent, ok := registry.GetAgent(agentID); ok {
agent.Tools.SetMediaStore(al.mediaStore)
}
}
}

// Atomically swap the config and registry under write lock
// This ensures readers see a consistent pair
al.mu.Lock()
Expand All @@ -1054,6 +1095,7 @@ func (al *AgentLoop) ReloadProviderAndConfig(

al.mu.Unlock()

oldMCPManager := al.mcp.replaceForReload(newMCPManager, newMCPManager != nil)
al.hookRuntime.reset(al)
configureHookManagerFromConfig(al.hooks, cfg)

Expand All @@ -1074,10 +1116,21 @@ func (al *AgentLoop) ReloadProviderAndConfig(
}
}
}
if oldMCPManager != nil {
if err := oldMCPManager.Close(); err != nil {
logger.ErrorCF("agent", "Failed to close previous MCP manager",
map[string]any{
"error": err.Error(),
})
}
}

logger.InfoCF("agent", "Provider and config reloaded successfully",
map[string]any{
"model": cfg.Agents.Defaults.GetModelName(),
"model": cfg.Agents.Defaults.GetModelName(),
"mcp_server_count": mcpSummary.serverCount,
"mcp_unique_tools": mcpSummary.uniqueTools,
"mcp_total_registrations": mcpSummary.totalRegistrations,
})

return nil
Expand Down Expand Up @@ -3454,6 +3507,9 @@ func (al *AgentLoop) buildCommandsRuntime(agent *AgentInstance, opts *processOpt
Config: cfg,
ListAgentIDs: registry.ListAgentIDs,
ListDefinitions: al.cmdRegistry.Definitions,
GetMCPStatus: func() string {
return formatMCPStatus(cfg, al.mcp.statusSnapshot())
},
Comment on lines 3509 to +3512
GetEnabledChannels: func() []string {
if al.channelManager == nil {
return nil
Expand Down
Loading
Loading