blob: 85d4a8214128b55eff4f21ae6d9f73bdfa0e2eac [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.
*/
//! Plan intent and model-reported progress. Neither is Host execution authority.
mod artifact;
pub mod owner;
pub mod repository;
mod transition;
pub use artifact::{Artifact, Complexity, Step};
use maka_agent_facts::event::Invocation;
use maka_agent_facts::execution::BehaviorId;
use maka_agent_facts::language::Language;
use maka_agent_ports::execution::Receipt;
use maka_agent_ports::execution::Submit;
use serde::{Deserialize, Serialize};
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("invalid Plan input: {0}")]
Invalid(String),
#[error("Plan changed or operation identity was reused")]
Conflict,
#[error("Plan storage is invalid: {0}")]
Corrupt(String),
#[error(transparent)]
Storage(#[from] maka_capabilities::storage::StoreError),
#[error(transparent)]
Execution(#[from] maka_capabilities::authorization::CommandError),
#[error(transparent)]
Capability(#[from] maka_capabilities::Error),
#[error(transparent)]
Tool(#[from] maka_agent_facts::tools::ToolError),
}
/// An exact operation is replayable even after subsequent state changes.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct Request<A = maka_capabilities::authorization::Id> {
pub operation_id: String,
pub expected_revision: u64,
pub command: Command<A>,
}
/// The caller must obtain the appropriate public Remote or invocation capability
/// before invoking these domain operations. Deserializing a command grants none.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(
tag = "kind",
rename_all = "snake_case",
rename_all_fields = "camelCase",
deny_unknown_fields
)]
pub enum Command<A = maka_capabilities::authorization::Id> {
Propose {
turn_id: String,
artifact: Artifact,
},
Revise {
proposal_id: String,
},
Abandon {
proposal_id: String,
},
Approve {
#[serde(default, skip_serializing_if = "Language::is_english")]
language: Language,
proposal_id: String,
proposal_revision: u64,
behavior: BehaviorId,
grant: A,
},
Dispatch {
execution_id: String,
},
Defer {
execution_id: String,
},
Reconcile {
execution_id: String,
grant: A,
},
Accept {
execution_id: String,
receipt: Receipt,
},
/// Only a definitive Host rejection; an ambiguous reply leaves admission pending.
Reject {
execution_id: String,
request_digest: String,
reason: String,
},
Progress {
execution_id: String,
invocation: Invocation,
steps: Vec<Progress>,
},
Interrupt {
execution_id: String,
invocation: Invocation,
reason: String,
},
Resume {
#[serde(default, skip_serializing_if = "Language::is_english")]
language: Language,
execution_id: String,
grant: A,
},
Cancel {
execution_id: String,
reason: String,
grant: Option<A>,
},
Settle {
execution_id: String,
invocation: Invocation,
outcome: Settlement,
},
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
pub enum Settlement {
Completed,
Interrupted { reason: String },
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct Snapshot<A = maka_capabilities::authorization::Id> {
pub revision: u64,
pub proposal: Option<Proposal>,
pub execution: Option<Execution<A>>,
}
impl<A> Default for Snapshot<A> {
fn default() -> Self {
Self {
revision: 0,
proposal: None,
execution: None,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
#[schemars(rename = "PlanProposal")]
pub struct Proposal {
pub id: String,
pub plan_id: String,
pub revision: u64,
pub turn_id: String,
pub artifact: Artifact,
pub status: ProposalStatus,
pub supersedes: Option<String>,
pub source_execution_id: Option<String>,
pub submitted_at: u64,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
#[schemars(rename = "PlanProposalStatus")]
pub enum ProposalStatus {
PendingApproval,
RevisionRequested,
Abandoned,
Approved,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct Execution<A = maka_capabilities::authorization::Id> {
pub id: String,
pub proposal_id: String,
pub artifact: Artifact,
pub steps: Vec<Progress>,
/// Frozen before calling Host. Recover the exact operation, never a new attempt.
pub request: Submit,
pub grant: A,
pub dispatched: bool,
pub cancellation: Option<String>,
pub phase: Phase,
pub updated_at: u64,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
pub enum Phase {
AwaitingAdmission,
Active {
receipt: Receipt,
},
Interrupted {
receipt: Option<Receipt>,
reason: String,
},
Completed {
receipt: Receipt,
},
Cancelled {
receipt: Option<Receipt>,
reason: String,
},
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(deny_unknown_fields)]
#[schemars(rename = "PlanProgress")]
pub struct Progress {
pub id: String,
pub status: StepStatus,
pub note: Option<String>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
#[schemars(rename = "PlanStepStatus")]
pub enum StepStatus {
Pending,
InProgress,
Completed,
Skipped,
}
fn invalid(message: impl ToString) -> Error {
Error::Invalid(message.to_string())
}
fn identifier(value: &str) -> Result<(), Error> {
if value.is_empty()
|| !value
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-')
{
return Err(invalid(
"identity must contain ASCII letters, digits, '_' or '-'",
));
}
Ok(())
}
fn text(value: &str) -> Result<(), Error> {
if value.trim().is_empty() || value.contains('\0') {
return Err(invalid("text is empty or contains NUL"));
}
Ok(())
}