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.

Triage suggestions read a JSON answer too, say what did not read, and are asked again (Part of #206) #209

Mergedsyntaqx merged suggest-retry into main
2 files+45−40/2 viewed
+6−0
1+-- Triage suggestions the first deploy of the plan's upkeep asked for and
2+-- could not read (items/plan_settle.rs read only four lines then, and kept
3+-- no answer it could not read): asked again, once, now that a JSON answer
4+-- reads too. Only open work items still in Triage with no suggestion.
5+UPDATE work_items SET suggested_at = NULL
6+WHERE state = 'open' AND COALESCE(horizon, 'triage') = 'triage' AND suggestion IS NULL AND suggested_at IS NOT NULL;
+39−4
1616
1717 use std::collections::HashSet;
1818
19−use g1t_contracts::ai::{AiBriefArgs, MAX_BRIEF_INPUT_CHARS};
19+use g1t_contracts::ai::{AiBriefArgs, MAX_BRIEF_INPUT_CHARS, MAX_BRIEF_TOKENS};
2020 use g1t_contracts::events::WorkItemEvent;
2121 use g1t_contracts::time::rfc3339;
2222 use g1t_contracts::work::*;
6464 /// say a horizon it may suggest. `known` are the open work items it may
6565 /// say this one repeats.
6666 pub(crate) fn read_suggestion(answer: &str, known: &[u32], at: &str) -> Option<PlanSuggestion> {
67+ // Some models answer with a JSON object instead of the four lines.
68+ let json = answer.find('{').zip(answer.rfind('}')).and_then(|(open, close)| serde_json::from_str::<serde_json::Value>(&answer[open..=close]).ok());
69+ if let Some(serde_json::Value::Object(fields)) = json {
70+ let text = |key: &str| match fields.get(key) {
71+ Some(serde_json::Value::String(text)) => text.clone(),
72+ Some(serde_json::Value::Number(number)) => number.to_string(),
73+ _ => String::new(),
74+ };
75+ let lines = format!(
76+ "horizon: {}\nappetite: {}\nduplicate: {}\nwhy: {}",
77+ text("horizon"),
78+ text("appetite"),
79+ if text("duplicate").is_empty() { text("duplicate_of") } else { text("duplicate") },
80+ text("why")
81+ );
82+ return read_lines(&lines, known, at);
83+ }
84+ read_lines(answer, known, at)
85+}
86+
87+/// [`read_suggestion`] of the four `key: value` lines.
88+fn read_lines(answer: &str, known: &[u32], at: &str) -> Option<PlanSuggestion> {
6789 let mut horizon = None;
6890 let mut appetite = None;
6991 let mut duplicate_of = None;
7395 continue;
7496 };
7597 let value = value.trim().trim_matches(|c: char| c == '*' || c == '`' || c == '"').trim();
76− match key.trim().trim_matches(|c: char| c == '*' || c == '-').trim().to_ascii_lowercase().as_str() {
98+ match key.trim().trim_matches(|c: char| c == '*' || c == '-' || c == '#' || c == '"').trim().to_ascii_lowercase().as_str() {
7799 "horizon" => horizon = Horizon::parse(value).filter(|h| matches!(h, Horizon::Now | Horizon::Next | Horizon::Later)),
78100 "appetite" => appetite = Appetite::parse(value),
79101 "duplicate" => {
170192 match g1t_kit::call::<_, Outcome<Option<String>>>(integrations, "ai_brief", &args).await {
171193 Ok(Outcome::Ok(Some(answer))) => Some(answer),
172194 Ok(Outcome::Ok(None)) => {
195+ worker::console_log!("{workspace}: the plan got no answer from the AI provider (none chosen, or an empty answer)");
173196 budget.without.insert(workspace.to_owned());
174197 None
175198 }
281304 listed.join("\n")
282305 );
283306 let at = rfc3339(now_ms());
284− let answer = self.brief(&item.workspace, SUGGEST_SYSTEM, input, 120, budget).await;
307+ let answer = self.brief(&item.workspace, SUGGEST_SYSTEM, input, MAX_BRIEF_TOKENS, budget).await;
285308 if answer.is_none() && !budget.without.contains(&item.workspace) {
286309 // Out of calls for this run: asked again on the next.
287310 break;
288311 }
289312 let suggestion = answer.as_deref().and_then(|answer| read_suggestion(answer, &known, &at));
313+ if let (Some(answer), None) = (&answer, &suggestion) {
314+ worker::console_log!("{}#{}: the AI provider's suggestion did not read: {}", item.workspace, item.number, clip(answer, 300));
315+ }
290316 // Asked once: with no provider, or no usable answer, it is not
291317 // asked again.
292318 self.db
365391 if decisions.is_empty() { none() } else { decisions.join("\n") },
366392 if pulls.is_empty() { none() } else { pulls.join("\n") },
367393 );
368− let answer = self.brief(&item.workspace, STANDING_SYSTEM, input, 120, budget).await;
394+ let answer = self.brief(&item.workspace, STANDING_SYSTEM, input, MAX_BRIEF_TOKENS, budget).await;
369395 if answer.is_none() && !budget.without.contains(&item.workspace) {
370396 break;
371397 }
405431 // A horizon it may not suggest, or none: no suggestion.
406432 assert_eq!(read_suggestion("horizon: set_aside\nwhy: old", &[], at), None);
407433 assert_eq!(read_suggestion("I think this should go next.", &[], at), None);
434+ // A JSON object, in a code fence or not.
435+ let json = "```json\n{\"horizon\": \"next\", \"appetite\": \"medium\", \"duplicate\": 12, \"why\": \"Same as #12.\"}\n```";
436+ let read = read_suggestion(json, &[12], at).unwrap();
437+ assert_eq!((read.horizon, read.appetite, read.duplicate_of, read.why.as_str()), (Horizon::Next, Some(Appetite::Medium), Some(12), "Same as #12."));
438+ let read = read_suggestion("{\"horizon\":\"later\",\"appetite\":\"small\",\"duplicate\":null,\"why\":\"Nice to have.\"}", &[], at).unwrap();
439+ assert_eq!((read.horizon, read.duplicate_of), (Horizon::Later, None));
440+ // Keys in quotes or headings.
441+ let read = read_suggestion("## Horizon: next\n\"appetite\": small\nwhy: ok", &[], at).unwrap();
442+ assert_eq!((read.horizon, read.appetite), (Horizon::Next, Some(Appetite::Small)));
408443 }
409444
410445 #[test]