feat(commands): refactor to /prefix registry, add /clear and /compact
- Migrate all : prefix commands (:cmd, :pico, :hipico, :edit, :usage, :help) to / prefix commands registered in the unified command registry - Remove handleExtensionCommand and handleModeCommand from loop.go - Add new commands: /clear (delete all chat history), /compact (summarize/compress history) - Add new commands: /cmd, /pico, /hipico, /edit, /usage as Definition in pkg/commands/ - Extend Runtime with session-mode callbacks (GetSessionMode, SetModeCmd, SetModePico), file editing (EditFile, GetWorkDir, GetWorkspace), token stats (GetTokenUsage), one-shot LLM query (RunOneShot), and session management (ClearSession, CompactSession) - All commands are auto-discoverable via /help and Telegram bot menu - Update editUsage() and handleEditCommand to use /edit prefix Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
1ddb5b00c6
commit
03bc632b3c
8 changed files with 327 additions and 157 deletions
|
|
@ -634,11 +634,16 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage)
|
|||
|
||||
route, agent, routeErr := al.resolveMessageRoute(msg)
|
||||
|
||||
// Compute session key upfront so session-scoped commands (/cmd, /pico, /clear…)
|
||||
// get the same key that the agent loop will use. Falls back to msg.SessionKey when
|
||||
// routing fails, which is fine — context-dependent commands guard against nil agent.
|
||||
sessionKey := resolveScopeKey(route, msg.SessionKey)
|
||||
|
||||
// Commands are checked before requiring a successful route.
|
||||
// Global commands (/help, /show, /switch) work even when routing fails;
|
||||
// context-dependent commands check their own Runtime fields and report
|
||||
// "unavailable" when the required capability is nil.
|
||||
if response, handled := al.handleCommand(ctx, msg, agent); handled {
|
||||
if response, handled := al.handleCommand(ctx, msg, agent, sessionKey); handled {
|
||||
return response, nil
|
||||
}
|
||||
|
||||
|
|
@ -653,49 +658,18 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage)
|
|||
}
|
||||
}
|
||||
|
||||
// Resolve session key from route, while preserving explicit agent-scoped keys.
|
||||
scopeKey := resolveScopeKey(route, msg.SessionKey)
|
||||
sessionKey := scopeKey
|
||||
|
||||
logger.InfoCF("agent", "Routed message",
|
||||
map[string]any{
|
||||
"agent_id": agent.ID,
|
||||
"scope_key": scopeKey,
|
||||
"scope_key": sessionKey,
|
||||
"session_key": sessionKey,
|
||||
"matched_by": route.MatchedBy,
|
||||
"route_agent": route.AgentID,
|
||||
"route_channel": route.Channel,
|
||||
})
|
||||
|
||||
// Handle mode-switching commands (:cmd, :pico, :hipico)
|
||||
content := strings.TrimSpace(msg.Content)
|
||||
if strings.HasPrefix(content, ":") {
|
||||
if response, handled := al.handleModeCommand(content, sessionKey, agent); handled {
|
||||
return response, nil
|
||||
}
|
||||
// :hipico <msg> falls through here — one-shot LLM call, stays in modeCmd
|
||||
if strings.HasPrefix(content, ":hipico") {
|
||||
userMessage := strings.TrimSpace(strings.TrimPrefix(content, ":hipico"))
|
||||
workDir := al.getSessionWorkDir(sessionKey)
|
||||
if workDir == "" {
|
||||
workDir = agent.Workspace
|
||||
}
|
||||
hipicoSessionKey := sessionKey + ":hipico"
|
||||
return al.runAgentLoop(ctx, agent, processOptions{
|
||||
SessionKey: hipicoSessionKey,
|
||||
Channel: msg.Channel,
|
||||
ChatID: msg.ChatID,
|
||||
UserMessage: userMessage,
|
||||
Media: msg.Media,
|
||||
DefaultResponse: defaultResponse,
|
||||
EnableSummary: false,
|
||||
SendResponse: false,
|
||||
WorkingDir: workDir,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Dispatch based on current session mode
|
||||
content := strings.TrimSpace(msg.Content)
|
||||
switch al.getSessionMode(sessionKey) {
|
||||
case modeCmd:
|
||||
return al.executeCmdMode(ctx, agent, content, sessionKey, msg.Channel, msg.ChatID)
|
||||
|
|
@ -1712,14 +1686,8 @@ func (al *AgentLoop) handleCommand(
|
|||
ctx context.Context,
|
||||
msg bus.InboundMessage,
|
||||
agent *AgentInstance,
|
||||
sessionKey string,
|
||||
) (string, bool) {
|
||||
content := strings.TrimSpace(msg.Content)
|
||||
|
||||
// Handle : prefixed extension commands (work across all channels)
|
||||
if strings.HasPrefix(content, ":") {
|
||||
return al.handleExtensionCommand(content)
|
||||
}
|
||||
|
||||
if !commands.HasCommandPrefix(msg.Content) {
|
||||
return "", false
|
||||
}
|
||||
|
|
@ -1728,7 +1696,7 @@ func (al *AgentLoop) handleCommand(
|
|||
return "", false
|
||||
}
|
||||
|
||||
rt := al.buildCommandsRuntime(agent)
|
||||
rt := al.buildCommandsRuntime(agent, sessionKey, msg)
|
||||
executor := commands.NewExecutor(al.cmdRegistry, rt)
|
||||
|
||||
var commandReply string
|
||||
|
|
@ -1757,7 +1725,7 @@ func (al *AgentLoop) handleCommand(
|
|||
}
|
||||
}
|
||||
|
||||
func (al *AgentLoop) buildCommandsRuntime(agent *AgentInstance) *commands.Runtime {
|
||||
func (al *AgentLoop) buildCommandsRuntime(agent *AgentInstance, sessionKey string, msg bus.InboundMessage) *commands.Runtime {
|
||||
rt := &commands.Runtime{
|
||||
Config: al.cfg,
|
||||
ListAgentIDs: al.registry.ListAgentIDs,
|
||||
|
|
@ -1777,7 +1745,107 @@ func (al *AgentLoop) buildCommandsRuntime(agent *AgentInstance) *commands.Runtim
|
|||
}
|
||||
return nil
|
||||
},
|
||||
|
||||
// Session mode (session-scoped)
|
||||
GetSessionMode: func() string {
|
||||
if al.getSessionMode(sessionKey) == modeCmd {
|
||||
return "cmd"
|
||||
}
|
||||
return "pico"
|
||||
},
|
||||
SetModeCmd: func() string {
|
||||
if agent == nil {
|
||||
return "Command unavailable in current context."
|
||||
}
|
||||
al.setSessionMode(sessionKey, modeCmd)
|
||||
workDir := al.getSessionWorkDir(sessionKey)
|
||||
if workDir == "" {
|
||||
workDir = agent.Workspace
|
||||
al.setSessionWorkDir(sessionKey, workDir)
|
||||
}
|
||||
return fmt.Sprintf("```\n%s$\n```\nType `/pico` to return to chat mode.", shortenHomePath(workDir))
|
||||
},
|
||||
SetModePico: func() string {
|
||||
al.setSessionMode(sessionKey, modePico)
|
||||
return "Switched to chat mode. Type /cmd to enter command mode."
|
||||
},
|
||||
GetWorkDir: func() string {
|
||||
if agent == nil {
|
||||
return ""
|
||||
}
|
||||
workDir := al.getSessionWorkDir(sessionKey)
|
||||
if workDir == "" {
|
||||
return agent.Workspace
|
||||
}
|
||||
return workDir
|
||||
},
|
||||
GetWorkspace: func() string {
|
||||
if agent == nil {
|
||||
return ""
|
||||
}
|
||||
return agent.Workspace
|
||||
},
|
||||
|
||||
// File editing — delegates to handleEditCommand with proper path resolution
|
||||
EditFile: func(content string) string {
|
||||
if agent == nil {
|
||||
return "Command unavailable in current context."
|
||||
}
|
||||
workDir := al.getSessionWorkDir(sessionKey)
|
||||
if workDir == "" {
|
||||
workDir = agent.Workspace
|
||||
}
|
||||
return al.handleEditCommand(content, workDir, agent.Workspace)
|
||||
},
|
||||
|
||||
// Token usage stats
|
||||
GetTokenUsage: func() (int64, int64, int64) {
|
||||
if agent == nil {
|
||||
return 0, 0, 0
|
||||
}
|
||||
return agent.TotalPromptTokens.Load(), agent.TotalCompletionTokens.Load(), agent.TotalRequests.Load()
|
||||
},
|
||||
|
||||
// One-shot AI query for /hipico (stays in modeCmd, separate session key)
|
||||
RunOneShot: func(ctx context.Context, message string) (string, error) {
|
||||
if agent == nil {
|
||||
return "", fmt.Errorf("no agent available")
|
||||
}
|
||||
workDir := al.getSessionWorkDir(sessionKey)
|
||||
if workDir == "" {
|
||||
workDir = agent.Workspace
|
||||
}
|
||||
return al.runAgentLoop(ctx, agent, processOptions{
|
||||
SessionKey: sessionKey + ":hipico",
|
||||
Channel: msg.Channel,
|
||||
ChatID: msg.ChatID,
|
||||
UserMessage: message,
|
||||
Media: msg.Media,
|
||||
DefaultResponse: defaultResponse,
|
||||
EnableSummary: false,
|
||||
SendResponse: false,
|
||||
WorkingDir: workDir,
|
||||
})
|
||||
},
|
||||
|
||||
// Session management
|
||||
ClearSession: func() error {
|
||||
if agent == nil {
|
||||
return fmt.Errorf("no agent available")
|
||||
}
|
||||
agent.Sessions.TruncateHistory(sessionKey, 0)
|
||||
agent.Sessions.SetSummary(sessionKey, "")
|
||||
return agent.Sessions.Save(sessionKey)
|
||||
},
|
||||
CompactSession: func() error {
|
||||
if agent == nil {
|
||||
return fmt.Errorf("no agent available")
|
||||
}
|
||||
al.summarizeSession(agent, sessionKey)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
if agent != nil {
|
||||
rt.GetModelInfo = func() (string, string) {
|
||||
return agent.Model, al.cfg.Agents.Defaults.Provider
|
||||
|
|
@ -1798,98 +1866,6 @@ func mapCommandError(result commands.ExecuteResult) string {
|
|||
return fmt.Sprintf("Failed to execute /%s: %v", result.Command, result.Err)
|
||||
}
|
||||
|
||||
// handleExtensionCommand handles : prefixed commands that work across all channels.
|
||||
func (al *AgentLoop) handleExtensionCommand(content string) (string, bool) {
|
||||
parts := strings.Fields(content)
|
||||
if len(parts) == 0 {
|
||||
return "", false
|
||||
}
|
||||
|
||||
cmd := parts[0]
|
||||
|
||||
switch cmd {
|
||||
case ":cmd", ":pico", ":hipico", ":edit":
|
||||
// Pass through to processMessage for mode handling (needs sessionKey from routing)
|
||||
return "", false
|
||||
|
||||
case ":help":
|
||||
return `:help - Show this help message
|
||||
:usage - Show model info and token usage
|
||||
:cmd - Switch to command mode (execute shell commands)
|
||||
:pico - Switch to chat mode (default, AI conversation)
|
||||
:hipico <msg> - Ask AI for help (from command mode, one-shot)
|
||||
:edit <file> - View/edit files (cmd mode)`, true
|
||||
|
||||
case ":usage":
|
||||
agent := al.registry.GetDefaultAgent()
|
||||
if agent == nil {
|
||||
return "No agent available.", true
|
||||
}
|
||||
promptTokens := agent.TotalPromptTokens.Load()
|
||||
completionTokens := agent.TotalCompletionTokens.Load()
|
||||
return fmt.Sprintf(`Model: %s
|
||||
Max tokens: %d
|
||||
Temperature: %.1f
|
||||
|
||||
Token usage (this session):
|
||||
Prompt tokens: %d
|
||||
Completion tokens: %d
|
||||
Total tokens: %d
|
||||
Requests: %d`,
|
||||
agent.Model,
|
||||
agent.MaxTokens,
|
||||
agent.Temperature,
|
||||
promptTokens,
|
||||
completionTokens,
|
||||
promptTokens+completionTokens,
|
||||
agent.TotalRequests.Load(),
|
||||
), true
|
||||
|
||||
default:
|
||||
// Don't intercept unrecognized : prefixed messages (e.g. :) :D :thinking:)
|
||||
// Let them pass through as normal chat messages
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
// handleModeCommand processes mode-switching commands (:cmd, :pico, :hipico).
|
||||
// Returns (response, handled). If handled is true, the caller should return the response directly.
|
||||
// For :hipico with a message, it returns ("", false) so processMessage continues with a one-shot LLM call.
|
||||
func (al *AgentLoop) handleModeCommand(content, sessionKey string, agent *AgentInstance) (string, bool) {
|
||||
parts := strings.Fields(content)
|
||||
if len(parts) == 0 {
|
||||
return "", false
|
||||
}
|
||||
|
||||
cmd := parts[0]
|
||||
|
||||
switch cmd {
|
||||
case ":cmd":
|
||||
al.setSessionMode(sessionKey, modeCmd)
|
||||
workDir := al.getSessionWorkDir(sessionKey)
|
||||
if workDir == "" {
|
||||
workDir = agent.Workspace
|
||||
al.setSessionWorkDir(sessionKey, workDir)
|
||||
}
|
||||
displayDir := shortenHomePath(workDir)
|
||||
return fmt.Sprintf("```\n%s$\n```\nType `:pico` to return to chat mode.", displayDir), true
|
||||
|
||||
case ":pico":
|
||||
al.setSessionMode(sessionKey, modePico)
|
||||
return "Switched to chat mode. Type :cmd to enter command mode.", true
|
||||
|
||||
case ":hipico":
|
||||
msg := strings.TrimSpace(strings.TrimPrefix(content, ":hipico"))
|
||||
if msg == "" {
|
||||
return "Usage: :hipico <message>\nExample: :hipico check the log files for errors", true
|
||||
}
|
||||
// Stay in modeCmd, just flag for one-shot LLM call — processMessage handles it
|
||||
return "", false
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
// executeCmdMode executes a shell command in command mode via ExecTool.
|
||||
// Output is formatted as a console code block for channel display.
|
||||
func (al *AgentLoop) executeCmdMode(
|
||||
|
|
@ -1907,15 +1883,6 @@ func (al *AgentLoop) executeCmdMode(
|
|||
return al.handleCdCommand(content, sessionKey, agent), nil
|
||||
}
|
||||
|
||||
// Handle :edit command
|
||||
if content == ":edit" || strings.HasPrefix(content, ":edit ") {
|
||||
workDir := al.getSessionWorkDir(sessionKey)
|
||||
if workDir == "" {
|
||||
workDir = agent.Workspace
|
||||
}
|
||||
return al.handleEditCommand(content, workDir, agent.Workspace), nil
|
||||
}
|
||||
|
||||
// Intercept interactive editors
|
||||
if msg := interceptEditor(content); msg != "" {
|
||||
return msg, nil
|
||||
|
|
@ -2023,7 +1990,7 @@ func shortenHomePath(path string) string {
|
|||
// :edit <file> -<N> → delete line N
|
||||
// :edit <file> -m """<content>""" → write full content (create if needed)
|
||||
func (al *AgentLoop) handleEditCommand(content, workDir, workspace string) string {
|
||||
raw := strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(content), ":edit"))
|
||||
raw := strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(content), "/edit"))
|
||||
if raw == "" {
|
||||
return editUsage()
|
||||
}
|
||||
|
|
@ -2044,12 +2011,12 @@ func (al *AgentLoop) handleEditCommand(content, workDir, workspace string) strin
|
|||
return fmt.Sprintf("Access denied: %s", err)
|
||||
}
|
||||
|
||||
// :edit <file> — show file content
|
||||
// /edit <file> — show file content
|
||||
if len(parts) == 1 && !strings.Contains(raw, "\n") {
|
||||
return editShowFile(filename)
|
||||
}
|
||||
|
||||
// :edit <file> -m """..."""
|
||||
// /edit <file> -m """..."""
|
||||
if len(parts) >= 2 && parts[1] == "-m" {
|
||||
return editMultiline(filename, raw)
|
||||
}
|
||||
|
|
@ -2081,11 +2048,11 @@ func resolveEditPath(name, workDir, workspace string) (string, error) {
|
|||
|
||||
func editUsage() string {
|
||||
return "Usage:\n" +
|
||||
" :edit <file> — view file\n" +
|
||||
" :edit <file> <N> <text> — replace line N\n" +
|
||||
" :edit <file> +<N> <text> — insert after line N\n" +
|
||||
" :edit <file> -<N> — delete line N\n" +
|
||||
" :edit <file> -m \"\"\" — write content\n" +
|
||||
" /edit <file> — view file\n" +
|
||||
" /edit <file> <N> <text> — replace line N\n" +
|
||||
" /edit <file> +<N> <text> — insert after line N\n" +
|
||||
" /edit <file> -<N> — delete line N\n" +
|
||||
" /edit <file> -m \"\"\" — write content\n" +
|
||||
" <content>\n" +
|
||||
" \"\"\""
|
||||
}
|
||||
|
|
@ -2095,7 +2062,7 @@ func editShowFile(path string) string {
|
|||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return fmt.Sprintf(
|
||||
"File not found: %s\nUse :edit %s -m \"\"\" to create it.",
|
||||
"File not found: %s\nUse /edit %s -m \"\"\" to create it.",
|
||||
shortenHomePath(path),
|
||||
filepath.Base(path),
|
||||
)
|
||||
|
|
@ -2262,9 +2229,9 @@ func interceptEditor(cmd string) string {
|
|||
name := parts[0]
|
||||
switch name {
|
||||
case "vim", "vi", "nvim", "nano", "emacs", "pico", "joe", "mcedit":
|
||||
return fmt.Sprintf("⚠ %s requires a terminal and cannot run here.\nUse :edit instead:\n\n"+
|
||||
":edit <file> — view file\n"+
|
||||
":edit <file> -m \"\"\" — write content\n"+
|
||||
return fmt.Sprintf("⚠ %s requires a terminal and cannot run here.\nUse /edit instead:\n\n"+
|
||||
"/edit <file> — view file\n"+
|
||||
"/edit <file> -m \"\"\" — write content\n"+
|
||||
"<content>\n"+
|
||||
"\"\"\"\n\n"+
|
||||
"Type :help for all commands.", name)
|
||||
|
|
|
|||
|
|
@ -12,5 +12,14 @@ func BuiltinDefinitions() []Definition {
|
|||
listCommand(),
|
||||
switchCommand(),
|
||||
checkCommand(),
|
||||
// Session mode commands (formerly : prefix)
|
||||
cmdModeCommand(),
|
||||
picoModeCommand(),
|
||||
hipicoCmnd(),
|
||||
editCommand(),
|
||||
// Info and session management
|
||||
usageCommand(),
|
||||
clearCommand(),
|
||||
compactCommand(),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
20
pkg/commands/cmd_clear.go
Normal file
20
pkg/commands/cmd_clear.go
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
package commands
|
||||
|
||||
import "context"
|
||||
|
||||
func clearCommand() Definition {
|
||||
return Definition{
|
||||
Name: "clear",
|
||||
Description: "Clear all chat history for this session",
|
||||
Usage: "/clear",
|
||||
Handler: func(_ context.Context, req Request, rt *Runtime) error {
|
||||
if rt == nil || rt.ClearSession == nil {
|
||||
return req.Reply(unavailableMsg)
|
||||
}
|
||||
if err := rt.ClearSession(); err != nil {
|
||||
return req.Reply("Failed to clear session: " + err.Error())
|
||||
}
|
||||
return req.Reply("Chat history cleared. Starting fresh.")
|
||||
},
|
||||
}
|
||||
}
|
||||
20
pkg/commands/cmd_compact.go
Normal file
20
pkg/commands/cmd_compact.go
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
package commands
|
||||
|
||||
import "context"
|
||||
|
||||
func compactCommand() Definition {
|
||||
return Definition{
|
||||
Name: "compact",
|
||||
Description: "Compress chat history to reduce context size",
|
||||
Usage: "/compact",
|
||||
Handler: func(_ context.Context, req Request, rt *Runtime) error {
|
||||
if rt == nil || rt.CompactSession == nil {
|
||||
return req.Reply(unavailableMsg)
|
||||
}
|
||||
if err := rt.CompactSession(); err != nil {
|
||||
return req.Reply("Failed to compact session: " + err.Error())
|
||||
}
|
||||
return req.Reply("Chat history compacted.")
|
||||
},
|
||||
}
|
||||
}
|
||||
18
pkg/commands/cmd_edit.go
Normal file
18
pkg/commands/cmd_edit.go
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
package commands
|
||||
|
||||
import "context"
|
||||
|
||||
func editCommand() Definition {
|
||||
return Definition{
|
||||
Name: "edit",
|
||||
Description: "View or edit files (works in both chat and command mode)",
|
||||
Usage: "/edit <file> [operation]",
|
||||
Handler: func(_ context.Context, req Request, rt *Runtime) error {
|
||||
if rt == nil || rt.EditFile == nil {
|
||||
return req.Reply(unavailableMsg)
|
||||
}
|
||||
// Pass the full text; EditFile strips the /edit prefix internally
|
||||
return req.Reply(rt.EditFile(req.Text))
|
||||
},
|
||||
}
|
||||
}
|
||||
63
pkg/commands/cmd_mode.go
Normal file
63
pkg/commands/cmd_mode.go
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func cmdModeCommand() Definition {
|
||||
return Definition{
|
||||
Name: "cmd",
|
||||
Description: "Switch to command mode (execute shell commands)",
|
||||
Usage: "/cmd",
|
||||
Handler: func(_ context.Context, req Request, rt *Runtime) error {
|
||||
if rt == nil || rt.SetModeCmd == nil {
|
||||
return req.Reply(unavailableMsg)
|
||||
}
|
||||
return req.Reply(rt.SetModeCmd())
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func picoModeCommand() Definition {
|
||||
return Definition{
|
||||
Name: "pico",
|
||||
Description: "Switch to chat mode (AI conversation)",
|
||||
Usage: "/pico",
|
||||
Handler: func(_ context.Context, req Request, rt *Runtime) error {
|
||||
if rt == nil || rt.SetModePico == nil {
|
||||
return req.Reply(unavailableMsg)
|
||||
}
|
||||
return req.Reply(rt.SetModePico())
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func hipicoCmnd() Definition {
|
||||
return Definition{
|
||||
Name: "hipico",
|
||||
Description: "Ask AI for one-shot help (works from command mode)",
|
||||
Usage: "/hipico <message>",
|
||||
Handler: func(ctx context.Context, req Request, rt *Runtime) error {
|
||||
if rt == nil || rt.RunOneShot == nil {
|
||||
return req.Reply(unavailableMsg)
|
||||
}
|
||||
// Strip the command prefix to get the message body
|
||||
msg := strings.TrimSpace(req.Text)
|
||||
for _, prefix := range []string{"/hipico", "!hipico"} {
|
||||
if strings.HasPrefix(msg, prefix) {
|
||||
msg = strings.TrimSpace(msg[len(prefix):])
|
||||
break
|
||||
}
|
||||
}
|
||||
if msg == "" {
|
||||
return req.Reply("Usage: /hipico <message>\nExample: /hipico check the log files for errors")
|
||||
}
|
||||
reply, err := rt.RunOneShot(ctx, msg)
|
||||
if err != nil {
|
||||
return req.Reply("Error: " + err.Error())
|
||||
}
|
||||
return req.Reply(reply)
|
||||
},
|
||||
}
|
||||
}
|
||||
47
pkg/commands/cmd_usage.go
Normal file
47
pkg/commands/cmd_usage.go
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func usageCommand() Definition {
|
||||
return Definition{
|
||||
Name: "usage",
|
||||
Description: "Show model info and token usage for this session",
|
||||
Usage: "/usage",
|
||||
Handler: func(_ context.Context, req Request, rt *Runtime) error {
|
||||
if rt == nil || rt.GetModelInfo == nil {
|
||||
return req.Reply(unavailableMsg)
|
||||
}
|
||||
|
||||
name, _ := rt.GetModelInfo()
|
||||
|
||||
var maxTokens int
|
||||
var temperature float64
|
||||
if rt.Config != nil {
|
||||
maxTokens = rt.Config.Agents.Defaults.MaxTokens
|
||||
if rt.Config.Agents.Defaults.Temperature != nil {
|
||||
temperature = *rt.Config.Agents.Defaults.Temperature
|
||||
}
|
||||
}
|
||||
|
||||
msg := fmt.Sprintf("Model: %s\nMax tokens: %d\nTemperature: %.1f",
|
||||
name, maxTokens, temperature)
|
||||
|
||||
if rt.GetTokenUsage != nil {
|
||||
prompt, completion, requests := rt.GetTokenUsage()
|
||||
msg += fmt.Sprintf(`
|
||||
|
||||
Token usage (this session):
|
||||
Prompt tokens: %d
|
||||
Completion tokens: %d
|
||||
Total tokens: %d
|
||||
Requests: %d`,
|
||||
prompt, completion, prompt+completion, requests)
|
||||
}
|
||||
|
||||
return req.Reply(msg)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,10 @@
|
|||
package commands
|
||||
|
||||
import "github.com/sipeed/picoclaw/pkg/config"
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/config"
|
||||
)
|
||||
|
||||
// Runtime provides runtime dependencies to command handlers. It is constructed
|
||||
// per-request by the agent loop so that per-request state (like session scope)
|
||||
|
|
@ -13,4 +17,26 @@ type Runtime struct {
|
|||
GetEnabledChannels func() []string
|
||||
SwitchModel func(value string) (oldModel string, err error)
|
||||
SwitchChannel func(value string) error
|
||||
|
||||
// Session mode control (session-scoped callbacks)
|
||||
GetSessionMode func() string // returns "pico" or "cmd"
|
||||
SetModeCmd func() string // switches to cmd mode, returns prompt string
|
||||
SetModePico func() string // switches to pico mode, returns status string
|
||||
|
||||
// Working directory (session-scoped)
|
||||
GetWorkDir func() string // current working directory for this session
|
||||
GetWorkspace func() string // agent workspace root
|
||||
|
||||
// File editing — delegates to the loop's handleEditCommand with proper path resolution
|
||||
EditFile func(content string) string
|
||||
|
||||
// Token and model usage stats
|
||||
GetTokenUsage func() (promptTokens, completionTokens, requests int64)
|
||||
|
||||
// One-shot AI query from cmd mode (/hipico)
|
||||
RunOneShot func(ctx context.Context, message string) (string, error)
|
||||
|
||||
// Session history management
|
||||
ClearSession func() error // clears all history and summary, saves
|
||||
CompactSession func() error // runs summarization synchronously
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue