Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
d0bab56
refactor(dgw): give the gateway state the provisioner task store
irvingoujAtDevolution Oct 6, 2026
f398d0c
feat(dgw): run AI analysis as a provisioner task
irvingoujAtDevolution Oct 7, 2026
e66ab4d
feat(dgw): add endpoints to start and read an AI analysis
irvingoujAtDevolution Oct 6, 2026
3b58f94
fix(dgw): store provisioner task jobs before reporting them queued
irvingoujAtDevolution Oct 8, 2026
3cb3ca3
fix(dgw): keep the stored job of a provisioner task on restart
irvingoujAtDevolution Oct 8, 2026
4400ea0
docs(dgw): add the provisioner tasks intent
irvingoujAtDevolution Oct 8, 2026
18572ef
docs(dgw): point the provisioner tasks intent to the task store crate
irvingoujAtDevolution Oct 8, 2026
d2c11a0
docs(dgw): add the jrec intent
irvingoujAtDevolution Oct 8, 2026
3cc6fe9
docs(dgw): expand the provisioner tasks intent
irvingoujAtDevolution Oct 8, 2026
fc1207f
test(dgw): keep the DVLS 2022.3 scope snapshot frozen
irvingoujAtDevolution Oct 13, 2026
232d466
fix(dgw): keep the AI provider URL out of Debug output
irvingoujAtDevolution Oct 13, 2026
576a537
fix(dgw): bound the AI analysis progress percent in the OpenAPI schema
irvingoujAtDevolution Oct 13, 2026
34728dc
fix(dgw): keep Gateway starting when provisioner tasks cannot be resumed
irvingoujAtDevolution Oct 13, 2026
79f65b0
style(dgw): separate items and group imports in the AI analysis code
irvingoujAtDevolution Oct 13, 2026
369a086
fix(dgw): ignore unknown fields in the AI analysis request
irvingoujAtDevolution Oct 13, 2026
b5bebdd
fix(dgw): start no AI analysis attempt that cannot finish before the …
irvingoujAtDevolution Oct 13, 2026
c5f72c8
refactor(dgw): keep only step, done and total in the AI analysis prog…
irvingoujAtDevolution Oct 13, 2026
645a294
fix(dgw): wait for the remux of the session before the AI analysis re…
irvingoujAtDevolution Oct 13, 2026
49c9c4c
fix(dgw): tie the AI analysis working files to their Task
irvingoujAtDevolution Oct 13, 2026
8e3c8dd
feat(dgw): start a new AI analysis Task of a recording from scratch
irvingoujAtDevolution Oct 13, 2026
74d0153
style(dgw): format the AI analysis task tests
irvingoujAtDevolution Oct 13, 2026
f3a9d67
feat(dgw): write the AI analysis in the language of the request
irvingoujAtDevolution Oct 20, 2026
634a41f
test(dgw): give the fixture of another task its language
irvingoujAtDevolution Oct 20, 2026
2873cca
fix(dgw): clear the earlier AI analysis on every attempt of a new Task
irvingoujAtDevolution Oct 20, 2026
b1e3d32
docs(dgw): describe the params of an AI analysis Task in the OpenAPI …
irvingoujAtDevolution Oct 20, 2026
815aeeb
test(dgw): run the AI analysis endpoint tests against the Gateway binary
irvingoujAtDevolution Oct 20, 2026
e14b283
fix(dgw): name the AI provider schema of the Task params as the reque…
irvingoujAtDevolution Oct 20, 2026
de9e7c7
fix(dgw): fail an analysis of a deleted recording as missing, not as …
irvingoujAtDevolution Oct 20, 2026
4dab45a
test(dgw): stop Gateway before its config folder goes
irvingoujAtDevolution Oct 20, 2026
b4e8a3c
style(dgw): use the imported AI provider name in the OpenAPI components
irvingoujAtDevolution Oct 20, 2026
e536bb4
test(dgw): check the reason of an analysis of a deleted recording
irvingoujAtDevolution Oct 20, 2026
d306d45
docs(dgw): describe the maximum output tokens with the range the serv…
irvingoujAtDevolution Oct 20, 2026
366ccba
refactor(dgw): keep the AI access in the secret store
irvingoujAtDevolution Oct 21, 2026
8b16370
docs(dgw): name the SecretStore in the tasks intent
irvingoujAtDevolution Oct 21, 2026
f231c48
docs(dgw): settle the AI analysis intent
irvingoujAtDevolution Oct 22, 2026
0471852
feat(dgw): let the expiry of a stored secret move
irvingoujAtDevolution Oct 22, 2026
178f802
feat(dgw): run the AI analysis Task as its intent says
irvingoujAtDevolution Oct 22, 2026
9cbcde9
fix(dgw): retry the end of a provisioner Task until it succeeds
irvingoujAtDevolution Oct 22, 2026
14f3251
fix(dgw): count an AI request as unreported until it is answered
irvingoujAtDevolution Oct 22, 2026
2a79588
fix(dgw): never list a late AI analysis log over a newer one
irvingoujAtDevolution Oct 22, 2026
5731649
fix(dgw): keep the manifest's temporary file out of jrec downloads
irvingoujAtDevolution Oct 22, 2026
850236c
fix(dgw): keep the documented AI analysis error kinds in step
irvingoujAtDevolution Oct 22, 2026
7fbfc20
docs(dgw): mark the AI analysis key expiry as untested
irvingoujAtDevolution Oct 22, 2026
cd9493b
refactor(dgw): name the progress values the AI analysis records
irvingoujAtDevolution Oct 22, 2026
aaee72b
test(dgw): name the AI error mapping test after what it checks
irvingoujAtDevolution Oct 22, 2026
481a104
fix(dgw): list the log of an earlier success instead of dropping it
irvingoujAtDevolution Oct 22, 2026
9d38aca
fix(dgw): retry the end when a failed success cannot queue it
irvingoujAtDevolution Oct 22, 2026
f94532d
fix(dgw): stop the build on an undocumented AI analysis error field
irvingoujAtDevolution Oct 22, 2026
90b21f8
refactor(dgw): name the manifest's temporary file after the manifest
irvingoujAtDevolution Oct 22, 2026
ee826cc
docs(dgw): say what keeps a moved secret expiry on time
irvingoujAtDevolution Oct 22, 2026
4762f95
fix(dgw): end a Task whose success and failure both went unrecorded
irvingoujAtDevolution Oct 22, 2026
a4caae6
fix(dgw): read only Tasks of the same session for an unlisted log
irvingoujAtDevolution Oct 22, 2026
a6e3fa8
refactor(dgw): keep one rule for what the end does to an unlisted log
irvingoujAtDevolution Oct 22, 2026
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
6 changes: 6 additions & 0 deletions Cargo.lock

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

