mirror of
https://github.com/usestrix/strix.git
synced 2026-11-09 05:19:39 -10:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e011253ebc | ||
|
|
5227960e99 |
No files matched your search
@@ -90,7 +90,6 @@ type Model struct {
|
||||
client *Client
|
||||
width, height int
|
||||
snapshot protocol.Snapshot
|
||||
windowTitle string
|
||||
input textarea.Model
|
||||
viewport viewport.Model
|
||||
viewportContent string
|
||||
|
||||
@@ -2,13 +2,10 @@ package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/x/ansi"
|
||||
"github.com/usestrix/strix/tui/internal/protocol"
|
||||
"github.com/usestrix/strix/tui/internal/render"
|
||||
)
|
||||
@@ -59,10 +56,6 @@ func (m *Model) handleEnvelope(envelope protocol.Envelope) tea.Cmd {
|
||||
// resize (not just refresh): status-row visibility changes the chat height.
|
||||
m.resizeViewport()
|
||||
m.resizeVulnerabilityViewport()
|
||||
if title := windowTitle(m.snapshot.Targets); title != m.windowTitle {
|
||||
m.windowTitle = title
|
||||
return setTerminalTitle(title)
|
||||
}
|
||||
case "collection_bootstrap":
|
||||
return m.handleCollectionBootstrap(envelope.Payload)
|
||||
case "collection_delta":
|
||||
@@ -125,26 +118,6 @@ func (m *Model) handleEnvelope(envelope protocol.Envelope) tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
// tea.SetWindowTitle only sends OSC 2 (window title); iTerm tabs show the OSC 1
|
||||
// icon name, which shells usually set, so send OSC 0 to set both.
|
||||
func setTerminalTitle(title string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
_, _ = os.Stdout.WriteString(ansi.SetIconNameWindowTitle(title))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func windowTitle(targets []string) string {
|
||||
switch len(targets) {
|
||||
case 0:
|
||||
return "strix"
|
||||
case 1:
|
||||
return targets[0]
|
||||
default:
|
||||
return fmt.Sprintf("%s +%d", targets[0], len(targets)-1)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) consumeMessages(messages []protocol.Message, setupMode bool) {
|
||||
if m.seenMessages == nil {
|
||||
m.seenMessages = map[string]bool{}
|
||||
|
||||
+20
-9
@@ -15,7 +15,11 @@ from typing import TYPE_CHECKING, Any
|
||||
|
||||
from agents.model_settings import ModelSettings
|
||||
from agents.models.interface import ModelTracing
|
||||
from openai.types.responses import ResponseOutputMessage, ResponseOutputText
|
||||
from openai.types.responses import (
|
||||
ResponseOutputMessage,
|
||||
ResponseOutputText,
|
||||
ResponseReasoningItem,
|
||||
)
|
||||
|
||||
from strix.config import load_settings
|
||||
from strix.config.models import StrixProvider
|
||||
@@ -288,15 +292,22 @@ def _checkpoint_item(summary: str) -> dict[str, Any]:
|
||||
|
||||
def _extract_text(response: ModelResponse) -> str:
|
||||
parts: list[str] = []
|
||||
reasoning: list[str] = []
|
||||
for item in response.output:
|
||||
if not isinstance(item, ResponseOutputMessage):
|
||||
continue
|
||||
parts.extend(
|
||||
chunk.text
|
||||
for chunk in item.content
|
||||
if isinstance(chunk, ResponseOutputText) and chunk.text
|
||||
)
|
||||
return "".join(parts)
|
||||
if isinstance(item, ResponseOutputMessage):
|
||||
parts.extend(
|
||||
chunk.text
|
||||
for chunk in item.content
|
||||
if isinstance(chunk, ResponseOutputText) and chunk.text
|
||||
)
|
||||
elif isinstance(item, ResponseReasoningItem):
|
||||
reasoning.extend(summary.text for summary in item.summary if summary.text)
|
||||
if parts or not reasoning:
|
||||
return "".join(parts)
|
||||
# Reasoning models can spend the whole output budget thinking and return no
|
||||
# message; their reasoning is a degraded but usable summary.
|
||||
logger.warning("compaction summary had no output text; using reasoning text")
|
||||
return "\n".join(reasoning)
|
||||
|
||||
|
||||
async def _summarize(model: str, prompt: str, max_tokens: int) -> str | None:
|
||||
|
||||
@@ -6,6 +6,8 @@ from types import SimpleNamespace
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import pytest
|
||||
from agents.extensions.models.litellm_model import InternalChatCompletionMessage
|
||||
from agents.models.chatcmpl_converter import Converter
|
||||
from litellm.exceptions import BadRequestError, ContextWindowExceededError, RateLimitError
|
||||
from openai.types.responses import ResponseOutputMessage, ResponseOutputText
|
||||
|
||||
@@ -242,6 +244,29 @@ async def test_summarize_routes_through_provider_with_settings(
|
||||
assert settings.max_tokens == 64
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_summarize_falls_back_to_reasoning_when_no_output_text(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
_patch_budget(monkeypatch, keep_tokens=30, window=4_000)
|
||||
message = InternalChatCompletionMessage(
|
||||
role="assistant", content=None, reasoning_content="REASONED SUMMARY"
|
||||
)
|
||||
response = SimpleNamespace(output=Converter.message_to_output_items(message))
|
||||
|
||||
class FakeModel:
|
||||
async def get_response(self, **_kwargs: Any) -> Any:
|
||||
return response
|
||||
|
||||
class FakeProvider:
|
||||
def get_model(self, _model_name: str | None) -> Any:
|
||||
return FakeModel()
|
||||
|
||||
monkeypatch.setattr(compaction, "StrixProvider", FakeProvider)
|
||||
|
||||
assert await compaction._summarize("m", "p", 64) == "REASONED SUMMARY"
|
||||
|
||||
|
||||
def test_fit_to_tokens_truncates_oversized_text(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(compaction, "count_tokens", lambda _m, t: len(t))
|
||||
text = "x" * 10_000
|
||||
|
||||
Reference in new issue
Block a user