Skip to content

feat(chat,agent): improve usability with summarization, new tools, and readline#128

Open
toasterbook88 wants to merge 4 commits into
mainfrom
fix/agent-tool-model-selection
Open

feat(chat,agent): improve usability with summarization, new tools, and readline#128
toasterbook88 wants to merge 4 commits into
mainfrom
fix/agent-tool-model-selection

Conversation

@toasterbook88
Copy link
Copy Markdown
Owner

This PR addresses critical usability issues discovered during evaluation of axis chat and axis agent commands:

Changes

Tool Output Summarization

  • axis_status, axis_facts, and axis_place now return human-readable summaries instead of raw JSON
  • Prevents local LLM hangs on large cluster snapshots (~20KB JSON → ~300-600 chars)

New Tools (4)

  • axis_summary: ultra-compact one-line cluster overview
  • axis_reservations: lists active reservations from state
  • read_file: reads files with path validation (capped at 8000 chars)
  • list_directory: lists directory entries with path validation

Execution Transparency

  • Agent now prints tool calls and one-line result summaries to stderr
  • Replaces opaque X chars returned messages

Conversation Persistence

  • Added --resume flag to both axis chat and axis agent
  • Saves conversation history to ~/.axis/chat-history.json and ~/.axis/agent-history.json
  • System messages are stripped on save to avoid duplication

Readline REPL

  • Replaced primitive bufio.Scanner with github.com/chzyer/readline
  • Arrow keys, history navigation, graceful ctrl+c exit
  • Plain fallback for non-TTY environments

Safety

  • Path validation prevents directory traversal (.. and absolute paths rejected)
  • Updated isReadOnlyTool to include new read-only tools

Testing

  • All tests pass (go test -race ./...)
  • Lint passes (make lint)
  • Coverage passes (make coverage) — total 72.7%
  • Added tests for new tools and persistence layer

Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com

William and others added 3 commits May 18, 2026 10:07
- Bump version to 0.10.3
- Refresh docs/current-state.md for v0.10.3
- Update summary golden files for version bump
- Add docs/roadmap-status.md with final v9 status
- Add CHANGELOG entry for v0.10.3

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The agent relies on tool calling, but choosePreferredModel() fell back to
alphabetically first installed model which was often gemma3n:e2b. That
model does not support /api/chat tools, producing a 400 Bad Request.

- Expand recommendedLocalModels to include tool-capable variants.
- Add toolCapablePrefixes list of known tool-capable families.
- Add nonToolFamilies exclusion list to skip embedding/vision variants.
- Update choosePreferredModel to prefer tool-capable families over all
  others when falling back to any installed model.
- Improve 400 error message to suggest tool-capable alternatives.
- Add tests for all new paths.

All existing tests continue to pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…d readline

This commit addresses critical usability issues discovered during evaluation
of axis chat and axis agent commands:

- Tool outputs are now human-readable summaries instead of raw JSON,
  preventing local LLM hangs on large cluster snapshots.
- Added 4 new tools: axis_summary, axis_reservations, read_file,
  list_directory.
- Added tool execution transparency: prints tool calls and one-line
  result summaries to stderr.
- Added conversation persistence with --resume flag for both chat and agent.
- Replaced primitive bufio.Scanner REPLs with readline-based editors
  supporting arrow keys, history, and ctrl+c graceful exit.
- Added path validation to file tools to prevent directory traversal.
- Updated isReadOnlyTool to include new read-only tools.
- Added tests for new tools and persistence.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request updates AXIS to v0.10.3, introducing conversation persistence with a new --resume flag and an enhanced REPL experience using the readline library. The agent's capabilities are expanded with new tools for file and directory access, and existing tools are refactored to return human-readable summaries instead of raw JSON to optimize LLM context usage. Furthermore, the model auto-selection logic now prioritizes tool-capable families to avoid 400 errors. Review feedback highlights several areas for improvement, including the need for LimitReader to prevent OOM when reading large files, resolving symbolic links during path validation to prevent directory traversal, ensuring UTF-8 safety during string truncation, and hardening file permissions for stored history.