14 changes: 10 additions & 4 deletions crates/devolutions-gateway-ai/examples/analyze_recording.rs
Original file line number Diff line number Diff line change
Expand Up @@ -246,10 +246,16 @@ async fn main() -> anyhow::Result<()> {
print_actions(&std::fs::read_to_string(&analysis.log_path)?)?;
println!();

let usage = analysis.usage.map_or_else(
|| "not reported".to_owned(),
|usage| format!("{} in, {} out", usage.input_tokens, usage.output_tokens),
);
let usage = match analysis.usage.unreported_requests {
0 => format!(
"{} in, {} out",
analysis.usage.input_tokens, analysis.usage.output_tokens
),
unreported => format!(
"{} in, {} out, and {unreported} requests not reported",
analysis.usage.input_tokens, analysis.usage.output_tokens
),
};
println!(" {} {}", style(DIM, "model "), analysis.model);
println!(" {} {}", style(DIM, "prompt "), analysis.prompt_version);
println!(" {} {usage}", style(DIM, "tokens "));
Expand Down
61 changes: 5 additions & 56 deletions crates/devolutions-gateway-ai/src/recording_analysis/checkpoint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,68 +8,26 @@ use anyhow::Context as _;
use camino::{Utf8Path, Utf8PathBuf};
use serde::{Deserialize, Serialize};

use crate::Response;
use crate::session_actions::Action;
use crate::{Response, Usage};

/// Tokens counted by the AI provider, as kept in checkpoints.
///
/// It mirrors [`Usage`], so a change of [`Usage`] never changes the checkpoint format.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct TokenUsage {
input_tokens: u64,
output_tokens: u64,
}

