Files
kandev/apps/backend/cmd/mock-agent/handler.go
T
2026-02-07 17:48:04 +00:00

264 lines
8.0 KiB
Go

package main
import (
"bufio"
"encoding/json"
"fmt"
"math/rand"
"strings"
"time"
)
// delayRange returns min/max delay in milliseconds based on model name.
func delayRange(model string) (int, int) {
switch model {
case "mock-fast":
return 10, 50
case "mock-slow":
return 500, 3000
default:
return 100, 500
}
}
// randomDelay sleeps for a random duration within the model's delay range.
func randomDelay(model string) {
lo, hi := delayRange(model)
ms := lo + rand.Intn(hi-lo+1)
time.Sleep(time.Duration(ms) * time.Millisecond)
}
// fixedDelay sleeps for a fixed duration (for e2e scenarios).
func fixedDelay(ms int) {
time.Sleep(time.Duration(ms) * time.Millisecond)
}
// handleUserPrompt routes a user prompt to the appropriate sequence generator.
func handleUserPrompt(enc *json.Encoder, scanner *bufio.Scanner, prompt, model string) {
prompt = strings.TrimSpace(prompt)
// Emit system message at the start of every turn
emitSystemMessage(enc)
// Some commands emit their own result; track whether we need the default one.
customResult := false
switch {
case strings.EqualFold(prompt, "all"):
emitAllTypes(enc, scanner, model)
case strings.EqualFold(prompt, "/error"):
emitError(enc, model)
customResult = true
case strings.EqualFold(prompt, "/slow") || strings.HasPrefix(strings.ToLower(prompt), "/slow "):
emitSlowResponse(enc, scanner, prompt, model)
case strings.EqualFold(prompt, "/thinking"):
emitThinkingSequence(enc, model)
case strings.HasPrefix(prompt, "/tool:"):
toolName := strings.TrimPrefix(prompt, "/tool:")
emitSpecificTool(enc, scanner, strings.TrimSpace(toolName), model)
case strings.HasPrefix(prompt, "/subagent"):
emitSubagentSequence(enc, scanner, model)
case strings.HasPrefix(prompt, "/e2e:"):
scenarioName := strings.TrimPrefix(prompt, "/e2e:")
emitPredefinedScenario(enc, scanner, strings.TrimSpace(scenarioName))
// e2e:error emits its own result
if strings.TrimSpace(scenarioName) == "error" {
customResult = true
}
case strings.HasPrefix(prompt, "/todo"):
emitTodoSequence(enc, model)
default:
emitRandomResponse(enc, scanner, prompt, model)
}
if !customResult {
emitResult(enc, false, "")
}
}
// emitSystemMessage writes the system message to stdout.
func emitSystemMessage(enc *json.Encoder) {
_ = enc.Encode(SystemMsg{
Type: TypeSystem,
SessionID: sessionID,
SessionStatus: "active",
})
}
// emitResult writes the final result message to stdout.
func emitResult(enc *json.Encoder, isError bool, errText string) {
var resultJSON json.RawMessage
if isError {
resultJSON, _ = json.Marshal(errText)
} else {
resultJSON, _ = json.Marshal(ResultData{
Text: "Mock agent completed successfully.",
SessionID: sessionID,
})
}
_ = enc.Encode(ResultMsg{
Type: TypeResult,
Result: resultJSON,
CostUSD: 0.0042,
DurationMS: 1500,
DurationAPIMS: 1200,
IsError: isError,
NumTurns: 1,
TotalInputTokens: 1500,
TotalOutputTokens: 500,
ModelUsage: map[string]ModelUsageStats{
"mock-default": {ContextWindow: 200000},
},
})
}
// emitError emits an error result.
func emitError(enc *json.Encoder, model string) {
randomDelay(model)
emitTextBlock(enc, "Simulating an error condition...", "")
randomDelay(model)
emitResult(enc, true, "Mock error: something went wrong during processing")
// Return without emitting the normal result (handler skips the final emitResult
// because we already emitted one). We handle this by making the caller check.
}
// emitSlowResponse generates a response with configurable total duration.
// Accepts "/slow" (defaults to 5s) or "/slow <duration>" (e.g. "/slow 60s", "/slow 2m").
func emitSlowResponse(enc *json.Encoder, _ *bufio.Scanner, prompt, model string) {
totalDuration := 5 * time.Second
parts := strings.Fields(prompt)
if len(parts) >= 2 {
if d, err := time.ParseDuration(parts[1]); err == nil && d > 0 {
totalDuration = d
}
}
// Divide total duration into steps
steps := 5
stepDelay := totalDuration / time.Duration(steps)
emitThinking(enc, model)
time.Sleep(stepDelay)
emitTextBlock(enc, fmt.Sprintf("Running slow response (%s total)...", totalDuration), "")
time.Sleep(stepDelay)
emitReadFile(enc, model)
time.Sleep(stepDelay)
emitCodeSearch(enc, model)
time.Sleep(stepDelay)
emitTextBlock(enc, fmt.Sprintf("Slow response complete after %s.", totalDuration), "")
time.Sleep(stepDelay)
}
// emitRandomResponse generates a random mix of 2-5 events.
func emitRandomResponse(enc *json.Encoder, _ *bufio.Scanner, prompt, model string) {
generators := []func(){
func() { emitThinking(enc, model) },
func() { emitTextBlock(enc, "I'll help you with that. Let me look into it.", "") },
func() { emitReadFile(enc, model) },
func() { emitCodeSearch(enc, model) },
func() { emitWebFetch(enc, model) },
}
// Always start with thinking
emitThinking(enc, model)
randomDelay(model)
// Pick 1-4 more random events
count := 1 + rand.Intn(4)
for i := 0; i < count; i++ {
idx := rand.Intn(len(generators))
generators[idx]()
randomDelay(model)
}
// End with a text summary
emitTextBlock(enc, "I've completed the analysis of your request: \""+prompt+"\". Everything looks good!", "")
}
// emitAllTypes emits one of every message type.
func emitAllTypes(enc *json.Encoder, scanner *bufio.Scanner, model string) {
emitThinking(enc, model)
randomDelay(model)
emitTextBlock(enc, "Starting comprehensive demonstration of all message types...", "")
randomDelay(model)
emitReadFile(enc, model)
randomDelay(model)
emitEditFile(enc, scanner, model)
randomDelay(model)
emitShellExec(enc, scanner, model)
randomDelay(model)
emitCodeSearch(enc, model)
randomDelay(model)
emitSubagent(enc, scanner, model)
randomDelay(model)
emitTodo(enc, model)
randomDelay(model)
emitWebFetch(enc, model)
randomDelay(model)
emitTextBlock(enc, "All message types demonstrated successfully!", "")
}
// emitThinkingSequence emits extended thinking/reasoning blocks.
func emitThinkingSequence(enc *json.Encoder, model string) {
thoughts := []string{
"Let me analyze this problem step by step...",
"First, I need to consider the architecture and how the components interact.",
"The key insight is that we need to handle both synchronous and asynchronous flows.",
"I should also consider edge cases: what happens when the input is empty? What about concurrent access?",
"After careful analysis, I believe the best approach is to use a channel-based pattern with proper synchronization.",
}
for _, thought := range thoughts {
randomDelay(model)
emitThinkingBlock(enc, thought, "")
}
randomDelay(model)
emitTextBlock(enc, "After careful reasoning, here is my analysis:\n\n1. The architecture is sound\n2. Error handling covers edge cases\n3. The implementation follows Go best practices", "")
}
// emitSpecificTool emits a single specific tool call.
func emitSpecificTool(enc *json.Encoder, scanner *bufio.Scanner, toolName, model string) {
switch strings.ToLower(toolName) {
case "read":
emitReadFile(enc, model)
case "edit":
emitEditFile(enc, scanner, model)
case "exec", "bash":
emitShellExec(enc, scanner, model)
case "search", "grep":
emitCodeSearch(enc, model)
case "webfetch", "web":
emitWebFetch(enc, model)
default:
emitTextBlock(enc, "Unknown tool: "+toolName+". Available: read, edit, exec, search, webfetch", "")
}
}
// emitTodoSequence emits a todo management sequence.
func emitTodoSequence(enc *json.Encoder, model string) {
emitThinking(enc, model)
randomDelay(model)
emitTextBlock(enc, "I'll create a task list for this work.", "")
randomDelay(model)
emitTodo(enc, model)
randomDelay(model)
emitTextBlock(enc, "Task list has been updated.", "")
}
// emitSubagentSequence emits a subagent Task sequence.
func emitSubagentSequence(enc *json.Encoder, scanner *bufio.Scanner, model string) {
emitThinking(enc, model)
randomDelay(model)
emitTextBlock(enc, "I'll delegate this to a subagent for parallel processing.", "")
randomDelay(model)
emitSubagent(enc, scanner, model)
randomDelay(model)
emitTextBlock(enc, "Subagent task completed successfully.", "")
}