@@ -107,6 +107,12 @@ func NewCoordinator(
}
c.currentAgent = agent
c.agents[config.AgentCoder] = agent
+
+ _, small, err := c.buildAgentModels(ctx)
+ if err != nil {
+ return nil, err
+ }
+ hooks.SetSmallModel(small.Model)
return c, nil
}
@@ -745,6 +751,7 @@ func (c *coordinator) UpdateModels(ctx context.Context) error {
return err
}
c.currentAgent.SetModels(large, small)
+ c.hooks.SetSmallModel(small.Model)
agentCfg, ok := c.cfg.Agents[config.AgentCoder]
if !ok {
@@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"log/slog"
+ "os"
"regexp"
"strings"
"sync"
@@ -231,11 +232,71 @@ func (s *service) executeHook(ctx context.Context, hook config.Hook, hookCtx Hoo
return nil, fmt.Errorf("unsupported hook type: %s", hook.Type)
}
+ if result != nil {
+ result.EventType = string(hookCtx.EventType)
+ }
+
return result, err
}
func (s *service) executePromptHook(ctx context.Context, hook config.Hook, hookCtx HookContext) (*message.HookOutput, error) {
- panic("not implemented")
+ contextJSON, err := json.Marshal(hookCtx)
+ if err != nil {
+ return nil, fmt.Errorf("failed to marshal hook context: %w", err)
+ }
+
+ var finalPrompt string
+ if strings.Contains(hook.Prompt, "$ARGUMENTS") {
+ finalPrompt = strings.ReplaceAll(hook.Prompt, "$ARGUMENTS", string(contextJSON))
+ } else {
+ finalPrompt = fmt.Sprintf("%s\n\nContext: %s", hook.Prompt, string(contextJSON))
+ }
+
+ timeout := DefaultHookTimeout
+ if hook.Timeout != nil {
+ timeout = time.Duration(*hook.Timeout) * time.Second
+ }
+
+ execCtx, cancel := context.WithTimeout(ctx, timeout)
+ defer cancel()
+
+ type readTranscriptParams struct{}
+ readTranscriptTool := fantasy.NewAgentTool(
+ "read_transcript",
+ "Used to read the conversation so far",
+ func(ctx context.Context, params readTranscriptParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
+ if hookCtx.TranscriptPath == "" {
+ return fantasy.NewTextErrorResponse("No transcript available"), nil
+ }
+ data, err := os.ReadFile(hookCtx.TranscriptPath)
+ if err != nil {
+ return fantasy.NewTextErrorResponse(err.Error()), nil
+ }
+ return fantasy.NewTextResponse(string(data)), nil
+ })
+
+ var output *message.HookOutput
+ outputTool := fantasy.NewAgentTool(
+ "output",
+ "Used to submit the output, remember you MUST call this tool at the end",
+ func(ctx context.Context, params message.HookOutput, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
+ output = ¶ms
+ return fantasy.NewTextResponse("ouptut submitted"), nil
+ })
+ agent := fantasy.NewAgent(
+ s.smallModel,
+ fantasy.WithSystemPrompt(`You are a helpful sub agent used in a larger agents conversation loop,
+ your goal is to intercept the conversation and fulfill the intermediate requests, makesure to ALWAYS use the output tool at the end to output your decision`),
+ fantasy.WithTools(readTranscriptTool, outputTool),
+ )
+
+ _, err = agent.Generate(execCtx, fantasy.AgentCall{
+ Prompt: finalPrompt,
+ })
+ if err != nil {
+ return nil, err
+ }
+ return output, nil
}
func (s *service) executeCommandHook(ctx context.Context, hook config.Hook, hookCtx HookContext) (*message.HookOutput, error) {
@@ -342,9 +403,8 @@ func parseHookOutput(stdout string) *message.HookOutput {
return &output
}
-// SetSmallModel implements Service.
func (s *service) SetSmallModel(model fantasy.LanguageModel) {
- panic("unimplemented")
+ s.smallModel = model
}
func (s *service) collectMatchingHooks(hookCtx HookContext) []config.Hook {
@@ -137,12 +137,13 @@ type Message struct {
}
type HookOutput struct {
- Stop bool `json:"stop"`
- Error string `json:"error"`
- Message string `json:"message"`
- Decision string `json:"decision"`
- UpdatedInput string `json:"updated_input"`
- AdditionalContext string `json:"additional_context"`
+ Stop bool `json:"stop,omitempty" description:"set to true if the execution should stop"`
+ EventType string `json:"event_type" description:"ignore"`
+ Error string `json:"error,omitempty"`
+ Message string `json:"message,omitempty" description:"a message to send to show the user"`
+ Decision string `json:"decision" description:"block, allow, deny, ask, only set if the request asks you to do so"`
+ UpdatedInput string `json:"updated_input" description:"the updated tool input json, only set if the user requests you update a tool input"`
+ AdditionalContext string `json:"additional_context" description:"additional context to send to the LLM, only set if the user asks to add additional context"`
}
func (m *Message) Content() TextContent {
@@ -10,6 +10,7 @@ import (
"charm.land/bubbles/v2/viewport"
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
+ "charm.land/lipgloss/v2/tree"
"github.com/charmbracelet/catwalk/pkg/catwalk"
"github.com/charmbracelet/x/ansi"
"github.com/charmbracelet/x/exp/ordered"
@@ -246,7 +247,36 @@ func (m *messageCmp) renderUserMessage() string {
}
joined := lipgloss.JoinVertical(lipgloss.Left, parts...)
- return m.style().Render(joined)
+ hooks := m.getHookOutputs(t)
+ if len(hooks) == 0 {
+ return m.style().Render(joined)
+ }
+ root := tree.Root(joined)
+ root.EnumeratorStyle(t.S().Subtle)
+ for _, h := range hooks {
+ root.Child(h)
+ }
+ return m.style().Render(root.Enumerator(RoundedEnumeratorWithWidth(0, 1)).String())
+}
+
+func (m *messageCmp) getHookOutputs(t *styles.Theme) []string {
+ var hooks []string
+ for _, h := range m.message.HookOutputs {
+ var hookStatus []string
+ hookStatus = append(hookStatus, t.S().Base.Foreground(t.Blue).Render(fmt.Sprintf(" hook:%s", h.EventType)))
+ if h.Stop {
+ hookStatus = append(hookStatus, t.S().Error.Render("hook stopped execution"))
+ } else if h.AdditionalContext != "" {
+ hookStatus = append(hookStatus, t.S().Subtle.Render("additional context added"))
+ }
+
+ if h.Message != "" {
+ hookStatus = append(hookStatus, t.S().Muted.Render(h.Message))
+ }
+ hookOutput := ansi.Truncate(strings.Join(hookStatus, " "), m.textWidth()-2, "…")
+ hooks = append(hooks, hookOutput)
+ }
+ return hooks
}
// toMarkdown converts text content to rendered markdown using the configured renderer