holon 0.14.1

A headless, event-driven runtime for long-lived agents
Documentation
use anyhow::Result;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::Value;

use crate::{
    runtime::RuntimeHandle,
    tool::spec::typed_spec,
    types::{ToolCapabilityFamily, TrustLevel, WorkItemReadiness, WorkItemState},
};

use super::{
    serialize_success,
    work_item_query::{
        active_wait_conditions_by_work_item, latest_delivery_summaries_by_work_item,
        lifecycle_view, query_context, view_for_record, WorkItemFocusView, WorkItemLifecycleView,
        WorkItemQueryContext, WorkItemView,
    },
    BuiltinToolDefinition,
};
use crate::tool::helpers::parse_tool_args;

pub(crate) const NAME: &str = "ListWorkItems";
const DEFAULT_LIMIT: usize = 20;
const MAX_LIMIT: usize = 100;

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
#[allow(dead_code)]
pub(crate) enum ListWorkItemsFilter {
    All,
    Open,
    Completed,
    Current,
    Queued,
    Blocked,
    WaitingForOperator,
    Runnable,
}

#[derive(Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub(crate) struct ListWorkItemsArgs {
    #[serde(default)]
    pub(crate) filter: Option<ListWorkItemsFilter>,
    #[serde(default)]
    pub(crate) limit: Option<usize>,
    #[serde(default)]
    pub(crate) include_todo_list: bool,
}

#[derive(Serialize, Deserialize)]
pub(crate) struct ListWorkItemsResult {
    pub(crate) context: WorkItemQueryContext,
    pub(crate) filter: ListWorkItemsFilter,
    pub(crate) returned: usize,
    pub(crate) total_matching: usize,
    pub(crate) limit: usize,
    pub(crate) work_items: Vec<WorkItemView>,
}

pub(crate) fn definition() -> Result<BuiltinToolDefinition> {
    Ok(BuiltinToolDefinition {
        family: ToolCapabilityFamily::CoreAgent,
        spec: typed_spec::<ListWorkItemsArgs>(
            NAME,
            "List recent work items with explicit current, open, completed, queued, blocked, waiting_for_operator, and runnable views. Use this before relying on memory briefs for work-item focus.",
        )?,
    })
}

pub(crate) async fn execute(
    runtime: &RuntimeHandle,
    _agent_id: &str,
    _trust: &TrustLevel,
    input: &Value,
) -> Result<crate::tool::ToolResult> {
    let args: ListWorkItemsArgs = parse_tool_args(NAME, input)?;
    let filter = args.filter.unwrap_or(ListWorkItemsFilter::All);
    let limit = args.limit.unwrap_or(DEFAULT_LIMIT).clamp(1, MAX_LIMIT);
    let context = query_context(runtime).await?;
    let mut records = runtime.latest_work_items().await?;
    records.sort_by(|left, right| right.updated_at.cmp(&left.updated_at));
    let matching = records
        .into_iter()
        .filter(|record| matches_filter(record, &context, &filter))
        .collect::<Vec<_>>();
    let total_matching = matching.len();
    let selected = matching.into_iter().take(limit).collect::<Vec<_>>();
    let delivery_summaries = if selected
        .iter()
        .any(|record| record.state == WorkItemState::Completed)
    {
        Some(latest_delivery_summaries_by_work_item(runtime)?)
    } else {
        None
    };
    let wait_conditions = active_wait_conditions_by_work_item(runtime, &selected)?;
    let mut work_items = Vec::with_capacity(selected.len());
    for record in selected {
        work_items.push(
            view_for_record(
                runtime,
                &context,
                record,
                args.include_todo_list,
                delivery_summaries.as_ref(),
                Some(&wait_conditions),
            )
            .await?,
        );
    }
    serialize_success(
        NAME,
        &ListWorkItemsResult {
            context,
            filter,
            returned: work_items.len(),
            total_matching,
            limit,
            work_items,
        },
    )
}

fn matches_filter(
    record: &crate::types::WorkItemRecord,
    context: &WorkItemQueryContext,
    filter: &ListWorkItemsFilter,
) -> bool {
    let is_current = context.current_work_item_id.as_deref() == Some(record.id.as_str())
        && record.state == WorkItemState::Open;
    match filter {
        ListWorkItemsFilter::All => true,
        ListWorkItemsFilter::Open => lifecycle_view(&record.state) == WorkItemLifecycleView::Open,
        ListWorkItemsFilter::Completed => {
            lifecycle_view(&record.state) == WorkItemLifecycleView::Completed
        }
        ListWorkItemsFilter::Current => is_current,
        ListWorkItemsFilter::Queued => {
            !is_current
                && super::work_item_query::focus_view(record, is_current)
                    == WorkItemFocusView::Queued
        }
        ListWorkItemsFilter::Blocked => {
            !is_current
                && super::work_item_query::focus_view(record, is_current)
                    == WorkItemFocusView::Blocked
        }
        ListWorkItemsFilter::WaitingForOperator => {
            record.readiness() == WorkItemReadiness::WaitingForOperator
        }
        ListWorkItemsFilter::Runnable => record.readiness() == WorkItemReadiness::Runnable,
    }
}