Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion external/eyrie
2 changes: 1 addition & 1 deletion go.mod

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions go.sum

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

68 changes: 58 additions & 10 deletions internal/engine/stream.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/GrayCodeAI/hawk/internal/hooks"
"github.com/GrayCodeAI/hawk/internal/observability/oteltrace"
"github.com/GrayCodeAI/hawk/internal/plugin"
"github.com/GrayCodeAI/hawk/internal/prompt"
"github.com/GrayCodeAI/hawk/internal/tool"

"github.com/GrayCodeAI/hawk/internal/ui/icons"
Expand Down Expand Up @@ -217,11 +218,33 @@ func (s *Session) agentLoop(ctx context.Context, ch chan<- StreamEvent) {
}
}

// Payload tiering: classify the latest user request once per turn.
// Early conversational turns get a minimal system prompt and no tool
// schemas; anything resembling work keeps the full prompt and the
// promoted tool surface. This keeps simple turns cheap on slow
// (local/remote) models without starving real requests of tools.
lastUserMsg := ""
for i := len(s.Persistence().RawMessages()) - 1; i >= 0; i-- {
m := s.Persistence().RawMessages()[i]
if m.Role == "user" && len(m.ToolResults) == 0 {
lastUserMsg = m.Content
break
}
}
smallTalk := lastUserMsg != "" && isSmallTalkPrompt(lastUserMsg) && !sessionHasToolUse(s.Persistence().RawMessages())

// Build the LLM ChatOptions via the ChatService. The service owns
// the GLMThinking toggle, output schema, anthropic caching flag,
// and the active provider/model — building opts manually here
// would duplicate that logic.
baseOpts := s.ChatLLM().BuildOptions(s.Persistence().System(), activeModel, maxTok, nil)
baseSystem := s.Persistence().System()
if smallTalk {
// The identity preamble already coaches the model to answer
// greetings without tools — the role/tool/practice sections
// below it only add prefill cost on this turn.
baseSystem = prompt.System()
}
baseOpts := s.ChatLLM().BuildOptions(baseSystem, activeModel, maxTok, nil)
opts := baseOpts
// Inject beliefs as ephemeral context (not persisted to s.Persistence().System())
if s.LifecycleSvc().Beliefs() != nil && s.LifecycleSvc().Beliefs().Size() > 0 {
Expand Down Expand Up @@ -273,18 +296,12 @@ func (s *Session) agentLoop(ctx context.Context, ch chan<- StreamEvent) {
// current request. This keeps the default schema compact while
// making URL, verification, git, and code-intelligence requests
// discoverable without requiring the model to guess ToolSearch.
lastUserMsg := ""
for i := len(s.Persistence().RawMessages()) - 1; i >= 0; i-- {
msg := s.Persistence().RawMessages()[i]
if msg.Role == "user" && len(msg.ToolResults) == 0 {
lastUserMsg = msg.Content
break
}
}
if lastUserMsg != "" {
s.Tools().Registry().PromoteForIntent(lastUserMsg)
}
opts.Tools = s.Tools().Registry().EyrieTools()
if !smallTalk {
opts.Tools = s.Tools().Registry().EyrieTools()
}
}

// Inject memory metadata from yaad
Expand Down Expand Up @@ -874,6 +891,37 @@ func (s *Session) agentLoop(ctx context.Context, ch chan<- StreamEvent) {
}
}

// isSmallTalkPrompt reports whether prompt is a pure conversational
// exchange (greeting, pleasantry, identity question) that needs neither the
// full system prompt nor any tool schemas. The system prompt instructs the
// model to answer these directly, so sending the tool surface would only
// add prompt-prefill cost.
func isSmallTalkPrompt(prompt string) bool {
text := strings.ToLower(strings.TrimSpace(prompt))
text = strings.Trim(text, " \t\r\n.,!?;:")
switch text {
case "hi", "hello", "hey", "how are you", "how are you doing", "how's it going", "what's up",
"who are you", "what can you do", "thanks", "thank you", "good morning", "good afternoon",
"good evening", "nice to meet you", "goodbye", "bye":
return true
default:
return false
}
}

// sessionHasToolUse reports whether any message in the conversation already
// executed a tool. Once tools are in play, later turns keep the full prompt
// and tool surface even if they read like small talk ("thanks"), so the
// follow-up context is not lost.
func sessionHasToolUse(msgs []types.EyrieMessage) bool {
for _, m := range msgs {
if len(m.ToolResults) > 0 {
return true
}
}
return false
}

// extractDataURI extracts the first base64 data URI from a string.
// Returns the full data URI (e.g., "data:image/png;base64,...") or empty string.
func extractDataURI(s string) string {
Expand Down
46 changes: 46 additions & 0 deletions internal/engine/stream_prompt_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package engine

import (
"testing"

"github.com/GrayCodeAI/hawk/internal/types"
)

func TestIsSmallTalkPrompt(t *testing.T) {
tests := []struct {
prompt string
skip bool
}{
{prompt: "Hi", skip: true},
{prompt: "Hello!", skip: true},
{prompt: "what can you do?", skip: true},
{prompt: "how's it going", skip: true},
{prompt: "good morning", skip: true},
{prompt: "thanks", skip: true},
{prompt: "Hi, inspect this repository", skip: false},
{prompt: "run the tests", skip: false},
{prompt: "hello there, who fixes this bug?", skip: false},
}
for _, tt := range tests {
t.Run(tt.prompt, func(t *testing.T) {
if got := isSmallTalkPrompt(tt.prompt); got != tt.skip {
t.Fatalf("isSmallTalkPrompt(%q) = %v, want %v", tt.prompt, got, tt.skip)
}
})
}
}

func TestSessionHasToolUse(t *testing.T) {
plain := []types.EyrieMessage{{Role: "user", Content: "hi"}}
used := []types.EyrieMessage{
{Role: "user", Content: "read stream.go"},
{Role: "assistant", ToolUse: []types.ToolCall{{Name: "Read"}}},
{Role: "user", Content: "thanks", ToolResults: []types.ToolResult{{}}},
}
if sessionHasToolUse(plain) {
t.Fatal("sessionHasToolUse(plain) = true, want false")
}
if !sessionHasToolUse(used) {
t.Fatal("sessionHasToolUse(used) = false, want true")
}
}
1 change: 1 addition & 0 deletions internal/sandbox/sandbox.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ RUN npm install --global npm@12.0.2 && \
npm cache clean --force && \
rm -rf /root/.npm && \
apt-get update && \
apt-get upgrade -y --no-install-recommends && \
apt-get install -y --no-install-recommends \
bash \
ca-certificates \
Expand Down
Loading