Skip to content

Commit 1cb7b60

Browse files
metiu1claude
andcommitted
v0.3.54: fix read_file line ranges, tool-name aliases, always force a final reply
- read_file now honors line_start/line_end (and offset/limit) instead of always returning the file head, so the model can navigate large files instead of re-reading the same chunk until it runs out of rounds. Params are advertised in both the builtin and coding tool defs. - Map hallucinated tool names (search→grep_search, print_tree/tree→list_directory, find/glob→glob_search, cat→read_file) onto the real tools so the call works. - Fixed the coding prompt to name the real tools (glob_search/grep_search). - The cloud tool loop now always does a final tool-less completion after running out of rounds, even if some interim text leaked, so the turn never ends empty. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 59f8180 commit 1cb7b60

8 files changed

Lines changed: 106 additions & 16 deletions

File tree

vortelio-pip/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "vortelio"
7-
version = "0.3.53"
7+
version = "0.3.54"
88
description = "Local-first AI platform. Run LLMs, generate images & video, transcribe audio, create 3D — on your own machine. OpenAI & Ollama API compatible. Apache 2.0."
99
readme = "README.md"
1010
requires-python = ">=3.8"
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.3.53"
1+
__version__ = "0.3.54"

vortelio-pip/src/vortelio_cli/__main__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import urllib.request
1111
from pathlib import Path
1212

13-
VERSION = "0.3.53"
13+
VERSION = "0.3.54"
1414
RELEASE_BASE = os.environ.get(
1515
"VORTELIO_RELEASE_BASE",
1616
f"https://github.com/metiu1/Vortelio/releases/download/v{VERSION}",
5.5 KB
Binary file not shown.

vortelio/internal/cloud/cloud.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -638,10 +638,17 @@ func chatOpenAIWithTools(p Provider, apiKey string, messages []Message, opts *To
638638
}
639639
}
640640

