mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-05-18 18:27:29 +08:00
feat(runtime): add process watch patterns, unified backup/import, pluggable context engine, token usage, and codex device login
This commit is contained in:
@@ -2,8 +2,10 @@ package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/YspCoder/clawgo/pkg/bus"
|
||||
)
|
||||
@@ -66,3 +68,63 @@ func TestProcessToolParsesStringIntegers(t *testing.T) {
|
||||
t.Fatalf("expected json list output, got %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessToolWatchPatternsMatchesLog(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
pm := NewProcessManager(t.TempDir())
|
||||
id, err := pm.Start(context.Background(), "printf 'READY\\n'; sleep 0.05", "")
|
||||
if err != nil {
|
||||
t.Fatalf("start failed: %v", err)
|
||||
}
|
||||
tool := NewProcessTool(pm)
|
||||
|
||||
out, err := tool.Execute(context.Background(), map[string]interface{}{
|
||||
"action": "watch_patterns",
|
||||
"session_id": id,
|
||||
"patterns": []interface{}{"ready"},
|
||||
"timeout_ms": 2000,
|
||||
"interval_ms": 50,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("execute failed: %v", err)
|
||||
}
|
||||
var payload map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(out), &payload); err != nil {
|
||||
t.Fatalf("invalid json output: %v (%s)", err, out)
|
||||
}
|
||||
if matched, _ := payload["matched"].(bool); !matched {
|
||||
t.Fatalf("expected matched response, got %v", payload)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessToolWatchPatternsTimesOut(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
pm := NewProcessManager(t.TempDir())
|
||||
id, err := pm.Start(context.Background(), "sleep 0.3", "")
|
||||
if err != nil {
|
||||
t.Fatalf("start failed: %v", err)
|
||||
}
|
||||
tool := NewProcessTool(pm)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
out, err := tool.Execute(ctx, map[string]interface{}{
|
||||
"action": "watch_patterns",
|
||||
"session_id": id,
|
||||
"patterns": "nomatch",
|
||||
"timeout_ms": "120",
|
||||
"interval_ms": "30",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("execute failed: %v", err)
|
||||
}
|
||||
var payload map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(out), &payload); err != nil {
|
||||
t.Fatalf("invalid json output: %v (%s)", err, out)
|
||||
}
|
||||
if timedOut, _ := payload["timed_out"].(bool); !timedOut {
|
||||
t.Fatalf("expected timed_out=true, got %v", payload)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package tools
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -11,15 +13,19 @@ type ProcessTool struct{ m *ProcessManager }
|
||||
func NewProcessTool(m *ProcessManager) *ProcessTool { return &ProcessTool{m: m} }
|
||||
func (t *ProcessTool) Name() string { return "process" }
|
||||
func (t *ProcessTool) Description() string {
|
||||
return "Manage background exec sessions: list, poll, log, kill"
|
||||
return "Manage background exec sessions: list, poll, log, kill, watch_patterns"
|
||||
}
|
||||
func (t *ProcessTool) Parameters() map[string]interface{} {
|
||||
return map[string]interface{}{"type": "object", "properties": map[string]interface{}{
|
||||
"action": map[string]interface{}{"type": "string", "description": "list|poll|log|kill"},
|
||||
"session_id": map[string]interface{}{"type": "string"},
|
||||
"offset": map[string]interface{}{"type": "integer"},
|
||||
"limit": map[string]interface{}{"type": "integer"},
|
||||
"timeout_ms": map[string]interface{}{"type": "integer"},
|
||||
"action": map[string]interface{}{"type": "string", "description": "list|poll|log|kill|watch_patterns"},
|
||||
"session_id": map[string]interface{}{"type": "string"},
|
||||
"offset": map[string]interface{}{"type": "integer"},
|
||||
"limit": map[string]interface{}{"type": "integer"},
|
||||
"timeout_ms": map[string]interface{}{"type": "integer"},
|
||||
"interval_ms": map[string]interface{}{"type": "integer"},
|
||||
"patterns": map[string]interface{}{"type": "array", "items": map[string]interface{}{"type": "string"}},
|
||||
"case_sensitive": map[string]interface{}{"type": "boolean"},
|
||||
"alert_on_exit": map[string]interface{}{"type": "boolean"},
|
||||
}, "required": []string{"action"}}
|
||||
}
|
||||
|
||||
@@ -76,7 +82,156 @@ func (t *ProcessTool) Execute(ctx context.Context, args map[string]interface{})
|
||||
}
|
||||
b, _ := json.Marshal(resp)
|
||||
return string(b), nil
|
||||
case "watch_patterns":
|
||||
patterns := MapStringListArg(args, "patterns")
|
||||
if len(patterns) == 0 {
|
||||
return "", fmt.Errorf("patterns is required")
|
||||
}
|
||||
timeout := MapIntArg(args, "timeout_ms", 30000)
|
||||
if timeout < 1 {
|
||||
timeout = 30000
|
||||
}
|
||||
interval := MapIntArg(args, "interval_ms", 250)
|
||||
if interval < 50 {
|
||||
interval = 50
|
||||
}
|
||||
if interval > timeout {
|
||||
interval = timeout
|
||||
}
|
||||
off := MapIntArg(args, "offset", 0)
|
||||
if off < 0 {
|
||||
off = 0
|
||||
}
|
||||
caseSensitive := false
|
||||
if v, ok := MapBoolArg(args, "case_sensitive"); ok {
|
||||
caseSensitive = v
|
||||
}
|
||||
alertOnExit := true
|
||||
if v, ok := MapBoolArg(args, "alert_on_exit"); ok {
|
||||
alertOnExit = v
|
||||
}
|
||||
return t.watchPatterns(ctx, sid, patterns, off, timeout, interval, caseSensitive, alertOnExit)
|
||||
default:
|
||||
return "", nil
|
||||
}
|
||||
}
|
||||
|
||||
func (t *ProcessTool) watchPatterns(ctx context.Context, sid string, patterns []string, offset, timeoutMs, intervalMs int, caseSensitive, alertOnExit bool) (string, error) {
|
||||
s, ok := t.m.Get(sid)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("session not found: %s", sid)
|
||||
}
|
||||
type watchPattern struct {
|
||||
original string
|
||||
lookup string
|
||||
}
|
||||
normalized := make([]watchPattern, 0, len(patterns))
|
||||
for _, p := range patterns {
|
||||
p = strings.TrimSpace(p)
|
||||
if p == "" {
|
||||
continue
|
||||
}
|
||||
lookup := p
|
||||
if !caseSensitive {
|
||||
lookup = strings.ToLower(p)
|
||||
}
|
||||
normalized = append(normalized, watchPattern{original: p, lookup: lookup})
|
||||
}
|
||||
if len(normalized) == 0 {
|
||||
return "", fmt.Errorf("patterns is required")
|
||||
}
|
||||
started := time.Now()
|
||||
deadline := started.Add(time.Duration(timeoutMs) * time.Millisecond)
|
||||
scanBuf := ""
|
||||
nextOffset := offset
|
||||
for {
|
||||
chunk, err := t.m.Log(sid, nextOffset, 16*1024)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if chunk != "" {
|
||||
nextOffset += len(chunk)
|
||||
scanBuf += chunk
|
||||
if len(scanBuf) > 24*1024 {
|
||||
scanBuf = scanBuf[len(scanBuf)-24*1024:]
|
||||
}
|
||||
haystack := scanBuf
|
||||
if !caseSensitive {
|
||||
haystack = strings.ToLower(haystack)
|
||||
}
|
||||
for _, pattern := range normalized {
|
||||
if strings.Contains(haystack, pattern.lookup) {
|
||||
resp := map[string]interface{}{
|
||||
"id": s.ID,
|
||||
"matched": true,
|
||||
"pattern": pattern.original,
|
||||
"running": processSessionRunning(s),
|
||||
"next_offset": nextOffset,
|
||||
"elapsed_ms": time.Since(started).Milliseconds(),
|
||||
}
|
||||
b, _ := json.Marshal(resp)
|
||||
return string(b), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
running, exitCode := processSessionState(s)
|
||||
if !running {
|
||||
resp := map[string]interface{}{
|
||||
"id": s.ID,
|
||||
"matched": false,
|
||||
"running": false,
|
||||
"exit_code": exitCode,
|
||||
"next_offset": nextOffset,
|
||||
"elapsed_ms": time.Since(started).Milliseconds(),
|
||||
}
|
||||
if alertOnExit {
|
||||
resp["event"] = "process_exited"
|
||||
}
|
||||
b, _ := json.Marshal(resp)
|
||||
return string(b), nil
|
||||
}
|
||||
now := time.Now()
|
||||
if now.After(deadline) {
|
||||
resp := map[string]interface{}{
|
||||
"id": s.ID,
|
||||
"matched": false,
|
||||
"running": true,
|
||||
"timed_out": true,
|
||||
"next_offset": nextOffset,
|
||||
"elapsed_ms": now.Sub(started).Milliseconds(),
|
||||
}
|
||||
b, _ := json.Marshal(resp)
|
||||
return string(b), nil
|
||||
}
|
||||
wait := time.Duration(intervalMs) * time.Millisecond
|
||||
if remaining := time.Until(deadline); wait > remaining {
|
||||
wait = remaining
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return "", ctx.Err()
|
||||
case <-time.After(wait):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func processSessionRunning(s *processSession) bool {
|
||||
if s == nil {
|
||||
return false
|
||||
}
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.ExitCode == nil
|
||||
}
|
||||
|
||||
func processSessionState(s *processSession) (running bool, exitCode interface{}) {
|
||||
if s == nil {
|
||||
return false, nil
|
||||
}
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
if s.ExitCode == nil {
|
||||
return true, nil
|
||||
}
|
||||
return false, *s.ExitCode
|
||||
}
|
||||
|
||||
@@ -37,6 +37,9 @@ type SubagentRun struct {
|
||||
IterationCount int `json:"iteration_count,omitempty"`
|
||||
AttemptCount int `json:"attempt_count,omitempty"`
|
||||
RestartCount int `json:"restart_count,omitempty"`
|
||||
PromptTokens int `json:"prompt_tokens,omitempty"`
|
||||
CompletionTokens int `json:"completion_tokens,omitempty"`
|
||||
TotalTokens int `json:"total_tokens,omitempty"`
|
||||
LastFailureCode string `json:"last_failure_code,omitempty"`
|
||||
ThreadID string `json:"thread_id,omitempty"`
|
||||
CorrelationID string `json:"correlation_id,omitempty"`
|
||||
@@ -872,6 +875,9 @@ func (sm *SubagentManager) applyExecutionStats(run *SubagentRun, stats *Subagent
|
||||
run.IterationCount += stats.Iterations
|
||||
run.AttemptCount += stats.Attempts
|
||||
run.RestartCount += stats.Restarts
|
||||
run.PromptTokens += stats.PromptTokens
|
||||
run.CompletionTokens += stats.CompletionTokens
|
||||
run.TotalTokens += stats.TotalTokens
|
||||
if strings.TrimSpace(stats.FailureCode) != "" {
|
||||
run.LastFailureCode = strings.TrimSpace(stats.FailureCode)
|
||||
}
|
||||
|
||||
@@ -3,10 +3,13 @@ package tools
|
||||
import "context"
|
||||
|
||||
type SubagentExecutionStats struct {
|
||||
Iterations int
|
||||
Attempts int
|
||||
Restarts int
|
||||
FailureCode string
|
||||
Iterations int
|
||||
Attempts int
|
||||
Restarts int
|
||||
PromptTokens int
|
||||
CompletionTokens int
|
||||
TotalTokens int
|
||||
FailureCode string
|
||||
}
|
||||
|
||||
type subagentExecutionStatsKey struct{}
|
||||
@@ -30,6 +33,9 @@ func RecordSubagentExecutionStats(ctx context.Context, delta SubagentExecutionSt
|
||||
stats.Iterations += delta.Iterations
|
||||
stats.Attempts += delta.Attempts
|
||||
stats.Restarts += delta.Restarts
|
||||
stats.PromptTokens += delta.PromptTokens
|
||||
stats.CompletionTokens += delta.CompletionTokens
|
||||
stats.TotalTokens += delta.TotalTokens
|
||||
if delta.FailureCode != "" {
|
||||
stats.FailureCode = delta.FailureCode
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user