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
fd33bc5
refactor(dgw): give the gateway state the provisioner task store
irvingoujAtDevolution Oct 6, 2026
20c7483
feat(dgw): run AI analysis as a provisioner task
irvingoujAtDevolution Oct 7, 2026
4be1d4d
feat(dgw): add endpoints to start and read an AI analysis
irvingoujAtDevolution Oct 6, 2026
c3a5c7e
fix(dgw): store provisioner task jobs before reporting them queued
irvingoujAtDevolution Oct 8, 2026
c14f64b
fix(dgw): keep the stored job of a provisioner task on restart
irvingoujAtDevolution Oct 8, 2026
929a801
docs(dgw): add the provisioner tasks intent
irvingoujAtDevolution Oct 8, 2026
d491a1a
docs(dgw): point the provisioner tasks intent to the task store crate
irvingoujAtDevolution Oct 8, 2026
3fe85cf
docs(dgw): add the jrec intent
irvingoujAtDevolution Oct 8, 2026
8e64bd8
docs(dgw): expand the provisioner tasks intent
irvingoujAtDevolution Oct 8, 2026
cefc2cb
test(dgw): keep the DVLS 2022.3 scope snapshot frozen
irvingoujAtDevolution Oct 13, 2026
2bd560e
fix(dgw): keep the AI provider URL out of Debug output
irvingoujAtDevolution Oct 13, 2026
5da268c
fix(dgw): bound the AI analysis progress percent in the OpenAPI schema
irvingoujAtDevolution Oct 13, 2026
5e482c0
fix(dgw): keep Gateway starting when provisioner tasks cannot be resumed
irvingoujAtDevolution Oct 13, 2026
dae452d
style(dgw): separate items and group imports in the AI analysis code
irvingoujAtDevolution Oct 13, 2026
1cebcdd
fix(dgw): ignore unknown fields in the AI analysis request
irvingoujAtDevolution Oct 13, 2026
5ca94b3
fix(dgw): start no AI analysis attempt that cannot finish before the …
irvingoujAtDevolution Oct 13, 2026
7ebf673
refactor(dgw): keep only step, done and total in the AI analysis prog…
irvingoujAtDevolution Oct 13, 2026
1e57ac1
fix(dgw): wait for the remux of the session before the AI analysis re…
irvingoujAtDevolution Oct 13, 2026
26b3fa8
fix(dgw): tie the AI analysis working files to their Task
irvingoujAtDevolution Oct 13, 2026
9ec73f9
feat(dgw): start a new AI analysis Task of a recording from scratch
irvingoujAtDevolution Oct 13, 2026
8ba1586
style(dgw): format the AI analysis task tests
irvingoujAtDevolution Oct 13, 2026
733854c
feat(dgw): write the AI analysis in the language of the request
irvingoujAtDevolution Oct 20, 2026
287da7f
test(dgw): give the fixture of another task its language
irvingoujAtDevolution Oct 20, 2026
81a5d9c
fix(dgw): clear the earlier AI analysis on every attempt of a new Task
irvingoujAtDevolution Oct 20, 2026
8975a0f
docs(dgw): describe the params of an AI analysis Task in the OpenAPI …
irvingoujAtDevolution Oct 20, 2026
45aefd7
test(dgw): run the AI analysis endpoint tests against the Gateway binary
irvingoujAtDevolution Oct 20, 2026
daafa3a
fix(dgw): name the AI provider schema of the Task params as the reque…
irvingoujAtDevolution Oct 20, 2026
a1929a6
fix(dgw): fail an analysis of a deleted recording as missing, not as …
irvingoujAtDevolution Oct 20, 2026
f2d63a1
test(dgw): stop Gateway before its config folder goes
irvingoujAtDevolution Oct 20, 2026
f4408da
style(dgw): use the imported AI provider name in the OpenAPI components
irvingoujAtDevolution Oct 20, 2026
ceeac66
test(dgw): check the reason of an analysis of a deleted recording
irvingoujAtDevolution Oct 20, 2026
6d3cd83
docs(dgw): describe the maximum output tokens with the range the serv…
irvingoujAtDevolution Oct 20, 2026
34c2178
refactor(dgw): keep the AI access in the secret store
irvingoujAtDevolution Oct 21, 2026
a4e8a98
docs(dgw): name the SecretStore in the tasks intent
irvingoujAtDevolution Oct 21, 2026
7719d74
docs(dgw): settle the AI analysis intent
irvingoujAtDevolution Oct 22, 2026
ce9f3e8
feat(dgw): let the expiry of a stored secret move
irvingoujAtDevolution Oct 22, 2026
a3d591b
feat(dgw): run the AI analysis Task as its intent says
irvingoujAtDevolution Oct 22, 2026
4783f3a
fix(dgw): retry the end of a provisioner Task until it succeeds
irvingoujAtDevolution Oct 22, 2026
12588c9
fix(dgw): count an AI request as unreported until it is answered
irvingoujAtDevolution Oct 22, 2026
1ce341f
fix(dgw): never list a late AI analysis log over a newer one
irvingoujAtDevolution Oct 22, 2026
55b667a
fix(dgw): keep the manifest's temporary file out of jrec downloads
irvingoujAtDevolution Oct 22, 2026
9367b79
fix(dgw): keep the documented AI analysis error kinds in step
irvingoujAtDevolution Oct 22, 2026
75325e5
docs(dgw): mark the AI analysis key expiry as untested
irvingoujAtDevolution Oct 22, 2026
ff73ce0
refactor(dgw): name the progress values the AI analysis records
irvingoujAtDevolution Oct 22, 2026
111756e
test(dgw): name the AI error mapping test after what it checks
irvingoujAtDevolution Oct 22, 2026
fc5763d
fix(dgw): list the log of an earlier success instead of dropping it
irvingoujAtDevolution Oct 22, 2026
e813020
fix(dgw): retry the end when a failed success cannot queue it
irvingoujAtDevolution Oct 22, 2026
2056a13
fix(dgw): stop the build on an undocumented AI analysis error field
irvingoujAtDevolution Oct 22, 2026
51566df
refactor(dgw): name the manifest's temporary file after the manifest
irvingoujAtDevolution Oct 22, 2026
3a973d6
docs(dgw): say what keeps a moved secret expiry on time
irvingoujAtDevolution Oct 22, 2026
d088d95
fix(dgw): end a Task whose success and failure both went unrecorded
irvingoujAtDevolution Oct 22, 2026
9066902
fix(dgw): read only Tasks of the same session for an unlisted log
irvingoujAtDevolution Oct 22, 2026
9480aa5
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