| /* | |
| * 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, | |
| ) | |
| } |