641-
// Rounds exhausted while the model was still calling tools and never wrote a
642-
// textual answer → the user would otherwise see silence. Force one final
643-
// completion with tools disabled so there is always a reply.
644-
if finalContent.Len() == 0 {
641+
// Reaching here means every round ended in tool calls and we ran out of
642+
// rounds without a concluding answer (a natural finish returns inside the
643+
// loop). Force one final completion with tools disabled so the user always
644+
// gets a real reply instead of silence — even if a little interim text leaked.
645+
{
646+
if finalContent.Len() > 0 {
647+
finalContent.WriteString("\n\n")
648+
if onToken != nil {
649+
onToken("\n\n")
650+
}
651+
}
645652
body := map[string]interface{}{"model": p.DefaultModel, "messages": msgs, "stream": true}
646653
data, _ := json.Marshal(body)
647654
if req, err := http.NewRequest("POST", p.BaseURL, bytes.NewReader(data)); err == nil {

vortelio/internal/runtime/tools.go

Lines changed: 64 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,31 @@ func (c *CompositeProvider) Tools() []ToolDef {
9797
return all
9898
}
9999

100+
// toolAliases maps common names models hallucinate (used to other agents) onto
101+
// the real tool names, so the call succeeds instead of wasting a round.
102+
var toolAliases = map[string]string{
103+
"search": "grep_search",
104+
"grep": "grep_search",
105+
"find": "glob_search",
106+
"glob": "glob_search",
107+
"tree": "list_directory",
108+
"print_tree": "list_directory",
109+
"ls": "list_directory",
110+
"cat": "read_file",
111+
"read": "read_file",
112+
}
113+
100114
func (c *CompositeProvider) Execute(name, args string) (string, error) {
115+
if alias, ok := toolAliases[name]; ok {
116+
// Only remap if a provider actually offers the canonical tool.
117+
for _, p := range c.providers {
118+
for _, t := range p.Tools() {
119+
if t.Function.Name == alias {
120+
name = alias
121+
}
122+
}
123+
}
124+
}
101125
for _, p := range c.providers {
102126
for _, t := range p.Tools() {
103127
if t.Function.Name == name {
@@ -150,8 +174,8 @@ func BuiltinTools() []ToolDef {
150174
Type: "function",
151175
Function: ToolFuncDef{
152176
Name: "read_file",
153-
Description: "Reads the contents of a file from the local filesystem.",
154-
Parameters: json.RawMessage(`{"type":"object","properties":{"path":{"type":"string","description":"Absolute or relative path to the file"}},"required":["path"]}`),
177+
Description: "Reads a text file. Optionally read only a line range with line_start/line_end (1-based, inclusive) to navigate large files.",
178+
Parameters: json.RawMessage(`{"type":"object","properties":{"path":{"type":"string","description":"Absolute or relative path to the file"},"line_start":{"type":"integer","description":"First line to return (1-based, inclusive). Optional."},"line_end":{"type":"integer","description":"Last line to return (1-based, inclusive). Optional."}},"required":["path"]}`),
155179
},
156180
},
157181
{
@@ -620,6 +644,12 @@ func evalFunc(name string, args []float64) (float64, error) {
620644
func toolReadFile(argsJSON string) (string, error) {
621645
var args struct {
622646
Path string `json:"path"`
647+
// Accept several common aliases models use for line ranges so navigation
648+
// works instead of silently re-reading the file head.
649+
LineStart *int `json:"line_start"`
650+
LineEnd *int `json:"line_end"`
651+
Offset *int `json:"offset"`
652+
Limit *int `json:"limit"`
623653
}
624654
if err := json.Unmarshal([]byte(argsJSON), &args); err != nil || args.Path == "" {
625655
return "", fmt.Errorf("path is required")
@@ -629,16 +659,46 @@ func toolReadFile(argsJSON string) (string, error) {
629659
return "", fmt.Errorf("cannot read file %q: %w", args.Path, err)
630660
}
631661
content := string(data)
662+
lines := strings.Split(content, "\n")
663+
total := len(lines)
664+
665+
// Resolve a 1-based inclusive [start,end] window from whichever params were
666+
// given (line_start/line_end, or offset+limit).
667+
start, end := 1, total
668+
if args.LineStart != nil {
669+
start = *args.LineStart
670+
} else if args.Offset != nil {
671+
start = *args.Offset + 1
672+
}
673+
if args.LineEnd != nil {
674+
end = *args.LineEnd
675+
} else if args.Limit != nil {
676+
end = start + *args.Limit - 1
677+
}
678+
ranged := args.LineStart != nil || args.LineEnd != nil || args.Offset != nil || args.Limit != nil
679+
if start < 1 {
680+
start = 1
681+
}
682+
if end > total {
683+
end = total
684+
}
685+
if ranged && start <= end {
686+
content = strings.Join(lines[start-1:end], "\n")
687+
}
688+
632689
const maxLen = 32000
633690
truncated := false
634691
if len(content) > maxLen {
635692
content = content[:maxLen]
636693
truncated = true
637694
}
638-
out := map[string]interface{}{"path": args.Path, "content": content}
695+
out := map[string]interface{}{"path": args.Path, "content": content, "total_lines": total}
696+
if ranged {
697+
out["line_start"], out["line_end"] = start, end
698+
}
639699
if truncated {
640700
out["truncated"] = true
641-
out["note"] = fmt.Sprintf("file truncated to %d chars", maxLen)
701+
out["note"] = fmt.Sprintf("content truncated to %d chars; request a smaller line range", maxLen)
642702
}
643703
b, _ := json.Marshal(out)
644704
return string(b), nil

vortelio/internal/server/server_agentic.go

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ func CodingSystemPrompt(autonomous bool) string {
198198
"Sei orientato al progetto corrente: le richieste dell'utente riguardano quasi sempre i file e il codice " +
199199
"di QUESTA cartella, non attività generiche. " +
200200
"Prima di rispondere su \"il progetto\", \"questo\", \"qui\" o un file citato, USA gli strumenti " +
201-
"(list_directory, read_file, glob, grep) per guardare i file reali: non indovinare e non inventare contenuti. " +
201+
"(list_directory, read_file, glob_search, grep_search) per guardare i file reali: non indovinare e non inventare contenuti. " +
202202
"Per modificare il progetto usa write_file / edit_file con percorsi relativi alla cartella di lavoro e " +
203203
"riferisci sempre il percorso esatto. Non affermare di aver creato o cambiato un file se non hai chiamato lo strumento. " +
204204
"Hai anche strumenti web (web_search) e di generazione media (immagini/audio/video/3D): usali solo quando " +
@@ -583,8 +583,8 @@ func newCodingProvider(cfg *AgenticConfig, emit rt.ToolEventEmitter) *codingProv
583583

584584
func (c *codingProvider) Tools() []rt.ToolDef {
585585
defs := []rt.ToolDef{
586-
toolDef("read_file", "Read a UTF-8 text file from the workspace.",
587-
`{"type":"object","properties":{"path":{"type":"string","description":"File path, relative to the workspace root or absolute."}},"required":["path"]}`),
586+
toolDef("read_file", "Read a UTF-8 text file from the workspace. Use line_start/line_end (1-based, inclusive) to read only part of a large file.",
587+
`{"type":"object","properties":{"path":{"type":"string","description":"File path, relative to the workspace root or absolute."},"line_start":{"type":"integer","description":"First line (1-based, inclusive). Optional."},"line_end":{"type":"integer","description":"Last line (1-based, inclusive). Optional."}},"required":["path"]}`),
588588
toolDef("list_directory", "List files and folders at a path in the workspace.",
589589
`{"type":"object","properties":{"path":{"type":"string","description":"Directory path. Defaults to workspace root."}},"required":[]}`),
590590
toolDef("glob_search", "Find files matching a glob pattern (e.g. **/*.go).",
@@ -720,7 +720,9 @@ func (c *codingProvider) resolvePath(p string) (string, error) {
720720

721721
func (c *codingProvider) readFile(argsJSON string) (string, error) {
722722
var a struct {
723-
Path string `json:"path"`
723+
Path string `json:"path"`
724+
LineStart *int `json:"line_start"`
725+
LineEnd *int `json:"line_end"`
724726
}
725727
json.Unmarshal([]byte(argsJSON), &a)
726728
full, err := c.resolvePath(a.Path)
@@ -734,6 +736,27 @@ func (c *codingProvider) readFile(argsJSON string) (string, error) {
734736
if len(data) > 200*1024 {
735737
data = data[:200*1024]
736738
}
739+
// Honor an optional 1-based inclusive line range so the model can navigate
740+
// large files instead of re-reading the head.
741+
if a.LineStart != nil || a.LineEnd != nil {
742+
lines := strings.Split(string(data), "\n")
743+
start, end := 1, len(lines)
744+
if a.LineStart != nil {
745+
start = *a.LineStart
746+
}
747+
if a.LineEnd != nil {
748+
end = *a.LineEnd
749+
}
750+
if start < 1 {
751+
start = 1
752+
}
753+
if end > len(lines) {
754+
end = len(lines)
755+
}
756+
if start <= end {
757+
return strings.Join(lines[start-1:end], "\n"), nil
758+
}
759+
}
737760
return string(data), nil
738761
}
739762

vortelio/internal/version/version.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@ package version
33
// Version è impostabile a build time via:
44
//
55
// -ldflags "-X github.com/vortelio/vortelio/internal/version.Version=X.Y.Z"
6-
var Version = "0.3.53"
6+
var Version = "0.3.54"

0 commit comments

Comments
 (0)