impl From<Usage> for TokenUsage {
fn from(usage: Usage) -> Self {
Self {
input_tokens: usage.input_tokens,
output_tokens: usage.output_tokens,
}
}
}

impl From<TokenUsage> for Usage {
fn from(usage: TokenUsage) -> Self {
Self {
input_tokens: usage.input_tokens,
output_tokens: usage.output_tokens,
}
}
}

pub(super) fn path(workspace: &Utf8Path, index: usize) -> Utf8PathBuf {
workspace.join(format!("chunk-{index:04}.json"))
}

/// Actions found in one chunk, with what the AI provider reported for the requests about it.
#[derive(Debug, Clone, PartialEq)]
/// Actions found in one chunk, with the model that described them.
#[derive(Debug, Clone, Default, PartialEq)]
pub(super) struct DescribedChunk {
pub(super) actions: Vec<Action>,
/// Model that answered, as reported by the provider.
pub(super) model: Option<String>,
/// Tokens of every request about the chunk, answers cut and asked again included; `None` when one was not reported.
pub(super) usage: Option<Usage>,
}

impl Default for DescribedChunk {
/// No action yet, and no token counted yet.
fn default() -> Self {
Self {
actions: Vec::new(),
model: None,
usage: Some(Usage::default()),
}
}
}

impl DescribedChunk {
/// Adds the answer to one request about the chunk.
pub(super) fn add(&mut self, response: Response<Vec<Action>>) {
self.actions.extend(response.output);
self.model = self.model.take().or(response.model);
self.usage = super::add_usage(self.usage, response.usage);
}
}

Expand All @@ -78,8 +36,6 @@ impl DescribedChunk {
struct SavedChunk {
#[serde(default, skip_serializing_if = "Option::is_none")]
model: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
usage: Option<TokenUsage>,
actions: Vec<SavedAction>,
}

Expand All @@ -100,7 +56,6 @@ pub(super) fn write(path: &Utf8Path, chunk: &DescribedChunk) -> anyhow::Result<(

let saved = SavedChunk {
model: chunk.model.clone(),
usage: chunk.usage.map(TokenUsage::from),
actions: chunk
.actions
.iter()
Expand Down Expand Up @@ -141,7 +96,6 @@ pub(super) fn read(path: &Utf8Path) -> anyhow::Result<DescribedChunk> {
Ok(DescribedChunk {
actions,
model: saved.model,
usage: saved.usage.map(Usage::from),
})
}

Expand Down Expand Up @@ -169,10 +123,6 @@ mod tests {
},
],
model: Some("gpt-test-2026-09-30".to_owned()),
usage: Some(Usage {
input_tokens: 10,
output_tokens: 20,
}),
};

write(&path, &chunk).expect("written");
Expand All @@ -183,18 +133,17 @@ mod tests {
}

#[test]
fn unreported_model_and_usage_stay_unknown() {
fn an_unreported_model_stays_unknown() {
// oracle: property round-trip
let dir = tempfile::tempdir().expect("temp dir");
let path = path(Utf8Path::from_path(dir.path()).expect("UTF-8"), 0);
let chunk = DescribedChunk {
actions: Vec::new(),
model: None,
usage: None,
};

write(&path, &chunk).expect("written");

assert_eq!(std::fs::read_to_string(&path).expect("checkpoint"), r#"{"actions":[]}"#);
assert_eq!(read(&path).expect("read"), chunk);
}
}
Loading
Loading