Combines three repositories into a single workspace: - go-agent → pkg/orchestrator (Clotho), pkg/jobrunner, pkg/loop, cmd/ - go-agentic → pkg/lifecycle (allowance, sessions, plans, dispatch) - php-devops → repos.yaml, setup.sh, scripts/, .core/ Module path: forge.lthn.ai/core/agent All packages build, all tests pass. Co-Authored-By: Virgil <virgil@lethean.io>
49 lines
1.3 KiB
Go
49 lines
1.3 KiB
Go
package loop
|
|
|
|
import (
|
|
"encoding/json"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
var toolBlockRe = regexp.MustCompile("(?s)```tool\\s*\n(.*?)\\s*```")
|
|
|
|
// ParseToolCalls extracts tool invocations from fenced ```tool blocks in
|
|
// model output. Only blocks tagged "tool" are matched; other fenced blocks
|
|
// (```go, ```json, etc.) pass through untouched. Malformed JSON is silently
|
|
// skipped. Returns the parsed calls and the cleaned text with tool blocks
|
|
// removed.
|
|
func ParseToolCalls(output string) ([]ToolUse, string) {
|
|
matches := toolBlockRe.FindAllStringSubmatchIndex(output, -1)
|
|
if len(matches) == 0 {
|
|
return nil, output
|
|
}
|
|
|
|
var calls []ToolUse
|
|
cleaned := output
|
|
|
|
// Walk matches in reverse so index arithmetic stays valid after each splice.
|
|
for i := len(matches) - 1; i >= 0; i-- {
|
|
m := matches[i]
|
|
fullStart, fullEnd := m[0], m[1]
|
|
bodyStart, bodyEnd := m[2], m[3]
|
|
|
|
body := strings.TrimSpace(output[bodyStart:bodyEnd])
|
|
if body == "" {
|
|
cleaned = cleaned[:fullStart] + cleaned[fullEnd:]
|
|
continue
|
|
}
|
|
|
|
var call ToolUse
|
|
if err := json.Unmarshal([]byte(body), &call); err != nil {
|
|
cleaned = cleaned[:fullStart] + cleaned[fullEnd:]
|
|
continue
|
|
}
|
|
|
|
calls = append([]ToolUse{call}, calls...)
|
|
cleaned = cleaned[:fullStart] + cleaned[fullEnd:]
|
|
}
|
|
|
|
cleaned = strings.TrimSpace(cleaned)
|
|
return calls, cleaned
|
|
}
|