Skip to content

Commit 9f327cf

Browse files
tellahospikewang
authored andcommitted
feat: goose2 context window usage in chat input (aaif-goose#8613)
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
1 parent 4700984 commit 9f327cf

22 files changed

Lines changed: 1035 additions & 73 deletions

crates/goose-acp/src/server.rs

Lines changed: 152 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ use sacp::schema::{
4040
SetSessionConfigOptionResponse, SetSessionModeRequest, SetSessionModeResponse,
4141
SetSessionModelRequest, SetSessionModelResponse, StopReason, TextContent, TextResourceContents,
4242
ToolCall, ToolCallContent, ToolCallId, ToolCallLocation, ToolCallStatus, ToolCallUpdate,
43-
ToolCallUpdateFields, ToolKind,
43+
ToolCallUpdateFields, ToolKind, Usage, UsageUpdate,
4444
};
4545
use sacp::util::MatchDispatchFrom;
4646
use sacp::{
@@ -603,6 +603,22 @@ fn build_config_options(
603603
]
604604
}
605605

606+
fn to_nonnegative_u64(value: Option<i32>) -> Option<u64> {
607+
value.and_then(|v| u64::try_from(v).ok())
608+
}
609+
610+
fn build_prompt_usage(session: &Session) -> Option<Usage> {
611+
let total = to_nonnegative_u64(session.total_tokens)?;
612+
let input = to_nonnegative_u64(session.input_tokens).unwrap_or(0);
613+
let output = to_nonnegative_u64(session.output_tokens).unwrap_or(0);
614+
Some(Usage::new(total, input, output))
615+
}
616+
617+
fn build_usage_update(session: &Session, context_limit: usize) -> UsageUpdate {
618+
let used = session.total_tokens.unwrap_or(0).max(0) as u64;
619+
UsageUpdate::new(used, context_limit as u64)
620+
}
621+
606622
impl GooseAcpAgent {
607623
pub fn permission_manager(&self) -> Arc<PermissionManager> {
608624
Arc::clone(&self.permission_manager)
@@ -1383,27 +1399,38 @@ impl GooseAcpAgent {
13831399
// Resolve provider + model from config so we can include the current
13841400
// model in the response without waiting for the full agent setup.
13851401
let resolved = resolve_provider_and_model(&self.config_dir, &goose_session).await;
1402+
let initial_usage_update = resolved
1403+
.as_ref()
1404+
.ok()
1405+
.map(|(_, mc)| build_usage_update(&goose_session, mc.context_limit()));
13861406
let (model_state, config_options) =
13871407
build_eager_config(&resolved, &mode_state, &goose_session).await;
1408+
let session_id = SessionId::new(thread_id.clone());
13881409

13891410
self.spawn_agent_setup(
13901411
cx,
13911412
agent_tx,
13921413
AgentSetupRequest {
1393-
session_id: SessionId::new(thread_id.clone()),
1414+
session_id: session_id.clone(),
13941415
goose_session,
13951416
mcp_servers: args.mcp_servers,
1396-
resolved_provider: resolved.ok(),
1417+
resolved_provider: resolved.as_ref().ok().cloned(),
13971418
},
13981419
);
13991420

1400-
let mut response = NewSessionResponse::new(SessionId::new(thread_id)).modes(mode_state);
1421+
let mut response = NewSessionResponse::new(session_id.clone()).modes(mode_state);
14011422
if let Some(ms) = model_state {
14021423
response = response.models(ms);
14031424
}
14041425
if let Some(co) = config_options {
14051426
response = response.config_options(co);
14061427
}
1428+
if let Some(usage_update) = initial_usage_update {
1429+
cx.send_notification(SessionNotification::new(
1430+
session_id,
1431+
SessionUpdate::UsageUpdate(usage_update),
1432+
))?;
1433+
}
14071434
debug!(
14081435
target: "perf",
14091436
sid = %sid,
@@ -1748,6 +1775,16 @@ impl GooseAcpAgent {
17481775
let mode_state = build_mode_state(loaded_mode)?;
17491776

17501777
let resolved = resolve_provider_and_model(&self.config_dir, &goose_session).await;
1778+
let initial_usage_update = resolved
1779+
.as_ref()
1780+
.ok()
1781+
.map(|(_, mc)| build_usage_update(&goose_session, mc.context_limit()))
1782+
.or_else(|| {
1783+
goose_session
1784+
.model_config
1785+
.as_ref()
1786+
.map(|mc| build_usage_update(&goose_session, mc.context_limit()))
1787+
});
17511788
let (model_state, config_options) =
17521789
build_eager_config(&resolved, &mode_state, &goose_session).await;
17531790

@@ -1769,6 +1806,12 @@ impl GooseAcpAgent {
17691806
if let Some(co) = config_options {
17701807
response = response.config_options(co);
17711808
}
1809+
if let Some(usage_update) = initial_usage_update {
1810+
cx.send_notification(SessionNotification::new(
1811+
args.session_id.clone(),
1812+
SessionUpdate::UsageUpdate(usage_update),
1813+
))?;
1814+
}
17721815
debug!(
17731816
target: "perf",
17741817
sid = %sid,
@@ -1883,10 +1926,30 @@ impl GooseAcpAgent {
18831926
}
18841927
}
18851928

1886-
let mut sessions = self.sessions.lock().await;
1887-
if let Some(session) = sessions.get_mut(&thread_id) {
1888-
session.cancel_token = None;
1929+
{
1930+
let mut sessions = self.sessions.lock().await;
1931+
if let Some(session) = sessions.get_mut(&thread_id) {
1932+
session.cancel_token = None;
1933+
}
18891934
}
1935+
1936+
let session = self
1937+
.session_manager
1938+
.get_session(&internal_session_id, false)
1939+
.await
1940+
.map_err(|e| {
1941+
sacp::Error::internal_error().data(format!("Failed to load session: {}", e))
1942+
})?;
1943+
let provider = agent.provider().await.map_err(|e| {
1944+
sacp::Error::internal_error().data(format!("Failed to get provider: {}", e))
1945+
})?;
1946+
let usage_update =
1947+
build_usage_update(&session, provider.get_model_config().context_limit());
1948+
cx.send_notification(SessionNotification::new(
1949+
args.session_id.clone(),
1950+
SessionUpdate::UsageUpdate(usage_update),
1951+
))?;
1952+
18901953
debug!(
18911954
target: "perf",
18921955
sid = %sid,
@@ -1895,11 +1958,17 @@ impl GooseAcpAgent {
18951958
cancelled = was_cancelled,
18961959
"perf: prompt done"
18971960
);
1898-
Ok(PromptResponse::new(if was_cancelled {
1961+
let stop_reason = if was_cancelled {
18991962
StopReason::Cancelled
19001963
} else {
19011964
StopReason::EndTurn
1902-
}))
1965+
};
1966+
1967+
let mut response = PromptResponse::new(stop_reason);
1968+
if let Some(usage) = build_prompt_usage(&session) {
1969+
response = response.usage(usage);
1970+
}
1971+
Ok(response)
19031972
}
19041973

19051974
async fn on_cancel(&self, args: CancelNotification) -> Result<(), sacp::Error> {
@@ -3487,6 +3556,80 @@ print(\"hello, world\")
34873556
.map(|locs| locs.into_iter().map(|loc| (loc.path, loc.line)).collect())
34883557
}
34893558

3559+
fn make_session_with_usage(
3560+
total_tokens: Option<i32>,
3561+
input_tokens: Option<i32>,
3562+
output_tokens: Option<i32>,
3563+
accumulated_total_tokens: Option<i32>,
3564+
accumulated_input_tokens: Option<i32>,
3565+
accumulated_output_tokens: Option<i32>,
3566+
) -> Session {
3567+
Session {
3568+
id: "session-1".to_string(),
3569+
working_dir: PathBuf::from("/tmp"),
3570+
name: "ACP Session".to_string(),
3571+
user_set_name: false,
3572+
session_type: SessionType::Acp,
3573+
created_at: Default::default(),
3574+
updated_at: Default::default(),
3575+
extension_data: goose::session::ExtensionData::default(),
3576+
total_tokens,
3577+
input_tokens,
3578+
output_tokens,
3579+
accumulated_total_tokens,
3580+
accumulated_input_tokens,
3581+
accumulated_output_tokens,
3582+
schedule_id: None,
3583+
recipe: None,
3584+
user_recipe_values: None,
3585+
conversation: None,
3586+
message_count: 0,
3587+
provider_name: None,
3588+
model_config: None,
3589+
goose_mode: GooseMode::default(),
3590+
thread_id: None,
3591+
}
3592+
}
3593+
3594+
#[test]
3595+
fn test_build_prompt_usage_uses_current_turn_tokens() {
3596+
let session = make_session_with_usage(
3597+
Some(120),
3598+
Some(80),
3599+
Some(40),
3600+
Some(360),
3601+
Some(210),
3602+
Some(150),
3603+
);
3604+
let usage = build_prompt_usage(&session).expect("usage should be present");
3605+
assert_eq!(usage.total_tokens, 120);
3606+
assert_eq!(usage.input_tokens, 80);
3607+
assert_eq!(usage.output_tokens, 40);
3608+
}
3609+
3610+
#[test]
3611+
fn test_build_prompt_usage_falls_back_to_current_tokens() {
3612+
let session = make_session_with_usage(Some(120), Some(80), Some(40), None, None, None);
3613+
let usage = build_prompt_usage(&session).expect("usage should be present");
3614+
assert_eq!(usage.total_tokens, 120);
3615+
assert_eq!(usage.input_tokens, 80);
3616+
assert_eq!(usage.output_tokens, 40);
3617+
}
3618+
3619+
#[test]
3620+
fn test_build_prompt_usage_requires_total_tokens() {
3621+
let session = make_session_with_usage(None, Some(80), Some(40), None, None, None);
3622+
assert!(build_prompt_usage(&session).is_none());
3623+
}
3624+
3625+
#[test]
3626+
fn test_build_usage_update_clamps_negative_used_to_zero() {
3627+
let session = make_session_with_usage(Some(-7), Some(0), Some(0), None, None, None);
3628+
let usage = build_usage_update(&session, 258_000);
3629+
assert_eq!(usage.used, 0);
3630+
assert_eq!(usage.size, 258_000);
3631+
}
3632+
34903633
#[test_case(
34913634
GooseMode::Auto
34923635
=> Ok(SessionModeState::new(

ui/goose2/justfile

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -88,10 +88,24 @@ dev:
8888
# Override with e.g. RUST_LOG=info just dev to disable.
8989
export RUST_LOG="${RUST_LOG:-perf=debug,info}"
9090
PROJECT_DIR=$(pwd)
91-
GOOSE_BIN="${PROJECT_DIR}/../../target/debug/goose"
92-
export GOOSE_BIN
91+
REPO_ROOT=$(cd ../.. && pwd)
92+
LOCAL_GOOSE_DEBUG="${REPO_ROOT}/target/debug/goose"
93+
LOCAL_GOOSE_RELEASE="${REPO_ROOT}/target/release/goose"
94+
if [[ -x "${LOCAL_GOOSE_DEBUG}" ]]; then
95+
export GOOSE_BIN="${LOCAL_GOOSE_DEBUG}"
96+
elif [[ -x "${LOCAL_GOOSE_RELEASE}" ]]; then
97+
export GOOSE_BIN="${LOCAL_GOOSE_RELEASE}"
98+
else
99+
unset GOOSE_BIN
100+
fi
93101
EXTRA_CONFIG_ARGS=(--config "{\"build\":{\"devUrl\":\"http://localhost:${VITE_PORT}\",\"beforeDevCommand\":{\"script\":\"cd ${PROJECT_DIR} && exec pnpm exec vite --port ${VITE_PORT} --strictPort\",\"cwd\":\".\",\"wait\":false}}}")
94102

103+
if [[ -n "${GOOSE_BIN:-}" ]]; then
104+
echo "Using local goose binary: ${GOOSE_BIN}"
105+
else
106+
echo "No local goose binary found under ${REPO_ROOT}/target; falling back to PATH"
107+
fi
108+
95109
# In worktrees, generate a labeled icon so you can tell instances apart
96110
if git rev-parse --is-inside-work-tree &>/dev/null; then
97111
GIT_DIR=$(git rev-parse --git-dir)
@@ -117,14 +131,29 @@ dev-debug:
117131
#!/usr/bin/env bash
118132
set -euo pipefail
119133

134+
VITE_PORT={{ vite_port }}
135+
export VITE_PORT
120136
# Enable perf logs in the child `goose serve` process by default.
121137
# Override with e.g. RUST_LOG=info just dev-debug to disable.
122138
export RUST_LOG="${RUST_LOG:-perf=debug,info}"
123-
PROJECT_DIR=$(pwd)
124-
GOOSE_BIN="${PROJECT_DIR}/../../target/debug/goose"
125-
export GOOSE_BIN
139+
REPO_ROOT=$(cd ../.. && pwd)
140+
LOCAL_GOOSE_DEBUG="${REPO_ROOT}/target/debug/goose"
141+
LOCAL_GOOSE_RELEASE="${REPO_ROOT}/target/release/goose"
142+
if [[ -x "${LOCAL_GOOSE_DEBUG}" ]]; then
143+
export GOOSE_BIN="${LOCAL_GOOSE_DEBUG}"
144+
elif [[ -x "${LOCAL_GOOSE_RELEASE}" ]]; then
145+
export GOOSE_BIN="${LOCAL_GOOSE_RELEASE}"
146+
else
147+
unset GOOSE_BIN
148+
fi
126149
EXTRA_CONFIG_ARGS=(--config "{\"build\":{\"devUrl\":\"http://localhost:${VITE_PORT}\",\"beforeDevCommand\":{\"script\":\"exec ./node_modules/.bin/vite --port ${VITE_PORT} --strictPort\",\"cwd\":\"..\",\"wait\":false}}}")
127150

151+
if [[ -n "${GOOSE_BIN:-}" ]]; then
152+
echo "Using local goose binary: ${GOOSE_BIN}"
153+
else
154+
echo "No local goose binary found under ${REPO_ROOT}/target; falling back to PATH"
155+
fi
156+
128157
# In worktrees, generate a labeled icon so you can tell instances apart
129158
if git rev-parse --is-inside-work-tree &>/dev/null; then
130159
GIT_DIR=$(git rev-parse --git-dir)

ui/goose2/scripts/check-file-sizes.mjs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,17 @@ const EXCEPTIONS = {
1313
"src/features/chat/ui/ChatView.tsx": {
1414
limit: 570,
1515
justification:
16-
"ACP prewarm guards, project-aware working dir selection, working context sync, and chat bootstrapping still live together here. Includes gated [perf:chatview] logging via perfLog (dev-only by default).",
16+
"ACP prewarm guards, project-aware working dir selection, working context sync, chat bootstrapping, context-ring compaction wiring, and gated [perf:chatview] logging via perfLog (dev-only by default).",
17+
},
18+
"src/features/chat/hooks/useChat.ts": {
19+
limit: 510,
20+
justification:
21+
"Session preparation, provider/model handoff, persona-aware sends, cancellation, and compaction replay still live in one chat lifecycle hook.",
22+
},
23+
"src/shared/api/acpNotificationHandler.ts": {
24+
limit: 550,
25+
justification:
26+
"ACP replay/live update handling, pending session buffering, model/config propagation, and streaming perf tracking still share one notification entrypoint.",
1727
},
1828
"src/features/chat/ui/__tests__/ContextPanel.test.tsx": {
1929
limit: 550,

0 commit comments

Comments
 (0)