fix(providers): correct Anthropic prompt caching + add datetime tool

- Move cache_control from request root (ignored by API) to per-block
  placement on last system block and last tool definition
- Change system prompt time format to date-only for better cache stability
- Add builtin datetime tool for precise timestamps (cron, memory, etc.)
- Add atMs past-time validation in cron handleUpdate (was only in handleAdd)
- Update cron description to guide model to use datetime tool first
This commit is contained in:
viettranx committed 2026-03-16 08:14:03 +07:00
1 parent 9468aae422
commit 0857321a6b
6 files changed
+81 -5

No files matched your search

+3
View File
@@ -165,6 +165,9 @@ func runGateway() {
skillsLoader, skillSearchTool, globalSkillsDir := setupSkillsSystem(cfg, workspace, dataDir, pgStores, toolsReg, providerRegistry, msgBus)
_ = skillSearchTool // used via wireExtras → skillsLoader; kept for type clarity
// DateTime tool (precise time for cron scheduling, memory timestamps, etc.)
toolsReg.Register(tools.NewDateTimeTool())
// Cron tool (agent-facing, matching TS cron-tool.ts)
toolsReg.Register(tools.NewCronTool(pgStores.Cron))
slog.Info("cron tool registered")
+1
View File
@@ -67,6 +67,7 @@ var coreToolSummaries = map[string]string{
"spawn": "Spawn a self-clone subagent to handle a task in the background",
"web_search": "Search the web",
"web_fetch": "Fetch and extract content from a URL",
"datetime": "Get current date/time with timezone support — use before creating cron jobs or time-sensitive operations",
"cron": "Manage scheduled jobs and reminders",
"skill_search": "Search available skills by keyword (weather, translate, github, etc.)",
"use_skill": "Invoke a skill by name and follow its instructions",
+1 -1
View File
@@ -86,7 +86,7 @@ func buildUserIdentitySection(ownerIDs []string) []string {
func buildTimeSection() []string {
now := time.Now()
return []string{
fmt.Sprintf("Current time: %s (UTC)", now.UTC().Format("2006-01-02 15:04 Monday")),
fmt.Sprintf("Current date: %s (UTC)", now.UTC().Format("2006-01-02 Monday")),
"",
}
}
+12 -4
View File
@@ -145,11 +145,15 @@ func (p *AnthropicProvider) buildRequestBody(model string, req ChatRequest, stre
}
}
// Add cache_control breakpoint to the last system block (caches system prompt prefix).
if len(systemBlocks) > 0 {
systemBlocks[len(systemBlocks)-1]["cache_control"] = map[string]any{"type": "ephemeral"}
}
body := map[string]any{
"model": model,
"max_tokens": 4096,
"messages": messages,
"cache_control": map[string]any{"type": "ephemeral"},
"model": model,
"max_tokens": 4096,
"messages": messages,
}
if stream {
@@ -172,6 +176,10 @@ func (p *AnthropicProvider) buildRequestBody(model string, req ChatRequest, stre
}
tools = append(tools, tool)
}
// Add cache_control breakpoint to the last tool (caches tool definitions prefix).
if len(tools) > 0 {
tools[len(tools)-1]["cache_control"] = map[string]any{"type": "ephemeral"}
}
body["tools"] = tools
}
+8
View File
@@ -64,6 +64,7 @@ CRITICAL CONSTRAINTS:
- message is required for add action
- schedule is required for add action
- Default: jobs run as isolated agent turns with the specified message
- Before creating or updating a scheduled job, call the datetime tool first to get the precise current time and unix_ms timestamp. Do NOT guess or estimate timestamps.
Use jobId as the canonical identifier; id is accepted for compatibility.`
}
@@ -309,6 +310,13 @@ func (t *CronTool) handleUpdate(args map[string]any, agentID, userID string) *Re
patchJSON, _ := json.Marshal(patchObj)
json.Unmarshal(patchJSON, &patch)
// Validate atMs not in the past when updating schedule
if patch.Schedule != nil && patch.Schedule.Kind == "at" && patch.Schedule.AtMS != nil {
if *patch.Schedule.AtMS <= time.Now().UnixMilli() {
return ErrorResult(fmt.Sprintf("schedule.atMs is in the past (%d). Use the datetime tool to get current time, then set a future timestamp. Current time is %d ms", *patch.Schedule.AtMS, time.Now().UnixMilli()))
}
}
job, err := t.cronStore.UpdateJob(jobID, patch)
if err != nil {
return ErrorResult(fmt.Sprintf("failed to update cron job: %v", err))
+56
View File
@@ -0,0 +1,56 @@
package tools
import (
"context"
"encoding/json"
"fmt"
"time"
)
// DateTimeTool provides precise current date/time with timezone support.
// Models use this before creating cron jobs or any time-sensitive operation
// instead of guessing timestamps from the system prompt's date-only field.
type DateTimeTool struct{}
func NewDateTimeTool() *DateTimeTool { return &DateTimeTool{} }
func (t *DateTimeTool) Name() string { return "datetime" }
func (t *DateTimeTool) Description() string {
return `Get the current date and time. Use this when you need precise timestamps for scheduling (cron jobs), logging, or any time-sensitive operation.
Returns current time in both UTC and the requested timezone.
If no timezone is provided, returns UTC only.`
}
func (t *DateTimeTool) Parameters() map[string]any {
return map[string]any{
"type": "object",
"properties": map[string]any{
"timezone": map[string]any{
"type": "string",
"description": "IANA timezone name (e.g. 'Asia/Ho_Chi_Minh', 'America/New_York'). If omitted, returns UTC only.",
},
},
}
}
func (t *DateTimeTool) Execute(_ context.Context, args map[string]any) *Result {
now := time.Now()
result := map[string]any{
"utc": now.UTC().Format(time.RFC3339),
"unix_ms": now.UnixMilli(),
}
if tz, ok := args["timezone"].(string); ok && tz != "" {
loc, err := time.LoadLocation(tz)
if err != nil {
return ErrorResult(fmt.Sprintf("invalid timezone '%s': use IANA names like 'Asia/Ho_Chi_Minh', 'America/New_York'", tz))
}
result["local"] = now.In(loc).Format(time.RFC3339)
result["timezone"] = tz
}
data, _ := json.MarshalIndent(result, "", " ")
return NewResult(string(data))
}