Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

Commit

g1t session heartbeat --usage-json reports what the session used

A harness's hooks heartbeat anyway, so the heartbeat is where they report the session's token usage for g1t to price. The CLI checks it is a list of models before sending it, reads it from stdin with -, and is 0.4.1, the first CLI that takes it, so the plugin can send it only to a CLI that has it. Part of #157

G1t-Session:
ses_01m573jm0ze1q9tram0b4dcva1
syntaqxcommitted Parentc41e4b7Browse files
6 files+67−70/6 viewed
+1−1
13961396
13971397 [[package]]
13981398 name = "g1t"
1399−version = "0.4.0"
1399+version = "0.4.1"
14001400 dependencies = [
14011401 "anstyle-query",
14021402 "base64 0.22.1",
+11−0
939939 g1t session heartbeat [OPTIONS]
940940 ```
941941
942+| Argument or flag | |
943+| --- | --- |
944+| `--usage-json <JSON>` | The session's token usage so far, per model, as JSON; - reads it from stdin. |
945+
946+```text
947+--usage-json reports the session's token usage so far, which g1t prices at Anthropic's list price:
948+a JSON list with one entry per model, each {model, tier?, input_tokens, output_tokens,
949+cache_write_5m_tokens, cache_write_1h_tokens, cache_read_tokens}. It is cumulative, so it replaces
950+what the session reported before. - reads it from stdin.
951+```
952+
942953 Calls [`update_session`](/reference/api/sessions/update-session/).
943954
944955 ### g1t session log
+1−1
11 [package]
22 name = "g1t"
3−version = "0.4.0"
3+version = "0.4.1"
44 edition.workspace = true
55 license.workspace = true
66 repository.workspace = true
+12−1
10111011 Starting again with the same --harness-session-id returns the same session.")]
10121012 Start(SessionStartArgs),
10131013 /// Say the current session is still working; renews its claims.
1014− Heartbeat,
1014+ #[command(after_help = "--usage-json reports the session's token usage so far, which g1t prices at Anthropic's list price:
1015+a JSON list with one entry per model, each {model, tier?, input_tokens, output_tokens,
1016+cache_write_5m_tokens, cache_write_1h_tokens, cache_read_tokens}. It is cumulative, so it replaces
1017+what the session reported before. - reads it from stdin.")]
1018+ Heartbeat(SessionHeartbeatArgs),
10151019 /// Record a note, prompt or message in the current session.
10161020 Log(SessionLogArgs),
10171021 /// Attach the current session to a work item, a pull request or a branch, or give it a title.
11521156 }
11531157
11541158 #[derive(Debug, Args)]
1159+pub struct SessionHeartbeatArgs {
1160+ /// The session's token usage so far, per model, as JSON; - reads it from stdin.
1161+ #[arg(long, value_name = "JSON")]
1162+ pub usage_json: Option<String>,
1163+}
1164+
1165+#[derive(Debug, Args)]
11551166 pub struct SessionEndArgs {
11561167 /// Why it ended: done, abandoned, handed_off...
11571168 #[arg(short, long)]
+41−4
1010 use std::path::PathBuf;
1111
1212 use crate::cli::{
13− SessionAcknowledgeArgs, SessionAttachArgs, SessionCommand, SessionEndArgs, SessionListArgs, SessionLogArgs,
13+ SessionAcknowledgeArgs, SessionAttachArgs, SessionCommand, SessionEndArgs, SessionHeartbeatArgs, SessionListArgs, SessionLogArgs,
1414 SessionStartArgs, SessionState, SessionTouchArgs, SessionViewArgs,
1515 };
1616 use crate::client::Client;
2222 pub fn run(ctx: &Ctx, command: SessionCommand) -> Result<()> {
2323 match command {
2424 SessionCommand::Start(args) => start(ctx, args),
25− SessionCommand::Heartbeat => heartbeat(ctx),
25+ SessionCommand::Heartbeat(args) => heartbeat(ctx, args),
2626 SessionCommand::Log(args) => log(ctx, args),
2727 SessionCommand::Attach(args) => attach(ctx, args),
2828 SessionCommand::End(args) => end(ctx, args),
181181 .send();
182182 }
183183
184−fn heartbeat(ctx: &Ctx) -> Result<()> {
184+/// Usage as `--usage-json` gives it: a JSON list of objects, each naming
185+/// its model. The server checks the counts.
186+fn usage_of(text: &str) -> Result<Value> {
187+ let value: Value = serde_json::from_str(text.trim())
188+ .map_err(|error| Error::usage(format!("--usage-json is not JSON: {error}.")))?;
189+ let named = |row: &Value| row["model"].as_str().is_some_and(|model| !model.trim().is_empty());
190+ match &value {
191+ Value::Array(rows) if rows.iter().all(named) => Ok(value),
192+ _ => Err(Error::usage(
193+ "--usage-json is a list of {model, tier, input_tokens, output_tokens, cache_write_5m_tokens, cache_write_1h_tokens, cache_read_tokens}.",
194+ )),
195+ }
196+}
197+
198+fn heartbeat(ctx: &Ctx, args: SessionHeartbeatArgs) -> Result<()> {
185199 let id = required()?;
200+ let usage = match args.usage_json.as_deref() {
201+ Some("-") => {
202+ let mut text = String::new();
203+ std::io::stdin().read_to_string(&mut text)?;
204+ Some(usage_of(&text)?)
205+ }
206+ Some(text) => Some(usage_of(text)?),
207+ None => None,
208+ };
186209 let (repo, client) = super::coordination_client(ctx, super::QUICK)?;
187− let found = client.op("update_session").repo(&repo).arg("id", &id).send()?;
210+ let mut request = client.op("update_session").repo(&repo).arg("id", &id);
211+ if let Some(usage) = usage {
212+ request = request.field("usage", usage);
213+ }
214+ let found = request.send()?;
188215 say_changes(&client, &repo, &id);
189216 if ctx.out.structured() {
190217 return ctx.out.emit(&session_of(found));
621648 use super::*;
622649
623650 #[test]
651+ fn usage_is_a_list_of_models_and_their_counts() {
652+ let usage = usage_of(r#" [{"model":"claude-opus-5-5","input_tokens":3}] "#).unwrap();
653+ assert_eq!(usage[0]["input_tokens"], 3);
654+ assert!(usage_of("[]").is_ok());
655+ assert!(usage_of("{}").is_err());
656+ assert!(usage_of(r#"[{"input_tokens":3}]"#).is_err());
657+ assert!(usage_of("not json").is_err());
658+ }
659+
660+ #[test]
624661 fn the_environment_overrides_the_kept_session() {
625662 let dir = std::env::temp_dir().join(format!("g1t-session-{}", std::process::id()));
626663 std::fs::create_dir_all(&dir).unwrap();
+1−0
61386138 Field { name: "result", kind: "string", required: false, values: &[] },
61396139 Field { name: "task", kind: "string", required: false, values: &[] },
61406140 Field { name: "title", kind: "string", required: false, values: &[] },
6141+ Field { name: "usage", kind: "array", required: false, values: &[] },
61416142 ],
61426143 },
61436144 Operation {

Comments

Sign in to comment.