Comment thread internal/agent/tools.go Outdated
if err != nil {
return "", err
}
data, err := os.ReadFile(clean)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Using os.ReadFile on a path provided by the user (via the LLM) poses a risk of Out-Of-Memory (OOM) crashes if the file is extremely large. Since the output is truncated to 8000 characters anyway, it is safer to use a LimitReader to avoid reading the entire file into memory.

Comment thread internal/agent/tools.go
Comment on lines +288 to +306
func validateToolPath(p string) (string, error) {
cwd, err := os.Getwd()
if err != nil {
return "", fmt.Errorf("cannot determine working directory: %w", err)
}
clean := filepath.Clean(p)
if !filepath.IsAbs(clean) {
clean = filepath.Join(cwd, clean)
}
// Ensure the resolved path is within cwd.
rel, err := filepath.Rel(cwd, clean)
if err != nil {
return "", fmt.Errorf("invalid path %q: %w", p, err)
}
if strings.HasPrefix(rel, "..") {
return "", fmt.Errorf("path %q escapes working directory", p)
}
return clean, nil
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

The current path validation is lexical and does not account for symbolic links. An attacker could potentially use a symlink within the working directory that points to a sensitive file outside of it. To properly prevent directory traversal, you should resolve symlinks using filepath.EvalSymlinks before performing the bounds check.

Comment thread internal/agent/tools.go Outdated
content := string(data)
const maxFileSize = 8000
if len(content) > maxFileSize {
content = content[:maxFileSize] + "\n... [truncated to 8000 chars]"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Slicing a string by byte index (e.g., content[:maxFileSize]) is unsafe for UTF-8 encoded strings as it can split a multi-byte character. Consider using the truncate helper function (once updated to be UTF-8 safe) to ensure valid boundaries.

Comment thread internal/agent/tools.go Outdated
Comment on lines 274 to 280
for _, e := range entries {
name := e.Name()
if e.IsDir() {
name += "/"
}
b.WriteString(name + "\n")
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The directory listing is not truncated. If a directory contains a very large number of entries, this tool will return an excessively long string, which could exceed the LLM's context window. Per the general rules, long lists should be truncated with an indication of the remaining count.

References
  1. When displaying a list of items in a user-facing message (e.g., warnings, logs), truncate the list if it can become excessively long and indicate that it has been truncated, for example by showing a count of the remaining items.

Comment on lines +127 to +135
func truncate(s string, maxLen int) string {
if len(s) <= maxLen {
return s
}
if maxLen <= 3 {
return s[:maxLen]
}
return s[:maxLen-3] + "..."
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Slicing a string by byte index is unsafe for UTF-8 encoded strings as it can split a multi-byte character. It is better to convert the string to a slice of runes to find a safe truncation point.

Comment thread internal/chat/persist.go Outdated

// PersistPath returns the default path for conversation history files.
func PersistPath(name string) string {
home, _ := os.UserHomeDir()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The error from os.UserHomeDir() is ignored. If the home directory cannot be determined, the history will be saved in a .axis directory relative to the current working directory, which is likely unintended.

Comment thread internal/chat/persist.go Outdated
Comment on lines +35 to +38
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
return fmt.Errorf("create history directory: %w", err)
}
if err := os.WriteFile(path, data, 0644); err != nil {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

The conversation history files and their parent directory are created with broad permissions (0755 and 0644). Since these files may contain sensitive information, it is recommended to restrict access to the current user only using 0700 for directories and 0600 for files.

- read_file: use io.LimitReader instead of os.ReadFile to prevent OOM
- validateToolPath: resolve symlinks with filepath.EvalSymlinks before
  directory traversal bounds check
- read_file: UTF-8 safe truncation using rune-based truncateRune helper
- list_directory: truncate output to max 100 entries with remaining count
- summarize.go/truncate: operate on runes instead of bytes for UTF-8 safety
- persist.go: handle os.UserHomeDir() error; restrict permissions to 0700/0600
- Guard all PersistPath usages against empty path (fallback to no persistence)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant