mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-14 02:15:57 +00:00
chore: upgrade Go 1.25 → 1.26 and apply go fix modernizations
- Update go.mod and Dockerfile to Go 1.26 - Apply `go fix ./...` stdlib modernizations across 170+ files - Add `go fix` to post-implementation checklist in CLAUDE.md - Fix go fix misapplied rewrite in loop_history.go
This commit is contained in:
1 parent
e593b9cf22
commit
bdb60de7ae
178 files changed
+1475
-1541
No files matched your search
@@ -69,6 +69,7 @@ cd ui/web && pnpm install && pnpm dev # Web dashboard (dev)
|
||||
After implementing or modifying Go code, run these checks:
|
||||
|
||||
```bash
|
||||
go fix ./... # Apply Go version upgrades (run before commit)
|
||||
go build ./... # Compile check
|
||||
go vet ./... # Static analysis
|
||||
go test -race ./tests/integration/ # Integration tests with race detector
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
# ── Stage 1: Build ──
|
||||
FROM golang:1.25-bookworm AS builder
|
||||
FROM golang:1.26-bookworm AS builder
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
|
||||
@@ -115,7 +115,7 @@ func wsConnect(conn *websocket.Conn, token string) error {
|
||||
// displaying events (tool calls, chunks) in real-time.
|
||||
func wsChatSend(conn *websocket.Conn, agentID, sessionKey, message string) (string, error) {
|
||||
reqID := uuid.NewString()[:8]
|
||||
params, _ := json.Marshal(map[string]interface{}{
|
||||
params, _ := json.Marshal(map[string]any{
|
||||
"message": message,
|
||||
"agentId": agentID,
|
||||
"sessionKey": sessionKey,
|
||||
@@ -159,7 +159,7 @@ func wsChatSend(conn *websocket.Conn, agentID, sessionKey, message string) (stri
|
||||
return "", fmt.Errorf("agent error (unknown)")
|
||||
}
|
||||
// Extract content from payload
|
||||
if payload, ok := resp.Payload.(map[string]interface{}); ok {
|
||||
if payload, ok := resp.Payload.(map[string]any); ok {
|
||||
if content, ok := payload["content"].(string); ok && content != "" {
|
||||
finalContent = content
|
||||
}
|
||||
@@ -178,7 +178,7 @@ func wsChatSend(conn *websocket.Conn, agentID, sessionKey, message string) (stri
|
||||
|
||||
// handleCLIEvent displays agent events in the terminal.
|
||||
func handleCLIEvent(evt protocol.EventFrame) {
|
||||
payload, ok := evt.Payload.(map[string]interface{})
|
||||
payload, ok := evt.Payload.(map[string]any)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
@@ -189,7 +189,7 @@ func handleCLIEvent(evt protocol.EventFrame) {
|
||||
case protocol.EventAgent:
|
||||
switch evtType {
|
||||
case protocol.AgentEventToolCall:
|
||||
if p, ok := payload["payload"].(map[string]interface{}); ok {
|
||||
if p, ok := payload["payload"].(map[string]any); ok {
|
||||
name, _ := p["toolName"].(string)
|
||||
if name == "" {
|
||||
name, _ = p["name"].(string)
|
||||
@@ -197,7 +197,7 @@ func handleCLIEvent(evt protocol.EventFrame) {
|
||||
fmt.Fprintf(os.Stderr, " [tool] %s\n", name)
|
||||
}
|
||||
case protocol.AgentEventToolResult:
|
||||
if p, ok := payload["payload"].(map[string]interface{}); ok {
|
||||
if p, ok := payload["payload"].(map[string]any); ok {
|
||||
isErr, _ := p["is_error"].(bool)
|
||||
name, _ := p["toolName"].(string)
|
||||
if name == "" {
|
||||
|
||||
+2
-2
@@ -39,7 +39,7 @@ func gatewayURL() string {
|
||||
}
|
||||
|
||||
// gatewayRequest sends an authenticated request to the running gateway.
|
||||
func gatewayRequest(method, path string) (map[string]interface{}, error) {
|
||||
func gatewayRequest(method, path string) (map[string]any, error) {
|
||||
url := gatewayURL() + path
|
||||
req, err := http.NewRequest(method, url, nil)
|
||||
if err != nil {
|
||||
@@ -57,7 +57,7 @@ func gatewayRequest(method, path string) (map[string]interface{}, error) {
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||
var result map[string]interface{}
|
||||
var result map[string]any
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
return nil, fmt.Errorf("invalid response from gateway: %s", string(body))
|
||||
}
|
||||
|
||||
+4
-4
@@ -68,15 +68,15 @@ func configValidateCmd() *cobra.Command {
|
||||
}
|
||||
|
||||
// redactConfig returns a JSON-safe copy with secrets masked.
|
||||
func redactConfig(cfg *config.Config) interface{} {
|
||||
func redactConfig(cfg *config.Config) any {
|
||||
data, _ := json.Marshal(cfg)
|
||||
var raw map[string]interface{}
|
||||
var raw map[string]any
|
||||
json.Unmarshal(data, &raw)
|
||||
redactMap(raw)
|
||||
return raw
|
||||
}
|
||||
|
||||
func redactMap(m map[string]interface{}) {
|
||||
func redactMap(m map[string]any) {
|
||||
secretKeys := map[string]bool{
|
||||
"apiKey": true, "api_key": true, "token": true,
|
||||
"botToken": true, "bot_token": true, "secret": true,
|
||||
@@ -89,7 +89,7 @@ func redactMap(m map[string]interface{}) {
|
||||
} else if s, ok := v.(string); ok && s != "" {
|
||||
m[k] = "****"
|
||||
}
|
||||
} else if sub, ok := v.(map[string]interface{}); ok {
|
||||
} else if sub, ok := v.(map[string]any); ok {
|
||||
redactMap(sub)
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -67,7 +67,7 @@ func cronToggleCmd() *cobra.Command {
|
||||
func cronListRPC(showDisabled, jsonOutput bool) {
|
||||
requireGateway()
|
||||
|
||||
params, _ := json.Marshal(map[string]interface{}{"includeDisabled": showDisabled})
|
||||
params, _ := json.Marshal(map[string]any{"includeDisabled": showDisabled})
|
||||
resp, err := gatewayRPC(protocol.MethodCronList, params)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
@@ -109,7 +109,7 @@ func cronDeleteRPC(jobID string) {
|
||||
func cronToggleRPC(jobID string, enabled bool) {
|
||||
requireGateway()
|
||||
|
||||
params, _ := json.Marshal(map[string]interface{}{"jobId": jobID, "enabled": enabled})
|
||||
params, _ := json.Marshal(map[string]any{"jobId": jobID, "enabled": enabled})
|
||||
resp, err := gatewayRPC(protocol.MethodCronToggle, params)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
|
||||
+1
-1
@@ -771,7 +771,7 @@ func runGateway() {
|
||||
|
||||
// Route activity events to Router (status registry) and DelegateManager (progress tracking).
|
||||
if agentEvent.Type == protocol.AgentEventActivity {
|
||||
payloadMap, _ := agentEvent.Payload.(map[string]interface{})
|
||||
payloadMap, _ := agentEvent.Payload.(map[string]any)
|
||||
phase, _ := payloadMap["phase"].(string)
|
||||
tool, _ := payloadMap["tool"].(string)
|
||||
iteration := 0
|
||||
|
||||
+1
-1
@@ -205,7 +205,7 @@ func gatewayRPC(method string, params json.RawMessage) (*protocol.ResponseFrame,
|
||||
defer conn.Close()
|
||||
|
||||
// Step 1: Send connect handshake
|
||||
connectParams, _ := json.Marshal(map[string]interface{}{
|
||||
connectParams, _ := json.Marshal(map[string]any{
|
||||
"token": cfg.Gateway.Token,
|
||||
"protocol": protocol.ProtocolVersion,
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module github.com/nextlevelbuilder/goclaw
|
||||
|
||||
go 1.25.5
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
github.com/adhocore/gronx v1.19.6
|
||||
@@ -16,6 +16,7 @@ require (
|
||||
github.com/mattn/go-runewidth v0.0.16
|
||||
github.com/mymmrac/telego v1.6.0
|
||||
github.com/redis/go-redis/v9 v9.18.0
|
||||
github.com/slack-go/slack v0.19.0
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/titanous/json5 v1.0.0
|
||||
go.opentelemetry.io/otel v1.40.0
|
||||
@@ -86,7 +87,6 @@ require (
|
||||
github.com/pires/go-proxyproto v0.8.1 // indirect
|
||||
github.com/prometheus-community/pro-bing v0.4.0 // indirect
|
||||
github.com/safchain/ethtool v0.3.0 // indirect
|
||||
github.com/slack-go/slack v0.19.0 // indirect
|
||||
github.com/spf13/cast v1.7.1 // indirect
|
||||
github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e // indirect
|
||||
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect
|
||||
|
||||
@@ -180,6 +180,8 @@ github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/go-rod/rod v0.116.2 h1:A5t2Ky2A+5eD/ZJQr1EfsQSe5rms5Xof/qj296e+ZqA=
|
||||
github.com/go-rod/rod v0.116.2/go.mod h1:H+CMO9SCNc2TJ2WfrG+pKhITz57uGNYU43qYHh438Mg=
|
||||
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
|
||||
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||
github.com/go4org/plan9netshell v0.0.0-20250324183649-788daa080737 h1:cf60tHxREO3g1nroKr2osU3JWZsJzkfi7rEg+oAB0Lo=
|
||||
github.com/go4org/plan9netshell v0.0.0-20250324183649-788daa080737/go.mod h1:MIS0jDzbU/vuM9MC4YnBITCv+RYuTRq8dJzmCrFsK9g=
|
||||
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 h1:sQspH8M4niEijh3PFscJRLDnkL547IeP7kpPe3uUhEg=
|
||||
|
||||
@@ -81,7 +81,7 @@ func ClassifyIntent(ctx context.Context, provider providers.Provider, model, use
|
||||
{Role: "user", Content: userMessage},
|
||||
},
|
||||
Model: model,
|
||||
Options: map[string]interface{}{
|
||||
Options: map[string]any{
|
||||
providers.OptMaxTokens: 20,
|
||||
providers.OptTemperature: 0.0,
|
||||
},
|
||||
|
||||
+13
-13
@@ -334,7 +334,7 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error)
|
||||
totalToolCalls := 0
|
||||
var finalContent string
|
||||
var finalThinking string
|
||||
var asyncToolCalls []string // track async spawn tool names for fallback
|
||||
var asyncToolCalls []string // track async spawn tool names for fallback
|
||||
var mediaResults []MediaResult // media files from tool MEDIA: results
|
||||
var deliverables []string // actual content from tool outputs (for team task results)
|
||||
var blockReplies int // count of block.reply events emitted (for dedup in consumer)
|
||||
@@ -347,7 +347,7 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error)
|
||||
// Team task orphan detection: track team_tasks create vs spawn calls.
|
||||
// If the LLM creates tasks but forgets to spawn, inject a reminder.
|
||||
var teamTaskCreates int // count of team_tasks action=create calls
|
||||
var teamTaskSpawns int // count of spawn calls with team_task_id
|
||||
var teamTaskSpawns int // count of spawn calls with team_task_id
|
||||
var teamTaskRetried bool // only retry once to prevent infinite loops
|
||||
|
||||
// Inject retry hook so channels can update placeholder on LLM retries.
|
||||
@@ -379,7 +379,7 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error)
|
||||
Type: protocol.AgentEventActivity,
|
||||
AgentID: l.id,
|
||||
RunID: req.RunID,
|
||||
Payload: map[string]interface{}{"phase": "thinking", "iteration": iteration},
|
||||
Payload: map[string]any{"phase": "thinking", "iteration": iteration},
|
||||
})
|
||||
|
||||
// Build provider request with policy-filtered tools
|
||||
@@ -399,7 +399,7 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error)
|
||||
Messages: messages,
|
||||
Tools: toolDefs,
|
||||
Model: l.model,
|
||||
Options: map[string]interface{}{
|
||||
Options: map[string]any{
|
||||
providers.OptMaxTokens: 8192,
|
||||
providers.OptTemperature: 0.7,
|
||||
providers.OptSessionKey: req.SessionKey,
|
||||
@@ -505,7 +505,7 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error)
|
||||
Type: protocol.AgentEventActivity,
|
||||
AgentID: l.id,
|
||||
RunID: req.RunID,
|
||||
Payload: map[string]interface{}{"phase": "compacting", "iteration": iteration},
|
||||
Payload: map[string]any{"phase": "compacting", "iteration": iteration},
|
||||
})
|
||||
if compacted := l.compactMessagesInPlace(ctx, messages); compacted != nil {
|
||||
messages = compacted
|
||||
@@ -562,7 +562,7 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error)
|
||||
Content: resp.Content,
|
||||
Thinking: resp.Thinking, // reasoning_content passback for thinking models (Kimi, DeepSeek)
|
||||
ToolCalls: resp.ToolCalls,
|
||||
Phase: resp.Phase, // preserve Codex phase metadata (gpt-5.3-codex)
|
||||
Phase: resp.Phase, // preserve Codex phase metadata (gpt-5.3-codex)
|
||||
RawAssistantContent: resp.RawAssistantContent, // preserve thinking blocks for Anthropic passback
|
||||
}
|
||||
messages = append(messages, assistantMsg)
|
||||
@@ -617,7 +617,7 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error)
|
||||
Type: protocol.AgentEventActivity,
|
||||
AgentID: l.id,
|
||||
RunID: req.RunID,
|
||||
Payload: map[string]interface{}{
|
||||
Payload: map[string]any{
|
||||
"phase": "tool_exec",
|
||||
"tool": toolNames[0],
|
||||
"tools": toolNames,
|
||||
@@ -634,7 +634,7 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error)
|
||||
Type: protocol.AgentEventToolCall,
|
||||
AgentID: l.id,
|
||||
RunID: req.RunID,
|
||||
Payload: map[string]interface{}{"name": tc.Name, "id": tc.ID, "arguments": truncateToolArgs(tc.Arguments, 500)},
|
||||
Payload: map[string]any{"name": tc.Name, "id": tc.ID, "arguments": truncateToolArgs(tc.Arguments, 500)},
|
||||
})
|
||||
|
||||
argsJSON, _ := json.Marshal(tc.Arguments)
|
||||
@@ -675,7 +675,7 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error)
|
||||
}
|
||||
}
|
||||
|
||||
toolResultPayload := map[string]interface{}{
|
||||
toolResultPayload := map[string]any{
|
||||
"name": tc.Name,
|
||||
"id": tc.ID,
|
||||
"is_error": result.IsError,
|
||||
@@ -748,7 +748,7 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error)
|
||||
Type: protocol.AgentEventToolCall,
|
||||
AgentID: l.id,
|
||||
RunID: req.RunID,
|
||||
Payload: map[string]interface{}{"name": tc.Name, "id": tc.ID, "arguments": truncateToolArgs(tc.Arguments, 500)},
|
||||
Payload: map[string]any{"name": tc.Name, "id": tc.ID, "arguments": truncateToolArgs(tc.Arguments, 500)},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -816,7 +816,7 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error)
|
||||
}
|
||||
}
|
||||
|
||||
parToolResultPayload := map[string]interface{}{
|
||||
parToolResultPayload := map[string]any{
|
||||
"name": r.tc.Name,
|
||||
"id": r.tc.ID,
|
||||
"is_error": r.result.IsError,
|
||||
@@ -981,8 +981,8 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error)
|
||||
}
|
||||
|
||||
// truncateToolArgs returns a copy of arguments with string values truncated to maxLen.
|
||||
func truncateToolArgs(args map[string]interface{}, maxLen int) map[string]interface{} {
|
||||
out := make(map[string]interface{}, len(args))
|
||||
func truncateToolArgs(args map[string]any, maxLen int) map[string]any {
|
||||
out := make(map[string]any, len(args))
|
||||
for k, v := range args {
|
||||
if s, ok := v.(string); ok && len(s) > maxLen {
|
||||
out[k] = truncateStr(s, maxLen)
|
||||
|
||||
@@ -66,7 +66,7 @@ func (l *Loop) compactMessagesInPlace(ctx context.Context, messages []providers.
|
||||
Content: "Provide a concise summary of this conversation, preserving key findings, data, and context:\n\n" + sb.String(),
|
||||
}},
|
||||
Model: l.model,
|
||||
Options: map[string]interface{}{"max_tokens": 1024, "temperature": 0.3},
|
||||
Options: map[string]any{"max_tokens": 1024, "temperature": 0.3},
|
||||
})
|
||||
if err != nil {
|
||||
slog.Warn("mid_loop_compaction_failed", "agent", l.id, "error", err)
|
||||
|
||||
@@ -120,7 +120,7 @@ func (l *Loop) buildMessages(ctx context.Context, history []providers.Message, s
|
||||
HasSpawn: l.tools != nil && hasSpawn,
|
||||
HasSkillSearch: hasSkillSearch,
|
||||
HasMCPToolSearch: hasMCPToolSearch,
|
||||
HasKnowledgeGraph: hasKG,
|
||||
HasKnowledgeGraph: hasKG,
|
||||
MCPToolDescs: mcpToolDescs,
|
||||
ContextFiles: contextFiles,
|
||||
AgentType: l.agentType,
|
||||
@@ -397,46 +397,47 @@ func (l *Loop) maybeSummarize(ctx context.Context, sessionKey string) {
|
||||
summary := l.sessions.GetSummary(sessionKey)
|
||||
toSummarize := history[:len(history)-keepLast]
|
||||
|
||||
var sb string
|
||||
var sb strings.Builder
|
||||
var mediaKinds []string
|
||||
for _, m := range toSummarize {
|
||||
if m.Role == "user" {
|
||||
sb += fmt.Sprintf("user: %s\n", m.Content)
|
||||
sb.WriteString(fmt.Sprintf("user: %s\n", m.Content))
|
||||
} else if m.Role == "assistant" {
|
||||
sb += fmt.Sprintf("assistant: %s\n", SanitizeAssistantContent(m.Content))
|
||||
sb.WriteString(fmt.Sprintf("assistant: %s\n", SanitizeAssistantContent(m.Content)))
|
||||
}
|
||||
for _, ref := range m.MediaRefs {
|
||||
mediaKinds = append(mediaKinds, ref.Kind)
|
||||
}
|
||||
}
|
||||
|
||||
prompt := "Provide a concise summary of this conversation, preserving key context:\n"
|
||||
var prompt strings.Builder
|
||||
prompt.WriteString("Provide a concise summary of this conversation, preserving key context:\n")
|
||||
if len(mediaKinds) > 0 {
|
||||
// Deduplicate and count media types for a compact note.
|
||||
counts := make(map[string]int)
|
||||
for _, k := range mediaKinds {
|
||||
counts[k]++
|
||||
}
|
||||
prompt += "\nNote: user shared media files ("
|
||||
prompt.WriteString("\nNote: user shared media files (")
|
||||
first := true
|
||||
for k, n := range counts {
|
||||
if !first {
|
||||
prompt += ", "
|
||||
prompt.WriteString(", ")
|
||||
}
|
||||
prompt += fmt.Sprintf("%d %s(s)", n, k)
|
||||
prompt.WriteString(fmt.Sprintf("%d %s(s)", n, k))
|
||||
first = false
|
||||
}
|
||||
prompt += ") which are no longer in context. Mention briefly if relevant.\n"
|
||||
prompt.WriteString(") which are no longer in context. Mention briefly if relevant.\n")
|
||||
}
|
||||
if summary != "" {
|
||||
prompt += "Existing context: " + summary + "\n"
|
||||
prompt.WriteString("Existing context: " + summary + "\n")
|
||||
}
|
||||
prompt += "\n" + sb
|
||||
prompt.WriteString("\n" + sb.String())
|
||||
|
||||
resp, err := l.provider.Chat(sctx, providers.ChatRequest{
|
||||
Messages: []providers.Message{{Role: "user", Content: prompt}},
|
||||
Messages: []providers.Message{{Role: "user", Content: prompt.String()}},
|
||||
Model: l.model,
|
||||
Options: map[string]interface{}{"max_tokens": 1024, "temperature": 0.3},
|
||||
Options: map[string]any{"max_tokens": 1024, "temperature": 0.3},
|
||||
})
|
||||
if err != nil {
|
||||
slog.Warn("summarization failed", "session", sessionKey, "error", err)
|
||||
|
||||
@@ -36,7 +36,7 @@ func (l *Loop) Run(ctx context.Context, req RunRequest) (*RunResult, error) {
|
||||
Type: protocol.AgentEventRunStarted,
|
||||
AgentID: l.id,
|
||||
RunID: req.RunID,
|
||||
Payload: map[string]interface{}{"message": req.Message},
|
||||
Payload: map[string]any{"message": req.Message},
|
||||
})
|
||||
|
||||
// Create trace
|
||||
@@ -132,7 +132,7 @@ func (l *Loop) Run(ctx context.Context, req RunRequest) (*RunResult, error) {
|
||||
Type: protocol.AgentEventRunCompleted,
|
||||
AgentID: l.id,
|
||||
RunID: req.RunID,
|
||||
Payload: map[string]interface{}{"content": result.Content},
|
||||
Payload: map[string]any{"content": result.Content},
|
||||
})
|
||||
if !isChildTrace && l.traceCollector != nil && traceID != uuid.Nil {
|
||||
l.traceCollector.FinishTrace(ctx, traceID, store.TraceStatusCompleted, "", truncateStr(result.Content, 500))
|
||||
|
||||
@@ -47,12 +47,12 @@ type Loop struct {
|
||||
maxToolCalls int
|
||||
workspace string
|
||||
|
||||
eventPub bus.EventPublisher // currently unused by Loop; kept for future use
|
||||
sessions store.SessionStore
|
||||
eventPub bus.EventPublisher // currently unused by Loop; kept for future use
|
||||
sessions store.SessionStore
|
||||
tools *tools.Registry
|
||||
toolPolicy *tools.PolicyEngine // optional: filters tools sent to LLM
|
||||
agentToolPolicy *config.ToolPolicySpec // per-agent tool policy from DB (nil = no restrictions)
|
||||
activeRuns atomic.Int32 // number of currently executing runs
|
||||
activeRuns atomic.Int32 // number of currently executing runs
|
||||
|
||||
// Per-session summarization lock: prevents concurrent summarize goroutines for the same session.
|
||||
summarizeMu sync.Map // sessionKey → *sync.Mutex
|
||||
@@ -65,10 +65,10 @@ type Loop struct {
|
||||
contextFiles []bootstrap.ContextFile
|
||||
|
||||
// Per-user file seeding + dynamic context loading
|
||||
ensureUserFiles EnsureUserFilesFunc
|
||||
contextFileLoader ContextFileLoaderFunc
|
||||
bootstrapCleanup BootstrapCleanupFunc
|
||||
userWorkspaces sync.Map // userID → string (expanded workspace path from user_agent_profiles)
|
||||
ensureUserFiles EnsureUserFilesFunc
|
||||
contextFileLoader ContextFileLoaderFunc
|
||||
bootstrapCleanup BootstrapCleanupFunc
|
||||
userWorkspaces sync.Map // userID → string (expanded workspace path from user_agent_profiles)
|
||||
|
||||
// Compaction config (memory flush settings)
|
||||
compactionCfg *config.CompactionConfig
|
||||
@@ -77,8 +77,8 @@ type Loop struct {
|
||||
contextPruningCfg *config.ContextPruningConfig
|
||||
|
||||
// Sandbox info
|
||||
sandboxEnabled bool
|
||||
sandboxContainerDir string
|
||||
sandboxEnabled bool
|
||||
sandboxContainerDir string
|
||||
sandboxWorkspaceAccess string
|
||||
|
||||
// Event callback for broadcasting agent events (run.started, chunk, tool.call, etc.)
|
||||
@@ -113,11 +113,11 @@ type Loop struct {
|
||||
|
||||
// AgentEvent is emitted during agent execution for WS broadcasting.
|
||||
type AgentEvent struct {
|
||||
Type string `json:"type"` // "run.started", "run.completed", "run.failed", "chunk", "tool.call", "tool.result"
|
||||
AgentID string `json:"agentId"`
|
||||
RunID string `json:"runId"`
|
||||
RunKind string `json:"runKind,omitempty"` // "delegation", "announce" — omitted for user-initiated runs
|
||||
Payload interface{} `json:"payload,omitempty"`
|
||||
Type string `json:"type"` // "run.started", "run.completed", "run.failed", "chunk", "tool.call", "tool.result"
|
||||
AgentID string `json:"agentId"`
|
||||
RunID string `json:"runId"`
|
||||
RunKind string `json:"runKind,omitempty"` // "delegation", "announce" — omitted for user-initiated runs
|
||||
Payload any `json:"payload,omitempty"`
|
||||
|
||||
// Delegation context (omitempty — only present when agent runs inside a delegation)
|
||||
DelegationID string `json:"delegationId,omitempty"`
|
||||
@@ -133,15 +133,15 @@ type AgentEvent struct {
|
||||
|
||||
// LoopConfig configures a new Loop.
|
||||
type LoopConfig struct {
|
||||
ID string
|
||||
Provider providers.Provider
|
||||
Model string
|
||||
ContextWindow int
|
||||
MaxIterations int
|
||||
MaxToolCalls int
|
||||
Workspace string
|
||||
Bus bus.EventPublisher
|
||||
Sessions store.SessionStore
|
||||
ID string
|
||||
Provider providers.Provider
|
||||
Model string
|
||||
ContextWindow int
|
||||
MaxIterations int
|
||||
MaxToolCalls int
|
||||
Workspace string
|
||||
Bus bus.EventPublisher
|
||||
Sessions store.SessionStore
|
||||
Tools *tools.Registry
|
||||
ToolPolicy *tools.PolicyEngine // optional: filters tools sent to LLM
|
||||
AgentToolPolicy *config.ToolPolicySpec // per-agent tool policy from DB (nil = no restrictions)
|
||||
@@ -161,8 +161,8 @@ type LoopConfig struct {
|
||||
ContextPruningCfg *config.ContextPruningConfig
|
||||
|
||||
// Sandbox info (injected into system prompt)
|
||||
SandboxEnabled bool
|
||||
SandboxContainerDir string // e.g. "/workspace"
|
||||
SandboxEnabled bool
|
||||
SandboxContainerDir string // e.g. "/workspace"
|
||||
SandboxWorkspaceAccess string // "none", "ro", "rw"
|
||||
|
||||
// Agent UUID for context propagation to tools
|
||||
@@ -178,9 +178,9 @@ type LoopConfig struct {
|
||||
TraceCollector *tracing.Collector
|
||||
|
||||
// Security: input guard for injection detection, max message size
|
||||
InputGuard *InputGuard // nil = auto-create when InjectionAction != "off"
|
||||
InjectionAction string // "log", "warn" (default), "block", "off"
|
||||
MaxMessageChars int // 0 = use default (32000)
|
||||
InputGuard *InputGuard // nil = auto-create when InjectionAction != "off"
|
||||
InjectionAction string // "log", "warn" (default), "block", "off"
|
||||
MaxMessageChars int // 0 = use default (32000)
|
||||
|
||||
// Global builtin tool settings (from builtin_tools table)
|
||||
BuiltinToolSettings tools.BuiltinToolSettings
|
||||
@@ -225,71 +225,71 @@ func NewLoop(cfg LoopConfig) *Loop {
|
||||
}
|
||||
|
||||
return &Loop{
|
||||
id: cfg.ID,
|
||||
agentUUID: cfg.AgentUUID,
|
||||
agentType: cfg.AgentType,
|
||||
provider: cfg.Provider,
|
||||
model: cfg.Model,
|
||||
contextWindow: cfg.ContextWindow,
|
||||
maxIterations: cfg.MaxIterations,
|
||||
maxToolCalls: cfg.MaxToolCalls,
|
||||
workspace: cfg.Workspace,
|
||||
eventPub: cfg.Bus,
|
||||
sessions: cfg.Sessions,
|
||||
tools: cfg.Tools,
|
||||
toolPolicy: cfg.ToolPolicy,
|
||||
agentToolPolicy: cfg.AgentToolPolicy,
|
||||
onEvent: cfg.OnEvent,
|
||||
ownerIDs: cfg.OwnerIDs,
|
||||
skillsLoader: cfg.SkillsLoader,
|
||||
skillAllowList: cfg.SkillAllowList,
|
||||
hasMemory: cfg.HasMemory,
|
||||
contextFiles: cfg.ContextFiles,
|
||||
ensureUserFiles: cfg.EnsureUserFiles,
|
||||
contextFileLoader: cfg.ContextFileLoader,
|
||||
bootstrapCleanup: cfg.BootstrapCleanup,
|
||||
compactionCfg: cfg.CompactionCfg,
|
||||
contextPruningCfg: cfg.ContextPruningCfg,
|
||||
sandboxEnabled: cfg.SandboxEnabled,
|
||||
sandboxContainerDir: cfg.SandboxContainerDir,
|
||||
id: cfg.ID,
|
||||
agentUUID: cfg.AgentUUID,
|
||||
agentType: cfg.AgentType,
|
||||
provider: cfg.Provider,
|
||||
model: cfg.Model,
|
||||
contextWindow: cfg.ContextWindow,
|
||||
maxIterations: cfg.MaxIterations,
|
||||
maxToolCalls: cfg.MaxToolCalls,
|
||||
workspace: cfg.Workspace,
|
||||
eventPub: cfg.Bus,
|
||||
sessions: cfg.Sessions,
|
||||
tools: cfg.Tools,
|
||||
toolPolicy: cfg.ToolPolicy,
|
||||
agentToolPolicy: cfg.AgentToolPolicy,
|
||||
onEvent: cfg.OnEvent,
|
||||
ownerIDs: cfg.OwnerIDs,
|
||||
skillsLoader: cfg.SkillsLoader,
|
||||
skillAllowList: cfg.SkillAllowList,
|
||||
hasMemory: cfg.HasMemory,
|
||||
contextFiles: cfg.ContextFiles,
|
||||
ensureUserFiles: cfg.EnsureUserFiles,
|
||||
contextFileLoader: cfg.ContextFileLoader,
|
||||
bootstrapCleanup: cfg.BootstrapCleanup,
|
||||
compactionCfg: cfg.CompactionCfg,
|
||||
contextPruningCfg: cfg.ContextPruningCfg,
|
||||
sandboxEnabled: cfg.SandboxEnabled,
|
||||
sandboxContainerDir: cfg.SandboxContainerDir,
|
||||
sandboxWorkspaceAccess: cfg.SandboxWorkspaceAccess,
|
||||
traceCollector: cfg.TraceCollector,
|
||||
inputGuard: guard,
|
||||
injectionAction: action,
|
||||
maxMessageChars: cfg.MaxMessageChars,
|
||||
builtinToolSettings: cfg.BuiltinToolSettings,
|
||||
thinkingLevel: cfg.ThinkingLevel,
|
||||
selfEvolve: cfg.SelfEvolve,
|
||||
groupWriterCache: cfg.GroupWriterCache,
|
||||
teamStore: cfg.TeamStore,
|
||||
mediaStore: cfg.MediaStore,
|
||||
traceCollector: cfg.TraceCollector,
|
||||
inputGuard: guard,
|
||||
injectionAction: action,
|
||||
maxMessageChars: cfg.MaxMessageChars,
|
||||
builtinToolSettings: cfg.BuiltinToolSettings,
|
||||
thinkingLevel: cfg.ThinkingLevel,
|
||||
selfEvolve: cfg.SelfEvolve,
|
||||
groupWriterCache: cfg.GroupWriterCache,
|
||||
teamStore: cfg.TeamStore,
|
||||
mediaStore: cfg.MediaStore,
|
||||
}
|
||||
}
|
||||
|
||||
// RunRequest is the input for processing a message through the agent.
|
||||
type RunRequest struct {
|
||||
SessionKey string // composite key: agent:{agentId}:{channel}:{peerKind}:{chatId}
|
||||
Message string // user message
|
||||
Media []bus.MediaFile // local media files with MIME types
|
||||
ForwardMedia []bus.MediaFile // media files to forward to output (from delegation results)
|
||||
Channel string // source channel instance name (e.g. "my-telegram-bot")
|
||||
ChannelType string // platform type (e.g. "zalo_personal", "telegram") — for system prompt context
|
||||
ChatID string // source chat ID
|
||||
PeerKind string // "direct" or "group" (for session key building and tool context)
|
||||
RunID string // unique run identifier
|
||||
UserID string // external user ID (TEXT, free-form) for multi-tenant scoping
|
||||
SenderID string // original individual sender ID (preserved in group chats for permission checks)
|
||||
Stream bool // whether to stream response chunks
|
||||
ExtraSystemPrompt string // optional: injected into system prompt (skills, subagent context, etc.)
|
||||
SkillFilter []string // per-request skill override: nil=use agent default, []=no skills, ["x","y"]=whitelist
|
||||
HistoryLimit int // max user turns to keep in context (0=unlimited, from channel config)
|
||||
ToolAllow []string // per-group tool allow list (nil = no restriction, supports "group:xxx")
|
||||
LocalKey string // composite key with topic/thread suffix for routing (e.g. "-100123:topic:42")
|
||||
ParentTraceID uuid.UUID // if set, reuse parent trace instead of creating new (announce runs)
|
||||
ParentRootSpanID uuid.UUID // if set, nest announce agent span under this parent span
|
||||
TraceName string // override trace name (default: "chat <agentID>")
|
||||
TraceTags []string // additional tags for the trace (e.g. "cron")
|
||||
MaxIterations int // per-request override (0 = use agent default, must be lower)
|
||||
SessionKey string // composite key: agent:{agentId}:{channel}:{peerKind}:{chatId}
|
||||
Message string // user message
|
||||
Media []bus.MediaFile // local media files with MIME types
|
||||
ForwardMedia []bus.MediaFile // media files to forward to output (from delegation results)
|
||||
Channel string // source channel instance name (e.g. "my-telegram-bot")
|
||||
ChannelType string // platform type (e.g. "zalo_personal", "telegram") — for system prompt context
|
||||
ChatID string // source chat ID
|
||||
PeerKind string // "direct" or "group" (for session key building and tool context)
|
||||
RunID string // unique run identifier
|
||||
UserID string // external user ID (TEXT, free-form) for multi-tenant scoping
|
||||
SenderID string // original individual sender ID (preserved in group chats for permission checks)
|
||||
Stream bool // whether to stream response chunks
|
||||
ExtraSystemPrompt string // optional: injected into system prompt (skills, subagent context, etc.)
|
||||
SkillFilter []string // per-request skill override: nil=use agent default, []=no skills, ["x","y"]=whitelist
|
||||
HistoryLimit int // max user turns to keep in context (0=unlimited, from channel config)
|
||||
ToolAllow []string // per-group tool allow list (nil = no restriction, supports "group:xxx")
|
||||
LocalKey string // composite key with topic/thread suffix for routing (e.g. "-100123:topic:42")
|
||||
ParentTraceID uuid.UUID // if set, reuse parent trace instead of creating new (announce runs)
|
||||
ParentRootSpanID uuid.UUID // if set, nest announce agent span under this parent span
|
||||
TraceName string // override trace name (default: "chat <agentID>")
|
||||
TraceTags []string // additional tags for the trace (e.g. "cron")
|
||||
MaxIterations int // per-request override (0 = use agent default, must be lower)
|
||||
|
||||
// Run classification
|
||||
RunKind string // "delegation", "announce" — empty for user-initiated runs
|
||||
@@ -305,19 +305,19 @@ type RunRequest struct {
|
||||
|
||||
// RunResult is the output of a completed agent run.
|
||||
type RunResult struct {
|
||||
Content string `json:"content"`
|
||||
RunID string `json:"runId"`
|
||||
Iterations int `json:"iterations"`
|
||||
Usage *providers.Usage `json:"usage,omitempty"`
|
||||
Media []MediaResult `json:"media,omitempty"` // media files from tool results (MEDIA: prefix)
|
||||
Deliverables []string `json:"deliverables,omitempty"` // actual content from tool outputs (for team task results)
|
||||
BlockReplies int `json:"blockReplies,omitempty"` // number of block.reply events emitted
|
||||
LastBlockReply string `json:"lastBlockReply,omitempty"` // last block reply content (for dedup)
|
||||
Content string `json:"content"`
|
||||
RunID string `json:"runId"`
|
||||
Iterations int `json:"iterations"`
|
||||
Usage *providers.Usage `json:"usage,omitempty"`
|
||||
Media []MediaResult `json:"media,omitempty"` // media files from tool results (MEDIA: prefix)
|
||||
Deliverables []string `json:"deliverables,omitempty"` // actual content from tool outputs (for team task results)
|
||||
BlockReplies int `json:"blockReplies,omitempty"` // number of block.reply events emitted
|
||||
LastBlockReply string `json:"lastBlockReply,omitempty"` // last block reply content (for dedup)
|
||||
}
|
||||
|
||||
// MediaResult represents a media file produced by a tool during the agent run.
|
||||
type MediaResult struct {
|
||||
Path string `json:"path"` // local file path
|
||||
Path string `json:"path"` // local file path
|
||||
ContentType string `json:"content_type,omitempty"` // MIME type
|
||||
AsVoice bool `json:"as_voice,omitempty"` // send as voice message (Telegram OGG)
|
||||
}
|
||||
@@ -173,12 +173,12 @@ func (l *Loop) runMemoryFlush(ctx context.Context, sessionKey string, settings *
|
||||
|
||||
// Run LLM iteration loop (max 5 iterations for flush)
|
||||
maxFlushIter := 5
|
||||
for i := 0; i < maxFlushIter; i++ {
|
||||
for range maxFlushIter {
|
||||
resp, err := l.provider.Chat(flushCtx, providers.ChatRequest{
|
||||
Messages: messages,
|
||||
Tools: toolDefs,
|
||||
Model: l.model,
|
||||
Options: map[string]interface{}{
|
||||
Options: map[string]any{
|
||||
"max_tokens": 4096,
|
||||
"temperature": 0.3,
|
||||
},
|
||||
|
||||
@@ -2,6 +2,7 @@ package agent
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -172,10 +173,8 @@ func agentToolPolicyForTeam(policy *config.ToolPolicySpec, isLead bool) *config.
|
||||
if policy == nil {
|
||||
policy = &config.ToolPolicySpec{}
|
||||
}
|
||||
for _, d := range policy.Deny {
|
||||
if d == "team_message" {
|
||||
return policy
|
||||
}
|
||||
if slices.Contains(policy.Deny, "team_message") {
|
||||
return policy
|
||||
}
|
||||
policy.Deny = append(policy.Deny, "team_message")
|
||||
return policy
|
||||
@@ -191,10 +190,8 @@ func agentToolPolicyWithMCP(policy *config.ToolPolicySpec, hasMCP bool) *config.
|
||||
policy = &config.ToolPolicySpec{}
|
||||
}
|
||||
// Check if group:mcp is already present
|
||||
for _, a := range policy.AlsoAllow {
|
||||
if a == "group:mcp" {
|
||||
return policy
|
||||
}
|
||||
if slices.Contains(policy.AlsoAllow, "group:mcp") {
|
||||
return policy
|
||||
}
|
||||
policy.AlsoAllow = append(policy.AlsoAllow, "group:mcp")
|
||||
return policy
|
||||
|
||||
@@ -22,8 +22,8 @@ type agentEntry struct {
|
||||
// AgentActivityStatus tracks the current phase of a running agent for status queries.
|
||||
type AgentActivityStatus struct {
|
||||
RunID string
|
||||
Phase string // "thinking", "tool_exec", "compacting"
|
||||
Tool string // current tool name (when Phase == "tool_exec")
|
||||
Phase string // "thinking", "tool_exec", "compacting"
|
||||
Tool string // current tool name (when Phase == "tool_exec")
|
||||
Iteration int
|
||||
StartedAt time.Time
|
||||
}
|
||||
@@ -34,9 +34,9 @@ type AgentActivityStatus struct {
|
||||
type Router struct {
|
||||
agents map[string]*agentEntry
|
||||
mu sync.RWMutex
|
||||
activeRuns sync.Map // runID → *ActiveRun
|
||||
sessionRuns sync.Map // sessionKey → runID (secondary index for O(1) IsSessionBusy)
|
||||
agentActivity sync.Map // sessionKey → *AgentActivityStatus
|
||||
activeRuns sync.Map // runID → *ActiveRun
|
||||
sessionRuns sync.Map // sessionKey → runID (secondary index for O(1) IsSessionBusy)
|
||||
agentActivity sync.Map // sessionKey → *AgentActivityStatus
|
||||
resolver ResolverFunc // optional: lazy creation from DB
|
||||
ttl time.Duration
|
||||
}
|
||||
@@ -262,7 +262,7 @@ func (r *Router) IsSessionBusy(sessionKey string) bool {
|
||||
// Returns the list of aborted run IDs.
|
||||
func (r *Router) AbortRunsForSession(sessionKey string) []string {
|
||||
var aborted []string
|
||||
r.activeRuns.Range(func(key, val interface{}) bool {
|
||||
r.activeRuns.Range(func(key, val any) bool {
|
||||
run := val.(*ActiveRun)
|
||||
if run.SessionKey == sessionKey {
|
||||
run.Cancel()
|
||||
|
||||
@@ -28,7 +28,7 @@ type toolCallRecord struct {
|
||||
}
|
||||
|
||||
// record adds a tool call to history and returns its argsHash.
|
||||
func (s *toolLoopState) record(toolName string, args map[string]interface{}) string {
|
||||
func (s *toolLoopState) record(toolName string, args map[string]any) string {
|
||||
h := hashToolCall(toolName, args)
|
||||
s.history = append(s.history, toolCallRecord{
|
||||
toolName: toolName,
|
||||
@@ -98,7 +98,7 @@ func (s *toolLoopState) detect(toolName string, argsHash string) (level, message
|
||||
}
|
||||
|
||||
// hashToolCall produces a deterministic hash of tool name + arguments.
|
||||
func hashToolCall(toolName string, args map[string]interface{}) string {
|
||||
func hashToolCall(toolName string, args map[string]any) string {
|
||||
s := toolName + ":" + stableJSON(args)
|
||||
h := sha256.Sum256([]byte(s))
|
||||
return fmt.Sprintf("%x", h[:16]) // 32 hex chars, enough for dedup
|
||||
@@ -111,9 +111,9 @@ func hashResult(content string) string {
|
||||
}
|
||||
|
||||
// stableJSON serializes a value with sorted keys for deterministic hashing.
|
||||
func stableJSON(v interface{}) string {
|
||||
func stableJSON(v any) string {
|
||||
switch val := v.(type) {
|
||||
case map[string]interface{}:
|
||||
case map[string]any:
|
||||
keys := make([]string, 0, len(val))
|
||||
for k := range val {
|
||||
keys = append(keys, k)
|
||||
@@ -124,7 +124,7 @@ func stableJSON(v interface{}) string {
|
||||
parts[i] = fmt.Sprintf("%q:%s", k, stableJSON(val[k]))
|
||||
}
|
||||
return "{" + strings.Join(parts, ",") + "}"
|
||||
case []interface{}:
|
||||
case []any:
|
||||
parts := make([]string, len(val))
|
||||
for i, elem := range val {
|
||||
parts[i] = stableJSON(elem)
|
||||
|
||||
@@ -6,8 +6,8 @@ func TestToolLoopDetection_NoLoop(t *testing.T) {
|
||||
var s toolLoopState
|
||||
|
||||
// 2 identical calls with same result → below threshold, no detection
|
||||
for i := 0; i < 2; i++ {
|
||||
h := s.record("list_files", map[string]interface{}{"path": "."})
|
||||
for i := range 2 {
|
||||
h := s.record("list_files", map[string]any{"path": "."})
|
||||
s.recordResult(h, "access denied")
|
||||
level, _ := s.detect("list_files", h)
|
||||
if level != "" {
|
||||
@@ -20,8 +20,8 @@ func TestToolLoopDetection_Warning(t *testing.T) {
|
||||
var s toolLoopState
|
||||
|
||||
var lastLevel string
|
||||
for i := 0; i < toolLoopWarningThreshold; i++ {
|
||||
h := s.record("list_files", map[string]interface{}{"path": "."})
|
||||
for range toolLoopWarningThreshold {
|
||||
h := s.record("list_files", map[string]any{"path": "."})
|
||||
s.recordResult(h, "access denied")
|
||||
lastLevel, _ = s.detect("list_files", h)
|
||||
}
|
||||
@@ -34,8 +34,8 @@ func TestToolLoopDetection_Critical(t *testing.T) {
|
||||
var s toolLoopState
|
||||
|
||||
var lastLevel string
|
||||
for i := 0; i < toolLoopCriticalThreshold; i++ {
|
||||
h := s.record("list_files", map[string]interface{}{"path": "."})
|
||||
for range toolLoopCriticalThreshold {
|
||||
h := s.record("list_files", map[string]any{"path": "."})
|
||||
s.recordResult(h, "access denied")
|
||||
lastLevel, _ = s.detect("list_files", h)
|
||||
}
|
||||
@@ -48,8 +48,8 @@ func TestToolLoopDetection_DifferentArgs(t *testing.T) {
|
||||
var s toolLoopState
|
||||
|
||||
// Same tool but different args each time → no detection
|
||||
for i := 0; i < 15; i++ {
|
||||
args := map[string]interface{}{"path": string(rune('a' + i))}
|
||||
for i := range 15 {
|
||||
args := map[string]any{"path": string(rune('a' + i))}
|
||||
h := s.record("list_files", args)
|
||||
s.recordResult(h, "access denied")
|
||||
level, _ := s.detect("list_files", h)
|
||||
@@ -63,8 +63,8 @@ func TestToolLoopDetection_DifferentResults(t *testing.T) {
|
||||
var s toolLoopState
|
||||
|
||||
// Same args but different results each time → progress, no detection
|
||||
for i := 0; i < 15; i++ {
|
||||
h := s.record("web_fetch", map[string]interface{}{"url": "https://example.com"})
|
||||
for i := range 15 {
|
||||
h := s.record("web_fetch", map[string]any{"url": "https://example.com"})
|
||||
s.recordResult(h, "result content "+string(rune('a'+i)))
|
||||
level, _ := s.detect("web_fetch", h)
|
||||
if level != "" {
|
||||
@@ -78,12 +78,12 @@ func TestToolLoopDetection_MixedTools(t *testing.T) {
|
||||
|
||||
// Alternate between two tools with same result → each tool only hit ~half
|
||||
// With 8 iterations, each tool is called 4 times → below critical (5)
|
||||
for i := 0; i < 8; i++ {
|
||||
for i := range 8 {
|
||||
toolName := "list_files"
|
||||
if i%2 == 1 {
|
||||
toolName = "read_file"
|
||||
}
|
||||
h := s.record(toolName, map[string]interface{}{"path": "."})
|
||||
h := s.record(toolName, map[string]any{"path": "."})
|
||||
s.recordResult(h, "error")
|
||||
level, _ := s.detect(toolName, h)
|
||||
// Each tool is only called 4 times, should at most warn
|
||||
@@ -95,8 +95,8 @@ func TestToolLoopDetection_MixedTools(t *testing.T) {
|
||||
|
||||
func TestStableJSON(t *testing.T) {
|
||||
// Same keys in different order → same hash
|
||||
a := stableJSON(map[string]interface{}{"b": 2, "a": 1})
|
||||
b := stableJSON(map[string]interface{}{"a": 1, "b": 2})
|
||||
a := stableJSON(map[string]any{"b": 2, "a": 1})
|
||||
b := stableJSON(map[string]any{"a": 1, "b": 2})
|
||||
if a != b {
|
||||
t.Fatalf("stableJSON not deterministic: %q != %q", a, b)
|
||||
}
|
||||
@@ -104,14 +104,14 @@ func TestStableJSON(t *testing.T) {
|
||||
|
||||
func TestHashToolCall(t *testing.T) {
|
||||
// Same input → same hash
|
||||
h1 := hashToolCall("list_files", map[string]interface{}{"path": "."})
|
||||
h2 := hashToolCall("list_files", map[string]interface{}{"path": "."})
|
||||
h1 := hashToolCall("list_files", map[string]any{"path": "."})
|
||||
h2 := hashToolCall("list_files", map[string]any{"path": "."})
|
||||
if h1 != h2 {
|
||||
t.Fatal("hashToolCall not deterministic")
|
||||
}
|
||||
|
||||
// Different tool → different hash
|
||||
h3 := hashToolCall("read_file", map[string]interface{}{"path": "."})
|
||||
h3 := hashToolCall("read_file", map[string]any{"path": "."})
|
||||
if h1 == h3 {
|
||||
t.Fatal("different tools should have different hashes")
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package bootstrap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"maps"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -253,9 +254,7 @@ func TestSeedUserFiles_IdempotentOnSecondCall(t *testing.T) {
|
||||
SeedUserFiles(context.Background(), as, agentID, "user-frank", store.AgentTypePredefined)
|
||||
|
||||
// Simulate what the first call wrote (move seededUserFiles → userFiles)
|
||||
for k, v := range as.seededUserFiles {
|
||||
as.userFiles[k] = v
|
||||
}
|
||||
maps.Copy(as.userFiles, as.seededUserFiles)
|
||||
as.seededUserFiles = make(map[string]string)
|
||||
|
||||
// Second call — must seed nothing (all files already exist)
|
||||
|
||||
@@ -43,8 +43,8 @@ type MediaAttachment struct {
|
||||
|
||||
// Event represents a server-side event to broadcast to WebSocket clients.
|
||||
type Event struct {
|
||||
Name string `json:"name"` // event name (e.g. "agent", "chat", "health")
|
||||
Payload interface{} `json:"payload,omitempty"`
|
||||
Name string `json:"name"` // event name (e.g. "agent", "chat", "health")
|
||||
Payload any `json:"payload,omitempty"`
|
||||
}
|
||||
|
||||
// Cache invalidation kind constants.
|
||||
|
||||
+16
-16
@@ -13,7 +13,7 @@ import (
|
||||
// HandleAgentEvent routes agent lifecycle events to streaming/reaction channels.
|
||||
// Called from the bus event subscriber — must be non-blocking.
|
||||
// eventType: "run.started", "chunk", "tool.call", "tool.result", "run.completed", "run.failed"
|
||||
func (m *Manager) HandleAgentEvent(eventType, runID string, payload interface{}) {
|
||||
func (m *Manager) HandleAgentEvent(eventType, runID string, payload any) {
|
||||
val, ok := m.runs.Load(runID)
|
||||
if !ok {
|
||||
return
|
||||
@@ -187,11 +187,11 @@ func (m *Manager) HandleAgentEvent(eventType, runID string, payload interface{})
|
||||
}
|
||||
|
||||
// extractPayloadString extracts a string field from a payload (map[string]string or map[string]interface{}).
|
||||
func extractPayloadString(payload interface{}, key string) string {
|
||||
func extractPayloadString(payload any, key string) string {
|
||||
switch p := payload.(type) {
|
||||
case map[string]string:
|
||||
return p[key]
|
||||
case map[string]interface{}:
|
||||
case map[string]any:
|
||||
if v, ok := p[key].(string); ok {
|
||||
return v
|
||||
}
|
||||
@@ -214,28 +214,28 @@ func copyRoutingMeta(src map[string]string) map[string]string {
|
||||
// toolStatusMap maps builtin tool names to user-friendly status messages.
|
||||
var toolStatusMap = map[string]string{
|
||||
// Filesystem
|
||||
"read_file": "📝 Reading file...",
|
||||
"write_file": "📝 Writing file...",
|
||||
"list_files": "📝 Listing files...",
|
||||
"edit": "📝 Editing file...",
|
||||
"read_file": "📝 Reading file...",
|
||||
"write_file": "📝 Writing file...",
|
||||
"list_files": "📝 Listing files...",
|
||||
"edit": "📝 Editing file...",
|
||||
// Runtime
|
||||
"exec": "⚡ Running code...",
|
||||
// Web
|
||||
"web_search": "🔍 Searching the web...",
|
||||
"web_fetch": "🔍 Fetching web content...",
|
||||
// Memory
|
||||
"memory_search": "🧠 Searching memory...",
|
||||
"memory_get": "🧠 Retrieving memory...",
|
||||
"memory_search": "🧠 Searching memory...",
|
||||
"memory_get": "🧠 Retrieving memory...",
|
||||
"knowledge_graph_search": "🧠 Querying knowledge graph...",
|
||||
// Media
|
||||
"read_image": "👁 Analyzing image...",
|
||||
"read_image": "👁 Analyzing image...",
|
||||
"read_document": "📄 Reading document...",
|
||||
"read_audio": "🎧 Processing audio...",
|
||||
"read_video": "🎬 Processing video...",
|
||||
"create_image": "🎨 Creating image...",
|
||||
"create_video": "🎬 Creating video...",
|
||||
"create_audio": "🎵 Creating audio...",
|
||||
"tts": "🔊 Generating speech...",
|
||||
"read_audio": "🎧 Processing audio...",
|
||||
"read_video": "🎬 Processing video...",
|
||||
"create_image": "🎨 Creating image...",
|
||||
"create_video": "🎬 Creating video...",
|
||||
"create_audio": "🎵 Creating audio...",
|
||||
"tts": "🔊 Generating speech...",
|
||||
// Browser
|
||||
"browser": "🌐 Browsing...",
|
||||
// Delegation & teams
|
||||
|
||||
@@ -100,12 +100,12 @@ func parseMessageContent(rawContent, messageType string) string {
|
||||
}
|
||||
|
||||
func parsePostContent(rawContent string) string {
|
||||
var post map[string]interface{}
|
||||
var post map[string]any
|
||||
if err := json.Unmarshal([]byte(rawContent), &post); err != nil {
|
||||
return rawContent
|
||||
}
|
||||
|
||||
var langContent interface{}
|
||||
var langContent any
|
||||
for _, lang := range []string{"zh_cn", "en_us"} {
|
||||
if lc, ok := post[lang]; ok {
|
||||
langContent = lc
|
||||
@@ -122,25 +122,25 @@ func parsePostContent(rawContent string) string {
|
||||
return rawContent
|
||||
}
|
||||
|
||||
langMap, ok := langContent.(map[string]interface{})
|
||||
langMap, ok := langContent.(map[string]any)
|
||||
if !ok {
|
||||
return rawContent
|
||||
}
|
||||
|
||||
contentArr, ok := langMap["content"].([]interface{})
|
||||
contentArr, ok := langMap["content"].([]any)
|
||||
if !ok {
|
||||
return rawContent
|
||||
}
|
||||
|
||||
var textParts []string
|
||||
for _, para := range contentArr {
|
||||
paraArr, ok := para.([]interface{})
|
||||
paraArr, ok := para.([]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
var lineParts []string
|
||||
for _, elem := range paraArr {
|
||||
elemMap, ok := elem.(map[string]interface{})
|
||||
elemMap, ok := elem.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ type Channel struct {
|
||||
dedup sync.Map // message_id → struct{}
|
||||
pairingDebounce sync.Map // senderID → time.Time
|
||||
reactions sync.Map // chatID → *reactionState
|
||||
approvedGroups sync.Map // chatID → true (in-memory cache for paired groups)
|
||||
approvedGroups sync.Map // chatID → true (in-memory cache for paired groups)
|
||||
groupAllowList []string
|
||||
groupHistory *channels.PendingHistory
|
||||
historyLimit int
|
||||
@@ -391,9 +391,9 @@ func resolveReceiveIDType(id string) string {
|
||||
// --- Content builders ---
|
||||
|
||||
func buildPostContent(text string) string {
|
||||
content := map[string]interface{}{
|
||||
"zh_cn": map[string]interface{}{
|
||||
"content": [][]map[string]interface{}{
|
||||
content := map[string]any{
|
||||
"zh_cn": map[string]any{
|
||||
"content": [][]map[string]any{
|
||||
{
|
||||
{
|
||||
"tag": "md",
|
||||
@@ -407,14 +407,14 @@ func buildPostContent(text string) string {
|
||||
return string(data)
|
||||
}
|
||||
|
||||
func buildMarkdownCard(text string) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
func buildMarkdownCard(text string) map[string]any {
|
||||
return map[string]any{
|
||||
"schema": "2.0",
|
||||
"config": map[string]interface{}{
|
||||
"config": map[string]any{
|
||||
"wide_screen_mode": true,
|
||||
},
|
||||
"body": map[string]interface{}{
|
||||
"elements": []map[string]interface{}{
|
||||
"body": map[string]any{
|
||||
"elements": []map[string]any{
|
||||
{
|
||||
"tag": "markdown",
|
||||
"content": text,
|
||||
|
||||
@@ -107,7 +107,7 @@ type apiResponse struct {
|
||||
}
|
||||
|
||||
// doJSON performs an authenticated JSON API call with auto token refresh.
|
||||
func (c *LarkClient) doJSON(ctx context.Context, method, path string, body interface{}) (*apiResponse, error) {
|
||||
func (c *LarkClient) doJSON(ctx context.Context, method, path string, body any) (*apiResponse, error) {
|
||||
resp, err := c.doJSONOnce(ctx, method, path, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -120,7 +120,7 @@ func (c *LarkClient) doJSON(ctx context.Context, method, path string, body inter
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (c *LarkClient) doJSONOnce(ctx context.Context, method, path string, body interface{}) (*apiResponse, error) {
|
||||
func (c *LarkClient) doJSONOnce(ctx context.Context, method, path string, body any) (*apiResponse, error) {
|
||||
token, err := c.getToken(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -112,7 +112,7 @@ func (c *LarkClient) CreateCard(ctx context.Context, cardType, data string) (str
|
||||
|
||||
func (c *LarkClient) UpdateCardSettings(ctx context.Context, cardID, settings string, seq int, uuid string) error {
|
||||
path := "/open-apis/cardkit/v1/cards/" + cardID
|
||||
resp, err := c.doJSON(ctx, "PATCH", path, map[string]interface{}{
|
||||
resp, err := c.doJSON(ctx, "PATCH", path, map[string]any{
|
||||
"settings": settings,
|
||||
"sequence": seq,
|
||||
"uuid": uuid,
|
||||
@@ -128,7 +128,7 @@ func (c *LarkClient) UpdateCardSettings(ctx context.Context, cardID, settings st
|
||||
|
||||
func (c *LarkClient) UpdateCardElement(ctx context.Context, cardID, elementID, content string, seq int, uuid string) error {
|
||||
path := fmt.Sprintf("/open-apis/cardkit/v1/cards/%s/elements/%s", cardID, elementID)
|
||||
resp, err := c.doJSON(ctx, "PATCH", path, map[string]interface{}{
|
||||
resp, err := c.doJSON(ctx, "PATCH", path, map[string]any{
|
||||
"content": content,
|
||||
"sequence": seq,
|
||||
"uuid": uuid,
|
||||
@@ -150,7 +150,7 @@ func (c *LarkClient) UpdateCardElement(ctx context.Context, cardID, elementID, c
|
||||
// Lark API: POST /open-apis/im/v1/messages/{message_id}/reactions
|
||||
func (c *LarkClient) AddMessageReaction(ctx context.Context, messageID, emojiType string) (string, error) {
|
||||
path := fmt.Sprintf("/open-apis/im/v1/messages/%s/reactions", messageID)
|
||||
body := map[string]interface{}{
|
||||
body := map[string]any{
|
||||
"reaction_type": map[string]string{
|
||||
"emoji_type": emojiType,
|
||||
},
|
||||
|
||||
@@ -16,12 +16,12 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
defaultPingInterval = 120 * time.Second
|
||||
defaultReconnectNonce = 30 // seconds max jitter
|
||||
defaultReconnectWait = 120 * time.Second
|
||||
frameTypeControl = 0
|
||||
frameTypeData = 1
|
||||
fragmentBufferTTL = 5 * time.Second
|
||||
defaultPingInterval = 120 * time.Second
|
||||
defaultReconnectNonce = 30 // seconds max jitter
|
||||
defaultReconnectWait = 120 * time.Second
|
||||
frameTypeControl = 0
|
||||
frameTypeData = 1
|
||||
fragmentBufferTTL = 5 * time.Second
|
||||
)
|
||||
|
||||
// WSEventHandler processes incoming WebSocket events.
|
||||
@@ -315,7 +315,7 @@ func (c *WSClient) sendResponse(original *wsFrame, headers map[string]string) {
|
||||
}
|
||||
respHeaders = append(respHeaders, wsHeader{Key: "biz_rt", Value: "0"})
|
||||
|
||||
respPayload, _ := json.Marshal(map[string]interface{}{
|
||||
respPayload, _ := json.Marshal(map[string]any{
|
||||
"code": 0,
|
||||
"msg": "success",
|
||||
})
|
||||
@@ -415,4 +415,3 @@ func (c *WSClient) reassemble(msgID string, total, seq int, data []byte) []byte
|
||||
delete(c.fragments, msgID)
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ func (ph *PendingHistory) runCompaction(historyKey string, cfg *CompactionConfig
|
||||
Content: "Summarize these group chat messages concisely, preserving key topics, decisions, names, and important context:\n\n" + sb.String(),
|
||||
}},
|
||||
Model: cfg.Model,
|
||||
Options: map[string]interface{}{"max_tokens": 512, "temperature": 0.3},
|
||||
Options: map[string]any{"max_tokens": 512, "temperature": 0.3},
|
||||
})
|
||||
if err != nil {
|
||||
slog.Warn("compaction.llm_failed", "channel", ph.channelName, "key", historyKey, "error", err)
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"maps"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -146,7 +147,7 @@ func coerceStringBools(data json.RawMessage) json.RawMessage {
|
||||
if len(data) == 0 {
|
||||
return data
|
||||
}
|
||||
var m map[string]interface{}
|
||||
var m map[string]any
|
||||
if json.Unmarshal(data, &m) != nil {
|
||||
return data
|
||||
}
|
||||
@@ -176,9 +177,7 @@ func (l *InstanceLoader) LoadedNames() map[string]struct{} {
|
||||
defer l.mu.Unlock()
|
||||
|
||||
result := make(map[string]struct{}, len(l.loaded))
|
||||
for k, v := range l.loaded {
|
||||
result[k] = v
|
||||
}
|
||||
maps.Copy(result, l.loaded)
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
@@ -107,13 +107,13 @@ func (m *Manager) GetChannel(name string) (Channel, bool) {
|
||||
}
|
||||
|
||||
// GetStatus returns the running status of all channels.
|
||||
func (m *Manager) GetStatus() map[string]interface{} {
|
||||
func (m *Manager) GetStatus() map[string]any {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
status := make(map[string]interface{})
|
||||
status := make(map[string]any)
|
||||
for name, channel := range m.channels {
|
||||
status[name] = map[string]interface{}{
|
||||
status[name] = map[string]any{
|
||||
"enabled": true,
|
||||
"running": channel.IsRunning(),
|
||||
}
|
||||
|
||||
@@ -298,12 +298,12 @@ func parseTableRow(line string) []string {
|
||||
// stripInlineMarkdown removes common inline markdown markers from text.
|
||||
// Used for table cells that render inside code blocks where formatting has no effect.
|
||||
var (
|
||||
reStripBoldAsterisks = regexp.MustCompile(`\*\*(.+?)\*\*`)
|
||||
reStripBoldUnderscores = regexp.MustCompile(`__(.+?)__`)
|
||||
reStripItalicAsterisk = regexp.MustCompile(`\*([^*]+)\*`)
|
||||
reStripBoldAsterisks = regexp.MustCompile(`\*\*(.+?)\*\*`)
|
||||
reStripBoldUnderscores = regexp.MustCompile(`__(.+?)__`)
|
||||
reStripItalicAsterisk = regexp.MustCompile(`\*([^*]+)\*`)
|
||||
reStripItalicUnderscore = regexp.MustCompile(`_([^_]+)_`)
|
||||
reStripStrikethrough = regexp.MustCompile(`~~(.+?)~~`)
|
||||
reStripInlineCode = regexp.MustCompile("`([^`]+)`")
|
||||
reStripStrikethrough = regexp.MustCompile(`~~(.+?)~~`)
|
||||
reStripInlineCode = regexp.MustCompile("`([^`]+)`")
|
||||
)
|
||||
|
||||
func stripInlineMarkdown(s string) string {
|
||||
@@ -325,10 +325,7 @@ func renderRow(cells []string, colWidths []int) string {
|
||||
cell = cells[j]
|
||||
}
|
||||
// Pad with spaces to align columns
|
||||
padding := w - displayWidth(cell)
|
||||
if padding < 0 {
|
||||
padding = 0
|
||||
}
|
||||
padding := max(w-displayWidth(cell), 0)
|
||||
parts = append(parts, " "+cell+strings.Repeat(" ", padding)+" ")
|
||||
}
|
||||
return "|" + strings.Join(parts, "|") + "|"
|
||||
|
||||
@@ -6,12 +6,13 @@ import (
|
||||
"github.com/nextlevelbuilder/goclaw/internal/config"
|
||||
)
|
||||
|
||||
func boolPtr(b bool) *bool { return &b }
|
||||
//go:fix inline
|
||||
func boolPtr(b bool) *bool { return new(b) }
|
||||
|
||||
func TestResolveTopicConfig_Defaults(t *testing.T) {
|
||||
cfg := config.TelegramConfig{
|
||||
GroupPolicy: "open",
|
||||
RequireMention: boolPtr(true),
|
||||
RequireMention: new(true),
|
||||
AllowFrom: []string{"user1"},
|
||||
}
|
||||
|
||||
@@ -37,7 +38,7 @@ func TestResolveTopicConfig_WildcardGroup(t *testing.T) {
|
||||
Groups: map[string]*config.TelegramGroupConfig{
|
||||
"*": {
|
||||
GroupPolicy: "allowlist",
|
||||
RequireMention: boolPtr(false),
|
||||
RequireMention: new(false),
|
||||
AllowFrom: []string{"admin1"},
|
||||
},
|
||||
},
|
||||
@@ -62,7 +63,7 @@ func TestResolveTopicConfig_SpecificGroupOverridesWildcard(t *testing.T) {
|
||||
Groups: map[string]*config.TelegramGroupConfig{
|
||||
"*": {
|
||||
GroupPolicy: "allowlist",
|
||||
RequireMention: boolPtr(false),
|
||||
RequireMention: new(false),
|
||||
AllowFrom: []string{"admin1"},
|
||||
SystemPrompt: "wildcard prompt",
|
||||
},
|
||||
@@ -96,12 +97,12 @@ func TestResolveTopicConfig_TopicOverridesGroup(t *testing.T) {
|
||||
GroupPolicy: "open",
|
||||
Groups: map[string]*config.TelegramGroupConfig{
|
||||
"-100123": {
|
||||
RequireMention: boolPtr(true),
|
||||
RequireMention: new(true),
|
||||
SystemPrompt: "group prompt",
|
||||
Skills: []string{"skill_a", "skill_b"},
|
||||
Topics: map[string]*config.TelegramTopicConfig{
|
||||
"42": {
|
||||
RequireMention: boolPtr(false),
|
||||
RequireMention: new(false),
|
||||
Skills: []string{"skill_c"},
|
||||
SystemPrompt: "topic prompt",
|
||||
},
|
||||
@@ -171,10 +172,10 @@ func TestResolveTopicConfig_DisabledTopic(t *testing.T) {
|
||||
cfg := config.TelegramConfig{
|
||||
Groups: map[string]*config.TelegramGroupConfig{
|
||||
"-100123": {
|
||||
Enabled: boolPtr(true),
|
||||
Enabled: new(true),
|
||||
Topics: map[string]*config.TelegramTopicConfig{
|
||||
"42": {
|
||||
Enabled: boolPtr(false),
|
||||
Enabled: new(false),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -230,7 +231,7 @@ func TestEffectiveRequireMention(t *testing.T) {
|
||||
}
|
||||
|
||||
// explicit requireMention overrides default
|
||||
r2 := resolvedTopicConfig{requireMention: boolPtr(false)}
|
||||
r2 := resolvedTopicConfig{requireMention: new(false)}
|
||||
if r2.effectiveRequireMention(true) != false {
|
||||
t.Error("effectiveRequireMention(true) with override=false should be false")
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ func (c *Channel) Send(_ context.Context, msg bus.OutboundMessage) error {
|
||||
return fmt.Errorf("whatsapp bridge not connected")
|
||||
}
|
||||
|
||||
payload := map[string]interface{}{
|
||||
payload := map[string]any{
|
||||
"type": "message",
|
||||
"to": msg.ChatID,
|
||||
"content": msg.Content,
|
||||
@@ -188,7 +188,7 @@ func (c *Channel) listenLoop() {
|
||||
continue
|
||||
}
|
||||
|
||||
var msg map[string]interface{}
|
||||
var msg map[string]any
|
||||
if err := json.Unmarshal(message, &msg); err != nil {
|
||||
slog.Warn("invalid whatsapp message JSON", "error", err)
|
||||
continue
|
||||
@@ -203,7 +203,7 @@ func (c *Channel) listenLoop() {
|
||||
|
||||
// handleIncomingMessage processes a message received from the bridge.
|
||||
// Expected format: {"type":"message","from":"...","chat":"...","content":"...","id":"...","from_name":"...","media":[...]}
|
||||
func (c *Channel) handleIncomingMessage(msg map[string]interface{}) {
|
||||
func (c *Channel) handleIncomingMessage(msg map[string]any) {
|
||||
senderID, ok := msg["from"].(string)
|
||||
if !ok || senderID == "" {
|
||||
return
|
||||
@@ -244,7 +244,7 @@ func (c *Channel) handleIncomingMessage(msg map[string]interface{}) {
|
||||
}
|
||||
|
||||
var media []string
|
||||
if mediaData, ok := msg["media"].([]interface{}); ok {
|
||||
if mediaData, ok := msg["media"].([]any); ok {
|
||||
media = make([]string, 0, len(mediaData))
|
||||
for _, m := range mediaData {
|
||||
if path, ok := m.(string); ok {
|
||||
@@ -368,7 +368,7 @@ func (c *Channel) sendPairingReply(senderID, chatID string) {
|
||||
return
|
||||
}
|
||||
|
||||
payload, _ := json.Marshal(map[string]interface{}{
|
||||
payload, _ := json.Marshal(map[string]any{
|
||||
"type": "message",
|
||||
"to": chatID,
|
||||
"content": replyText,
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"maps"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
@@ -294,13 +295,13 @@ func loadLoginPage(ctx context.Context, sess *Session) (string, error) {
|
||||
}
|
||||
|
||||
var qrHeaders = http.Header{
|
||||
"Accept": {"*/*"},
|
||||
"Content-Type": {"application/x-www-form-urlencoded"},
|
||||
"Sec-Fetch-Dest": {"empty"},
|
||||
"Sec-Fetch-Mode": {"cors"},
|
||||
"Sec-Fetch-Site": {"same-origin"},
|
||||
"Referer": {"https://id.zalo.me/account?continue=https%3A%2F%2Fzalo.me%2Fpc"},
|
||||
"Referrer-Policy": {"strict-origin-when-cross-origin"},
|
||||
"Accept": {"*/*"},
|
||||
"Content-Type": {"application/x-www-form-urlencoded"},
|
||||
"Sec-Fetch-Dest": {"empty"},
|
||||
"Sec-Fetch-Mode": {"cors"},
|
||||
"Sec-Fetch-Site": {"same-origin"},
|
||||
"Referer": {"https://id.zalo.me/account?continue=https%3A%2F%2Fzalo.me%2Fpc"},
|
||||
"Referrer-Policy": {"strict-origin-when-cross-origin"},
|
||||
}
|
||||
|
||||
func qrPost(ctx context.Context, sess *Session, endpoint string, formData map[string]string) (*http.Response, error) {
|
||||
@@ -310,9 +311,7 @@ func qrPost(ctx context.Context, sess *Session, endpoint string, formData map[st
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("User-Agent", sess.UserAgent)
|
||||
for k, v := range qrHeaders {
|
||||
req.Header[k] = v
|
||||
}
|
||||
maps.Copy(req.Header, qrHeaders)
|
||||
return sess.Client.Do(req)
|
||||
}
|
||||
|
||||
@@ -456,9 +455,7 @@ func qrGetUserInfo(ctx context.Context, sess *Session) (*QRUserInfo, error) {
|
||||
|
||||
func setDefaultHeaders(req *http.Request, sess *Session) {
|
||||
h := defaultHeaders(sess)
|
||||
for k, v := range h {
|
||||
req.Header[k] = v
|
||||
}
|
||||
maps.Copy(req.Header, h)
|
||||
}
|
||||
|
||||
// readJSON decodes a potentially gzip/deflate-encoded JSON response.
|
||||
|
||||
@@ -104,13 +104,14 @@ func generateSignKey(typeStr string, params map[string]any) string {
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
signStr := "zsecure" + typeStr
|
||||
var signStr strings.Builder
|
||||
signStr.WriteString("zsecure" + typeStr)
|
||||
for _, k := range keys {
|
||||
if v := params[k]; v != nil {
|
||||
signStr += convertToString(v)
|
||||
signStr.WriteString(convertToString(v))
|
||||
}
|
||||
}
|
||||
hash := md5.Sum([]byte(signStr))
|
||||
hash := md5.Sum([]byte(signStr.String()))
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
@@ -131,11 +132,11 @@ func getEncryptParam(sess *Session, typeStr string) (params map[string]any, enk
|
||||
}
|
||||
|
||||
params = map[string]any{
|
||||
"zcid": zcid,
|
||||
"enc_ver": DefaultEncryptVer,
|
||||
"zcid_ext": zcidExt,
|
||||
"params": encKey.encData,
|
||||
"type": DefaultAPIType,
|
||||
"zcid": zcid,
|
||||
"enc_ver": DefaultEncryptVer,
|
||||
"zcid_ext": zcidExt,
|
||||
"params": encKey.encData,
|
||||
"type": DefaultAPIType,
|
||||
"client_version": DefaultAPIVersion,
|
||||
}
|
||||
|
||||
|
||||
@@ -145,7 +145,7 @@ func TestReverseCopy(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRandomHexString(t *testing.T) {
|
||||
for i := 0; i < 10; i++ {
|
||||
for range 10 {
|
||||
s := randomHexString(6, 12)
|
||||
if len(s) < 6 || len(s) > 12 {
|
||||
t.Errorf("randomHexString(6,12) = len %d, want 6-12", len(s))
|
||||
|
||||
@@ -35,14 +35,14 @@ const (
|
||||
// Channel connects to the Zalo OA Bot API.
|
||||
type Channel struct {
|
||||
*channels.BaseChannel
|
||||
token string
|
||||
dmPolicy string
|
||||
mediaMaxMB int
|
||||
blockReply *bool
|
||||
pairingService store.PairingStore
|
||||
token string
|
||||
dmPolicy string
|
||||
mediaMaxMB int
|
||||
blockReply *bool
|
||||
pairingService store.PairingStore
|
||||
pairingDebounce sync.Map // senderID → time.Time
|
||||
stopCh chan struct{}
|
||||
client *http.Client
|
||||
stopCh chan struct{}
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
// New creates a new Zalo channel.
|
||||
@@ -379,7 +379,7 @@ type zaloUpdate struct {
|
||||
Message *zaloMessage `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
func (c *Channel) callAPI(method string, body interface{}) (json.RawMessage, error) {
|
||||
func (c *Channel) callAPI(method string, body any) (json.RawMessage, error) {
|
||||
url := fmt.Sprintf("%s/bot%s/%s", apiBase, c.token, method)
|
||||
|
||||
var reqBody io.Reader
|
||||
@@ -436,7 +436,7 @@ func (c *Channel) getMe() (*zaloBotInfo, error) {
|
||||
}
|
||||
|
||||
func (c *Channel) getUpdates(timeout int) ([]zaloUpdate, error) {
|
||||
params := map[string]interface{}{
|
||||
params := map[string]any{
|
||||
"timeout": timeout,
|
||||
}
|
||||
|
||||
@@ -453,7 +453,7 @@ func (c *Channel) getUpdates(timeout int) ([]zaloUpdate, error) {
|
||||
}
|
||||
|
||||
func (c *Channel) sendMessage(chatID, text string) error {
|
||||
params := map[string]interface{}{
|
||||
params := map[string]any{
|
||||
"chat_id": chatID,
|
||||
"text": text,
|
||||
}
|
||||
@@ -463,7 +463,7 @@ func (c *Channel) sendMessage(chatID, text string) error {
|
||||
}
|
||||
|
||||
func (c *Channel) sendPhoto(chatID, photoURL, caption string) error {
|
||||
params := map[string]interface{}{
|
||||
params := map[string]any{
|
||||
"chat_id": chatID,
|
||||
"photo": photoURL,
|
||||
}
|
||||
|
||||
+40
-40
@@ -19,7 +19,7 @@ func (f *FlexibleStringSlice) UnmarshalJSON(data []byte) error {
|
||||
*f = ss
|
||||
return nil
|
||||
}
|
||||
var raw []interface{}
|
||||
var raw []any
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -46,22 +46,22 @@ type Config struct {
|
||||
Gateway GatewayConfig `json:"gateway"`
|
||||
Tools ToolsConfig `json:"tools"`
|
||||
Sessions SessionsConfig `json:"sessions"`
|
||||
Database DatabaseConfig `json:"database,omitempty"`
|
||||
Tts TtsConfig `json:"tts,omitempty"`
|
||||
Cron CronConfig `json:"cron,omitempty"`
|
||||
Telemetry TelemetryConfig `json:"telemetry,omitempty"`
|
||||
Tailscale TailscaleConfig `json:"tailscale,omitempty"`
|
||||
Database DatabaseConfig `json:"database"`
|
||||
Tts TtsConfig `json:"tts"`
|
||||
Cron CronConfig `json:"cron"`
|
||||
Telemetry TelemetryConfig `json:"telemetry"`
|
||||
Tailscale TailscaleConfig `json:"tailscale"`
|
||||
Bindings []AgentBinding `json:"bindings,omitempty"`
|
||||
mu sync.RWMutex
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// TailscaleConfig configures the optional Tailscale tsnet listener.
|
||||
// Requires building with -tags tsnet. Auth key from env only (never persisted).
|
||||
type TailscaleConfig struct {
|
||||
Hostname string `json:"hostname"` // Tailscale machine name (e.g. "goclaw-gateway")
|
||||
StateDir string `json:"state_dir,omitempty"` // persistent state directory (default: os.UserConfigDir/tsnet-goclaw)
|
||||
AuthKey string `json:"-"` // from env GOCLAW_TSNET_AUTH_KEY only
|
||||
Ephemeral bool `json:"ephemeral,omitempty"` // remove node on exit (default false)
|
||||
Hostname string `json:"hostname"` // Tailscale machine name (e.g. "goclaw-gateway")
|
||||
StateDir string `json:"state_dir,omitempty"` // persistent state directory (default: os.UserConfigDir/tsnet-goclaw)
|
||||
AuthKey string `json:"-"` // from env GOCLAW_TSNET_AUTH_KEY only
|
||||
Ephemeral bool `json:"ephemeral,omitempty"` // remove node on exit (default false)
|
||||
EnableTLS bool `json:"enable_tls,omitempty"` // use ListenTLS for auto HTTPS certs
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ type AgentBinding struct {
|
||||
|
||||
// BindingMatch specifies what messages this binding applies to.
|
||||
type BindingMatch struct {
|
||||
Channel string `json:"channel"` // "telegram", "discord", "slack", etc.
|
||||
Channel string `json:"channel"` // "telegram", "discord", "slack", etc.
|
||||
AccountID string `json:"accountId,omitempty"` // bot account ID
|
||||
Peer *BindingPeer `json:"peer,omitempty"` // specific DM/group
|
||||
GuildID string `json:"guildId,omitempty"` // Discord guild
|
||||
@@ -106,21 +106,21 @@ type AgentsConfig struct {
|
||||
|
||||
// AgentDefaults are default settings for all agents.
|
||||
type AgentDefaults struct {
|
||||
Workspace string `json:"workspace"`
|
||||
RestrictToWorkspace bool `json:"restrict_to_workspace"`
|
||||
Provider string `json:"provider"`
|
||||
Model string `json:"model"`
|
||||
MaxTokens int `json:"max_tokens"`
|
||||
Temperature float64 `json:"temperature"`
|
||||
MaxToolIterations int `json:"max_tool_iterations"`
|
||||
ContextWindow int `json:"context_window"`
|
||||
MaxToolCalls int `json:"max_tool_calls,omitempty"` // max total tool calls per run (0 = unlimited, default 25)
|
||||
AgentType string `json:"agent_type,omitempty"` // "open" (default) or "predefined"
|
||||
Subagents *SubagentsConfig `json:"subagents,omitempty"`
|
||||
Sandbox *SandboxConfig `json:"sandbox,omitempty"`
|
||||
Workspace string `json:"workspace"`
|
||||
RestrictToWorkspace bool `json:"restrict_to_workspace"`
|
||||
Provider string `json:"provider"`
|
||||
Model string `json:"model"`
|
||||
MaxTokens int `json:"max_tokens"`
|
||||
Temperature float64 `json:"temperature"`
|
||||
MaxToolIterations int `json:"max_tool_iterations"`
|
||||
ContextWindow int `json:"context_window"`
|
||||
MaxToolCalls int `json:"max_tool_calls,omitempty"` // max total tool calls per run (0 = unlimited, default 25)
|
||||
AgentType string `json:"agent_type,omitempty"` // "open" (default) or "predefined"
|
||||
Subagents *SubagentsConfig `json:"subagents,omitempty"`
|
||||
Sandbox *SandboxConfig `json:"sandbox,omitempty"`
|
||||
Memory *MemoryConfig `json:"memory,omitempty"`
|
||||
Compaction *CompactionConfig `json:"compaction,omitempty"`
|
||||
ContextPruning *ContextPruningConfig `json:"contextPruning,omitempty"`
|
||||
Compaction *CompactionConfig `json:"compaction,omitempty"`
|
||||
ContextPruning *ContextPruningConfig `json:"contextPruning,omitempty"`
|
||||
// Bootstrap context truncation limits (matching TS bootstrapMaxChars / bootstrapTotalMaxChars)
|
||||
BootstrapMaxChars int `json:"bootstrapMaxChars,omitempty"` // per-file max before truncation (default 20000)
|
||||
BootstrapTotalMaxChars int `json:"bootstrapTotalMaxChars,omitempty"` // total budget across all files (default 24000)
|
||||
@@ -137,7 +137,7 @@ type CompactionConfig struct {
|
||||
MaxHistoryShare float64 `json:"maxHistoryShare,omitempty"` // max share of context for history (default 0.75)
|
||||
MinMessages int `json:"minMessages,omitempty"` // min messages before compaction triggers (default 50)
|
||||
KeepLastMessages int `json:"keepLastMessages,omitempty"` // messages to keep after compaction (default 4)
|
||||
MemoryFlush *MemoryFlushConfig `json:"memoryFlush,omitempty"` // pre-compaction flush
|
||||
MemoryFlush *MemoryFlushConfig `json:"memoryFlush,omitempty"` // pre-compaction flush
|
||||
}
|
||||
|
||||
// MemoryFlushConfig configures the pre-compaction memory flush.
|
||||
@@ -146,20 +146,20 @@ type MemoryFlushConfig struct {
|
||||
Enabled *bool `json:"enabled,omitempty"` // default true (nil = enabled)
|
||||
SoftThresholdTokens int `json:"softThresholdTokens,omitempty"` // flush when within N tokens of compaction (default 4000)
|
||||
Prompt string `json:"prompt,omitempty"` // user prompt for flush turn
|
||||
SystemPrompt string `json:"systemPrompt,omitempty"` // system prompt for flush turn
|
||||
SystemPrompt string `json:"systemPrompt,omitempty"` // system prompt for flush turn
|
||||
}
|
||||
|
||||
// ContextPruningConfig configures in-memory context pruning of old tool results.
|
||||
// Matching TS src/agents/pi-extensions/context-pruning/settings.ts.
|
||||
// Mode "cache-ttl": prune when context exceeds softTrimRatio of context window.
|
||||
type ContextPruningConfig struct {
|
||||
Mode string `json:"mode,omitempty"` // "off" (default), "cache-ttl"
|
||||
KeepLastAssistants int `json:"keepLastAssistants,omitempty"` // protect last N assistant msgs (default 3)
|
||||
SoftTrimRatio float64 `json:"softTrimRatio,omitempty"` // start soft trim at this % of window (default 0.3)
|
||||
HardClearRatio float64 `json:"hardClearRatio,omitempty"` // start hard clear at this % (default 0.5)
|
||||
Mode string `json:"mode,omitempty"` // "off" (default), "cache-ttl"
|
||||
KeepLastAssistants int `json:"keepLastAssistants,omitempty"` // protect last N assistant msgs (default 3)
|
||||
SoftTrimRatio float64 `json:"softTrimRatio,omitempty"` // start soft trim at this % of window (default 0.3)
|
||||
HardClearRatio float64 `json:"hardClearRatio,omitempty"` // start hard clear at this % (default 0.5)
|
||||
MinPrunableToolChars int `json:"minPrunableToolChars,omitempty"` // min chars in prunable tools before acting (default 50000)
|
||||
SoftTrim *ContextPruningSoftTrim `json:"softTrim,omitempty"`
|
||||
HardClear *ContextPruningHardClear `json:"hardClear,omitempty"`
|
||||
SoftTrim *ContextPruningSoftTrim `json:"softTrim,omitempty"`
|
||||
HardClear *ContextPruningHardClear `json:"hardClear,omitempty"`
|
||||
}
|
||||
|
||||
// ContextPruningSoftTrim configures how long tool results are trimmed.
|
||||
@@ -205,9 +205,9 @@ type SandboxConfig struct {
|
||||
Env map[string]string `json:"env,omitempty"` // extra environment variables
|
||||
|
||||
// Enhanced security
|
||||
User string `json:"user,omitempty"` // container user (e.g. "1000:1000", "nobody")
|
||||
TmpfsSizeMB int `json:"tmpfs_size_mb,omitempty"` // default tmpfs size in MB (0 = Docker default)
|
||||
MaxOutputBytes int `json:"max_output_bytes,omitempty"` // limit exec output capture (default 1MB)
|
||||
User string `json:"user,omitempty"` // container user (e.g. "1000:1000", "nobody")
|
||||
TmpfsSizeMB int `json:"tmpfs_size_mb,omitempty"` // default tmpfs size in MB (0 = Docker default)
|
||||
MaxOutputBytes int `json:"max_output_bytes,omitempty"` // limit exec output capture (default 1MB)
|
||||
|
||||
// Pruning (matching TS SandboxPruneSettings)
|
||||
IdleHours int `json:"idle_hours,omitempty"` // prune containers idle > N hours (default 24)
|
||||
@@ -355,9 +355,9 @@ type AgentSpec struct {
|
||||
MaxToolIterations int `json:"max_tool_iterations,omitempty"`
|
||||
ContextWindow int `json:"context_window,omitempty"`
|
||||
MaxToolCalls int `json:"max_tool_calls,omitempty"` // per-agent override
|
||||
AgentType string `json:"agent_type,omitempty"` // "open" or "predefined"
|
||||
Skills []string `json:"skills,omitempty"` // nil = all skills allowed
|
||||
Tools *ToolPolicySpec `json:"tools,omitempty"` // per-agent tool policy
|
||||
AgentType string `json:"agent_type,omitempty"` // "open" or "predefined"
|
||||
Skills []string `json:"skills,omitempty"` // nil = all skills allowed
|
||||
Tools *ToolPolicySpec `json:"tools,omitempty"` // per-agent tool policy
|
||||
Workspace string `json:"workspace,omitempty"`
|
||||
Default bool `json:"default,omitempty"`
|
||||
Sandbox *SandboxConfig `json:"sandbox,omitempty"`
|
||||
|
||||
@@ -70,11 +70,11 @@ type TelegramTopicConfig struct {
|
||||
}
|
||||
|
||||
type DiscordConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Token string `json:"token"`
|
||||
AllowFrom FlexibleStringSlice `json:"allow_from"`
|
||||
DMPolicy string `json:"dm_policy,omitempty"` // "open" (default), "allowlist", "disabled"
|
||||
GroupPolicy string `json:"group_policy,omitempty"` // "open" (default), "allowlist", "disabled"
|
||||
Enabled bool `json:"enabled"`
|
||||
Token string `json:"token"`
|
||||
AllowFrom FlexibleStringSlice `json:"allow_from"`
|
||||
DMPolicy string `json:"dm_policy,omitempty"` // "open" (default), "allowlist", "disabled"
|
||||
GroupPolicy string `json:"group_policy,omitempty"` // "open" (default), "allowlist", "disabled"
|
||||
RequireMention *bool `json:"require_mention,omitempty"` // require @bot mention in groups (default true)
|
||||
HistoryLimit int `json:"history_limit,omitempty"` // max pending group messages for context (default 50, 0=disabled)
|
||||
BlockReply *bool `json:"block_reply,omitempty"` // override gateway block_reply (nil = inherit)
|
||||
@@ -88,22 +88,22 @@ type DiscordConfig struct {
|
||||
|
||||
type SlackConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
BotToken string `json:"bot_token"` // xoxb-... (Bot User OAuth Token)
|
||||
AppToken string `json:"app_token"` // xapp-... (App-Level Token for Socket Mode)
|
||||
UserToken string `json:"user_token,omitempty"` // xoxp-... (Optional: custom bot identity)
|
||||
BotToken string `json:"bot_token"` // xoxb-... (Bot User OAuth Token)
|
||||
AppToken string `json:"app_token"` // xapp-... (App-Level Token for Socket Mode)
|
||||
UserToken string `json:"user_token,omitempty"` // xoxp-... (Optional: custom bot identity)
|
||||
AllowFrom FlexibleStringSlice `json:"allow_from"`
|
||||
DMPolicy string `json:"dm_policy,omitempty"` // "pairing" (default), "allowlist", "open", "disabled"
|
||||
GroupPolicy string `json:"group_policy,omitempty"` // "open" (default), "pairing", "allowlist", "disabled"
|
||||
RequireMention *bool `json:"require_mention,omitempty"` // require @bot mention in channels (default true)
|
||||
HistoryLimit int `json:"history_limit,omitempty"` // max pending group messages for context (default 50, 0=disabled)
|
||||
DMStream *bool `json:"dm_stream,omitempty"` // enable streaming for DMs (default false)
|
||||
GroupStream *bool `json:"group_stream,omitempty"` // enable streaming for groups (default false)
|
||||
NativeStream *bool `json:"native_stream,omitempty"` // use Slack ChatStreamer API if available (default false)
|
||||
ReactionLevel string `json:"reaction_level,omitempty"` // "off" (default), "minimal", "full"
|
||||
BlockReply *bool `json:"block_reply,omitempty"` // override gateway block_reply (nil = inherit)
|
||||
DebounceDelay int `json:"debounce_delay,omitempty"` // ms delay before dispatching rapid messages (default 300, 0=disabled)
|
||||
ThreadTTL *int `json:"thread_ttl,omitempty"` // hours before thread participation expires (default 24, 0=disabled — always require @mention)
|
||||
MediaMaxBytes int64 `json:"media_max_bytes,omitempty"` // max file download size in bytes (default 20MB)
|
||||
DMPolicy string `json:"dm_policy,omitempty"` // "pairing" (default), "allowlist", "open", "disabled"
|
||||
GroupPolicy string `json:"group_policy,omitempty"` // "open" (default), "pairing", "allowlist", "disabled"
|
||||
RequireMention *bool `json:"require_mention,omitempty"` // require @bot mention in channels (default true)
|
||||
HistoryLimit int `json:"history_limit,omitempty"` // max pending group messages for context (default 50, 0=disabled)
|
||||
DMStream *bool `json:"dm_stream,omitempty"` // enable streaming for DMs (default false)
|
||||
GroupStream *bool `json:"group_stream,omitempty"` // enable streaming for groups (default false)
|
||||
NativeStream *bool `json:"native_stream,omitempty"` // use Slack ChatStreamer API if available (default false)
|
||||
ReactionLevel string `json:"reaction_level,omitempty"` // "off" (default), "minimal", "full"
|
||||
BlockReply *bool `json:"block_reply,omitempty"` // override gateway block_reply (nil = inherit)
|
||||
DebounceDelay int `json:"debounce_delay,omitempty"` // ms delay before dispatching rapid messages (default 300, 0=disabled)
|
||||
ThreadTTL *int `json:"thread_ttl,omitempty"` // hours before thread participation expires (default 24, 0=disabled — always require @mention)
|
||||
MediaMaxBytes int64 `json:"media_max_bytes,omitempty"` // max file download size in bytes (default 20MB)
|
||||
}
|
||||
|
||||
type WhatsAppConfig struct {
|
||||
@@ -159,7 +159,7 @@ type FeishuConfig struct {
|
||||
Streaming *bool `json:"streaming,omitempty"` // default true
|
||||
ReactionLevel string `json:"reaction_level,omitempty"` // "off" (default), "minimal", "full" — typing emoji reactions
|
||||
HistoryLimit int `json:"history_limit,omitempty"`
|
||||
BlockReply *bool `json:"block_reply,omitempty"` // override gateway block_reply (nil = inherit)
|
||||
BlockReply *bool `json:"block_reply,omitempty"` // override gateway block_reply (nil = inherit)
|
||||
STTProxyURL string `json:"stt_proxy_url,omitempty"`
|
||||
STTAPIKey string `json:"stt_api_key,omitempty"`
|
||||
STTTenantID string `json:"stt_tenant_id,omitempty"`
|
||||
@@ -169,22 +169,22 @@ type FeishuConfig struct {
|
||||
|
||||
// ProvidersConfig maps provider name to its config.
|
||||
type ProvidersConfig struct {
|
||||
Anthropic ProviderConfig `json:"anthropic"`
|
||||
OpenAI ProviderConfig `json:"openai"`
|
||||
OpenRouter ProviderConfig `json:"openrouter"`
|
||||
Groq ProviderConfig `json:"groq"`
|
||||
Gemini ProviderConfig `json:"gemini"`
|
||||
DeepSeek ProviderConfig `json:"deepseek"`
|
||||
Mistral ProviderConfig `json:"mistral"`
|
||||
XAI ProviderConfig `json:"xai"`
|
||||
MiniMax ProviderConfig `json:"minimax"`
|
||||
Cohere ProviderConfig `json:"cohere"`
|
||||
Perplexity ProviderConfig `json:"perplexity"`
|
||||
DashScope ProviderConfig `json:"dashscope"`
|
||||
Bailian ProviderConfig `json:"bailian"`
|
||||
Zai ProviderConfig `json:"zai"`
|
||||
ZaiCoding ProviderConfig `json:"zai_coding"`
|
||||
ClaudeCLI ClaudeCLIConfig `json:"claude_cli"`
|
||||
Anthropic ProviderConfig `json:"anthropic"`
|
||||
OpenAI ProviderConfig `json:"openai"`
|
||||
OpenRouter ProviderConfig `json:"openrouter"`
|
||||
Groq ProviderConfig `json:"groq"`
|
||||
Gemini ProviderConfig `json:"gemini"`
|
||||
DeepSeek ProviderConfig `json:"deepseek"`
|
||||
Mistral ProviderConfig `json:"mistral"`
|
||||
XAI ProviderConfig `json:"xai"`
|
||||
MiniMax ProviderConfig `json:"minimax"`
|
||||
Cohere ProviderConfig `json:"cohere"`
|
||||
Perplexity ProviderConfig `json:"perplexity"`
|
||||
DashScope ProviderConfig `json:"dashscope"`
|
||||
Bailian ProviderConfig `json:"bailian"`
|
||||
Zai ProviderConfig `json:"zai"`
|
||||
ZaiCoding ProviderConfig `json:"zai_coding"`
|
||||
ClaudeCLI ClaudeCLIConfig `json:"claude_cli"`
|
||||
}
|
||||
|
||||
// ClaudeCLIConfig configures the Claude CLI provider (uses subscription, not API key).
|
||||
@@ -259,13 +259,13 @@ type GatewayConfig struct {
|
||||
|
||||
// ToolsConfig controls tool availability, policy, and web search.
|
||||
type ToolsConfig struct {
|
||||
Profile string `json:"profile,omitempty"` // global profile: "minimal", "coding", "messaging", "full"
|
||||
Allow []string `json:"allow,omitempty"` // global allow list (tool names or "group:xxx")
|
||||
Deny []string `json:"deny,omitempty"` // global deny list
|
||||
AlsoAllow []string `json:"alsoAllow,omitempty"` // additive: adds without removing existing
|
||||
ByProvider map[string]*ToolPolicySpec `json:"byProvider,omitempty"` // per-provider overrides
|
||||
ExecApproval ExecApprovalCfg `json:"execApproval,omitempty"` // exec command approval settings
|
||||
WebFetch WebFetchPolicyConfig `json:"web_fetch"` // domain policy for URL fetching
|
||||
Profile string `json:"profile,omitempty"` // global profile: "minimal", "coding", "messaging", "full"
|
||||
Allow []string `json:"allow,omitempty"` // global allow list (tool names or "group:xxx")
|
||||
Deny []string `json:"deny,omitempty"` // global deny list
|
||||
AlsoAllow []string `json:"alsoAllow,omitempty"` // additive: adds without removing existing
|
||||
ByProvider map[string]*ToolPolicySpec `json:"byProvider,omitempty"` // per-provider overrides
|
||||
ExecApproval ExecApprovalCfg `json:"execApproval"` // exec command approval settings
|
||||
WebFetch WebFetchPolicyConfig `json:"web_fetch"` // domain policy for URL fetching
|
||||
Web WebToolsConfig `json:"web"`
|
||||
Browser BrowserToolConfig `json:"browser"`
|
||||
RateLimitPerHour int `json:"rate_limit_per_hour,omitempty"` // max tool executions per hour per session (0 = disabled)
|
||||
@@ -307,9 +307,9 @@ type WebFetchPolicyConfig struct {
|
||||
|
||||
// BrowserToolConfig controls the browser automation tool.
|
||||
type BrowserToolConfig struct {
|
||||
Enabled bool `json:"enabled"` // enable the browser tool (default false)
|
||||
Headless bool `json:"headless,omitempty"` // run Chrome in headless mode (ignored when RemoteURL is set)
|
||||
RemoteURL string `json:"remote_url,omitempty"` // CDP endpoint for remote Chrome sidecar, e.g. "ws://chrome:9222"
|
||||
Enabled bool `json:"enabled"` // enable the browser tool (default false)
|
||||
Headless bool `json:"headless,omitempty"` // run Chrome in headless mode (ignored when RemoteURL is set)
|
||||
RemoteURL string `json:"remote_url,omitempty"` // CDP endpoint for remote Chrome sidecar, e.g. "ws://chrome:9222"
|
||||
}
|
||||
|
||||
// ToolPolicySpec defines a tool policy at any level (global, per-agent, per-provider).
|
||||
@@ -370,10 +370,10 @@ type TtsConfig struct {
|
||||
Mode string `json:"mode,omitempty"` // "final" (default), "all"
|
||||
MaxLength int `json:"max_length,omitempty"` // max text length before truncation (default 1500)
|
||||
TimeoutMs int `json:"timeout_ms,omitempty"` // API timeout in ms (default 30000)
|
||||
OpenAI TtsOpenAIConfig `json:"openai,omitempty"`
|
||||
ElevenLabs TtsElevenLabsConfig `json:"elevenlabs,omitempty"`
|
||||
Edge TtsEdgeConfig `json:"edge,omitempty"`
|
||||
MiniMax TtsMiniMaxConfig `json:"minimax,omitempty"`
|
||||
OpenAI TtsOpenAIConfig `json:"openai"`
|
||||
ElevenLabs TtsElevenLabsConfig `json:"elevenlabs"`
|
||||
Edge TtsEdgeConfig `json:"edge"`
|
||||
MiniMax TtsMiniMaxConfig `json:"minimax"`
|
||||
}
|
||||
|
||||
// TtsOpenAIConfig configures the OpenAI TTS provider.
|
||||
|
||||
@@ -40,10 +40,9 @@ func ExecuteWithRetry(fn func() (string, error), cfg RetryConfig) (result string
|
||||
|
||||
// backoffWithJitter computes delay = min(base * 2^attempt, max) + jitter(±25%).
|
||||
func backoffWithJitter(base, max time.Duration, attempt int) time.Duration {
|
||||
delay := base << uint(attempt) // base * 2^attempt
|
||||
if delay > max {
|
||||
delay = max
|
||||
}
|
||||
delay := min(
|
||||
// base * 2^attempt
|
||||
base<<uint(attempt), max)
|
||||
|
||||
// Jitter: ±25% of delay
|
||||
quarter := delay / 4
|
||||
|
||||
@@ -104,10 +104,10 @@ func (cs *Service) AddJob(name string, schedule Schedule, message string, delive
|
||||
|
||||
now := nowMS()
|
||||
job := Job{
|
||||
ID: generateID(),
|
||||
Name: name,
|
||||
AgentID: agentID,
|
||||
Enabled: true,
|
||||
ID: generateID(),
|
||||
Name: name,
|
||||
AgentID: agentID,
|
||||
Enabled: true,
|
||||
Schedule: schedule,
|
||||
Payload: Payload{
|
||||
Kind: "agent_turn",
|
||||
@@ -259,14 +259,13 @@ func (cs *Service) UpdateJob(jobID string, patch JobPatch) (*Job, error) {
|
||||
}
|
||||
|
||||
// Status returns the service status.
|
||||
func (cs *Service) Status() map[string]interface{} {
|
||||
func (cs *Service) Status() map[string]any {
|
||||
cs.mu.Lock()
|
||||
defer cs.mu.Unlock()
|
||||
|
||||
return map[string]interface{}{
|
||||
return map[string]any{
|
||||
"enabled": cs.running,
|
||||
"jobs": len(cs.store.Jobs),
|
||||
"nextWakeAtMs": cs.getNextWakeMS(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ type LogTee struct {
|
||||
|
||||
// Ring buffer of recent entries for replay on subscribe.
|
||||
ringMu sync.RWMutex
|
||||
ring []map[string]interface{}
|
||||
ring []map[string]any
|
||||
ringPos int
|
||||
ringFul bool
|
||||
}
|
||||
@@ -48,7 +48,7 @@ func NewLogTee(inner slog.Handler) *LogTee {
|
||||
return &LogTee{
|
||||
inner: inner,
|
||||
clients: make(map[string]*logSubscriber),
|
||||
ring: make([]map[string]interface{}, ringBufferSize),
|
||||
ring: make([]map[string]any, ringBufferSize),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,14 +109,14 @@ func (t *LogTee) Handle(ctx context.Context, r slog.Record) error {
|
||||
}
|
||||
|
||||
// buildEntry creates the WS payload from a log record, redacting sensitive attrs.
|
||||
func (t *LogTee) buildEntry(r slog.Record) map[string]interface{} {
|
||||
entry := map[string]interface{}{
|
||||
func (t *LogTee) buildEntry(r slog.Record) map[string]any {
|
||||
entry := map[string]any{
|
||||
"timestamp": r.Time.UnixMilli(),
|
||||
"level": levelName(r.Level),
|
||||
"message": r.Message,
|
||||
}
|
||||
|
||||
attrs := map[string]interface{}{}
|
||||
attrs := map[string]any{}
|
||||
r.Attrs(func(a slog.Attr) bool {
|
||||
key := a.Key
|
||||
val := a.Value.String()
|
||||
@@ -168,10 +168,10 @@ func (t *LogTee) Subscribe(client *Client, level slog.Level) {
|
||||
|
||||
// Replay ring buffer entries at the requested level.
|
||||
t.ringMu.RLock()
|
||||
var entries []map[string]interface{}
|
||||
var entries []map[string]any
|
||||
if t.ringFul {
|
||||
// Buffer is full — read from ringPos (oldest) to ringPos-1 (newest).
|
||||
for i := 0; i < ringBufferSize; i++ {
|
||||
for i := range ringBufferSize {
|
||||
idx := (t.ringPos + i) % ringBufferSize
|
||||
e := t.ring[idx]
|
||||
if e != nil && logLevelValue(e["level"]) >= level {
|
||||
@@ -194,7 +194,7 @@ func (t *LogTee) Subscribe(client *Client, level slog.Level) {
|
||||
}
|
||||
|
||||
// Send sentinel so the client knows tailing started.
|
||||
client.SendEvent(*protocol.NewEvent("log", map[string]interface{}{
|
||||
client.SendEvent(*protocol.NewEvent("log", map[string]any{
|
||||
"timestamp": time.Now().UnixMilli(),
|
||||
"level": "info",
|
||||
"message": "Log tailing started",
|
||||
@@ -223,7 +223,7 @@ func levelName(l slog.Level) string {
|
||||
}
|
||||
|
||||
// logLevelValue converts a level name string back to slog.Level for filtering.
|
||||
func logLevelValue(v interface{}) slog.Level {
|
||||
func logLevelValue(v any) slog.Level {
|
||||
s, _ := v.(string)
|
||||
switch s {
|
||||
case "error":
|
||||
|
||||
@@ -88,7 +88,7 @@ func (m *AgentLinksMethods) handleList(ctx context.Context, client *gateway.Clie
|
||||
return
|
||||
}
|
||||
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"links": links,
|
||||
"count": len(links),
|
||||
}))
|
||||
@@ -182,7 +182,7 @@ func (m *AgentLinksMethods) handleCreate(ctx context.Context, client *gateway.Cl
|
||||
}
|
||||
m.emitTeamCacheInvalidate()
|
||||
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"link": link,
|
||||
}))
|
||||
|
||||
@@ -281,7 +281,7 @@ func (m *AgentLinksMethods) handleUpdate(ctx context.Context, client *gateway.Cl
|
||||
m.invalidateLinkAgents(ctx, linkID)
|
||||
m.emitTeamCacheInvalidate()
|
||||
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{"ok": true}))
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{"ok": true}))
|
||||
|
||||
// Emit agent_link.updated event
|
||||
if m.msgBus != nil {
|
||||
@@ -350,7 +350,7 @@ func (m *AgentLinksMethods) handleDelete(ctx context.Context, client *gateway.Cl
|
||||
}
|
||||
m.emitTeamCacheInvalidate()
|
||||
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{"ok": true}))
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{"ok": true}))
|
||||
|
||||
// Emit agent_link.deleted event
|
||||
if m.msgBus != nil && link != nil {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"slices"
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/agent"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/config"
|
||||
@@ -34,12 +35,7 @@ func (m *AgentsMethods) isOwnerUser(userID string) bool {
|
||||
if userID == "" {
|
||||
return false
|
||||
}
|
||||
for _, id := range m.cfg.Gateway.OwnerIDs {
|
||||
if id == userID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
return slices.Contains(m.cfg.Gateway.OwnerIDs, userID)
|
||||
}
|
||||
|
||||
func (m *AgentsMethods) Register(router *gateway.MethodRouter) {
|
||||
@@ -74,7 +70,7 @@ func (m *AgentsMethods) handleAgent(_ context.Context, client *gateway.Client, r
|
||||
return
|
||||
}
|
||||
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"id": loop.ID(),
|
||||
"isRunning": loop.IsRunning(),
|
||||
}))
|
||||
@@ -96,7 +92,7 @@ func (m *AgentsMethods) handleAgentWait(_ context.Context, client *gateway.Clien
|
||||
}
|
||||
|
||||
// Return current status (blocking wait is a future enhancement).
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"id": loop.ID(),
|
||||
"status": "idle",
|
||||
}))
|
||||
@@ -124,12 +120,12 @@ func (m *AgentsMethods) handleList(ctx context.Context, client *gateway.Client,
|
||||
return
|
||||
}
|
||||
|
||||
infos := make([]map[string]interface{}, 0, len(agents))
|
||||
infos := make([]map[string]any, 0, len(agents))
|
||||
for _, a := range agents {
|
||||
if a.Status != store.AgentStatusActive {
|
||||
continue
|
||||
}
|
||||
infos = append(infos, map[string]interface{}{
|
||||
infos = append(infos, map[string]any{
|
||||
"id": a.AgentKey,
|
||||
"name": a.DisplayName,
|
||||
"model": a.Model,
|
||||
@@ -139,7 +135,7 @@ func (m *AgentsMethods) handleList(ctx context.Context, client *gateway.Client,
|
||||
"isRunning": m.agents.IsRunning(a.AgentKey),
|
||||
})
|
||||
}
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"agents": infos,
|
||||
}))
|
||||
return
|
||||
@@ -147,7 +143,7 @@ func (m *AgentsMethods) handleList(ctx context.Context, client *gateway.Client,
|
||||
|
||||
// Fallback: return router-cached agents.
|
||||
infos := m.agents.ListInfo()
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"agents": infos,
|
||||
}))
|
||||
}
|
||||
@@ -157,7 +157,7 @@ func (m *AgentsMethods) handleCreate(ctx context.Context, client *gateway.Client
|
||||
os.MkdirAll(ws, 0755)
|
||||
bootstrap.EnsureWorkspaceFiles(ws)
|
||||
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"ok": true,
|
||||
"agentId": agentID,
|
||||
"name": params.Name,
|
||||
|
||||
@@ -92,7 +92,7 @@ func (m *AgentsMethods) handleDelete(ctx context.Context, client *gateway.Client
|
||||
}
|
||||
}
|
||||
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"ok": true,
|
||||
"agentId": params.AgentID,
|
||||
"removedBindings": removedBindings,
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/bootstrap"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/config"
|
||||
@@ -55,23 +56,23 @@ func (m *AgentsMethods) handleFilesList(ctx context.Context, client *gateway.Cli
|
||||
dbMap[f.FileName] = f
|
||||
}
|
||||
|
||||
files := make([]map[string]interface{}, 0, len(allowedAgentFiles))
|
||||
files := make([]map[string]any, 0, len(allowedAgentFiles))
|
||||
for _, name := range allowedAgentFiles {
|
||||
if f, ok := dbMap[name]; ok {
|
||||
files = append(files, map[string]interface{}{
|
||||
files = append(files, map[string]any{
|
||||
"name": name,
|
||||
"missing": false,
|
||||
"size": len(f.Content),
|
||||
})
|
||||
} else {
|
||||
files = append(files, map[string]interface{}{
|
||||
files = append(files, map[string]any{
|
||||
"name": name,
|
||||
"missing": true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"agentId": params.AgentID,
|
||||
"files": files,
|
||||
}))
|
||||
@@ -80,19 +81,19 @@ func (m *AgentsMethods) handleFilesList(ctx context.Context, client *gateway.Cli
|
||||
|
||||
// --- Fallback: filesystem ---
|
||||
ws := m.resolveWorkspace(params.AgentID)
|
||||
files := make([]map[string]interface{}, 0, len(allowedAgentFiles))
|
||||
files := make([]map[string]any, 0, len(allowedAgentFiles))
|
||||
|
||||
for _, name := range allowedAgentFiles {
|
||||
p := filepath.Join(ws, name)
|
||||
info, err := os.Stat(p)
|
||||
if err != nil {
|
||||
files = append(files, map[string]interface{}{
|
||||
files = append(files, map[string]any{
|
||||
"name": name,
|
||||
"path": p,
|
||||
"missing": true,
|
||||
})
|
||||
} else {
|
||||
files = append(files, map[string]interface{}{
|
||||
files = append(files, map[string]any{
|
||||
"name": name,
|
||||
"path": p,
|
||||
"missing": false,
|
||||
@@ -102,7 +103,7 @@ func (m *AgentsMethods) handleFilesList(ctx context.Context, client *gateway.Cli
|
||||
}
|
||||
}
|
||||
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"agentId": params.AgentID,
|
||||
"workspace": ws,
|
||||
"files": files,
|
||||
@@ -150,9 +151,9 @@ func (m *AgentsMethods) handleFilesGet(ctx context.Context, client *gateway.Clie
|
||||
|
||||
for _, f := range dbFiles {
|
||||
if f.FileName == params.Name {
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"agentId": params.AgentID,
|
||||
"file": map[string]interface{}{
|
||||
"file": map[string]any{
|
||||
"name": params.Name,
|
||||
"missing": false,
|
||||
"size": len(f.Content),
|
||||
@@ -164,9 +165,9 @@ func (m *AgentsMethods) handleFilesGet(ctx context.Context, client *gateway.Clie
|
||||
}
|
||||
|
||||
// File not found in DB
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"agentId": params.AgentID,
|
||||
"file": map[string]interface{}{
|
||||
"file": map[string]any{
|
||||
"name": params.Name,
|
||||
"missing": true,
|
||||
},
|
||||
@@ -180,10 +181,10 @@ func (m *AgentsMethods) handleFilesGet(ctx context.Context, client *gateway.Clie
|
||||
|
||||
info, err := os.Stat(p)
|
||||
if err != nil {
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"agentId": params.AgentID,
|
||||
"workspace": ws,
|
||||
"file": map[string]interface{}{
|
||||
"file": map[string]any{
|
||||
"name": params.Name,
|
||||
"path": p,
|
||||
"missing": true,
|
||||
@@ -193,10 +194,10 @@ func (m *AgentsMethods) handleFilesGet(ctx context.Context, client *gateway.Clie
|
||||
}
|
||||
|
||||
content, _ := os.ReadFile(p)
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"agentId": params.AgentID,
|
||||
"workspace": ws,
|
||||
"file": map[string]interface{}{
|
||||
"file": map[string]any{
|
||||
"name": params.Name,
|
||||
"path": p,
|
||||
"missing": false,
|
||||
@@ -253,9 +254,9 @@ func (m *AgentsMethods) handleFilesSet(ctx context.Context, client *gateway.Clie
|
||||
m.interceptor.InvalidateAgent(ag.ID)
|
||||
}
|
||||
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"agentId": params.AgentID,
|
||||
"file": map[string]interface{}{
|
||||
"file": map[string]any{
|
||||
"name": params.Name,
|
||||
"missing": false,
|
||||
"size": len(params.Content),
|
||||
@@ -276,10 +277,10 @@ func (m *AgentsMethods) handleFilesSet(ctx context.Context, client *gateway.Clie
|
||||
}
|
||||
|
||||
info, _ := os.Stat(p)
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"agentId": params.AgentID,
|
||||
"workspace": ws,
|
||||
"file": map[string]interface{}{
|
||||
"file": map[string]any{
|
||||
"name": params.Name,
|
||||
"path": p,
|
||||
"missing": false,
|
||||
@@ -300,10 +301,5 @@ func (m *AgentsMethods) resolveWorkspace(agentID string) string {
|
||||
}
|
||||
|
||||
func isAllowedFile(name string) bool {
|
||||
for _, f := range allowedAgentFiles {
|
||||
if f == name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
return slices.Contains(allowedAgentFiles, name)
|
||||
}
|
||||
@@ -36,7 +36,7 @@ func (m *AgentsMethods) handleIdentityGet(_ context.Context, client *gateway.Cli
|
||||
}
|
||||
}
|
||||
|
||||
result := map[string]interface{}{
|
||||
result := map[string]any{
|
||||
"agentId": params.AgentID,
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ func (m *AgentsMethods) handleIdentityGet(_ context.Context, client *gateway.Cli
|
||||
// parseIdentityContent parses IDENTITY.md content string and extracts Key: Value fields.
|
||||
func parseIdentityContent(content string) map[string]string {
|
||||
result := make(map[string]string)
|
||||
for _, line := range strings.Split(content, "\n") {
|
||||
for line := range strings.SplitSeq(content, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if strings.HasPrefix(line, "#") || line == "" {
|
||||
continue
|
||||
|
||||
@@ -155,7 +155,7 @@ func (m *AgentsMethods) handleUpdate(ctx context.Context, client *gateway.Client
|
||||
}
|
||||
}
|
||||
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"ok": true,
|
||||
"agentId": params.AgentID,
|
||||
}))
|
||||
|
||||
@@ -54,12 +54,12 @@ func (m *ChannelInstancesMethods) handleList(ctx context.Context, client *gatewa
|
||||
}
|
||||
|
||||
// Mask credentials in response — never expose secrets via WS.
|
||||
result := make([]map[string]interface{}, 0, len(instances))
|
||||
result := make([]map[string]any, 0, len(instances))
|
||||
for _, inst := range instances {
|
||||
result = append(result, maskInstance(inst))
|
||||
}
|
||||
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"instances": result,
|
||||
}))
|
||||
}
|
||||
@@ -160,7 +160,7 @@ func (m *ChannelInstancesMethods) handleUpdate(ctx context.Context, client *gate
|
||||
return
|
||||
}
|
||||
|
||||
var updates map[string]interface{}
|
||||
var updates map[string]any
|
||||
if err := json.Unmarshal(params.Updates, &updates); err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidUpdates)))
|
||||
return
|
||||
@@ -173,7 +173,7 @@ func (m *ChannelInstancesMethods) handleUpdate(ctx context.Context, client *gate
|
||||
}
|
||||
|
||||
m.emitCacheInvalidate()
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{"status": "updated"}))
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{"status": "updated"}))
|
||||
}
|
||||
|
||||
func (m *ChannelInstancesMethods) handleDelete(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
@@ -209,31 +209,31 @@ func (m *ChannelInstancesMethods) handleDelete(ctx context.Context, client *gate
|
||||
}
|
||||
|
||||
m.emitCacheInvalidate()
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{"status": "deleted"}))
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{"status": "deleted"}))
|
||||
}
|
||||
|
||||
// maskInstance returns a map representation with credentials masked.
|
||||
func maskInstance(inst store.ChannelInstanceData) map[string]interface{} {
|
||||
result := map[string]interface{}{
|
||||
"id": inst.ID,
|
||||
"name": inst.Name,
|
||||
"display_name": inst.DisplayName,
|
||||
"channel_type": inst.ChannelType,
|
||||
"agent_id": inst.AgentID,
|
||||
"config": inst.Config,
|
||||
"enabled": inst.Enabled,
|
||||
"is_default": store.IsDefaultChannelInstance(inst.Name),
|
||||
"has_credentials": len(inst.Credentials) > 0,
|
||||
"created_by": inst.CreatedBy,
|
||||
"created_at": inst.CreatedAt,
|
||||
"updated_at": inst.UpdatedAt,
|
||||
func maskInstance(inst store.ChannelInstanceData) map[string]any {
|
||||
result := map[string]any{
|
||||
"id": inst.ID,
|
||||
"name": inst.Name,
|
||||
"display_name": inst.DisplayName,
|
||||
"channel_type": inst.ChannelType,
|
||||
"agent_id": inst.AgentID,
|
||||
"config": inst.Config,
|
||||
"enabled": inst.Enabled,
|
||||
"is_default": store.IsDefaultChannelInstance(inst.Name),
|
||||
"has_credentials": len(inst.Credentials) > 0,
|
||||
"created_by": inst.CreatedBy,
|
||||
"created_at": inst.CreatedAt,
|
||||
"updated_at": inst.UpdatedAt,
|
||||
}
|
||||
|
||||
// Mask credentials: show keys with "***" values
|
||||
if len(inst.Credentials) > 0 {
|
||||
var raw map[string]interface{}
|
||||
var raw map[string]any
|
||||
if json.Unmarshal(inst.Credentials, &raw) == nil {
|
||||
masked := make(map[string]interface{}, len(raw))
|
||||
masked := make(map[string]any, len(raw))
|
||||
for k := range raw {
|
||||
masked[k] = "***"
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ func (m *ChannelsMethods) Register(router *gateway.MethodRouter) {
|
||||
func (m *ChannelsMethods) handleList(_ context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
enabled := m.manager.GetEnabledChannels()
|
||||
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"channels": enabled,
|
||||
}))
|
||||
}
|
||||
@@ -36,7 +36,7 @@ func (m *ChannelsMethods) handleList(_ context.Context, client *gateway.Client,
|
||||
func (m *ChannelsMethods) handleStatus(_ context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
status := m.manager.GetStatus()
|
||||
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"channels": status,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -155,7 +155,7 @@ func (m *ChatMethods) handleSend(ctx context.Context, client *gateway.Client, re
|
||||
return
|
||||
}
|
||||
|
||||
resp := map[string]interface{}{
|
||||
resp := map[string]any{
|
||||
"runId": result.RunID,
|
||||
"content": result.Content,
|
||||
"usage": result.Usage,
|
||||
@@ -191,7 +191,7 @@ func (m *ChatMethods) handleHistory(ctx context.Context, client *gateway.Client,
|
||||
|
||||
history := m.sessions.GetHistory(sessionKey)
|
||||
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"messages": history,
|
||||
}))
|
||||
}
|
||||
@@ -237,7 +237,7 @@ func (m *ChatMethods) handleInject(ctx context.Context, client *gateway.Client,
|
||||
Content: text,
|
||||
})
|
||||
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"ok": true,
|
||||
"messageId": messageID,
|
||||
}))
|
||||
@@ -281,7 +281,7 @@ func (m *ChatMethods) handleAbort(ctx context.Context, client *gateway.Client, r
|
||||
abortedIDs = m.agents.AbortRunsForSession(params.SessionKey)
|
||||
}
|
||||
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"ok": true,
|
||||
"aborted": len(abortedIDs) > 0,
|
||||
"runIds": abortedIDs,
|
||||
|
||||
@@ -36,7 +36,7 @@ func (m *ConfigMethods) Register(router *gateway.MethodRouter) {
|
||||
}
|
||||
|
||||
func (m *ConfigMethods) handleGet(_ context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"config": m.cfg.MaskedCopy(),
|
||||
"hash": m.cfg.Hash(),
|
||||
"path": m.cfgPath,
|
||||
@@ -93,7 +93,7 @@ func (m *ConfigMethods) handleApply(ctx context.Context, client *gateway.Client,
|
||||
m.cfg.ApplyEnvOverrides()
|
||||
m.broadcastChanged()
|
||||
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"ok": true,
|
||||
"path": m.cfgPath,
|
||||
"config": m.cfg.MaskedCopy(),
|
||||
@@ -165,7 +165,7 @@ func (m *ConfigMethods) handlePatch(ctx context.Context, client *gateway.Client,
|
||||
m.cfg.ApplyEnvOverrides()
|
||||
m.broadcastChanged()
|
||||
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"ok": true,
|
||||
"path": m.cfgPath,
|
||||
"config": m.cfg.MaskedCopy(),
|
||||
@@ -184,37 +184,37 @@ func (m *ConfigMethods) broadcastChanged() {
|
||||
// handleSchema returns the config JSON schema for UI form generation.
|
||||
// Matching TS config.schema (src/gateway/server-methods/config.ts:276-289).
|
||||
func (m *ConfigMethods) handleSchema(_ context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
schema := map[string]interface{}{
|
||||
schema := map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"agents": map[string]interface{}{
|
||||
"properties": map[string]any{
|
||||
"agents": map[string]any{
|
||||
"type": "object",
|
||||
"description": "Agent configuration (defaults + per-agent overrides)",
|
||||
},
|
||||
"channels": map[string]interface{}{
|
||||
"channels": map[string]any{
|
||||
"type": "object",
|
||||
"description": "Channel configuration (telegram, discord, slack, etc.)",
|
||||
},
|
||||
"providers": map[string]interface{}{
|
||||
"providers": map[string]any{
|
||||
"type": "object",
|
||||
"description": "AI provider API keys and settings",
|
||||
},
|
||||
"gateway": map[string]interface{}{
|
||||
"gateway": map[string]any{
|
||||
"type": "object",
|
||||
"description": "Gateway server settings (host, port, token)",
|
||||
},
|
||||
"tools": map[string]interface{}{
|
||||
"tools": map[string]any{
|
||||
"type": "object",
|
||||
"description": "Tool configuration (browser, exec, web search)",
|
||||
},
|
||||
"sessions": map[string]interface{}{
|
||||
"sessions": map[string]any{
|
||||
"type": "object",
|
||||
"description": "Session storage configuration",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"json": schema,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ func (m *CronMethods) handleList(_ context.Context, client *gateway.Client, req
|
||||
|
||||
jobs := m.service.ListJobs(params.IncludeDisabled, "", "")
|
||||
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"jobs": jobs,
|
||||
"status": m.service.Status(),
|
||||
}))
|
||||
@@ -84,7 +84,7 @@ func (m *CronMethods) handleCreate(ctx context.Context, client *gateway.Client,
|
||||
return
|
||||
}
|
||||
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"job": job,
|
||||
}))
|
||||
}
|
||||
@@ -108,7 +108,7 @@ func (m *CronMethods) handleDelete(ctx context.Context, client *gateway.Client,
|
||||
return
|
||||
}
|
||||
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"deleted": true,
|
||||
}))
|
||||
}
|
||||
@@ -133,7 +133,7 @@ func (m *CronMethods) handleToggle(ctx context.Context, client *gateway.Client,
|
||||
return
|
||||
}
|
||||
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"jobId": params.JobID,
|
||||
"enabled": params.Enabled,
|
||||
}))
|
||||
@@ -146,8 +146,8 @@ func (m *CronMethods) handleStatus(_ context.Context, client *gateway.Client, re
|
||||
func (m *CronMethods) handleUpdate(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
locale := store.LocaleFromContext(ctx)
|
||||
var params struct {
|
||||
JobID string `json:"jobId"`
|
||||
ID string `json:"id"` // alias (matching TS)
|
||||
JobID string `json:"jobId"`
|
||||
ID string `json:"id"` // alias (matching TS)
|
||||
Patch store.CronJobPatch `json:"patch"`
|
||||
}
|
||||
if req.Params != nil {
|
||||
@@ -169,7 +169,7 @@ func (m *CronMethods) handleUpdate(ctx context.Context, client *gateway.Client,
|
||||
return
|
||||
}
|
||||
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"job": job,
|
||||
}))
|
||||
}
|
||||
@@ -204,7 +204,7 @@ func (m *CronMethods) handleRun(ctx context.Context, client *gateway.Client, req
|
||||
}
|
||||
|
||||
// Respond immediately — job execution happens in background
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"ok": true,
|
||||
"ran": true,
|
||||
}))
|
||||
@@ -233,7 +233,7 @@ func (m *CronMethods) handleRuns(_ context.Context, client *gateway.Client, req
|
||||
}
|
||||
|
||||
entries, total := m.service.GetRunLog(jobID, params.Limit, params.Offset)
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"entries": entries,
|
||||
"total": total,
|
||||
}))
|
||||
|
||||
@@ -87,7 +87,7 @@ func (m *DelegationsMethods) handleList(ctx context.Context, client *gateway.Cli
|
||||
}
|
||||
}
|
||||
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"records": records,
|
||||
"total": total,
|
||||
}))
|
||||
|
||||
@@ -28,7 +28,7 @@ func (m *ExecApprovalMethods) Register(router *gateway.MethodRouter) {
|
||||
|
||||
func (m *ExecApprovalMethods) handleList(_ context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
if m.manager == nil {
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"pending": []any{},
|
||||
}))
|
||||
return
|
||||
@@ -52,7 +52,7 @@ func (m *ExecApprovalMethods) handleList(_ context.Context, client *gateway.Clie
|
||||
})
|
||||
}
|
||||
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"pending": items,
|
||||
}))
|
||||
}
|
||||
@@ -87,7 +87,7 @@ func (m *ExecApprovalMethods) handleApprove(ctx context.Context, client *gateway
|
||||
return
|
||||
}
|
||||
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"resolved": true,
|
||||
"decision": string(decision),
|
||||
}))
|
||||
@@ -117,7 +117,7 @@ func (m *ExecApprovalMethods) handleDeny(ctx context.Context, client *gateway.Cl
|
||||
return
|
||||
}
|
||||
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"resolved": true,
|
||||
"decision": "deny",
|
||||
}))
|
||||
|
||||
@@ -38,13 +38,13 @@ func (m *LogsMethods) handleTail(ctx context.Context, client *gateway.Client, re
|
||||
case "start":
|
||||
level := parseLogLevel(params.Level)
|
||||
m.logTee.Subscribe(client, level)
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"status": "tailing",
|
||||
"level": params.Level,
|
||||
}))
|
||||
case "stop":
|
||||
m.logTee.Unsubscribe(client.ID())
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"status": "stopped",
|
||||
}))
|
||||
default:
|
||||
|
||||
@@ -74,7 +74,7 @@ func (m *PairingMethods) handleRequest(ctx context.Context, client *gateway.Clie
|
||||
return
|
||||
}
|
||||
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"code": code,
|
||||
}))
|
||||
}
|
||||
@@ -113,7 +113,7 @@ func (m *PairingMethods) handleApprove(ctx context.Context, client *gateway.Clie
|
||||
m.broadcaster(*protocol.NewEvent(protocol.EventDevicePairRes, map[string]any{"action": "approved"}))
|
||||
}
|
||||
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"paired": paired,
|
||||
}))
|
||||
}
|
||||
@@ -141,7 +141,7 @@ func (m *PairingMethods) handleDeny(ctx context.Context, client *gateway.Client,
|
||||
m.broadcaster(*protocol.NewEvent(protocol.EventDevicePairRes, map[string]any{"action": "denied"}))
|
||||
}
|
||||
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"denied": true,
|
||||
}))
|
||||
}
|
||||
@@ -150,7 +150,7 @@ func (m *PairingMethods) handleList(_ context.Context, client *gateway.Client, r
|
||||
pending := m.service.ListPending()
|
||||
paired := m.service.ListPaired()
|
||||
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"pending": pending,
|
||||
"paired": paired,
|
||||
}))
|
||||
@@ -191,7 +191,7 @@ func (m *PairingMethods) handleRevoke(ctx context.Context, client *gateway.Clien
|
||||
})
|
||||
}
|
||||
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"revoked": true,
|
||||
}))
|
||||
}
|
||||
@@ -213,7 +213,7 @@ func (m *PairingMethods) handleBrowserPairingStatus(ctx context.Context, client
|
||||
}
|
||||
|
||||
if m.service.IsPaired(params.SenderID, "browser") {
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"status": "approved",
|
||||
}))
|
||||
return
|
||||
@@ -223,14 +223,14 @@ func (m *PairingMethods) handleBrowserPairingStatus(ctx context.Context, client
|
||||
pending := m.service.ListPending()
|
||||
for _, p := range pending {
|
||||
if p.SenderID == params.SenderID && p.Channel == "browser" {
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"status": "pending",
|
||||
}))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"status": "expired",
|
||||
}))
|
||||
}
|
||||
@@ -55,7 +55,7 @@ func (m *SendMethods) handleSend(ctx context.Context, client *gateway.Client, re
|
||||
Content: params.Message,
|
||||
})
|
||||
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"ok": true,
|
||||
"channel": params.Channel,
|
||||
"to": params.To,
|
||||
|
||||
@@ -48,7 +48,7 @@ func (m *SessionsMethods) handleList(_ context.Context, client *gateway.Client,
|
||||
Limit: params.Limit,
|
||||
Offset: params.Offset,
|
||||
})
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"sessions": result.Sessions,
|
||||
"total": result.Total,
|
||||
"limit": params.Limit,
|
||||
@@ -71,7 +71,7 @@ func (m *SessionsMethods) handlePreview(ctx context.Context, client *gateway.Cli
|
||||
history := m.sessions.GetHistory(params.Key)
|
||||
summary := m.sessions.GetSummary(params.Key)
|
||||
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"key": params.Key,
|
||||
"messages": history,
|
||||
"summary": summary,
|
||||
@@ -116,7 +116,7 @@ func (m *SessionsMethods) handlePatch(ctx context.Context, client *gateway.Clien
|
||||
// Save changes to DB
|
||||
m.sessions.Save(params.Key)
|
||||
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"ok": true,
|
||||
"key": params.Key,
|
||||
}))
|
||||
@@ -135,7 +135,7 @@ func (m *SessionsMethods) handleDelete(ctx context.Context, client *gateway.Clie
|
||||
return
|
||||
}
|
||||
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"ok": true,
|
||||
}))
|
||||
}
|
||||
@@ -150,7 +150,7 @@ func (m *SessionsMethods) handleReset(ctx context.Context, client *gateway.Clien
|
||||
|
||||
m.sessions.Reset(params.Key)
|
||||
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"ok": true,
|
||||
}))
|
||||
}
|
||||
@@ -30,9 +30,9 @@ func (m *SkillsMethods) Register(router *gateway.MethodRouter) {
|
||||
func (m *SkillsMethods) handleList(_ context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
allSkills := m.store.ListSkills()
|
||||
|
||||
result := make([]map[string]interface{}, 0, len(allSkills))
|
||||
result := make([]map[string]any, 0, len(allSkills))
|
||||
for _, s := range allSkills {
|
||||
entry := map[string]interface{}{
|
||||
entry := map[string]any{
|
||||
"name": s.Name,
|
||||
"slug": s.Slug,
|
||||
"description": s.Description,
|
||||
@@ -51,7 +51,7 @@ func (m *SkillsMethods) handleList(_ context.Context, client *gateway.Client, re
|
||||
result = append(result, entry)
|
||||
}
|
||||
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"skills": result,
|
||||
}))
|
||||
}
|
||||
@@ -77,7 +77,7 @@ func (m *SkillsMethods) handleGet(ctx context.Context, client *gateway.Client, r
|
||||
|
||||
content, _ := m.store.LoadSkill(params.Name)
|
||||
|
||||
resp := map[string]interface{}{
|
||||
resp := map[string]any{
|
||||
"name": info.Name,
|
||||
"slug": info.Slug,
|
||||
"description": info.Description,
|
||||
@@ -99,15 +99,15 @@ func (m *SkillsMethods) handleGet(ctx context.Context, client *gateway.Client, r
|
||||
|
||||
// skillUpdater is an optional interface for stores that support skill updates (e.g. PGSkillStore).
|
||||
type skillUpdater interface {
|
||||
UpdateSkill(id uuid.UUID, updates map[string]interface{}) error
|
||||
UpdateSkill(id uuid.UUID, updates map[string]any) error
|
||||
}
|
||||
|
||||
func (m *SkillsMethods) handleUpdate(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
locale := store.LocaleFromContext(ctx)
|
||||
var params struct {
|
||||
Name string `json:"name"`
|
||||
ID string `json:"id"`
|
||||
Updates map[string]interface{} `json:"updates"`
|
||||
Name string `json:"name"`
|
||||
ID string `json:"id"`
|
||||
Updates map[string]any `json:"updates"`
|
||||
}
|
||||
if req.Params != nil {
|
||||
json.Unmarshal(req.Params, ¶ms)
|
||||
|
||||
@@ -64,7 +64,7 @@ func (m *TeamsMethods) handleList(ctx context.Context, client *gateway.Client, r
|
||||
return
|
||||
}
|
||||
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"teams": teams,
|
||||
"count": len(teams),
|
||||
}))
|
||||
@@ -164,7 +164,7 @@ func (m *TeamsMethods) handleCreate(ctx context.Context, client *gateway.Client,
|
||||
}
|
||||
}
|
||||
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"team": team,
|
||||
}))
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ func (m *TeamsMethods) handleGet(ctx context.Context, client *gateway.Client, re
|
||||
return
|
||||
}
|
||||
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"team": team,
|
||||
"members": members,
|
||||
}))
|
||||
@@ -107,7 +107,7 @@ func (m *TeamsMethods) handleDelete(ctx context.Context, client *gateway.Client,
|
||||
}
|
||||
}
|
||||
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{"ok": true}))
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{"ok": true}))
|
||||
|
||||
// Emit team.deleted event
|
||||
if m.msgBus != nil && team != nil {
|
||||
@@ -157,7 +157,7 @@ func (m *TeamsMethods) handleTaskList(ctx context.Context, client *gateway.Clien
|
||||
return
|
||||
}
|
||||
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"tasks": tasks,
|
||||
"count": len(tasks),
|
||||
}))
|
||||
@@ -166,8 +166,8 @@ func (m *TeamsMethods) handleTaskList(ctx context.Context, client *gateway.Clien
|
||||
// --- Update (settings) ---
|
||||
|
||||
type teamsUpdateParams struct {
|
||||
TeamID string `json:"teamId"`
|
||||
Settings map[string]interface{} `json:"settings"`
|
||||
TeamID string `json:"teamId"`
|
||||
Settings map[string]any `json:"settings"`
|
||||
}
|
||||
|
||||
func (m *TeamsMethods) handleUpdate(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
@@ -225,7 +225,7 @@ func (m *TeamsMethods) handleUpdate(ctx context.Context, client *gateway.Client,
|
||||
|
||||
m.invalidateTeamCaches(ctx, teamID)
|
||||
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{"ok": true}))
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{"ok": true}))
|
||||
|
||||
// Emit team.updated event
|
||||
if m.msgBus != nil {
|
||||
@@ -280,7 +280,7 @@ func (m *TeamsMethods) handleKnownUsers(ctx context.Context, client *gateway.Cli
|
||||
return
|
||||
}
|
||||
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"users": users,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ func (m *TeamsMethods) handleAddMember(ctx context.Context, client *gateway.Clie
|
||||
// Invalidate caches for all team members
|
||||
m.invalidateTeamCaches(ctx, teamID)
|
||||
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{"ok": true}))
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{"ok": true}))
|
||||
|
||||
// Emit team.member.added event
|
||||
if m.msgBus != nil {
|
||||
@@ -170,7 +170,7 @@ func (m *TeamsMethods) handleRemoveMember(ctx context.Context, client *gateway.C
|
||||
}
|
||||
}
|
||||
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{"ok": true}))
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{"ok": true}))
|
||||
|
||||
// Emit team.member.removed event
|
||||
if m.msgBus != nil && removedAgent != nil {
|
||||
|
||||
@@ -83,17 +83,11 @@ func (m *UsageMethods) handleGet(_ context.Context, client *gateway.Client, req
|
||||
total := len(records)
|
||||
|
||||
// Apply offset + limit
|
||||
offset := params.Offset
|
||||
if offset > total {
|
||||
offset = total
|
||||
}
|
||||
end := offset + params.Limit
|
||||
if end > total {
|
||||
end = total
|
||||
}
|
||||
offset := min(params.Offset, total)
|
||||
end := min(offset+params.Limit, total)
|
||||
records = records[offset:end]
|
||||
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"records": records,
|
||||
"total": total,
|
||||
"limit": params.Limit,
|
||||
@@ -132,7 +126,7 @@ func (m *UsageMethods) handleSummary(_ context.Context, client *gateway.Client,
|
||||
totalRecords++
|
||||
}
|
||||
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"byAgent": byAgent,
|
||||
"totalRecords": totalRecords,
|
||||
}))
|
||||
|
||||
+13
-14
@@ -142,12 +142,12 @@ func (r *MethodRouter) handleConnect(ctx context.Context, client *Client, req *p
|
||||
client.pairingCode = code
|
||||
client.pairingPending = true
|
||||
// Not authenticated — can only call browser.pairing.status
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"protocol": protocol.ProtocolVersion,
|
||||
"status": "pending_pairing",
|
||||
"pairing_code": code,
|
||||
"sender_id": client.id,
|
||||
"server": map[string]interface{}{
|
||||
"server": map[string]any{
|
||||
"name": "goclaw",
|
||||
"version": "0.2.0",
|
||||
},
|
||||
@@ -164,11 +164,11 @@ func (r *MethodRouter) handleConnect(ctx context.Context, client *Client, req *p
|
||||
}
|
||||
|
||||
func (r *MethodRouter) sendConnectResponse(client *Client, reqID string) {
|
||||
client.SendResponse(protocol.NewOKResponse(reqID, map[string]interface{}{
|
||||
client.SendResponse(protocol.NewOKResponse(reqID, map[string]any{
|
||||
"protocol": protocol.ProtocolVersion,
|
||||
"role": string(client.role),
|
||||
"user_id": client.userID,
|
||||
"server": map[string]interface{}{
|
||||
"server": map[string]any{
|
||||
"name": "goclaw",
|
||||
"version": "0.2.0",
|
||||
},
|
||||
@@ -219,14 +219,14 @@ func (r *MethodRouter) handleHealth(ctx context.Context, client *Client, req *pr
|
||||
toolCount = s.tools.Count()
|
||||
}
|
||||
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
|
||||
"status": "ok",
|
||||
"version": s.version,
|
||||
"uptime": uptimeMs,
|
||||
"mode": mode,
|
||||
"database": dbStatus,
|
||||
"tools": toolCount,
|
||||
"clients": clientList,
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"status": "ok",
|
||||
"version": s.version,
|
||||
"uptime": uptimeMs,
|
||||
"mode": mode,
|
||||
"database": dbStatus,
|
||||
"tools": toolCount,
|
||||
"clients": clientList,
|
||||
"currentId": client.ID(),
|
||||
}))
|
||||
}
|
||||
@@ -248,11 +248,10 @@ func (r *MethodRouter) handleStatus(ctx context.Context, client *Client, req *pr
|
||||
}
|
||||
}
|
||||
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"agents": agents,
|
||||
"agentTotal": agentTotal,
|
||||
"clients": len(r.server.clients),
|
||||
"sessions": sessionCount,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
// user-facing error messages. Messages use fmt.Sprintf-style templates.
|
||||
package i18n
|
||||
|
||||
import "maps"
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Supported locales.
|
||||
@@ -21,9 +23,7 @@ func register(locale string, msgs map[string]string) {
|
||||
if catalogs[locale] == nil {
|
||||
catalogs[locale] = make(map[string]string, len(msgs))
|
||||
}
|
||||
for k, v := range msgs {
|
||||
catalogs[locale][k] = v
|
||||
}
|
||||
maps.Copy(catalogs[locale], msgs)
|
||||
}
|
||||
|
||||
// T returns a localized message for the given key.
|
||||
|
||||
@@ -46,7 +46,7 @@ func (e *Extractor) Extract(ctx context.Context, text string) (*ExtractionResult
|
||||
{Role: "user", Content: text},
|
||||
},
|
||||
Model: e.model,
|
||||
Options: map[string]interface{}{
|
||||
Options: map[string]any{
|
||||
"max_tokens": 8192,
|
||||
"temperature": 0.0,
|
||||
},
|
||||
|
||||
@@ -19,7 +19,7 @@ type BridgeTool struct {
|
||||
toolName string // original MCP tool name
|
||||
registeredName string // may include prefix: "{prefix}__{toolName}"
|
||||
description string
|
||||
inputSchema map[string]interface{} // JSON Schema for parameters
|
||||
inputSchema map[string]any // JSON Schema for parameters
|
||||
client *mcpclient.Client
|
||||
timeoutSec int
|
||||
connected *atomic.Bool
|
||||
@@ -73,9 +73,9 @@ func ensureMCPPrefix(prefix, serverName string) string {
|
||||
return prefix
|
||||
}
|
||||
|
||||
func (t *BridgeTool) Name() string { return t.registeredName }
|
||||
func (t *BridgeTool) Description() string { return t.description }
|
||||
func (t *BridgeTool) Parameters() map[string]interface{} { return t.inputSchema }
|
||||
func (t *BridgeTool) Name() string { return t.registeredName }
|
||||
func (t *BridgeTool) Description() string { return t.description }
|
||||
func (t *BridgeTool) Parameters() map[string]any { return t.inputSchema }
|
||||
|
||||
// ServerName returns the name of the MCP server this tool belongs to.
|
||||
func (t *BridgeTool) ServerName() string { return t.serverName }
|
||||
@@ -83,7 +83,7 @@ func (t *BridgeTool) ServerName() string { return t.serverName }
|
||||
// OriginalName returns the original MCP tool name (without prefix).
|
||||
func (t *BridgeTool) OriginalName() string { return t.toolName }
|
||||
|
||||
func (t *BridgeTool) Execute(ctx context.Context, args map[string]interface{}) *tools.Result {
|
||||
func (t *BridgeTool) Execute(ctx context.Context, args map[string]any) *tools.Result {
|
||||
if !t.connected.Load() {
|
||||
return tools.ErrorResult(fmt.Sprintf("MCP server %q is disconnected", t.serverName))
|
||||
}
|
||||
@@ -113,8 +113,8 @@ func (t *BridgeTool) Execute(ctx context.Context, args map[string]interface{}) *
|
||||
}
|
||||
|
||||
// inputSchemaToMap converts mcp.ToolInputSchema to the map format expected by tools.Tool.Parameters().
|
||||
func inputSchemaToMap(schema mcpgo.ToolInputSchema) map[string]interface{} {
|
||||
m := map[string]interface{}{
|
||||
func inputSchemaToMap(schema mcpgo.ToolInputSchema) map[string]any {
|
||||
m := map[string]any{
|
||||
"type": schema.Type,
|
||||
}
|
||||
if schema.Type == "" {
|
||||
|
||||
@@ -3,6 +3,6 @@ package mcp
|
||||
import "encoding/json"
|
||||
|
||||
// jsonUnmarshal is a thin wrapper to keep the main files import-clean.
|
||||
func jsonUnmarshal(data []byte, v interface{}) error {
|
||||
func jsonUnmarshal(data []byte, v any) error {
|
||||
return json.Unmarshal(data, v)
|
||||
}
|
||||
@@ -269,10 +269,7 @@ func (m *Manager) tryReconnect(ctx context.Context, ss *serverState) {
|
||||
attempt := ss.reconnAttempts
|
||||
ss.mu.Unlock()
|
||||
|
||||
backoff := initialBackoff * time.Duration(1<<(attempt-1))
|
||||
if backoff > maxBackoff {
|
||||
backoff = maxBackoff
|
||||
}
|
||||
backoff := min(initialBackoff*time.Duration(1<<(attempt-1)), maxBackoff)
|
||||
|
||||
slog.Info("mcp.server.reconnecting",
|
||||
"server", ss.name,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package mcp
|
||||
|
||||
import "strings"
|
||||
|
||||
func mapToEnvSlice(env map[string]string) []string {
|
||||
if len(env) == 0 {
|
||||
return nil
|
||||
@@ -23,14 +25,14 @@ func toSet(items []string) map[string]struct{} {
|
||||
}
|
||||
|
||||
func joinErrors(errs []string) string {
|
||||
result := ""
|
||||
var result strings.Builder
|
||||
for i, e := range errs {
|
||||
if i > 0 {
|
||||
result += "; "
|
||||
result.WriteString("; ")
|
||||
}
|
||||
result += e
|
||||
result.WriteString(e)
|
||||
}
|
||||
return result
|
||||
return result.String()
|
||||
}
|
||||
|
||||
// jsonBytesToStringSlice converts JSONB []byte to []string. Returns nil on error.
|
||||
|
||||
@@ -47,15 +47,15 @@ func (t *MCPToolSearchTool) Description() string {
|
||||
"Discovered tools become immediately available for use."
|
||||
}
|
||||
|
||||
func (t *MCPToolSearchTool) Parameters() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
func (t *MCPToolSearchTool) Parameters() map[string]any {
|
||||
return map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"query": map[string]interface{}{
|
||||
"properties": map[string]any{
|
||||
"query": map[string]any{
|
||||
"type": "string",
|
||||
"description": "English keywords describing the operation you need (e.g. 'create github issue', 'query postgres', 'send slack message')",
|
||||
},
|
||||
"max_results": map[string]interface{}{
|
||||
"max_results": map[string]any{
|
||||
"type": "integer",
|
||||
"description": "Maximum number of tools to return (default: 5)",
|
||||
},
|
||||
@@ -64,7 +64,7 @@ func (t *MCPToolSearchTool) Parameters() map[string]interface{} {
|
||||
}
|
||||
}
|
||||
|
||||
func (t *MCPToolSearchTool) Execute(ctx context.Context, args map[string]interface{}) *tools.Result {
|
||||
func (t *MCPToolSearchTool) Execute(ctx context.Context, args map[string]any) *tools.Result {
|
||||
query, _ := args["query"].(string)
|
||||
if query == "" {
|
||||
return tools.ErrorResult("query parameter is required")
|
||||
@@ -91,7 +91,7 @@ func (t *MCPToolSearchTool) Execute(ctx context.Context, args map[string]interfa
|
||||
}
|
||||
t.manager.ActivateTools(names)
|
||||
|
||||
data, _ := json.MarshalIndent(map[string]interface{}{
|
||||
data, _ := json.MarshalIndent(map[string]any{
|
||||
"tools": results,
|
||||
"count": len(results),
|
||||
}, "", " ")
|
||||
|
||||
@@ -128,7 +128,7 @@ func (p *OpenAIEmbeddingProvider) Name() string { return p.name }
|
||||
func (p *OpenAIEmbeddingProvider) Model() string { return p.model }
|
||||
|
||||
func (p *OpenAIEmbeddingProvider) Embed(ctx context.Context, texts []string) ([][]float32, error) {
|
||||
reqBody := map[string]interface{}{
|
||||
reqBody := map[string]any{
|
||||
"input": texts,
|
||||
"model": p.model,
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
package permissions
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
@@ -90,25 +91,15 @@ func (pe *PolicyEngine) CanAccessWithScopes(scopes []Scope, method string) bool
|
||||
|
||||
// RoleFromScopes determines the effective role from a set of scopes.
|
||||
func RoleFromScopes(scopes []Scope) Role {
|
||||
for _, s := range scopes {
|
||||
if s == ScopeAdmin {
|
||||
return RoleAdmin
|
||||
}
|
||||
}
|
||||
hasWrite := false
|
||||
for _, s := range scopes {
|
||||
if s == ScopeWrite {
|
||||
hasWrite = true
|
||||
break
|
||||
}
|
||||
if slices.Contains(scopes, ScopeAdmin) {
|
||||
return RoleAdmin
|
||||
}
|
||||
hasWrite := slices.Contains(scopes, ScopeWrite)
|
||||
if hasWrite {
|
||||
return RoleOperator
|
||||
}
|
||||
for _, s := range scopes {
|
||||
if s == ScopeRead {
|
||||
return RoleViewer
|
||||
}
|
||||
if slices.Contains(scopes, ScopeRead) {
|
||||
return RoleViewer
|
||||
}
|
||||
return RoleViewer
|
||||
}
|
||||
@@ -166,12 +157,7 @@ func isAdminMethod(method string) bool {
|
||||
protocol.MethodTeamsDelete,
|
||||
protocol.MethodTeamsTaskList,
|
||||
}
|
||||
for _, m := range adminMethods {
|
||||
if method == m {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
return slices.Contains(adminMethods, method)
|
||||
}
|
||||
|
||||
func isWriteMethod(method string) bool {
|
||||
|
||||
@@ -12,9 +12,9 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
defaultClaudeModel = "claude-sonnet-4-5-20250929"
|
||||
anthropicAPIBase = "https://api.anthropic.com/v1"
|
||||
anthropicAPIVersion = "2023-06-01"
|
||||
defaultClaudeModel = "claude-sonnet-4-5-20250929"
|
||||
anthropicAPIBase = "https://api.anthropic.com/v1"
|
||||
anthropicAPIVersion = "2023-06-01"
|
||||
)
|
||||
|
||||
// AnthropicProvider implements Provider using the Anthropic Claude API via net/http.
|
||||
@@ -55,9 +55,9 @@ func WithAnthropicBaseURL(baseURL string) AnthropicOption {
|
||||
}
|
||||
}
|
||||
|
||||
func (p *AnthropicProvider) Name() string { return "anthropic" }
|
||||
func (p *AnthropicProvider) DefaultModel() string { return p.defaultModel }
|
||||
func (p *AnthropicProvider) SupportsThinking() bool { return true }
|
||||
func (p *AnthropicProvider) Name() string { return "anthropic" }
|
||||
func (p *AnthropicProvider) DefaultModel() string { return p.defaultModel }
|
||||
func (p *AnthropicProvider) SupportsThinking() bool { return true }
|
||||
|
||||
func (p *AnthropicProvider) Chat(ctx context.Context, req ChatRequest) (*ChatResponse, error) {
|
||||
model := req.Model
|
||||
@@ -83,7 +83,7 @@ func (p *AnthropicProvider) Chat(ctx context.Context, req ChatRequest) (*ChatRes
|
||||
})
|
||||
}
|
||||
|
||||
func (p *AnthropicProvider) doRequest(ctx context.Context, body interface{}) (io.ReadCloser, error) {
|
||||
func (p *AnthropicProvider) doRequest(ctx context.Context, body any) (io.ReadCloser, error) {
|
||||
data, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("anthropic: marshal request: %w", err)
|
||||
@@ -99,7 +99,7 @@ func (p *AnthropicProvider) doRequest(ctx context.Context, body interface{}) (io
|
||||
httpReq.Header.Set("anthropic-version", anthropicAPIVersion)
|
||||
|
||||
// Add beta header for interleaved thinking when thinking is enabled
|
||||
if bodyMap, ok := body.(map[string]interface{}); ok {
|
||||
if bodyMap, ok := body.(map[string]any); ok {
|
||||
if _, hasThinking := bodyMap["thinking"]; hasThinking {
|
||||
httpReq.Header.Set("anthropic-beta", "interleaved-thinking-2025-05-14")
|
||||
}
|
||||
@@ -138,7 +138,7 @@ func (p *AnthropicProvider) parseResponse(resp *anthropicResponse) *ChatResponse
|
||||
case "redacted_thinking":
|
||||
// Encrypted thinking — cannot display but must preserve for passback
|
||||
case "tool_use":
|
||||
args := make(map[string]interface{})
|
||||
args := make(map[string]any)
|
||||
_ = json.Unmarshal(block.Input, &args)
|
||||
result.ToolCalls = append(result.ToolCalls, ToolCall{
|
||||
ID: block.ID,
|
||||
@@ -182,8 +182,8 @@ func (p *AnthropicProvider) parseResponse(resp *anthropicResponse) *ChatResponse
|
||||
|
||||
type anthropicResponse struct {
|
||||
Content []anthropicContentBlock `json:"content"`
|
||||
StopReason string `json:"stop_reason"`
|
||||
Usage anthropicUsage `json:"usage"`
|
||||
StopReason string `json:"stop_reason"`
|
||||
Usage anthropicUsage `json:"usage"`
|
||||
}
|
||||
|
||||
type anthropicContentBlock struct {
|
||||
@@ -221,8 +221,8 @@ type anthropicContentBlockDeltaEvent struct {
|
||||
Delta struct {
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text,omitempty"`
|
||||
Thinking string `json:"thinking,omitempty"` // for thinking_delta
|
||||
Signature string `json:"signature,omitempty"` // for signature_delta
|
||||
Thinking string `json:"thinking,omitempty"` // for thinking_delta
|
||||
Signature string `json:"signature,omitempty"` // for signature_delta
|
||||
PartialJSON string `json:"partial_json,omitempty"`
|
||||
} `json:"delta"`
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import "encoding/json"
|
||||
func (p *AnthropicProvider) buildRawBlock(blockType string, result *ChatResponse, toolCallJSON map[int]string, _ int) json.RawMessage {
|
||||
switch blockType {
|
||||
case "thinking":
|
||||
block := map[string]interface{}{
|
||||
block := map[string]any{
|
||||
"type": "thinking",
|
||||
"thinking": result.Thinking,
|
||||
}
|
||||
@@ -15,7 +15,7 @@ func (p *AnthropicProvider) buildRawBlock(blockType string, result *ChatResponse
|
||||
return b
|
||||
}
|
||||
case "text":
|
||||
block := map[string]interface{}{
|
||||
block := map[string]any{
|
||||
"type": "text",
|
||||
"text": result.Content,
|
||||
}
|
||||
@@ -26,13 +26,13 @@ func (p *AnthropicProvider) buildRawBlock(blockType string, result *ChatResponse
|
||||
if len(result.ToolCalls) > 0 {
|
||||
tc := result.ToolCalls[len(result.ToolCalls)-1]
|
||||
// Parse accumulated JSON for this tool call
|
||||
args := make(map[string]interface{})
|
||||
args := make(map[string]any)
|
||||
for i, rawJSON := range toolCallJSON {
|
||||
if i == len(result.ToolCalls)-1 && rawJSON != "" {
|
||||
_ = json.Unmarshal([]byte(rawJSON), &args)
|
||||
}
|
||||
}
|
||||
block := map[string]interface{}{
|
||||
block := map[string]any{
|
||||
"type": "tool_use",
|
||||
"id": tc.ID,
|
||||
"name": tc.Name,
|
||||
@@ -44,7 +44,7 @@ func (p *AnthropicProvider) buildRawBlock(blockType string, result *ChatResponse
|
||||
}
|
||||
case "redacted_thinking":
|
||||
// Pass through as-is (we don't have the encrypted data in streaming)
|
||||
block := map[string]interface{}{
|
||||
block := map[string]any{
|
||||
"type": "redacted_thinking",
|
||||
}
|
||||
if b, err := json.Marshal(block); err == nil {
|
||||
@@ -54,26 +54,26 @@ func (p *AnthropicProvider) buildRawBlock(blockType string, result *ChatResponse
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *AnthropicProvider) buildRequestBody(model string, req ChatRequest, stream bool) map[string]interface{} {
|
||||
func (p *AnthropicProvider) buildRequestBody(model string, req ChatRequest, stream bool) map[string]any {
|
||||
// Separate system messages and build conversation messages
|
||||
var systemBlocks []map[string]interface{}
|
||||
var messages []map[string]interface{}
|
||||
var systemBlocks []map[string]any
|
||||
var messages []map[string]any
|
||||
|
||||
for _, msg := range req.Messages {
|
||||
switch msg.Role {
|
||||
case "system":
|
||||
systemBlocks = append(systemBlocks, map[string]interface{}{
|
||||
systemBlocks = append(systemBlocks, map[string]any{
|
||||
"type": "text",
|
||||
"text": msg.Content,
|
||||
})
|
||||
|
||||
case "user":
|
||||
if len(msg.Images) > 0 {
|
||||
var blocks []map[string]interface{}
|
||||
var blocks []map[string]any
|
||||
for _, img := range msg.Images {
|
||||
blocks = append(blocks, map[string]interface{}{
|
||||
blocks = append(blocks, map[string]any{
|
||||
"type": "image",
|
||||
"source": map[string]interface{}{
|
||||
"source": map[string]any{
|
||||
"type": "base64",
|
||||
"media_type": img.MimeType,
|
||||
"data": img.Data,
|
||||
@@ -81,17 +81,17 @@ func (p *AnthropicProvider) buildRequestBody(model string, req ChatRequest, stre
|
||||
})
|
||||
}
|
||||
if msg.Content != "" {
|
||||
blocks = append(blocks, map[string]interface{}{
|
||||
blocks = append(blocks, map[string]any{
|
||||
"type": "text",
|
||||
"text": msg.Content,
|
||||
})
|
||||
}
|
||||
messages = append(messages, map[string]interface{}{
|
||||
messages = append(messages, map[string]any{
|
||||
"role": "user",
|
||||
"content": blocks,
|
||||
})
|
||||
} else {
|
||||
messages = append(messages, map[string]interface{}{
|
||||
messages = append(messages, map[string]any{
|
||||
"role": "user",
|
||||
"content": msg.Content,
|
||||
})
|
||||
@@ -103,7 +103,7 @@ func (p *AnthropicProvider) buildRequestBody(model string, req ChatRequest, stre
|
||||
if msg.RawAssistantContent != nil {
|
||||
var rawBlocks []json.RawMessage
|
||||
if json.Unmarshal(msg.RawAssistantContent, &rawBlocks) == nil && len(rawBlocks) > 0 {
|
||||
messages = append(messages, map[string]interface{}{
|
||||
messages = append(messages, map[string]any{
|
||||
"role": "assistant",
|
||||
"content": rawBlocks,
|
||||
})
|
||||
@@ -111,30 +111,30 @@ func (p *AnthropicProvider) buildRequestBody(model string, req ChatRequest, stre
|
||||
}
|
||||
}
|
||||
|
||||
var blocks []map[string]interface{}
|
||||
var blocks []map[string]any
|
||||
if msg.Content != "" {
|
||||
blocks = append(blocks, map[string]interface{}{
|
||||
blocks = append(blocks, map[string]any{
|
||||
"type": "text",
|
||||
"text": msg.Content,
|
||||
})
|
||||
}
|
||||
for _, tc := range msg.ToolCalls {
|
||||
blocks = append(blocks, map[string]interface{}{
|
||||
blocks = append(blocks, map[string]any{
|
||||
"type": "tool_use",
|
||||
"id": tc.ID,
|
||||
"name": tc.Name,
|
||||
"input": tc.Arguments,
|
||||
})
|
||||
}
|
||||
messages = append(messages, map[string]interface{}{
|
||||
messages = append(messages, map[string]any{
|
||||
"role": "assistant",
|
||||
"content": blocks,
|
||||
})
|
||||
|
||||
case "tool":
|
||||
messages = append(messages, map[string]interface{}{
|
||||
messages = append(messages, map[string]any{
|
||||
"role": "user",
|
||||
"content": []map[string]interface{}{
|
||||
"content": []map[string]any{
|
||||
{
|
||||
"type": "tool_result",
|
||||
"tool_use_id": msg.ToolCallID,
|
||||
@@ -145,11 +145,11 @@ func (p *AnthropicProvider) buildRequestBody(model string, req ChatRequest, stre
|
||||
}
|
||||
}
|
||||
|
||||
body := map[string]interface{}{
|
||||
body := map[string]any{
|
||||
"model": model,
|
||||
"max_tokens": 4096,
|
||||
"messages": messages,
|
||||
"cache_control": map[string]interface{}{"type": "ephemeral"},
|
||||
"cache_control": map[string]any{"type": "ephemeral"},
|
||||
}
|
||||
|
||||
if stream {
|
||||
@@ -162,10 +162,10 @@ func (p *AnthropicProvider) buildRequestBody(model string, req ChatRequest, stre
|
||||
|
||||
// Translate tools to Anthropic format
|
||||
if len(req.Tools) > 0 {
|
||||
var tools []map[string]interface{}
|
||||
var tools []map[string]any
|
||||
for _, t := range req.Tools {
|
||||
cleanedParams := CleanSchemaForProvider("anthropic", t.Function.Parameters)
|
||||
tool := map[string]interface{}{
|
||||
tool := map[string]any{
|
||||
"name": t.Function.Name,
|
||||
"description": t.Function.Description,
|
||||
"input_schema": cleanedParams,
|
||||
@@ -186,7 +186,7 @@ func (p *AnthropicProvider) buildRequestBody(model string, req ChatRequest, stre
|
||||
// Enable extended thinking if thinking_level is set
|
||||
if level, ok := req.Options[OptThinkingLevel].(string); ok && level != "" && level != "off" {
|
||||
budget := anthropicThinkingBudget(level)
|
||||
body["thinking"] = map[string]interface{}{
|
||||
body["thinking"] = map[string]any{
|
||||
"type": "enabled",
|
||||
"budget_tokens": budget,
|
||||
}
|
||||
|
||||
@@ -44,8 +44,8 @@ func (p *AnthropicProvider) ChatStream(ctx context.Context, req ChatRequest, onC
|
||||
line := scanner.Text()
|
||||
|
||||
// Track event type
|
||||
if strings.HasPrefix(line, "event: ") {
|
||||
currentEvent = strings.TrimPrefix(line, "event: ")
|
||||
if after, ok := strings.CutPrefix(line, "event: "); ok {
|
||||
currentEvent = after
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ func (p *AnthropicProvider) ChatStream(ctx context.Context, req ChatRequest, onC
|
||||
result.ToolCalls = append(result.ToolCalls, ToolCall{
|
||||
ID: ev.ContentBlock.ID,
|
||||
Name: strings.TrimSpace(ev.ContentBlock.Name),
|
||||
Arguments: make(map[string]interface{}),
|
||||
Arguments: make(map[string]any),
|
||||
})
|
||||
}
|
||||
// Store raw content_block for later reconstruction
|
||||
@@ -155,7 +155,7 @@ func (p *AnthropicProvider) ChatStream(ctx context.Context, req ChatRequest, onC
|
||||
// Parse accumulated tool call JSON arguments
|
||||
for i, rawJSON := range toolCallJSON {
|
||||
if rawJSON != "" {
|
||||
args := make(map[string]interface{})
|
||||
args := make(map[string]any)
|
||||
_ = json.Unmarshal([]byte(rawJSON), &args)
|
||||
result.ToolCalls[i].Arguments = args
|
||||
}
|
||||
|
||||
@@ -46,30 +46,30 @@ func BuildCLIHooksConfig(workspace string, restrictToWorkspace bool) (string, fu
|
||||
|
||||
// generateSettingsJSON creates Claude CLI settings with PreToolUse hooks.
|
||||
func generateSettingsJSON(hookPath string) []byte {
|
||||
settings := map[string]interface{}{
|
||||
"hooks": map[string]interface{}{
|
||||
"PreToolUse": []map[string]interface{}{
|
||||
settings := map[string]any{
|
||||
"hooks": map[string]any{
|
||||
"PreToolUse": []map[string]any{
|
||||
{
|
||||
"matcher": "Bash",
|
||||
"hooks": []map[string]interface{}{
|
||||
"hooks": []map[string]any{
|
||||
{"type": "command", "command": hookPath},
|
||||
},
|
||||
},
|
||||
{
|
||||
"matcher": "Write",
|
||||
"hooks": []map[string]interface{}{
|
||||
"hooks": []map[string]any{
|
||||
{"type": "command", "command": hookPath},
|
||||
},
|
||||
},
|
||||
{
|
||||
"matcher": "Edit",
|
||||
"hooks": []map[string]interface{}{
|
||||
"hooks": []map[string]any{
|
||||
{"type": "command", "command": hookPath},
|
||||
},
|
||||
},
|
||||
{
|
||||
"matcher": "Read",
|
||||
"hooks": []map[string]interface{}{
|
||||
"hooks": []map[string]any{
|
||||
{"type": "command", "command": hookPath},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"maps"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -33,9 +34,9 @@ type MCPServerLookup func(ctx context.Context, agentID string) []MCPServerEntry
|
||||
// MCPConfigData holds the base MCP server entries built at startup.
|
||||
// Per-session configs are written via WriteMCPConfig with agent context injected.
|
||||
type MCPConfigData struct {
|
||||
Servers map[string]interface{} // external MCP server entries (stdio/sse/http)
|
||||
GatewayAddr string
|
||||
GatewayToken string
|
||||
Servers map[string]any // external MCP server entries (stdio/sse/http)
|
||||
GatewayAddr string
|
||||
GatewayToken string
|
||||
AgentMCPLookup MCPServerLookup // optional: resolves per-agent MCP servers from DB
|
||||
}
|
||||
|
||||
@@ -43,7 +44,7 @@ type MCPConfigData struct {
|
||||
// Does NOT include the goclaw-bridge entry — that's added per-session
|
||||
// with agent context headers in WriteMCPConfig.
|
||||
func BuildCLIMCPConfigData(servers map[string]*config.MCPServerConfig, gatewayAddr string, gatewayToken ...string) *MCPConfigData {
|
||||
mcpServers := make(map[string]interface{}, len(servers))
|
||||
mcpServers := make(map[string]any, len(servers))
|
||||
|
||||
for name, srv := range servers {
|
||||
if !srv.IsEnabled() {
|
||||
@@ -109,10 +110,8 @@ func (d *MCPConfigData) writeMCPConfigInternal(ctx context.Context, sessionKey,
|
||||
|
||||
// Shallow-copy the outer map so we can add the bridge entry without mutating the shared base.
|
||||
// Inner server entries are not modified, so shallow copy is sufficient.
|
||||
servers := make(map[string]interface{}, len(d.Servers)+1)
|
||||
for k, v := range d.Servers {
|
||||
servers[k] = v
|
||||
}
|
||||
servers := make(map[string]any, len(d.Servers)+1)
|
||||
maps.Copy(servers, d.Servers)
|
||||
|
||||
// Inject per-agent MCP servers from DB (if lookup is configured and agentID is set)
|
||||
if d.AgentMCPLookup != nil && agentID != "" {
|
||||
@@ -153,7 +152,7 @@ func (d *MCPConfigData) writeMCPConfigInternal(ctx context.Context, sessionKey,
|
||||
headers["X-Bridge-Sig"] = SignBridgeContext(d.GatewayToken, agentID, userID, channel, chatID, peerKind)
|
||||
}
|
||||
|
||||
bridgeEntry := map[string]interface{}{
|
||||
bridgeEntry := map[string]any{
|
||||
"url": fmt.Sprintf("http://%s/mcp/bridge", d.GatewayAddr),
|
||||
"type": "http",
|
||||
}
|
||||
@@ -167,7 +166,7 @@ func (d *MCPConfigData) writeMCPConfigInternal(ctx context.Context, sessionKey,
|
||||
return ""
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(map[string]interface{}{"mcpServers": servers}, "", " ")
|
||||
data, err := json.MarshalIndent(map[string]any{"mcpServers": servers}, "", " ")
|
||||
if err != nil {
|
||||
slog.Warn("claude-cli: failed to marshal mcp config", "error", err)
|
||||
return ""
|
||||
@@ -202,10 +201,9 @@ func (d *MCPConfigData) writeMCPConfigInternal(ctx context.Context, sessionKey,
|
||||
return path
|
||||
}
|
||||
|
||||
|
||||
// mcpServerEntryToConfig converts an MCPServerEntry to the CLI MCP config format.
|
||||
func mcpServerEntryToConfig(srv MCPServerEntry) map[string]interface{} {
|
||||
entry := make(map[string]interface{})
|
||||
func mcpServerEntryToConfig(srv MCPServerEntry) map[string]any {
|
||||
entry := make(map[string]any)
|
||||
switch srv.Transport {
|
||||
case "stdio":
|
||||
if srv.Command != "" {
|
||||
|
||||
@@ -15,7 +15,7 @@ func parseJSONResponse(data []byte) (*ChatResponse, error) {
|
||||
}
|
||||
|
||||
// Fallback: CLI may output one JSON object per line.
|
||||
for _, line := range bytes.Split(data, []byte("\n")) {
|
||||
for line := range bytes.SplitSeq(data, []byte("\n")) {
|
||||
line = bytes.TrimSpace(line)
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
|
||||
@@ -131,7 +131,7 @@ func extractFromMessages(msgs []Message) (systemPrompt, userMsg string, images [
|
||||
}
|
||||
|
||||
// extractStringOpt gets a string value from Options map by key.
|
||||
func extractStringOpt(opts map[string]interface{}, key string) string {
|
||||
func extractStringOpt(opts map[string]any, key string) string {
|
||||
if opts == nil {
|
||||
return ""
|
||||
}
|
||||
@@ -144,7 +144,7 @@ func extractStringOpt(opts map[string]interface{}, key string) string {
|
||||
}
|
||||
|
||||
// extractBoolOpt gets a bool value from Options map by key.
|
||||
func extractBoolOpt(opts map[string]interface{}, key string) bool {
|
||||
func extractBoolOpt(opts map[string]any, key string) bool {
|
||||
if opts == nil {
|
||||
return false
|
||||
}
|
||||
@@ -157,7 +157,7 @@ func extractBoolOpt(opts map[string]interface{}, key string) bool {
|
||||
}
|
||||
|
||||
// bridgeContextFromOpts builds a BridgeContext from the Options map.
|
||||
func bridgeContextFromOpts(opts map[string]interface{}) BridgeContext {
|
||||
func bridgeContextFromOpts(opts map[string]any) BridgeContext {
|
||||
return BridgeContext{
|
||||
AgentID: extractStringOpt(opts, OptAgentID),
|
||||
UserID: extractStringOpt(opts, OptUserID),
|
||||
@@ -209,12 +209,12 @@ func sessionFileExists(workDir string, sessionID uuid.UUID) bool {
|
||||
|
||||
// buildStreamJSONInput creates stream-json stdin for vision (images + text).
|
||||
func buildStreamJSONInput(text string, images []ImageContent) *bytes.Reader {
|
||||
var contentBlocks []map[string]interface{}
|
||||
var contentBlocks []map[string]any
|
||||
|
||||
for _, img := range images {
|
||||
contentBlocks = append(contentBlocks, map[string]interface{}{
|
||||
contentBlocks = append(contentBlocks, map[string]any{
|
||||
"type": "image",
|
||||
"source": map[string]interface{}{
|
||||
"source": map[string]any{
|
||||
"type": "base64",
|
||||
"media_type": img.MimeType,
|
||||
"data": img.Data,
|
||||
@@ -223,15 +223,15 @@ func buildStreamJSONInput(text string, images []ImageContent) *bytes.Reader {
|
||||
}
|
||||
|
||||
if text != "" {
|
||||
contentBlocks = append(contentBlocks, map[string]interface{}{
|
||||
contentBlocks = append(contentBlocks, map[string]any{
|
||||
"type": "text",
|
||||
"text": text,
|
||||
})
|
||||
}
|
||||
|
||||
msg := map[string]interface{}{
|
||||
msg := map[string]any{
|
||||
"type": "user",
|
||||
"message": map[string]interface{}{
|
||||
"message": map[string]any{
|
||||
"role": "user",
|
||||
"content": contentBlocks,
|
||||
},
|
||||
@@ -246,8 +246,8 @@ func filterCLIEnv(environ []string) []string {
|
||||
var filtered []string
|
||||
for _, e := range environ {
|
||||
key := e
|
||||
if idx := strings.IndexByte(e, '='); idx >= 0 {
|
||||
key = e[:idx]
|
||||
if before, _, ok := strings.Cut(e, "="); ok {
|
||||
key = before
|
||||
}
|
||||
// Filter out variables that could cause nested CLI conflicts
|
||||
if strings.HasPrefix(key, "CLAUDE") {
|
||||
|
||||
@@ -44,7 +44,7 @@ func NewCodexProvider(name string, tokenSource TokenSource, apiBase, defaultMode
|
||||
}
|
||||
}
|
||||
|
||||
func (p *CodexProvider) Name() string { return p.name }
|
||||
func (p *CodexProvider) Name() string { return p.name }
|
||||
func (p *CodexProvider) DefaultModel() string { return p.defaultModel }
|
||||
func (p *CodexProvider) SupportsThinking() bool { return true }
|
||||
|
||||
@@ -97,7 +97,7 @@ func (p *CodexProvider) ChatStream(ctx context.Context, req ChatRequest, onChunk
|
||||
if acc.name == "" {
|
||||
continue
|
||||
}
|
||||
args := make(map[string]interface{})
|
||||
args := make(map[string]any)
|
||||
_ = json.Unmarshal([]byte(acc.rawArgs), &args)
|
||||
result.ToolCalls = append(result.ToolCalls, ToolCall{
|
||||
ID: acc.callID,
|
||||
|
||||
@@ -11,14 +11,14 @@ import (
|
||||
)
|
||||
|
||||
// buildRequestBody converts internal ChatRequest to Responses API format.
|
||||
func (p *CodexProvider) buildRequestBody(req ChatRequest, stream bool) map[string]interface{} {
|
||||
func (p *CodexProvider) buildRequestBody(req ChatRequest, stream bool) map[string]any {
|
||||
model := req.Model
|
||||
if model == "" {
|
||||
model = p.defaultModel
|
||||
}
|
||||
|
||||
var instructions string
|
||||
var input []interface{}
|
||||
var input []any
|
||||
|
||||
for _, m := range req.Messages {
|
||||
switch m.Role {
|
||||
@@ -31,25 +31,25 @@ func (p *CodexProvider) buildRequestBody(req ChatRequest, stream bool) map[strin
|
||||
|
||||
case "user":
|
||||
if len(m.Images) > 0 {
|
||||
var parts []map[string]interface{}
|
||||
var parts []map[string]any
|
||||
for _, img := range m.Images {
|
||||
parts = append(parts, map[string]interface{}{
|
||||
parts = append(parts, map[string]any{
|
||||
"type": "input_image",
|
||||
"image_url": fmt.Sprintf("data:%s;base64,%s", img.MimeType, img.Data),
|
||||
})
|
||||
}
|
||||
if m.Content != "" {
|
||||
parts = append(parts, map[string]interface{}{
|
||||
parts = append(parts, map[string]any{
|
||||
"type": "input_text",
|
||||
"text": m.Content,
|
||||
})
|
||||
}
|
||||
input = append(input, map[string]interface{}{
|
||||
input = append(input, map[string]any{
|
||||
"role": "user",
|
||||
"content": parts,
|
||||
})
|
||||
} else {
|
||||
input = append(input, map[string]interface{}{
|
||||
input = append(input, map[string]any{
|
||||
"role": "user",
|
||||
"content": m.Content,
|
||||
})
|
||||
@@ -60,7 +60,7 @@ func (p *CodexProvider) buildRequestBody(req ChatRequest, stream bool) map[strin
|
||||
for _, tc := range m.ToolCalls {
|
||||
argsJSON, _ := json.Marshal(tc.Arguments)
|
||||
callID := toFcID(tc.ID)
|
||||
input = append(input, map[string]interface{}{
|
||||
input = append(input, map[string]any{
|
||||
"type": "function_call",
|
||||
"id": callID,
|
||||
"call_id": callID,
|
||||
@@ -70,10 +70,10 @@ func (p *CodexProvider) buildRequestBody(req ChatRequest, stream bool) map[strin
|
||||
}
|
||||
}
|
||||
if m.Content != "" {
|
||||
item := map[string]interface{}{
|
||||
item := map[string]any{
|
||||
"type": "message",
|
||||
"role": "assistant",
|
||||
"content": []map[string]interface{}{
|
||||
"content": []map[string]any{
|
||||
{"type": "output_text", "text": m.Content},
|
||||
},
|
||||
}
|
||||
@@ -84,7 +84,7 @@ func (p *CodexProvider) buildRequestBody(req ChatRequest, stream bool) map[strin
|
||||
}
|
||||
|
||||
case "tool":
|
||||
input = append(input, map[string]interface{}{
|
||||
input = append(input, map[string]any{
|
||||
"type": "function_call_output",
|
||||
"call_id": toFcID(m.ToolCallID),
|
||||
"output": m.Content,
|
||||
@@ -92,7 +92,7 @@ func (p *CodexProvider) buildRequestBody(req ChatRequest, stream bool) map[strin
|
||||
}
|
||||
}
|
||||
|
||||
body := map[string]interface{}{
|
||||
body := map[string]any{
|
||||
"model": model,
|
||||
"input": input,
|
||||
"stream": stream,
|
||||
@@ -105,9 +105,9 @@ func (p *CodexProvider) buildRequestBody(req ChatRequest, stream bool) map[strin
|
||||
body["instructions"] = instructions
|
||||
|
||||
if len(req.Tools) > 0 {
|
||||
var tools []map[string]interface{}
|
||||
var tools []map[string]any
|
||||
for _, t := range req.Tools {
|
||||
tools = append(tools, map[string]interface{}{
|
||||
tools = append(tools, map[string]any{
|
||||
"type": "function",
|
||||
"name": t.Function.Name,
|
||||
"description": t.Function.Description,
|
||||
@@ -118,13 +118,13 @@ func (p *CodexProvider) buildRequestBody(req ChatRequest, stream bool) map[strin
|
||||
}
|
||||
|
||||
if level, ok := req.Options[OptThinkingLevel].(string); ok && level != "" && level != "off" {
|
||||
body["reasoning"] = map[string]interface{}{"effort": level}
|
||||
body["reasoning"] = map[string]any{"effort": level}
|
||||
}
|
||||
|
||||
return body
|
||||
}
|
||||
|
||||
func (p *CodexProvider) doRequest(ctx context.Context, body interface{}) (io.ReadCloser, error) {
|
||||
func (p *CodexProvider) doRequest(ctx context.Context, body any) (io.ReadCloser, error) {
|
||||
data, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: marshal request: %w", p.name, err)
|
||||
|
||||
@@ -20,7 +20,7 @@ func (s *staticTokenSource) Token() (string, error) {
|
||||
}
|
||||
|
||||
// mustJSON marshals v to JSON string; panics on error.
|
||||
func mustJSON(v interface{}) string {
|
||||
func mustJSON(v any) string {
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -68,7 +68,7 @@ func TestCodexProviderBuildRequestBody(t *testing.T) {
|
||||
{Role: "assistant", Content: "Hi there!"},
|
||||
{Role: "user", Content: "How are you?"},
|
||||
},
|
||||
Options: map[string]interface{}{
|
||||
Options: map[string]any{
|
||||
OptMaxTokens: 1024,
|
||||
OptTemperature: 0.7,
|
||||
},
|
||||
@@ -97,7 +97,7 @@ func TestCodexProviderBuildRequestBody(t *testing.T) {
|
||||
}
|
||||
|
||||
// Check input items (should exclude system messages)
|
||||
input, ok := body["input"].([]interface{})
|
||||
input, ok := body["input"].([]any)
|
||||
if !ok {
|
||||
t.Fatalf("input is not []interface{}: %T", body["input"])
|
||||
}
|
||||
@@ -124,10 +124,10 @@ func TestCodexProviderBuildRequestBodyWithTools(t *testing.T) {
|
||||
Function: ToolFunctionSchema{
|
||||
Name: "get_weather",
|
||||
Description: "Get current weather",
|
||||
Parameters: map[string]interface{}{
|
||||
Parameters: map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"city": map[string]interface{}{"type": "string"},
|
||||
"properties": map[string]any{
|
||||
"city": map[string]any{"type": "string"},
|
||||
},
|
||||
"required": []string{"city"},
|
||||
},
|
||||
@@ -138,7 +138,7 @@ func TestCodexProviderBuildRequestBodyWithTools(t *testing.T) {
|
||||
|
||||
body := p.buildRequestBody(req, true)
|
||||
|
||||
tools, ok := body["tools"].([]map[string]interface{})
|
||||
tools, ok := body["tools"].([]map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("tools is not []map[string]interface{}: %T", body["tools"])
|
||||
}
|
||||
@@ -167,7 +167,7 @@ func TestCodexProviderBuildRequestBodyToolCallMessages(t *testing.T) {
|
||||
{
|
||||
Role: "assistant",
|
||||
ToolCalls: []ToolCall{
|
||||
{ID: "call_123", Name: "get_weather", Arguments: map[string]interface{}{"city": "London"}},
|
||||
{ID: "call_123", Name: "get_weather", Arguments: map[string]any{"city": "London"}},
|
||||
},
|
||||
},
|
||||
{Role: "tool", ToolCallID: "call_123", Content: `{"temp": 15}`},
|
||||
@@ -176,7 +176,7 @@ func TestCodexProviderBuildRequestBodyToolCallMessages(t *testing.T) {
|
||||
|
||||
body := p.buildRequestBody(req, false)
|
||||
|
||||
input, ok := body["input"].([]interface{})
|
||||
input, ok := body["input"].([]any)
|
||||
if !ok {
|
||||
t.Fatalf("input is not []interface{}: %T", body["input"])
|
||||
}
|
||||
@@ -185,7 +185,7 @@ func TestCodexProviderBuildRequestBodyToolCallMessages(t *testing.T) {
|
||||
}
|
||||
|
||||
// Second item should be function_call
|
||||
fc, ok := input[1].(map[string]interface{})
|
||||
fc, ok := input[1].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("input[1] is not map: %T", input[1])
|
||||
}
|
||||
@@ -197,7 +197,7 @@ func TestCodexProviderBuildRequestBodyToolCallMessages(t *testing.T) {
|
||||
}
|
||||
|
||||
// Third item should be function_call_output
|
||||
fco, ok := input[2].(map[string]interface{})
|
||||
fco, ok := input[2].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("input[2] is not map: %T", input[2])
|
||||
}
|
||||
@@ -214,12 +214,12 @@ func TestCodexProviderBuildRequestBodyThinking(t *testing.T) {
|
||||
|
||||
req := ChatRequest{
|
||||
Messages: []Message{{Role: "user", Content: "Think about this"}},
|
||||
Options: map[string]interface{}{OptThinkingLevel: "high"},
|
||||
Options: map[string]any{OptThinkingLevel: "high"},
|
||||
}
|
||||
|
||||
body := p.buildRequestBody(req, false)
|
||||
|
||||
reasoning, ok := body["reasoning"].(map[string]interface{})
|
||||
reasoning, ok := body["reasoning"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("reasoning is not map: %T", body["reasoning"])
|
||||
}
|
||||
@@ -245,7 +245,7 @@ func TestCodexProviderChat(t *testing.T) {
|
||||
}
|
||||
|
||||
// Verify request body
|
||||
var body map[string]interface{}
|
||||
var body map[string]any
|
||||
json.NewDecoder(r.Body).Decode(&body)
|
||||
if body["model"] != "gpt-4o" {
|
||||
t.Errorf("request model = %v, want gpt-4o", body["model"])
|
||||
@@ -599,17 +599,17 @@ func TestCodexProviderBuildRequestBodyWithImages(t *testing.T) {
|
||||
|
||||
body := p.buildRequestBody(req, false)
|
||||
|
||||
input, ok := body["input"].([]interface{})
|
||||
input, ok := body["input"].([]any)
|
||||
if !ok || len(input) != 1 {
|
||||
t.Fatalf("expected 1 input item, got %v", body["input"])
|
||||
}
|
||||
|
||||
item, ok := input[0].(map[string]interface{})
|
||||
item, ok := input[0].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("input[0] is not map: %T", input[0])
|
||||
}
|
||||
|
||||
content, ok := item["content"].([]map[string]interface{})
|
||||
content, ok := item["content"].([]map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("content is not []map: %T", item["content"])
|
||||
}
|
||||
@@ -625,4 +625,3 @@ func TestCodexProviderBuildRequestBodyWithImages(t *testing.T) {
|
||||
t.Errorf("content[1] type = %v, want input_text", content[1]["type"])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package providers
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"maps"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -29,7 +30,7 @@ func NewDashScopeProvider(apiKey, apiBase, defaultModel string) *DashScopeProvid
|
||||
}
|
||||
}
|
||||
|
||||
func (p *DashScopeProvider) Name() string { return "dashscope" }
|
||||
func (p *DashScopeProvider) Name() string { return "dashscope" }
|
||||
func (p *DashScopeProvider) SupportsThinking() bool { return true }
|
||||
|
||||
// ChatStream handles DashScope's limitation: tools + streaming cannot coexist.
|
||||
@@ -39,10 +40,8 @@ func (p *DashScopeProvider) ChatStream(ctx context.Context, req ChatRequest, onC
|
||||
// Map thinking_level to DashScope-specific params before passing to OpenAI base
|
||||
if level, ok := req.Options[OptThinkingLevel].(string); ok && level != "" && level != "off" {
|
||||
// Clone Options to avoid mutating caller's map
|
||||
opts := make(map[string]interface{}, len(req.Options)+2)
|
||||
for k, v := range req.Options {
|
||||
opts[k] = v
|
||||
}
|
||||
opts := make(map[string]any, len(req.Options)+2)
|
||||
maps.Copy(opts, req.Options)
|
||||
opts[OptEnableThinking] = true
|
||||
opts[OptThinkingBudget] = dashscopeThinkingBudget(level)
|
||||
delete(opts, OptThinkingLevel) // don't pass generic key to OpenAI buildRequestBody
|
||||
|
||||
@@ -47,11 +47,11 @@ func (p *OpenAIProvider) WithChatPath(path string) *OpenAIProvider {
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *OpenAIProvider) Name() string { return p.name }
|
||||
func (p *OpenAIProvider) DefaultModel() string { return p.defaultModel }
|
||||
func (p *OpenAIProvider) SupportsThinking() bool { return true }
|
||||
func (p *OpenAIProvider) APIKey() string { return p.apiKey }
|
||||
func (p *OpenAIProvider) APIBase() string { return p.apiBase }
|
||||
func (p *OpenAIProvider) Name() string { return p.name }
|
||||
func (p *OpenAIProvider) DefaultModel() string { return p.defaultModel }
|
||||
func (p *OpenAIProvider) SupportsThinking() bool { return true }
|
||||
func (p *OpenAIProvider) APIKey() string { return p.apiKey }
|
||||
func (p *OpenAIProvider) APIBase() string { return p.apiBase }
|
||||
|
||||
// resolveModel returns the model ID to use for a request.
|
||||
// For OpenRouter, model IDs require a provider prefix (e.g. "anthropic/claude-sonnet-4-5-20250929").
|
||||
@@ -189,7 +189,7 @@ func (p *OpenAIProvider) ChatStream(ctx context.Context, req ChatRequest, onChun
|
||||
// Parse accumulated tool call arguments
|
||||
for i := 0; i < len(accumulators); i++ {
|
||||
acc := accumulators[i]
|
||||
args := make(map[string]interface{})
|
||||
args := make(map[string]any)
|
||||
_ = json.Unmarshal([]byte(acc.rawArgs), &args)
|
||||
acc.Arguments = args
|
||||
if acc.thoughtSig != "" {
|
||||
@@ -209,7 +209,7 @@ func (p *OpenAIProvider) ChatStream(ctx context.Context, req ChatRequest, onChun
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (p *OpenAIProvider) buildRequestBody(model string, req ChatRequest, stream bool) map[string]interface{} {
|
||||
func (p *OpenAIProvider) buildRequestBody(model string, req ChatRequest, stream bool) map[string]any {
|
||||
// Gemini 2.5+: collapse tool_call cycles missing thought_signature.
|
||||
// Gemini requires thought_signature echoed back on every tool_call; models that
|
||||
// don't return it (e.g. gemini-3-flash) will cause HTTP 400 if sent as-is.
|
||||
@@ -223,9 +223,9 @@ func (p *OpenAIProvider) buildRequestBody(model string, req ChatRequest, stream
|
||||
// This is necessary because our internal Message/ToolCall structs don't match
|
||||
// the OpenAI API format (tool_calls need type+function wrapper, arguments as JSON string).
|
||||
// Also omits empty content on assistant messages with tool_calls (Gemini compatibility).
|
||||
msgs := make([]map[string]interface{}, 0, len(inputMessages))
|
||||
msgs := make([]map[string]any, 0, len(inputMessages))
|
||||
for _, m := range inputMessages {
|
||||
msg := map[string]interface{}{
|
||||
msg := map[string]any{
|
||||
"role": m.Role,
|
||||
}
|
||||
|
||||
@@ -237,17 +237,17 @@ func (p *OpenAIProvider) buildRequestBody(model string, req ChatRequest, stream
|
||||
// Include content; omit empty content for assistant messages with tool_calls
|
||||
// (Gemini rejects empty content → "must include at least one parts field").
|
||||
if m.Role == "user" && len(m.Images) > 0 {
|
||||
var parts []map[string]interface{}
|
||||
var parts []map[string]any
|
||||
for _, img := range m.Images {
|
||||
parts = append(parts, map[string]interface{}{
|
||||
parts = append(parts, map[string]any{
|
||||
"type": "image_url",
|
||||
"image_url": map[string]interface{}{
|
||||
"image_url": map[string]any{
|
||||
"url": fmt.Sprintf("data:%s;base64,%s", img.MimeType, img.Data),
|
||||
},
|
||||
})
|
||||
}
|
||||
if m.Content != "" {
|
||||
parts = append(parts, map[string]interface{}{
|
||||
parts = append(parts, map[string]any{
|
||||
"type": "text",
|
||||
"text": m.Content,
|
||||
})
|
||||
@@ -260,17 +260,17 @@ func (p *OpenAIProvider) buildRequestBody(model string, req ChatRequest, stream
|
||||
// Convert tool_calls to OpenAI wire format:
|
||||
// {id, type: "function", function: {name, arguments: "<json string>"}}
|
||||
if len(m.ToolCalls) > 0 {
|
||||
toolCalls := make([]map[string]interface{}, len(m.ToolCalls))
|
||||
toolCalls := make([]map[string]any, len(m.ToolCalls))
|
||||
for i, tc := range m.ToolCalls {
|
||||
argsJSON, _ := json.Marshal(tc.Arguments)
|
||||
fn := map[string]interface{}{
|
||||
fn := map[string]any{
|
||||
"name": tc.Name,
|
||||
"arguments": string(argsJSON),
|
||||
}
|
||||
if sig := tc.Metadata["thought_signature"]; sig != "" {
|
||||
fn["thought_signature"] = sig
|
||||
}
|
||||
toolCalls[i] = map[string]interface{}{
|
||||
toolCalls[i] = map[string]any{
|
||||
"id": tc.ID,
|
||||
"type": "function",
|
||||
"function": fn,
|
||||
@@ -286,7 +286,7 @@ func (p *OpenAIProvider) buildRequestBody(model string, req ChatRequest, stream
|
||||
msgs = append(msgs, msg)
|
||||
}
|
||||
|
||||
body := map[string]interface{}{
|
||||
body := map[string]any{
|
||||
"model": model,
|
||||
"messages": msgs,
|
||||
"stream": stream,
|
||||
@@ -298,7 +298,7 @@ func (p *OpenAIProvider) buildRequestBody(model string, req ChatRequest, stream
|
||||
}
|
||||
|
||||
if stream {
|
||||
body["stream_options"] = map[string]interface{}{
|
||||
body["stream_options"] = map[string]any{
|
||||
"include_usage": true,
|
||||
}
|
||||
}
|
||||
@@ -327,7 +327,7 @@ func (p *OpenAIProvider) buildRequestBody(model string, req ChatRequest, stream
|
||||
return body
|
||||
}
|
||||
|
||||
func (p *OpenAIProvider) doRequest(ctx context.Context, body interface{}) (io.ReadCloser, error) {
|
||||
func (p *OpenAIProvider) doRequest(ctx context.Context, body any) (io.ReadCloser, error) {
|
||||
data, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: marshal request: %w", p.name, err)
|
||||
@@ -370,7 +370,7 @@ func (p *OpenAIProvider) parseResponse(resp *openAIResponse) *ChatResponse {
|
||||
result.FinishReason = resp.Choices[0].FinishReason
|
||||
|
||||
for _, tc := range msg.ToolCalls {
|
||||
args := make(map[string]interface{})
|
||||
args := make(map[string]any)
|
||||
_ = json.Unmarshal([]byte(tc.Function.Arguments), &args)
|
||||
call := ToolCall{
|
||||
ID: tc.ID,
|
||||
@@ -404,4 +404,3 @@ func (p *OpenAIProvider) parseResponse(resp *openAIResponse) *ChatResponse {
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package providers
|
||||
|
||||
import "slices"
|
||||
|
||||
import "strings"
|
||||
|
||||
// Unsupported schema keys by provider.
|
||||
@@ -34,7 +36,7 @@ func CleanToolSchemas(providerName string, tools []ToolDefinition) []ToolDefinit
|
||||
}
|
||||
|
||||
// CleanSchemaForProvider cleans a single parameters map for a provider.
|
||||
func CleanSchemaForProvider(providerName string, params map[string]interface{}) map[string]interface{} {
|
||||
func CleanSchemaForProvider(providerName string, params map[string]any) map[string]any {
|
||||
removeKeys := unsupportedKeysForProvider(providerName)
|
||||
if removeKeys == nil {
|
||||
return params
|
||||
@@ -54,21 +56,21 @@ func unsupportedKeysForProvider(name string) []string {
|
||||
}
|
||||
|
||||
// cleanSchema recursively removes unsupported keys from a JSON Schema map.
|
||||
func cleanSchema(schema map[string]interface{}, removeKeys []string) map[string]interface{} {
|
||||
func cleanSchema(schema map[string]any, removeKeys []string) map[string]any {
|
||||
if schema == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := make(map[string]interface{}, len(schema))
|
||||
result := make(map[string]any, len(schema))
|
||||
for k, v := range schema {
|
||||
if shouldRemoveKey(k, removeKeys) {
|
||||
continue
|
||||
}
|
||||
|
||||
switch val := v.(type) {
|
||||
case map[string]interface{}:
|
||||
case map[string]any:
|
||||
result[k] = cleanSchema(val, removeKeys)
|
||||
case []interface{}:
|
||||
case []any:
|
||||
result[k] = cleanSchemaSlice(val, removeKeys)
|
||||
default:
|
||||
result[k] = v
|
||||
@@ -78,10 +80,10 @@ func cleanSchema(schema map[string]interface{}, removeKeys []string) map[string]
|
||||
}
|
||||
|
||||
// cleanSchemaSlice recurses into arrays (e.g. "anyOf", "oneOf", "allOf").
|
||||
func cleanSchemaSlice(items []interface{}, removeKeys []string) []interface{} {
|
||||
result := make([]interface{}, len(items))
|
||||
func cleanSchemaSlice(items []any, removeKeys []string) []any {
|
||||
result := make([]any, len(items))
|
||||
for i, item := range items {
|
||||
if m, ok := item.(map[string]interface{}); ok {
|
||||
if m, ok := item.(map[string]any); ok {
|
||||
result[i] = cleanSchema(m, removeKeys)
|
||||
} else {
|
||||
result[i] = item
|
||||
@@ -91,10 +93,5 @@ func cleanSchemaSlice(items []interface{}, removeKeys []string) []interface{} {
|
||||
}
|
||||
|
||||
func shouldRemoveKey(key string, removeKeys []string) bool {
|
||||
for _, rk := range removeKeys {
|
||||
if key == rk {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
return slices.Contains(removeKeys, key)
|
||||
}
|
||||
@@ -10,17 +10,17 @@ func TestCleanToolSchemas_Gemini(t *testing.T) {
|
||||
Function: ToolFunctionSchema{
|
||||
Name: "test",
|
||||
Description: "desc",
|
||||
Parameters: map[string]interface{}{
|
||||
Parameters: map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"name": map[string]interface{}{
|
||||
"properties": map[string]any{
|
||||
"name": map[string]any{
|
||||
"type": "string",
|
||||
"default": "world",
|
||||
},
|
||||
},
|
||||
"$defs": map[string]interface{}{"Foo": "bar"},
|
||||
"$defs": map[string]any{"Foo": "bar"},
|
||||
"additionalProperties": false,
|
||||
"examples": []interface{}{"a"},
|
||||
"examples": []any{"a"},
|
||||
},
|
||||
},
|
||||
}}
|
||||
@@ -43,8 +43,8 @@ func TestCleanToolSchemas_Gemini(t *testing.T) {
|
||||
}
|
||||
|
||||
// Nested "default" should be removed
|
||||
props := params["properties"].(map[string]interface{})
|
||||
nameSchema := props["name"].(map[string]interface{})
|
||||
props := params["properties"].(map[string]any)
|
||||
nameSchema := props["name"].(map[string]any)
|
||||
if _, ok := nameSchema["default"]; ok {
|
||||
t.Error("expected nested 'default' to be removed for gemini")
|
||||
}
|
||||
@@ -54,15 +54,15 @@ func TestCleanToolSchemas_Gemini(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCleanToolSchemas_Anthropic(t *testing.T) {
|
||||
params := map[string]interface{}{
|
||||
params := map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"url": map[string]interface{}{
|
||||
"properties": map[string]any{
|
||||
"url": map[string]any{
|
||||
"type": "string",
|
||||
"$ref": "#/$defs/URL",
|
||||
},
|
||||
},
|
||||
"$defs": map[string]interface{}{"URL": "..."},
|
||||
"$defs": map[string]any{"URL": "..."},
|
||||
"additionalProperties": false,
|
||||
"default": "x",
|
||||
}
|
||||
@@ -73,8 +73,8 @@ func TestCleanToolSchemas_Anthropic(t *testing.T) {
|
||||
if _, ok := cleaned["$defs"]; ok {
|
||||
t.Error("expected $defs removed for anthropic")
|
||||
}
|
||||
props := cleaned["properties"].(map[string]interface{})
|
||||
urlSchema := props["url"].(map[string]interface{})
|
||||
props := cleaned["properties"].(map[string]any)
|
||||
urlSchema := props["url"].(map[string]any)
|
||||
if _, ok := urlSchema["$ref"]; ok {
|
||||
t.Error("expected nested $ref removed for anthropic")
|
||||
}
|
||||
@@ -93,7 +93,7 @@ func TestCleanToolSchemas_Unknown(t *testing.T) {
|
||||
Type: "function",
|
||||
Function: ToolFunctionSchema{
|
||||
Name: "test",
|
||||
Parameters: map[string]interface{}{
|
||||
Parameters: map[string]any{
|
||||
"$ref": "something",
|
||||
"default": "val",
|
||||
},
|
||||
@@ -122,13 +122,13 @@ func TestCleanSchema_NilParams(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCleanSchema_NestedArray(t *testing.T) {
|
||||
params := map[string]interface{}{
|
||||
"anyOf": []interface{}{
|
||||
map[string]interface{}{
|
||||
params := map[string]any{
|
||||
"anyOf": []any{
|
||||
map[string]any{
|
||||
"type": "string",
|
||||
"default": "x",
|
||||
},
|
||||
map[string]interface{}{
|
||||
map[string]any{
|
||||
"type": "number",
|
||||
"$ref": "#/defs/Num",
|
||||
"default": 42,
|
||||
@@ -137,12 +137,12 @@ func TestCleanSchema_NestedArray(t *testing.T) {
|
||||
}
|
||||
|
||||
cleaned := CleanSchemaForProvider("gemini", params)
|
||||
anyOf := cleaned["anyOf"].([]interface{})
|
||||
anyOf := cleaned["anyOf"].([]any)
|
||||
if len(anyOf) != 2 {
|
||||
t.Fatalf("expected 2 items, got %d", len(anyOf))
|
||||
}
|
||||
|
||||
first := anyOf[0].(map[string]interface{})
|
||||
first := anyOf[0].(map[string]any)
|
||||
if _, ok := first["default"]; ok {
|
||||
t.Error("expected 'default' removed in array item")
|
||||
}
|
||||
@@ -150,7 +150,7 @@ func TestCleanSchema_NestedArray(t *testing.T) {
|
||||
t.Error("expected 'type' to remain in array item")
|
||||
}
|
||||
|
||||
second := anyOf[1].(map[string]interface{})
|
||||
second := anyOf[1].(map[string]any)
|
||||
if _, ok := second["$ref"]; ok {
|
||||
t.Error("expected '$ref' removed in array item")
|
||||
}
|
||||
@@ -160,13 +160,13 @@ func TestCleanSchema_NestedArray(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCleanSchema_DeepNesting(t *testing.T) {
|
||||
params := map[string]interface{}{
|
||||
params := map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"config": map[string]interface{}{
|
||||
"properties": map[string]any{
|
||||
"config": map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"nested": map[string]interface{}{
|
||||
"properties": map[string]any{
|
||||
"nested": map[string]any{
|
||||
"type": "string",
|
||||
"default": "deep",
|
||||
"$ref": "#/deep",
|
||||
@@ -177,10 +177,10 @@ func TestCleanSchema_DeepNesting(t *testing.T) {
|
||||
}
|
||||
|
||||
cleaned := CleanSchemaForProvider("gemini", params)
|
||||
props := cleaned["properties"].(map[string]interface{})
|
||||
config := props["config"].(map[string]interface{})
|
||||
innerProps := config["properties"].(map[string]interface{})
|
||||
nested := innerProps["nested"].(map[string]interface{})
|
||||
props := cleaned["properties"].(map[string]any)
|
||||
config := props["config"].(map[string]any)
|
||||
innerProps := config["properties"].(map[string]any)
|
||||
nested := innerProps["nested"].(map[string]any)
|
||||
|
||||
if _, ok := nested["default"]; ok {
|
||||
t.Error("expected deeply nested 'default' removed")
|
||||
|
||||
+23
-23
@@ -7,12 +7,12 @@ import (
|
||||
|
||||
// Options keys used in ChatRequest.Options across providers.
|
||||
const (
|
||||
OptMaxTokens = "max_tokens"
|
||||
OptTemperature = "temperature"
|
||||
OptThinkingLevel = "thinking_level"
|
||||
OptMaxTokens = "max_tokens"
|
||||
OptTemperature = "temperature"
|
||||
OptThinkingLevel = "thinking_level"
|
||||
OptReasoningEffort = "reasoning_effort"
|
||||
OptEnableThinking = "enable_thinking"
|
||||
OptThinkingBudget = "thinking_budget"
|
||||
OptEnableThinking = "enable_thinking"
|
||||
OptThinkingBudget = "thinking_budget"
|
||||
)
|
||||
|
||||
// TokenSource provides an OAuth access token (with auto-refresh).
|
||||
@@ -45,10 +45,10 @@ type ThinkingCapable interface {
|
||||
|
||||
// ChatRequest contains the input for a Chat/ChatStream call.
|
||||
type ChatRequest struct {
|
||||
Messages []Message `json:"messages"`
|
||||
Tools []ToolDefinition `json:"tools,omitempty"`
|
||||
Model string `json:"model,omitempty"`
|
||||
Options map[string]interface{} `json:"options,omitempty"`
|
||||
Messages []Message `json:"messages"`
|
||||
Tools []ToolDefinition `json:"tools,omitempty"`
|
||||
Model string `json:"model,omitempty"`
|
||||
Options map[string]any `json:"options,omitempty"`
|
||||
}
|
||||
|
||||
// ChatResponse is the result from an LLM call.
|
||||
@@ -70,9 +70,9 @@ type ChatResponse struct {
|
||||
|
||||
// StreamChunk is a piece of a streaming response.
|
||||
type StreamChunk struct {
|
||||
Content string `json:"content,omitempty"`
|
||||
Thinking string `json:"thinking,omitempty"`
|
||||
Done bool `json:"done,omitempty"`
|
||||
Content string `json:"content,omitempty"`
|
||||
Thinking string `json:"thinking,omitempty"`
|
||||
Done bool `json:"done,omitempty"`
|
||||
}
|
||||
|
||||
// ImageContent represents a base64-encoded image for vision-capable models.
|
||||
@@ -92,11 +92,11 @@ type MediaRef struct {
|
||||
|
||||
// Message represents a conversation message.
|
||||
type Message struct {
|
||||
Role string `json:"role"` // "system", "user", "assistant", "tool"
|
||||
Role string `json:"role"` // "system", "user", "assistant", "tool"
|
||||
Content string `json:"content"`
|
||||
Thinking string `json:"thinking,omitempty"` // reasoning_content for thinking models (Kimi, DeepSeek, etc.)
|
||||
Images []ImageContent `json:"images,omitempty"` // vision: base64 images (runtime only, not persisted)
|
||||
MediaRefs []MediaRef `json:"media_refs,omitempty"` // persistent media file references
|
||||
Thinking string `json:"thinking,omitempty"` // reasoning_content for thinking models (Kimi, DeepSeek, etc.)
|
||||
Images []ImageContent `json:"images,omitempty"` // vision: base64 images (runtime only, not persisted)
|
||||
MediaRefs []MediaRef `json:"media_refs,omitempty"` // persistent media file references
|
||||
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
|
||||
ToolCallID string `json:"tool_call_id,omitempty"` // for role="tool" responses
|
||||
|
||||
@@ -113,10 +113,10 @@ type Message struct {
|
||||
|
||||
// ToolCall represents a tool invocation requested by the LLM.
|
||||
type ToolCall struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Arguments map[string]interface{} `json:"arguments"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"` // provider-specific (e.g. Gemini thought_signature)
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Arguments map[string]any `json:"arguments"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"` // provider-specific (e.g. Gemini thought_signature)
|
||||
}
|
||||
|
||||
// ToolDefinition describes a tool available to the LLM.
|
||||
@@ -127,9 +127,9 @@ type ToolDefinition struct {
|
||||
|
||||
// ToolFunctionSchema is the schema for a function tool.
|
||||
type ToolFunctionSchema struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Parameters map[string]interface{} `json:"parameters"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Parameters map[string]any `json:"parameters"`
|
||||
}
|
||||
|
||||
// Usage tracks token consumption.
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"maps"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -282,9 +283,7 @@ func (m *DockerManager) Release(ctx context.Context, key string) error {
|
||||
func (m *DockerManager) ReleaseAll(ctx context.Context) error {
|
||||
m.mu.Lock()
|
||||
sbs := make(map[string]*DockerSandbox, len(m.sandboxes))
|
||||
for k, v := range m.sandboxes {
|
||||
sbs[k] = v
|
||||
}
|
||||
maps.Copy(sbs, m.sandboxes)
|
||||
m.sandboxes = make(map[string]*DockerSandbox)
|
||||
m.mu.Unlock()
|
||||
|
||||
@@ -297,7 +296,7 @@ func (m *DockerManager) ReleaseAll(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// Stats returns information about active sandboxes.
|
||||
func (m *DockerManager) Stats() map[string]interface{} {
|
||||
func (m *DockerManager) Stats() map[string]any {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
@@ -306,7 +305,7 @@ func (m *DockerManager) Stats() map[string]interface{} {
|
||||
containers[key] = sb.containerID
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
return map[string]any{
|
||||
"mode": m.config.Mode,
|
||||
"image": m.config.Image,
|
||||
"active": len(m.sandboxes),
|
||||
|
||||
+18
-18
@@ -67,36 +67,36 @@ type Config struct {
|
||||
// Security hardening (matching TS buildSandboxCreateArgs)
|
||||
ReadOnlyRoot bool `json:"read_only_root"`
|
||||
CapDrop []string `json:"cap_drop,omitempty"`
|
||||
Tmpfs []string `json:"tmpfs,omitempty"` // e.g. "/tmp", "/tmp:size=64m"
|
||||
TmpfsSizeMB int `json:"tmpfs_size_mb,omitempty"` // default size for tmpfs mounts without explicit :size= (0 = Docker default)
|
||||
Tmpfs []string `json:"tmpfs,omitempty"` // e.g. "/tmp", "/tmp:size=64m"
|
||||
TmpfsSizeMB int `json:"tmpfs_size_mb,omitempty"` // default size for tmpfs mounts without explicit :size= (0 = Docker default)
|
||||
PidsLimit int `json:"pids_limit,omitempty"`
|
||||
User string `json:"user,omitempty"` // container user (e.g. "1000:1000", "nobody")
|
||||
User string `json:"user,omitempty"` // container user (e.g. "1000:1000", "nobody")
|
||||
MaxOutputBytes int `json:"max_output_bytes,omitempty"` // limit exec stdout+stderr capture (default 1MB, 0 = unlimited)
|
||||
SetupCommand string `json:"setup_command,omitempty"`
|
||||
ContainerPrefix string `json:"container_prefix,omitempty"`
|
||||
Workdir string `json:"workdir,omitempty"` // container workdir (default "/workspace")
|
||||
|
||||
// Pruning (matching TS SandboxPruneSettings)
|
||||
IdleHours int `json:"idle_hours,omitempty"` // prune containers idle > N hours (default 24)
|
||||
MaxAgeDays int `json:"max_age_days,omitempty"` // prune containers older than N days (default 7)
|
||||
IdleHours int `json:"idle_hours,omitempty"` // prune containers idle > N hours (default 24)
|
||||
MaxAgeDays int `json:"max_age_days,omitempty"` // prune containers older than N days (default 7)
|
||||
PruneIntervalMin int `json:"prune_interval_min,omitempty"` // check interval in minutes (default 5)
|
||||
}
|
||||
|
||||
// DefaultConfig returns sensible defaults matching TS sandbox defaults.
|
||||
func DefaultConfig() Config {
|
||||
return Config{
|
||||
Mode: ModeOff,
|
||||
Image: "goclaw-sandbox:bookworm-slim",
|
||||
WorkspaceAccess: AccessRW,
|
||||
Scope: ScopeSession,
|
||||
MemoryMB: 512,
|
||||
CPUs: 1.0,
|
||||
TimeoutSec: 300,
|
||||
NetworkEnabled: false,
|
||||
ReadOnlyRoot: true,
|
||||
CapDrop: []string{"ALL"},
|
||||
Tmpfs: []string{"/tmp", "/var/tmp", "/run"},
|
||||
MaxOutputBytes: 1 << 20, // 1MB
|
||||
Mode: ModeOff,
|
||||
Image: "goclaw-sandbox:bookworm-slim",
|
||||
WorkspaceAccess: AccessRW,
|
||||
Scope: ScopeSession,
|
||||
MemoryMB: 512,
|
||||
CPUs: 1.0,
|
||||
TimeoutSec: 300,
|
||||
NetworkEnabled: false,
|
||||
ReadOnlyRoot: true,
|
||||
CapDrop: []string{"ALL"},
|
||||
Tmpfs: []string{"/tmp", "/var/tmp", "/run"},
|
||||
MaxOutputBytes: 1 << 20, // 1MB
|
||||
ContainerPrefix: "goclaw-sbx-",
|
||||
Workdir: "/workspace",
|
||||
IdleHours: 24,
|
||||
@@ -183,7 +183,7 @@ type Manager interface {
|
||||
Stop()
|
||||
|
||||
// Stats returns info about active sandboxes.
|
||||
Stats() map[string]interface{}
|
||||
Stats() map[string]any
|
||||
}
|
||||
|
||||
// ErrSandboxDisabled is returned when sandbox mode is "off".
|
||||
|
||||
@@ -18,7 +18,7 @@ func TestLane_ConcurrencyLimit(t *testing.T) {
|
||||
var maxActive atomic.Int32
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for i := 0; i < 6; i++ {
|
||||
for range 6 {
|
||||
wg.Add(1)
|
||||
err := lane.Submit(context.Background(), func() {
|
||||
defer wg.Done()
|
||||
@@ -139,7 +139,7 @@ func TestScheduler_SessionSerialization(t *testing.T) {
|
||||
sessionKey := "agent:default:test-session"
|
||||
|
||||
var outcomes []<-chan RunOutcome
|
||||
for i := 0; i < 3; i++ {
|
||||
for i := range 3 {
|
||||
ch := sched.Schedule(ctx, "main", agent.RunRequest{
|
||||
SessionKey: sessionKey,
|
||||
Message: "hello",
|
||||
|
||||
@@ -463,7 +463,7 @@ func stripFrontmatter(content string) string {
|
||||
|
||||
func parseSimpleYAML(content string) map[string]string {
|
||||
result := make(map[string]string)
|
||||
for _, line := range strings.Split(content, "\n") {
|
||||
for line := range strings.SplitSeq(content, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
|
||||
@@ -19,7 +19,7 @@ type CronJob struct {
|
||||
|
||||
// CronSchedule defines when a job should run.
|
||||
type CronSchedule struct {
|
||||
Kind string `json:"kind"` // "at", "every", "cron"
|
||||
Kind string `json:"kind"` // "at", "every", "cron"
|
||||
AtMS *int64 `json:"atMs,omitempty"`
|
||||
EveryMS *int64 `json:"everyMs,omitempty"`
|
||||
Expr string `json:"expr,omitempty"`
|
||||
@@ -79,10 +79,10 @@ type CronJobPatch struct {
|
||||
|
||||
// CronEvent represents a job lifecycle event sent to subscribers.
|
||||
type CronEvent struct {
|
||||
Action string `json:"action"` // "running", "completed", "error"
|
||||
Action string `json:"action"` // "running", "completed", "error"
|
||||
JobID string `json:"jobId"`
|
||||
JobName string `json:"jobName,omitempty"`
|
||||
Status string `json:"status,omitempty"` // final status for completed/error
|
||||
Status string `json:"status,omitempty"` // final status for completed/error
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ type CronStore interface {
|
||||
UpdateJob(jobID string, patch CronJobPatch) (*CronJob, error)
|
||||
EnableJob(jobID string, enabled bool) error
|
||||
GetRunLog(jobID string, limit, offset int) ([]CronRunLogEntry, int)
|
||||
Status() map[string]interface{}
|
||||
Status() map[string]any
|
||||
|
||||
// Lifecycle
|
||||
Start() error
|
||||
|
||||
+11
-10
@@ -5,6 +5,7 @@ import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -318,7 +319,7 @@ func (s *PGAgentStore) ListAccessible(ctx context.Context, userID string) ([]sto
|
||||
// --- Scan helpers ---
|
||||
|
||||
type agentRowScanner interface {
|
||||
Scan(dest ...interface{}) error
|
||||
Scan(dest ...any) error
|
||||
}
|
||||
|
||||
func scanAgentRow(row agentRowScanner) (*store.AgentData, error) {
|
||||
@@ -380,7 +381,7 @@ func execMapUpdateWhere(ctx context.Context, db *sql.DB, table string, updates m
|
||||
return nil
|
||||
}
|
||||
var setClauses []string
|
||||
var args []interface{}
|
||||
var args []any
|
||||
i := 1
|
||||
for col, val := range updates {
|
||||
setClauses = append(setClauses, fmt.Sprintf("%s = $%d", col, i))
|
||||
@@ -405,25 +406,25 @@ func execMapUpdateWhere(ctx context.Context, db *sql.DB, table string, updates m
|
||||
}
|
||||
|
||||
func joinStrings(s []string, sep string) string {
|
||||
result := ""
|
||||
var result strings.Builder
|
||||
for i, v := range s {
|
||||
if i > 0 {
|
||||
result += sep
|
||||
result.WriteString(sep)
|
||||
}
|
||||
result += v
|
||||
result.WriteString(v)
|
||||
}
|
||||
return result
|
||||
return result.String()
|
||||
}
|
||||
|
||||
func replaceIDX(s, replacement string) string {
|
||||
result := ""
|
||||
var result strings.Builder
|
||||
for i := 0; i < len(s); i++ {
|
||||
if i+4 <= len(s) && s[i:i+4] == "$IDX" {
|
||||
result += replacement
|
||||
result.WriteString(replacement)
|
||||
i += 3
|
||||
} else {
|
||||
result += string(s[i])
|
||||
result.WriteString(string(s[i]))
|
||||
}
|
||||
}
|
||||
return result
|
||||
return result.String()
|
||||
}
|
||||
@@ -66,7 +66,7 @@ func (s *PGBuiltinToolStore) Update(ctx context.Context, name string, updates ma
|
||||
switch sv := v.(type) {
|
||||
case json.RawMessage:
|
||||
allowed["settings"] = []byte(sv)
|
||||
case map[string]interface{}:
|
||||
case map[string]any:
|
||||
b, err := json.Marshal(sv)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal settings: %w", err)
|
||||
@@ -85,7 +85,7 @@ func (s *PGBuiltinToolStore) Update(ctx context.Context, name string, updates ma
|
||||
|
||||
// Build UPDATE with name as PK (not UUID)
|
||||
var setClauses []string
|
||||
var args []interface{}
|
||||
var args []any
|
||||
i := 1
|
||||
for col, val := range allowed {
|
||||
setClauses = append(setClauses, fmt.Sprintf("%s = $%d", col, i))
|
||||
|
||||
@@ -192,9 +192,9 @@ func (s *PGChannelInstanceStore) ListAll(ctx context.Context) ([]store.ChannelIn
|
||||
return s.scanInstances(rows)
|
||||
}
|
||||
|
||||
func buildChannelInstanceWhere(opts store.ChannelInstanceListOpts) (string, []interface{}) {
|
||||
func buildChannelInstanceWhere(opts store.ChannelInstanceListOpts) (string, []any) {
|
||||
var conditions []string
|
||||
var args []interface{}
|
||||
var args []any
|
||||
argIdx := 1
|
||||
|
||||
if opts.Search != "" {
|
||||
|
||||
Loaded 100 of 178 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user