blob: 34b5efb151bfa38757cccd716c6784db40479a85 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
use crate::{Inner, RunContext, RunError, context_input, model_attempt};
use maka_agent_facts::context::CheckpointMode;
use maka_agent_facts::context::CompactOutcome;
use maka_agent_facts::context::ContextCheckpoint;
use maka_agent_facts::context::TextSummary;
use maka_agent_facts::context::default_compaction_prompt;
use maka_agent_facts::event::Fact;
use maka_agent_facts::event::RuntimeEvent;
use std::sync::Arc;
use tokio_util::sync::CancellationToken;
pub(super) type Candidate = (CompactOutcome, Option<RuntimeEvent>);
pub(super) struct Job {
input: RunContext,
source: model_attempt::RequestSource,
mode: CheckpointMode,
prompt: Vec<maka_model::prompt::Message>,
surface: Arc<crate::request_composition::Surface>,
definitions: Vec<maka_model::ToolDefinition>,
}
pub(super) async fn run(
inner: &Arc<Inner>,
input: &mut RunContext,
mode: &CheckpointMode,
cancellation: &CancellationToken,
continuation_base: Option<u64>,
) -> Result<Candidate, RunError> {
let Some(job) = capture(inner, input, mode, cancellation, continuation_base).await? else {
return Ok((
CompactOutcome::Unchanged {
reason: "no_new_history".into(),
},
None,
));
};
job.execute(inner, cancellation).await
}
pub(super) async fn capture(
inner: &Arc<Inner>,
input: &mut RunContext,
mode: &CheckpointMode,
cancellation: &CancellationToken,
continuation_base: Option<u64>,
) -> Result<Option<Job>, RunError> {
input.refresh_model(cancellation).await?;
let source = tokio::select! {
biased;
_ = cancellation.cancelled() => return Err(RunError::Cancelled),
source = inner.log.prepare_context_compaction(
&input.invocation.session_id,
Some(&input.invocation.invocation_id),
mode,
) => source?,
};
let has_new_history = source
.tail
.iter()
.any(|event| match event {
maka_event_log::context::ContextEvent::ModelItems(step) => !step.items.is_empty(),
maka_event_log::context::ContextEvent::Canonical(stored) => matches!(
&stored.event.fact,
Fact::InvocationOpened {
input: maka_agent_facts::input::InvocationInput::Message { .. },
..
} | Fact::MessageSteered { .. }
| Fact::ModelCompleted { .. }
) || matches!(&stored.event.fact, Fact::MessageImported { record, .. } if record.is_conversation()),
maka_event_log::context::ContextEvent::Archived(_) => false,
});
let prompt = model_attempt::prompt(
inner,
input,
&source,
cancellation,
continuation_base,
&input.invocation.invocation_id,
false,
)
.await?;
if !has_new_history
&& (source.baseline.is_none() || !context_input::needs_compaction(input, &prompt))
{
return Ok(None);
}
let adapter = inner.model.binding(&input.provider)?;
let prior = match &source.latest_main {
maka_event_log::context::LatestMainContext::Selected(latest)
if latest.model_id == input.provider.model
&& latest.route_identity == model_attempt::route_identity(input)?
&& latest.connection_id.as_deref()
== input
.configuration
.model
.as_ref()
.map(|model| model.connection_id.as_str()) =>
{
inner
.log
.completed_request_composition(&input.invocation.session_id, latest.sequence)
.await?
}
_ => None,
};
let mut composition =
prior.unwrap_or_else(|| maka_agent_facts::composition::RequestComposition {
system_prompt: input
.configuration
.system_prompt
.as_ref()
.map(|prompt| prompt.text.clone()),
dynamic_context: vec![],
tool_catalog_digest: maka_agent_facts::artifact::content_digest(b"[]"),
tools: vec![],
provider_options: Some(input.provider_options.clone()),
max_output_tokens: None,
sources: input
.configuration
.system_prompt
.as_ref()
.map(|prompt| prompt.sources.clone())
.unwrap_or_default(),
});
// A relocated project can change the admitted cwd since the last Main.
composition.dynamic_context = crate::request_composition::session_context(input);
// Reuse the Main system/tool prefix for provider prefix caching. The summary
// instruction is appended after history; execution binds the current adapter
// and model revision on this admitted route.
composition.sources.retain(|source| {
!matches!(
source.kind,
maka_agent_facts::composition::SourceKind::ModelAdapter
| maka_agent_facts::composition::SourceKind::ModelProvider
)
});
composition.sources.push(adapter.source().clone());
composition
.sources
.extend(input.model_revision.iter().cloned());
let definitions = composition.tools.clone();
let mut captured = input.clone();
if let Some(options) = &composition.provider_options {
captured.provider_options = options.clone();
}
let surface = Arc::new(crate::request_composition::Surface::summary(
adapter,
composition,
context_input::summary_output_budget(input),
)?);
let prompt = surface.apply(prompt);
Ok(Some(Job {
input: captured,
source: model_attempt::RequestSource::from(&source),
mode: mode.clone(),
prompt,
surface,
definitions,
}))
}
impl Job {
pub async fn execute(
self,
inner: &Arc<Inner>,
cancellation: &CancellationToken,
) -> Result<Candidate, RunError> {
let Self {
input,
source,
mode,
prompt,
surface,
definitions,
} = self;
let instruction = maka_model::prompt::Message::user(
input
.configuration
.compaction_prompt
.clone()
.unwrap_or_else(|| default_compaction_prompt(input.configuration.language).into()),
);
let mut input_budget = context_input::summary_budget(&input)
.saturating_sub(context_input::estimate(std::slice::from_ref(&instruction)))
.saturating_sub(context_input::json_tokens(&definitions));
let mut overflowed = false;
loop {
let mut request = if overflowed {
context_input::fit_summary(&prompt, input_budget, input.configuration.language)
} else {
prompt.clone()
};
request.push(instruction.clone());
let result = model_attempt::execute_once(
inner,
&input,
&source,
request,
definitions.clone(),
model_attempt::Attempt::Summary {
surface: surface.clone(),
},
cancellation,
&mut false,
&mut None,
)
.await;
let (step_id, output) = match result {
Ok(output) => output,
Err(RunError::Model(maka_model::ModelError::ContextOverflow {
observed_output: false,
})) if input_budget > 256 => {
input_budget = input_budget.min(context_input::estimate(&prompt)).max(1) / 2;
overflowed = true;
continue;
}
Err(error) => return Err(error),
};
let summary = match TextSummary::from_model_step(&output) {
Ok(summary) => summary,
Err(defect) => return Ok(failed(&defect.to_string())),
};
let checkpoint = RuntimeEvent::new(
input.invocation.clone(),
Fact::ContextCheckpointRecorded {
checkpoint: ContextCheckpoint {
mode: mode.clone(),
covered_through: source.evidence.high_water,
source_digest: source.evidence.digest.clone(),
previous_checkpoint_id: source.checkpoint.clone(),
summary,
summary_step_id: step_id,
},
},
);
return Ok((
CompactOutcome::Compacted {
checkpoint_id: checkpoint.id.clone(),
},
Some(checkpoint),
));
}
}
}
fn failed(reason: &str) -> (CompactOutcome, Option<RuntimeEvent>) {
(
CompactOutcome::Failed {
reason: reason.into(),
},
None,
)
}