style: fix golines, gofmt, and gosmopolitan lint errors
- Run golines (max 120 chars) on long lines
- Run gofmt with interface{} -> any rewrite rule
- Add nolint:gosmopolitan for intentional CJK strings
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
5801176dfe
commit
dca46e6799
16 changed files with 235 additions and 93 deletions
|
|
@ -205,7 +205,11 @@ func gatewayCmd(debug bool, orchestration bool) error {
|
|||
// Auto-detect Tailscale hostname and fetch TLS cert
|
||||
hostname, tsErr := tailscale.DetectHostname()
|
||||
if tsErr != nil {
|
||||
logger.InfoCF("miniapp", "Tailscale not available, Mini App disabled", map[string]any{"error": tsErr.Error()})
|
||||
logger.InfoCF(
|
||||
"miniapp",
|
||||
"Tailscale not available, Mini App disabled",
|
||||
map[string]any{"error": tsErr.Error()},
|
||||
)
|
||||
} else {
|
||||
certDir := filepath.Join(cfg.WorkspacePath(), "state", "certs")
|
||||
certFile, keyFile, certErr := tailscale.FetchCert(hostname, certDir)
|
||||
|
|
@ -224,7 +228,14 @@ func gatewayCmd(debug bool, orchestration bool) error {
|
|||
dataProvider := &agentLoopDataProvider{loop: agentLoop, workspace: cfg.WorkspacePath()}
|
||||
sender := &telegramCommandSender{bus: msgBus}
|
||||
miniappNotifier = miniapp.NewStateNotifier()
|
||||
handler := miniapp.NewHandler(dataProvider, sender, cfg.Channels.Telegram.Token, miniappNotifier, cfg.Channels.Telegram.AllowFrom, cfg.WorkspacePath())
|
||||
handler := miniapp.NewHandler(
|
||||
dataProvider,
|
||||
sender,
|
||||
cfg.Channels.Telegram.Token,
|
||||
miniappNotifier,
|
||||
cfg.Channels.Telegram.AllowFrom,
|
||||
cfg.WorkspacePath(),
|
||||
)
|
||||
agentLoop.OnStateChange = miniappNotifier.Notify
|
||||
if b := agentLoop.GetOrchBroadcaster(); b != nil {
|
||||
handler.SetOrchBroadcaster(b)
|
||||
|
|
@ -251,7 +262,11 @@ func gatewayCmd(debug bool, orchestration bool) error {
|
|||
}
|
||||
}()
|
||||
if useTLS {
|
||||
fmt.Printf("✓ Health endpoints available at https://%s:%d/health and /ready (TLS)\n", cfg.Gateway.Host, cfg.Gateway.Port)
|
||||
fmt.Printf(
|
||||
"✓ Health endpoints available at https://%s:%d/health and /ready (TLS)\n",
|
||||
cfg.Gateway.Host,
|
||||
cfg.Gateway.Port,
|
||||
)
|
||||
} else {
|
||||
fmt.Printf("✓ Health endpoints available at http://%s:%d/health and /ready\n", cfg.Gateway.Host, cfg.Gateway.Port)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -119,7 +119,12 @@ type processOptions struct {
|
|||
|
||||
const defaultResponse = "I've completed processing but have no response to give. Increase `max_tool_iterations` in config.json."
|
||||
|
||||
func NewAgentLoop(cfg *config.Config, msgBus *bus.MessageBus, provider providers.LLMProvider, enableStats ...bool) *AgentLoop {
|
||||
func NewAgentLoop(
|
||||
cfg *config.Config,
|
||||
msgBus *bus.MessageBus,
|
||||
provider providers.LLMProvider,
|
||||
enableStats ...bool,
|
||||
) *AgentLoop {
|
||||
registry := NewAgentRegistry(cfg, provider)
|
||||
|
||||
// Set up shared fallback chain
|
||||
|
|
@ -282,7 +287,14 @@ func registerSharedTools(
|
|||
PerplexityMaxResults: cfg.Tools.Web.Perplexity.MaxResults,
|
||||
PerplexityEnabled: cfg.Tools.Web.Perplexity.Enabled,
|
||||
}
|
||||
subagentManager := tools.NewSubagentManager(provider, agent.Model, agent.Workspace, msgBus, al.reporter(), webSearchOpts)
|
||||
subagentManager := tools.NewSubagentManager(
|
||||
provider,
|
||||
agent.Model,
|
||||
agent.Workspace,
|
||||
msgBus,
|
||||
al.reporter(),
|
||||
webSearchOpts,
|
||||
)
|
||||
subagentManager.SetLLMOptions(agent.MaxTokens, agent.Temperature)
|
||||
spawnTool := tools.NewSpawnTool(subagentManager)
|
||||
currentAgentID := agentID
|
||||
|
|
@ -480,7 +492,10 @@ func (al *AgentLoop) SetChannelManager(cm *channels.Manager) {
|
|||
// It caches created providers by "provider/model" key so each combination is
|
||||
// only resolved once. Looks up model_list first (new format), then falls back
|
||||
// to the legacy providers section via CreateProviderByName.
|
||||
func (al *AgentLoop) resolveProvider(providerName, modelName string, fallback providers.LLMProvider) providers.LLMProvider {
|
||||
func (al *AgentLoop) resolveProvider(
|
||||
providerName, modelName string,
|
||||
fallback providers.LLMProvider,
|
||||
) providers.LLMProvider {
|
||||
key := strings.ToLower(providerName + "/" + modelName)
|
||||
if key == "/" {
|
||||
return fallback
|
||||
|
|
@ -497,14 +512,14 @@ func (al *AgentLoop) resolveProvider(providerName, modelName string, fallback pr
|
|||
return p
|
||||
}
|
||||
logger.WarnCF("agent", "Failed to create provider from model_list, trying legacy",
|
||||
map[string]interface{}{"provider": providerName, "model": modelName, "error": err.Error()})
|
||||
map[string]any{"provider": providerName, "model": modelName, "error": err.Error()})
|
||||
}
|
||||
|
||||
// Fall back to legacy providers section.
|
||||
p, err := providers.CreateProviderByName(al.cfg, providerName)
|
||||
if err != nil {
|
||||
logger.WarnCF("agent", "Failed to create provider for fallback, using primary",
|
||||
map[string]interface{}{"provider": providerName, "error": err.Error()})
|
||||
map[string]any{"provider": providerName, "error": err.Error()})
|
||||
return fallback
|
||||
}
|
||||
al.providerCache[key] = p
|
||||
|
|
@ -630,7 +645,7 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage)
|
|||
lower := strings.ToLower(content)
|
||||
|
||||
// Check for stop keywords
|
||||
stopKeywords := []string{"stop", "cancel", "abort", "停止", "中止", "やめて"}
|
||||
stopKeywords := []string{"stop", "cancel", "abort", "停止", "中止", "やめて"} //nolint:gosmopolitan // intentional CJK stop words
|
||||
isStop := false
|
||||
for _, kw := range stopKeywords {
|
||||
if lower == kw {
|
||||
|
|
@ -939,7 +954,8 @@ func (al *AgentLoop) runAgentLoop(ctx context.Context, agent *AgentInstance, opt
|
|||
// 1a. Set session-specific working directory for bootstrap file lookup.
|
||||
// Prefer the tool-detected project directory (touch_dir) from the session tracker,
|
||||
// resolved as an absolute path under workspace. Fall back to worktree or workspace.
|
||||
if active := al.sessions.ListActive(); len(active) > 0 && active[0].SessionKey == opts.SessionKey && active[0].TouchDir != "" {
|
||||
if active := al.sessions.ListActive(); len(active) > 0 && active[0].SessionKey == opts.SessionKey &&
|
||||
active[0].TouchDir != "" {
|
||||
agent.ContextBuilder.SetWorkDir(filepath.Join(agent.Workspace, active[0].TouchDir))
|
||||
} else {
|
||||
agent.ContextBuilder.SetWorkDir(agent.EffectiveWorkspace(opts.SessionKey))
|
||||
|
|
@ -1015,7 +1031,9 @@ func (al *AgentLoop) runAgentLoop(ctx context.Context, agent *AgentInstance, opt
|
|||
sb.WriteString("\n\n## Background Execution\n")
|
||||
sb.WriteString("You are running as a background heartbeat with no conversation history. ")
|
||||
sb.WriteString("MEMORY.md is the only shared state between heartbeats. ")
|
||||
sb.WriteString("After completing each plan step, immediately use edit_file to mark it [x] in memory/MEMORY.md.")
|
||||
sb.WriteString(
|
||||
"After completing each plan step, immediately use edit_file to mark it [x] in memory/MEMORY.md.",
|
||||
)
|
||||
messages[0].Content = sb.String()
|
||||
}
|
||||
}
|
||||
|
|
@ -1053,14 +1071,15 @@ func (al *AgentLoop) runAgentLoop(ctx context.Context, agent *AgentInstance, opt
|
|||
|
||||
// 5a. Auto-advance plan phases after LLM iteration
|
||||
postStatus := agent.ContextBuilder.GetPlanStatus()
|
||||
if agent.ContextBuilder.HasActivePlan() && (postStatus == "executing" || postStatus == "review" || postStatus == "completed") {
|
||||
if agent.ContextBuilder.HasActivePlan() &&
|
||||
(postStatus == "executing" || postStatus == "review" || postStatus == "completed") {
|
||||
// Intercept: if AI changed status to executing or review without user approval
|
||||
// (from interviewing or review), validate and hold at "review".
|
||||
if preStatus == "interviewing" || (preStatus == "review" && postStatus == "executing") {
|
||||
if err := agent.ContextBuilder.ValidatePlanStructure(); err != nil {
|
||||
_ = agent.ContextBuilder.SetPlanStatus("interviewing")
|
||||
logger.WarnCF("agent", "Reverted plan to interviewing: "+err.Error(),
|
||||
map[string]interface{}{"agent_id": agent.ID})
|
||||
map[string]any{"agent_id": agent.ID})
|
||||
// Inject rejection into session history so LLM sees it next iteration
|
||||
rejectionMsg := "[System] Plan rejected: " + err.Error() + ". Fix and try again."
|
||||
agent.Sessions.AddMessage(opts.SessionKey, "user", rejectionMsg)
|
||||
|
|
@ -1080,7 +1099,7 @@ func (al *AgentLoop) runAgentLoop(ctx context.Context, agent *AgentInstance, opt
|
|||
// Safeguard: executing but no phases (shouldn't happen, but be safe).
|
||||
_ = agent.ContextBuilder.SetPlanStatus("interviewing")
|
||||
logger.WarnCF("agent", "Reverted plan to interviewing: no phases defined",
|
||||
map[string]interface{}{"agent_id": agent.ID})
|
||||
map[string]any{"agent_id": agent.ID})
|
||||
} else if agent.ContextBuilder.IsPlanComplete() {
|
||||
// Mark plan as completed (keep memory for review; user can /plan clear)
|
||||
total := agent.ContextBuilder.GetTotalPhases()
|
||||
|
|
@ -1194,7 +1213,8 @@ func buildTaskReminder(userMessage string, lastBlocker string) providers.Message
|
|||
truncatedBlocker := utils.Truncate(lastBlocker, blockerMaxChars)
|
||||
content = fmt.Sprintf(
|
||||
"[TASK REMINDER]\nOriginal task:\n---\n%s\n---\nLast blocker:\n---\n%s\n---\nFix the blocker if essential, or find an alternative. If all steps are complete, move on.",
|
||||
truncatedTask, truncatedBlocker,
|
||||
truncatedTask,
|
||||
truncatedBlocker,
|
||||
)
|
||||
} else {
|
||||
content = fmt.Sprintf(
|
||||
|
|
@ -1247,7 +1267,7 @@ var optFlagPattern = regexp.MustCompile(`\s+--?\w[\w-]*(=\S*)?`)
|
|||
|
||||
// extractExecProjectDir extracts the basename of an exec cd target.
|
||||
// Returns "" if the command has no cd prefix.
|
||||
func extractExecProjectDir(args map[string]interface{}) string {
|
||||
func extractExecProjectDir(args map[string]any) string {
|
||||
cmd, _ := args["command"].(string)
|
||||
if cmd == "" {
|
||||
return ""
|
||||
|
|
@ -1338,7 +1358,7 @@ func (al *AgentLoop) targetReasoningChannelID(channelName string) (chatID string
|
|||
// For exec: extracts the command and strips the leading "cd <workspace> && ".
|
||||
// For file tools: extracts the path and strips the workspace prefix.
|
||||
// Falls back to raw JSON truncation.
|
||||
func buildArgsSnippet(toolName string, args map[string]interface{}, workspace string) string {
|
||||
func buildArgsSnippet(toolName string, args map[string]any, workspace string) string {
|
||||
switch toolName {
|
||||
case "exec":
|
||||
cmd, _ := args["command"].(string)
|
||||
|
|
@ -1829,7 +1849,12 @@ type streamToolCallAcc struct {
|
|||
}
|
||||
|
||||
// buildAccumulatedResponse constructs an LLMResponse from accumulated stream data.
|
||||
func buildAccumulatedResponse(content, reasoning string, toolCalls []streamToolCallAcc, finishReason string, usage *providers.UsageInfo) *providers.LLMResponse {
|
||||
func buildAccumulatedResponse(
|
||||
content, reasoning string,
|
||||
toolCalls []streamToolCallAcc,
|
||||
finishReason string,
|
||||
usage *providers.UsageInfo,
|
||||
) *providers.LLMResponse {
|
||||
resp := &providers.LLMResponse{
|
||||
Content: content,
|
||||
Reasoning: reasoning,
|
||||
|
|
@ -2426,7 +2451,14 @@ func (al *AgentLoop) runLLMIteration(
|
|||
if wt := agent.GetWorktree(opts.SessionKey); wt != nil {
|
||||
toolCtx = tools.WithWorkspaceOverride(toolCtx, wt.Path)
|
||||
}
|
||||
toolResult := agent.Tools.ExecuteWithContext(toolCtx, tc.Name, tc.Arguments, opts.Channel, opts.ChatID, asyncCallback)
|
||||
toolResult := agent.Tools.ExecuteWithContext(
|
||||
toolCtx,
|
||||
tc.Name,
|
||||
tc.Arguments,
|
||||
opts.Channel,
|
||||
opts.ChatID,
|
||||
asyncCallback,
|
||||
)
|
||||
toolDuration := time.Since(toolStart)
|
||||
|
||||
// Update tool log entry with result
|
||||
|
|
@ -2539,7 +2571,7 @@ func (al *AgentLoop) runLLMIteration(
|
|||
messages = append(messages, reminderMsg)
|
||||
lastReminderIdx = len(messages) - 1
|
||||
logger.DebugCF("agent", "Injected task reminder",
|
||||
map[string]interface{}{
|
||||
map[string]any{
|
||||
"agent_id": agent.ID,
|
||||
"iteration": iteration,
|
||||
"has_blocker": lastBlocker != "",
|
||||
|
|
@ -2551,7 +2583,7 @@ func (al *AgentLoop) runLLMIteration(
|
|||
if reminder, ok := buildPlanReminder(planSnapshot); ok {
|
||||
messages = append(messages, reminder)
|
||||
logger.DebugCF("agent", "Injected plan reminder",
|
||||
map[string]interface{}{
|
||||
map[string]any{
|
||||
"agent_id": agent.ID,
|
||||
"iteration": iteration,
|
||||
"plan_status": planSnapshot,
|
||||
|
|
@ -2565,7 +2597,8 @@ func (al *AgentLoop) runLLMIteration(
|
|||
if touchDir := al.sessions.GetTouchDir(opts.SessionKey); touchDir != "" {
|
||||
agent.ContextBuilder.SetWorkDir(filepath.Join(agent.Workspace, touchDir))
|
||||
}
|
||||
if newPrompt := agent.ContextBuilder.BuildSystemPrompt(); len(messages) > 0 && messages[0].Content != newPrompt {
|
||||
if newPrompt := agent.ContextBuilder.BuildSystemPrompt(); len(messages) > 0 &&
|
||||
messages[0].Content != newPrompt {
|
||||
messages[0].Content = newPrompt
|
||||
al.lastSystemPrompt.Store(newPrompt)
|
||||
al.promptDirty.Store(false)
|
||||
|
|
@ -2577,11 +2610,11 @@ func (al *AgentLoop) runLLMIteration(
|
|||
// make one final LLM call without tools to force a text response.
|
||||
if finalContent == "" && iteration >= maxIter {
|
||||
logger.WarnCF("agent", "Max iterations reached, forcing final response without tools",
|
||||
map[string]interface{}{
|
||||
map[string]any{
|
||||
"agent_id": agent.ID,
|
||||
"iteration": iteration,
|
||||
})
|
||||
forceResp, forceErr := agent.Provider.Chat(ctx, messages, nil, agent.Model, map[string]interface{}{
|
||||
forceResp, forceErr := agent.Provider.Chat(ctx, messages, nil, agent.Model, map[string]any{
|
||||
"max_tokens": agent.MaxTokens,
|
||||
"temperature": agent.Temperature,
|
||||
"prompt_cache_key": agent.ID,
|
||||
|
|
@ -2633,7 +2666,7 @@ func (al *AgentLoop) maybeSummarize(agent *AgentInstance, sessionKey, channel, c
|
|||
go func() {
|
||||
defer al.summarizing.Delete(summarizeKey)
|
||||
logger.InfoCF("agent", "Memory threshold reached, optimizing conversation history",
|
||||
map[string]interface{}{
|
||||
map[string]any{
|
||||
"session_key": sessionKey,
|
||||
"history_len": len(newHistory),
|
||||
"token_estimate": tokenEstimate,
|
||||
|
|
@ -3127,7 +3160,8 @@ func (al *AgentLoop) handleSessionCommand(args []string) string {
|
|||
}
|
||||
|
||||
s := al.stats.GetStats()
|
||||
return fmt.Sprintf("Session Statistics\n\nToday (%s):\n Prompts: %d\n LLM calls: %d\n Tokens: %s (in: %s, out: %s)\n\nAll time (since %s):\n Prompts: %d\n LLM calls: %d\n Tokens: %s (in: %s, out: %s)",
|
||||
return fmt.Sprintf(
|
||||
"Session Statistics\n\nToday (%s):\n Prompts: %d\n LLM calls: %d\n Tokens: %s (in: %s, out: %s)\n\nAll time (since %s):\n Prompts: %d\n LLM calls: %d\n Tokens: %s (in: %s, out: %s)",
|
||||
s.Today.Date,
|
||||
s.Today.Prompts,
|
||||
s.Today.Requests,
|
||||
|
|
@ -3369,7 +3403,7 @@ func filterInterviewTools(defs []providers.ToolDefinition) []providers.ToolDefin
|
|||
// plan is in a pre-execution state. Uses the shared interviewAllowedTools map for
|
||||
// name-level gating, then applies argument-level constraints for write-type tools
|
||||
// (MEMORY.md only) and exec (read-only commands only).
|
||||
func isToolAllowedDuringInterview(toolName string, args map[string]interface{}) bool {
|
||||
func isToolAllowedDuringInterview(toolName string, args map[string]any) bool {
|
||||
norm := tools.NormalizeToolName(toolName)
|
||||
if !interviewAllowedTools[norm] {
|
||||
return false
|
||||
|
|
|
|||
|
|
@ -1466,7 +1466,7 @@ Test
|
|||
func TestIsToolAllowedDuringInterview_FuzzyNames(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args map[string]interface{}
|
||||
args map[string]any
|
||||
want bool
|
||||
}{
|
||||
// Exact names — read tools allowed
|
||||
|
|
@ -1484,38 +1484,38 @@ func TestIsToolAllowedDuringInterview_FuzzyNames(t *testing.T) {
|
|||
{"message", nil, true},
|
||||
{"Message", nil, true},
|
||||
// Write to MEMORY.md — allowed
|
||||
{"edit_file", map[string]interface{}{"path": "/ws/memory/MEMORY.md"}, true},
|
||||
{"editfile", map[string]interface{}{"path": "/ws/memory/MEMORY.md"}, true},
|
||||
{"EditFile", map[string]interface{}{"path": "/ws/memory/MEMORY.md"}, true},
|
||||
{"edit_file", map[string]any{"path": "/ws/memory/MEMORY.md"}, true},
|
||||
{"editfile", map[string]any{"path": "/ws/memory/MEMORY.md"}, true},
|
||||
{"EditFile", map[string]any{"path": "/ws/memory/MEMORY.md"}, true},
|
||||
// Write to non-MEMORY.md — blocked
|
||||
{"edit_file", map[string]interface{}{"path": "/ws/main.go"}, false},
|
||||
{"editfile", map[string]interface{}{"path": "/ws/main.go"}, false},
|
||||
{"edit_file", map[string]any{"path": "/ws/main.go"}, false},
|
||||
{"editfile", map[string]any{"path": "/ws/main.go"}, false},
|
||||
// exec — read-only commands allowed
|
||||
{"exec", map[string]interface{}{"command": "find . -name '*.py'"}, true},
|
||||
{"exec", map[string]interface{}{"command": "ls -la"}, true},
|
||||
{"exec", map[string]interface{}{"command": "grep -r TODO ."}, true},
|
||||
{"exec", map[string]interface{}{"command": "cat README.md"}, true},
|
||||
{"exec", map[string]any{"command": "find . -name '*.py'"}, true},
|
||||
{"exec", map[string]any{"command": "ls -la"}, true},
|
||||
{"exec", map[string]any{"command": "grep -r TODO ."}, true},
|
||||
{"exec", map[string]any{"command": "cat README.md"}, true},
|
||||
// exec — cd prefix stripped
|
||||
{"exec", map[string]interface{}{"command": "cd /home/user/project && find . -type f"}, true},
|
||||
{"exec", map[string]interface{}{"command": "cd /tmp && rm -rf *"}, false},
|
||||
{"exec", map[string]any{"command": "cd /home/user/project && find . -type f"}, true},
|
||||
{"exec", map[string]any{"command": "cd /tmp && rm -rf *"}, false},
|
||||
// exec — write operators blocked
|
||||
{"exec", map[string]interface{}{"command": "find . > output.txt"}, false},
|
||||
{"exec", map[string]interface{}{"command": "ls -la >> log.txt"}, false},
|
||||
{"exec", map[string]interface{}{"command": "cat foo | tee bar.txt"}, false},
|
||||
{"exec", map[string]any{"command": "find . > output.txt"}, false},
|
||||
{"exec", map[string]any{"command": "ls -la >> log.txt"}, false},
|
||||
{"exec", map[string]any{"command": "cat foo | tee bar.txt"}, false},
|
||||
// exec — path traversal blocked
|
||||
{"exec", map[string]interface{}{"command": "cat ../../etc/passwd"}, false},
|
||||
{"exec", map[string]interface{}{"command": "find ../../"}, false},
|
||||
{"exec", map[string]interface{}{"command": "ls ../secret"}, false},
|
||||
{"exec", map[string]any{"command": "cat ../../etc/passwd"}, false},
|
||||
{"exec", map[string]any{"command": "find ../../"}, false},
|
||||
{"exec", map[string]any{"command": "ls ../secret"}, false},
|
||||
// exec — absolute paths blocked
|
||||
{"exec", map[string]interface{}{"command": "cat /etc/passwd"}, false},
|
||||
{"exec", map[string]interface{}{"command": "find /etc -name '*.conf'"}, false},
|
||||
{"exec", map[string]interface{}{"command": "ls /root"}, false},
|
||||
{"exec", map[string]any{"command": "cat /etc/passwd"}, false},
|
||||
{"exec", map[string]any{"command": "find /etc -name '*.conf'"}, false},
|
||||
{"exec", map[string]any{"command": "ls /root"}, false},
|
||||
// exec — write commands blocked
|
||||
{"exec", map[string]interface{}{"command": "rm -rf /"}, false},
|
||||
{"exec", map[string]interface{}{"command": "mv a b"}, false},
|
||||
{"exec", map[string]any{"command": "rm -rf /"}, false},
|
||||
{"exec", map[string]any{"command": "mv a b"}, false},
|
||||
// exec — no args / empty command blocked
|
||||
{"exec", nil, false},
|
||||
{"exec", map[string]interface{}{"command": ""}, false},
|
||||
{"exec", map[string]any{"command": ""}, false},
|
||||
{"Exec", nil, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
|
|
@ -1530,56 +1530,60 @@ func TestBuildArgsSnippet_ExecStripsCD(t *testing.T) {
|
|||
tests := []struct {
|
||||
name string
|
||||
tool string
|
||||
args map[string]interface{}
|
||||
args map[string]any
|
||||
workspace string
|
||||
wantSnip string
|
||||
}{
|
||||
{
|
||||
name: "exec strips cd prefix",
|
||||
tool: "exec",
|
||||
args: map[string]interface{}{"command": "cd /home/user/workspace/project/my-projects && pytest tests/test_integration.py"},
|
||||
args: map[string]any{
|
||||
"command": "cd /home/user/workspace/project/my-projects && pytest tests/test_integration.py",
|
||||
},
|
||||
workspace: "/home/user/workspace",
|
||||
wantSnip: "pytest tests/test_integration.py",
|
||||
},
|
||||
{
|
||||
name: "exec no cd prefix, flags stripped",
|
||||
tool: "exec",
|
||||
args: map[string]interface{}{"command": "ls -la"},
|
||||
args: map[string]any{"command": "ls -la"},
|
||||
workspace: "/ws",
|
||||
wantSnip: "ls",
|
||||
},
|
||||
{
|
||||
name: "exec empty command",
|
||||
tool: "exec",
|
||||
args: map[string]interface{}{},
|
||||
args: map[string]any{},
|
||||
workspace: "/ws",
|
||||
wantSnip: "{}",
|
||||
},
|
||||
{
|
||||
name: "read_file strips workspace",
|
||||
tool: "read_file",
|
||||
args: map[string]interface{}{"path": "/home/user/workspace/src/main.go"},
|
||||
args: map[string]any{"path": "/home/user/workspace/src/main.go"},
|
||||
workspace: "/home/user/workspace",
|
||||
wantSnip: "src/main.go",
|
||||
},
|
||||
{
|
||||
name: "edit_file shows path",
|
||||
tool: "edit_file",
|
||||
args: map[string]interface{}{"path": "/ws/config.json", "old_text": "old value here"},
|
||||
args: map[string]any{"path": "/ws/config.json", "old_text": "old value here"},
|
||||
workspace: "/ws",
|
||||
wantSnip: "config.json",
|
||||
},
|
||||
{
|
||||
name: "file tool long path prioritizes filename",
|
||||
tool: "read_file",
|
||||
args: map[string]interface{}{"path": "/ws/projects/terra-py-form/src/terra_py_form/hot/state/backend.py"},
|
||||
args: map[string]any{
|
||||
"path": "/ws/projects/terra-py-form/src/terra_py_form/hot/state/backend.py",
|
||||
},
|
||||
workspace: "/ws",
|
||||
wantSnip: "projects/terra-py-form/src/terra_py_form/hot/sta\u2026/backend.py",
|
||||
},
|
||||
{
|
||||
name: "unknown tool shows raw JSON",
|
||||
tool: "web_search",
|
||||
args: map[string]interface{}{"query": "hello"},
|
||||
args: map[string]any{"query": "hello"},
|
||||
workspace: "/ws",
|
||||
wantSnip: `{"query":"hello"}`,
|
||||
},
|
||||
|
|
@ -1610,19 +1614,31 @@ func TestFormatCompactEntry(t *testing.T) {
|
|||
},
|
||||
{
|
||||
name: "exec long entry truncated from end",
|
||||
entry: toolLogEntry{Name: "[2] exec", ArgsSnip: "pytest tests/integration/test_very_long_name.py", Result: "✗ 3.0s"},
|
||||
entry: toolLogEntry{
|
||||
Name: "[2] exec",
|
||||
ArgsSnip: "pytest tests/integration/test_very_long_name.py",
|
||||
Result: "✗ 3.0s",
|
||||
},
|
||||
wantMark: "✗",
|
||||
},
|
||||
{
|
||||
name: "file tool omits duration, shows filename",
|
||||
entry: toolLogEntry{Name: "[3] edit_file", ArgsSnip: "projects/terra/src/deep/nested/backend.py", Result: "✓ 0.0s"},
|
||||
entry: toolLogEntry{
|
||||
Name: "[3] edit_file",
|
||||
ArgsSnip: "projects/terra/src/deep/nested/backend.py",
|
||||
Result: "✓ 0.0s",
|
||||
},
|
||||
wantSub: "backend.py",
|
||||
wantMark: "✓",
|
||||
noTime: true,
|
||||
},
|
||||
{
|
||||
name: "file tool path truncates from start",
|
||||
entry: toolLogEntry{Name: "[4] read_file", ArgsSnip: "projects/terra-py-form/src/terra_py_form/hot/state/backend.py", Result: "✓ 0.1s"},
|
||||
entry: toolLogEntry{
|
||||
Name: "[4] read_file",
|
||||
ArgsSnip: "projects/terra-py-form/src/terra_py_form/hot/state/backend.py",
|
||||
Result: "✓ 0.1s",
|
||||
},
|
||||
wantSub: "backend.py",
|
||||
wantMark: "✓",
|
||||
noTime: true,
|
||||
|
|
@ -1745,7 +1761,7 @@ func TestExtractExecProjectDir(t *testing.T) {
|
|||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
args := map[string]interface{}{"command": tt.cmd}
|
||||
args := map[string]any{"command": tt.cmd}
|
||||
got := extractExecProjectDir(args)
|
||||
if got != tt.want {
|
||||
t.Errorf("extractExecProjectDir(%q) = %q, want %q", tt.cmd, got, tt.want)
|
||||
|
|
|
|||
|
|
@ -520,12 +520,22 @@ func (ms *MemoryStore) getInterviewContextFrom(content string) string {
|
|||
sb.WriteString("- Tooling preferences (test framework, linter, formatter, CI)\n")
|
||||
sb.WriteString("- Key commands the user already runs (build, test, deploy)\n")
|
||||
sb.WriteString("\n### Rules\n")
|
||||
sb.WriteString("- NEVER remove or overwrite the header block (`# Active Plan`, `> Task:`, `> Status:`, `> Phase:` lines). The system parses these to track state.\n")
|
||||
sb.WriteString("- After each answer, use edit_file to append findings to the ## Context section of memory/MEMORY.md.\n")
|
||||
sb.WriteString("- When you have enough information, use edit_file to add ## Phase, ## Commands, and ## Context sections BELOW the header block.\n")
|
||||
sb.WriteString("- Each step MUST use checkbox syntax: `- [ ] description`. The system parses checkboxes to track progress.\n")
|
||||
sb.WriteString(
|
||||
"- NEVER remove or overwrite the header block (`# Active Plan`, `> Task:`, `> Status:`, `> Phase:` lines). The system parses these to track state.\n",
|
||||
)
|
||||
sb.WriteString(
|
||||
"- After each answer, use edit_file to append findings to the ## Context section of memory/MEMORY.md.\n",
|
||||
)
|
||||
sb.WriteString(
|
||||
"- When you have enough information, use edit_file to add ## Phase, ## Commands, and ## Context sections BELOW the header block.\n",
|
||||
)
|
||||
sb.WriteString(
|
||||
"- Each step MUST use checkbox syntax: `- [ ] description`. The system parses checkboxes to track progress.\n",
|
||||
)
|
||||
sb.WriteString("- Organize into 2-5 phases with 3-5 steps each.\n")
|
||||
sb.WriteString("- After writing Phases, change `> Status: interviewing` to `> Status: review` via edit_file. The user must approve with /plan start before execution begins.\n")
|
||||
sb.WriteString(
|
||||
"- After writing Phases, change `> Status: interviewing` to `> Status: review` via edit_file. The user must approve with /plan start before execution begins.\n",
|
||||
)
|
||||
sb.WriteString("\n### Target Format (MANDATORY — system parses this exact structure)\n")
|
||||
sb.WriteString("\n")
|
||||
sb.WriteString("# Active Plan\n")
|
||||
|
|
|
|||
|
|
@ -435,7 +435,8 @@ func TestAgentDefaults_PlanModel_StringParse(t *testing.T) {
|
|||
if cfg.Agents.Defaults.PlanModel != "anthropic/claude-sonnet-4-6" {
|
||||
t.Errorf("PlanModel = %q, want 'anthropic/claude-sonnet-4-6'", cfg.Agents.Defaults.PlanModel)
|
||||
}
|
||||
if len(cfg.Agents.Defaults.PlanModelFallbacks) != 1 || cfg.Agents.Defaults.PlanModelFallbacks[0] != "openai/gpt-4o" {
|
||||
if len(cfg.Agents.Defaults.PlanModelFallbacks) != 1 ||
|
||||
cfg.Agents.Defaults.PlanModelFallbacks[0] != "openai/gpt-4o" {
|
||||
t.Errorf("PlanModelFallbacks = %v, want [openai/gpt-4o]", cfg.Agents.Defaults.PlanModelFallbacks)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,10 @@ func TestSanitizeBranchName(t *testing.T) {
|
|||
{" spaces ", "plan/spaces"},
|
||||
{"UPPER-case_Mix", "plan/upper-case-mix"},
|
||||
{"a/b/c", "plan/a-b-c"},
|
||||
{"very long task name that exceeds the forty character limit for safety", "plan/very-long-task-name-that-exceeds-the-for"},
|
||||
{
|
||||
"very long task name that exceeds the forty character limit for safety",
|
||||
"plan/very-long-task-name-that-exceeds-the-for",
|
||||
},
|
||||
{"---leading-trailing---", "plan/leading-trailing"},
|
||||
{"special!@#$%chars", "plan/special-chars"},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -311,12 +311,25 @@ func TestSanitizeFields(t *testing.T) {
|
|||
},
|
||||
{
|
||||
name: "sensitive keys masked",
|
||||
input: map[string]any{"token": "abc123", "api_key": "sk-xxx", "secret": "s3cr3t", "password": "pass", "authorization": "Bearer tok"},
|
||||
input: map[string]any{
|
||||
"token": "abc123",
|
||||
"api_key": "sk-xxx",
|
||||
"secret": "s3cr3t",
|
||||
"password": "pass",
|
||||
"authorization": "Bearer tok",
|
||||
},
|
||||
maskedK: []string{"token", "api_key", "secret", "password", "authorization"},
|
||||
},
|
||||
{
|
||||
name: "case insensitive",
|
||||
input: map[string]any{"Token": "abc", "API_KEY": "xyz", "Secret": "s", "PASSWORD": "p", "Authorization": "a", "Credential": "c"},
|
||||
input: map[string]any{
|
||||
"Token": "abc",
|
||||
"API_KEY": "xyz",
|
||||
"Secret": "s",
|
||||
"PASSWORD": "p",
|
||||
"Authorization": "a",
|
||||
"Credential": "c",
|
||||
},
|
||||
maskedK: []string{"Token", "API_KEY", "Secret", "PASSWORD", "Authorization", "Credential"},
|
||||
},
|
||||
{
|
||||
|
|
@ -326,7 +339,12 @@ func TestSanitizeFields(t *testing.T) {
|
|||
},
|
||||
{
|
||||
name: "mixed keys",
|
||||
input: map[string]any{"token": "sensitive", "msg_signature": "safe", "corp_secret": "sensitive2", "nonce": "safe2"},
|
||||
input: map[string]any{
|
||||
"token": "sensitive",
|
||||
"msg_signature": "safe",
|
||||
"corp_secret": "sensitive2",
|
||||
"nonce": "safe2",
|
||||
},
|
||||
maskedK: []string{"token", "corp_secret"},
|
||||
safeK: []string{"msg_signature", "nonce"},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -134,7 +134,13 @@ func (h *Handler) apiEvents(w http.ResponseWriter, r *http.Request) {
|
|||
sendSSEIfChanged(w, flusher, "skills", h.provider.ListSkills(), &lastSkills)
|
||||
sendSSEIfChanged(w, flusher, "dev", h.devStatus(), &lastDev)
|
||||
sendSSEIfChanged(w, flusher, "context", h.provider.GetContextInfo(), &lastContext)
|
||||
sendSSEIfChanged(w, flusher, "prompt", map[string]string{"prompt": h.provider.GetSystemPrompt()}, &lastPrompt)
|
||||
sendSSEIfChanged(
|
||||
w,
|
||||
flusher,
|
||||
"prompt",
|
||||
map[string]string{"prompt": h.provider.GetSystemPrompt()},
|
||||
&lastPrompt,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,7 +40,14 @@ type Handler struct {
|
|||
}
|
||||
|
||||
// NewHandler creates a new Mini App handler.
|
||||
func NewHandler(provider DataProvider, sender CommandSender, botToken string, notifier *StateNotifier, allowList []string, workspace string) *Handler {
|
||||
func NewHandler(
|
||||
provider DataProvider,
|
||||
sender CommandSender,
|
||||
botToken string,
|
||||
notifier *StateNotifier,
|
||||
allowList []string,
|
||||
workspace string,
|
||||
) *Handler {
|
||||
return &Handler{
|
||||
provider: provider,
|
||||
sender: sender,
|
||||
|
|
|
|||
|
|
@ -1086,7 +1086,7 @@ func TestExtractXMLToolCalls_MismatchedCloseTag(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestStripXMLToolCalls_MismatchedCloseTag(t *testing.T) {
|
||||
func TestStripXMLToolCalls_MismatchedCloseTag(t *testing.T) { //nolint:gosmopolitan // CJK test data
|
||||
text := `今テスト走らせるね。
|
||||
<minimax:toolcall>
|
||||
<invoke name="exec">
|
||||
|
|
|
|||
|
|
@ -48,7 +48,13 @@ func NewHTTPProviderWithOptions(apiKey, apiBase, proxy string, opts ...openai_co
|
|||
}
|
||||
}
|
||||
|
||||
func (p *HTTPProvider) Chat(ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]interface{}) (*LLMResponse, error) {
|
||||
func (p *HTTPProvider) Chat(
|
||||
ctx context.Context,
|
||||
messages []Message,
|
||||
tools []ToolDefinition,
|
||||
model string,
|
||||
options map[string]any,
|
||||
) (*LLMResponse, error) {
|
||||
resp, err := p.delegate.Chat(ctx, messages, tools, model, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
|||
|
|
@ -554,7 +554,17 @@ func normalizeModel(model, apiBase string) string {
|
|||
|
||||
prefix := strings.ToLower(model[:idx])
|
||||
switch prefix {
|
||||
case "openai", "moonshot", "nvidia", "groq", "ollama", "deepseek", "google", "openrouter", "zhipu", "minimax", "mistral":
|
||||
case "openai",
|
||||
"moonshot",
|
||||
"nvidia",
|
||||
"groq",
|
||||
"ollama",
|
||||
"deepseek",
|
||||
"google",
|
||||
"openrouter",
|
||||
"zhipu",
|
||||
"minimax",
|
||||
"mistral":
|
||||
return model[idx+1:]
|
||||
default:
|
||||
return model
|
||||
|
|
|
|||
|
|
@ -177,7 +177,8 @@ func findToolCallBlock(text string) (blockStart, blockEnd int, content string, f
|
|||
}
|
||||
// Also consume a preceding [TOOLCALL] marker if present.
|
||||
start := invokePos
|
||||
if loc := reBracketMarker.FindStringIndex(before[:start]); loc != nil && strings.TrimSpace(before[loc[1]:start]) == "" {
|
||||
if loc := reBracketMarker.FindStringIndex(before[:start]); loc != nil &&
|
||||
strings.TrimSpace(before[loc[1]:start]) == "" {
|
||||
start = loc[0]
|
||||
}
|
||||
return start, cm[1], text[invokePos:cm[0]], true
|
||||
|
|
@ -245,7 +246,7 @@ func parseInvokeElements(text string, callIdx *int) []ToolCall {
|
|||
toolName := invokeBody[nameStart : nameStart+nameEnd]
|
||||
|
||||
// Extract parameters
|
||||
args := make(map[string]interface{})
|
||||
args := make(map[string]any)
|
||||
paramRemaining := invokeBody
|
||||
for {
|
||||
pStart := strings.Index(paramRemaining, "<parameter")
|
||||
|
|
|
|||
|
|
@ -74,7 +74,14 @@ func (t *DevPreviewTool) Execute(ctx context.Context, args map[string]any) *Tool
|
|||
if err := t.manager.ActivateDevTarget(id); err != nil {
|
||||
return ErrorResult(fmt.Sprintf("failed to activate dev target: %v", err))
|
||||
}
|
||||
return SilentResult(fmt.Sprintf("Dev preview started (id=%s, name=%s). Target: %s\nUsers can view it in the Mini App Dev tab.", id, name, target))
|
||||
return SilentResult(
|
||||
fmt.Sprintf(
|
||||
"Dev preview started (id=%s, name=%s). Target: %s\nUsers can view it in the Mini App Dev tab.",
|
||||
id,
|
||||
name,
|
||||
target,
|
||||
),
|
||||
)
|
||||
|
||||
case "stop":
|
||||
if err := t.manager.DeactivateDevTarget(); err != nil {
|
||||
|
|
|
|||
|
|
@ -418,7 +418,9 @@ func (t *ExecTool) executeBg(command, cwd string) *ToolResult {
|
|||
}
|
||||
if running >= bgMaxProcesses {
|
||||
t.bgMu.Unlock()
|
||||
return ErrorResult(fmt.Sprintf("maximum background processes reached (%d). Kill an existing one first.", bgMaxProcesses))
|
||||
return ErrorResult(
|
||||
fmt.Sprintf("maximum background processes reached (%d). Kill an existing one first.", bgMaxProcesses),
|
||||
)
|
||||
}
|
||||
|
||||
t.bgNextID++
|
||||
|
|
|
|||
|
|
@ -21,7 +21,13 @@ func newBlockingProvider() *blockingProvider {
|
|||
return &blockingProvider{ready: make(chan struct{})}
|
||||
}
|
||||
|
||||
func (p *blockingProvider) Chat(ctx context.Context, _ []providers.Message, _ []providers.ToolDefinition, _ string, _ map[string]any) (*providers.LLMResponse, error) {
|
||||
func (p *blockingProvider) Chat(
|
||||
ctx context.Context,
|
||||
_ []providers.Message,
|
||||
_ []providers.ToolDefinition,
|
||||
_ string,
|
||||
_ map[string]any,
|
||||
) (*providers.LLMResponse, error) {
|
||||
close(p.ready) // signal: we are now blocking
|
||||
<-ctx.Done()
|
||||
return nil, ctx.Err()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue