pub(super) fn bound_questions(
state: &mut InquiryState,
events: &mut Vec<InquiryEvent>,
limits: &InquiryLimits,
reason: &str,
) -> Result<(), String> {
let queued = state
.questions
.iter()
.filter(|question| question.status == a3s::research::QuestionStatus::Queued)
.cloned()
.collect::<Vec<_>>();
bound_question_batch(state, events, limits, &queued, reason)
}
pub(super) fn bound_question_batch(
state: &mut InquiryState,
events: &mut Vec<InquiryEvent>,
limits: &InquiryLimits,
questions: &[Question],
reason: &str,
) -> Result<(), String> {
let reason = bounded_question_event_reason(reason, limits.max_text_chars);
for question in questions {
apply_event(
state,
events,
InquiryEvent::QuestionBounded {
question_id: question.id.clone(),
reason: reason.clone(),
},
limits,
)?;
}
Ok(())
}
fn bounded_question_event_reason(reason: &str, maximum: usize) -> String {
let normalized = reason.split_whitespace().collect::<Vec<_>>().join(" ");
let detail = if normalized.is_empty() {
"question resolution ended without a diagnostic"
} else {
normalized.as_str()
};
detail.chars().take(maximum).collect()
}
pub(super) fn queue_plan_questions(
plan: &Value,
state: &mut InquiryState,
events: &mut Vec<InquiryEvent>,
limits: &InquiryLimits,
) -> Result<(), String> {
let tracks = plan
.get("tracks")
.and_then(Value::as_array)
.ok_or_else(|| "DeepResearch plan has no tracks".to_string())?;
let mut questions = Vec::new();
for (track_index, track) in tracks.iter().enumerate() {
let track = track
.as_object()
.ok_or_else(|| "DeepResearch plan contains a non-object track".to_string())?;
let obligation_id = required_text(track, "id")?;
let material = track
.get("material")
.and_then(Value::as_bool)
.ok_or_else(|| "DeepResearch plan track omitted boolean `material`".to_string())?;
let prompts = string_array(
track.get("questions"),
"track questions",
limits.max_questions,
)?;
let completion_criterion_count = track
.get("completion_criteria")
.and_then(Value::as_array)
.map(Vec::len)
.filter(|count| *count > 0)
.ok_or_else(|| {
format!("DeepResearch plan track `{obligation_id}` has no completion criteria")
})?;
let question_count = prompts.len();
for (question_index, prompt) in prompts.into_iter().enumerate() {
let id = format!("question:plan-{}-{}", track_index + 1, question_index + 1);
let mut question = Question::queued(id, None, prompt);
question.obligation_ids = vec![obligation_id.to_string()];
question.completion_criterion_indexes = if question_count == completion_criterion_count
{
vec![question_index]
} else if question_count == 1 {
(0..completion_criterion_count).collect()
} else if completion_criterion_count == 1 {
vec![0]
} else {
return Err(format!(
"DeepResearch plan track `{obligation_id}` cannot map {question_count} questions onto {completion_criterion_count} completion criteria"
));
};
question.material = material;
question.round = 0;
questions.push(question);
}
}
if questions.is_empty() {
return Err("DeepResearch plan did not queue any research question".to_string());
}
apply_event(
state,
events,
InquiryEvent::QuestionsQueued { questions },
limits,
)
}
pub(super) fn workflow_args_with_plan(
mut args: Value,
plan: Value,
run_id: Option<&str>,
) -> Result<Value, String> {
exact_string_array(plan.get("search_queries"), "search_queries", 4)?;
let plan = normalize_planner_budget(plan)?;
let input = args
.get_mut("input")
.and_then(Value::as_object_mut)
.ok_or_else(|| "DeepResearch workflow args have no input object".to_string())?;
input.insert("research_plan".to_string(), plan);
input.insert(
"execution_mode".to_string(),
Value::String("collect_only".to_string()),
);
input.insert("research_plan_fixture".to_string(), Value::Bool(false));
input.remove("run_started_at_ms");
if let Some(run_id) = run_id {
args.as_object_mut()
.ok_or_else(|| "DeepResearch workflow args are not an object".to_string())?
.insert("run_id".to_string(), Value::String(run_id.to_string()));
}
Ok(args)
}
pub(super) fn host_fallback_plan(workflow_args: &Value) -> Result<PlannedInquiry, String> {
let query = workflow_args
.pointer("/input/query")
.and_then(Value::as_str)
.map(str::trim)
.filter(|query| !query.is_empty())
.ok_or_else(|| "DeepResearch fallback contract requires a non-empty query".to_string())?;
let local_only = workflow_args
.pointer("/input/evidence_scope")
.and_then(Value::as_str)
== Some("local_only");
let report_title = bounded_fallback_text(query, 160);
let focus = bounded_fallback_text(query, 500);
let question = bounded_fallback_text(query, 240);
let plan = serde_json::json!({
"report_title": report_title,
"freshness_required": false,
"workspace_evidence_required": local_only,
"tracks": [{
"id": "request.primary",
"title": bounded_fallback_text(query, 160),
"focus": focus,
"material": true,
"questions": [question.clone()],
"completion_criteria": [question],
"evidence_requirements": {
"primary_source_required": false,
"independent_corroboration_required": false
}
}],
"search_queries": if local_only { Vec::<String>::new() } else { vec![query.to_string()] },
"seed_urls": [],
"budget": {
"retrieval_timeout_ms": 90_000,
"direct_searches": if local_only { 0 } else { 1 },
"direct_fetches": if local_only { 0 } else { 8 }
},
"stop_conditions": [
"Material evidence is retained or the request is explicitly bounded."
]
});
validate_plan(plan)
}
pub(super) fn host_plan_from_outline(
workflow_args: &Value,
outline: Value,
) -> Result<PlannedInquiry, String> {
let query = workflow_args
.pointer("/input/query")
.and_then(Value::as_str)
.map(str::trim)
.filter(|query| !query.is_empty())
.ok_or_else(|| "DeepResearch outline contract requires a non-empty query".to_string())?;
let local_only = workflow_args
.pointer("/input/evidence_scope")
.and_then(Value::as_str)
== Some("local_only");
let mut outline = close_semantic_outline(outline)?;
let targets = semantic_outline_track_targets(&outline)?;
let tracks = targets
.into_iter()
.map(|target| {
let mut target = target
.as_object()
.cloned()
.ok_or_else(|| "DeepResearch outline contains a non-object target".to_string())?;
let title = required_text(&target, "title")?.to_string();
target.insert("focus".to_string(), Value::String(title.clone()));
target.insert(
"questions".to_string(),
Value::Array(vec![Value::String(title.clone())]),
);
target.insert(
"completion_criteria".to_string(),
Value::Array(vec![Value::String(title)]),
);
target.insert(
"evidence_requirements".to_string(),
serde_json::json!({
"primary_source_required": false,
"independent_corroboration_required": false
}),
);
Ok(Value::Object(target))
})
.collect::<Result<Vec<_>, String>>()?;
let object = outline
.as_object_mut()
.ok_or_else(|| "DeepResearch outline planner returned a non-object fragment".to_string())?;
object.insert("tracks".to_string(), Value::Array(tracks));
if local_only {
object.insert("workspace_evidence_required".to_string(), Value::Bool(true));
}
object.insert(
"search_queries".to_string(),
if local_only {
Value::Array(Vec::new())
} else {
Value::Array(vec![Value::String(query.to_string())])
},
);
object.insert("seed_urls".to_string(), Value::Array(Vec::new()));
object.insert(
"budget".to_string(),
serde_json::json!({
"retrieval_timeout_ms": 90_000,
"direct_searches": if local_only { 0 } else { 1 },
"direct_fetches": if local_only { 0 } else { 8 }
}),
);
validate_plan(outline)
}
pub(super) fn bootstrap_workflow_args(args: Value, run_id: &str) -> Result<Value, String> {
let plan = host_fallback_plan(&args)?;
let mut args = workflow_args_with_plan(args, plan.value, Some(run_id))?;
args.pointer_mut("/input/execution_mode")
.ok_or_else(|| "DeepResearch bootstrap args omitted execution mode".to_string())?
.clone_from(&Value::String("bootstrap_acquisition".to_string()));
Ok(args)
}
pub(super) fn attach_bootstrap_acquisition(
workflow_args: &mut Value,
acquisition: Value,
) -> Result<(), String> {
let sources = acquisition
.pointer("/packet/sources")
.and_then(Value::as_array)
.filter(|sources| !sources.is_empty())
.ok_or_else(|| {
"DeepResearch bootstrap acquisition contains no reusable raw source packet".to_string()
})?;
if sources.len() > 16 {
return Err(
"DeepResearch bootstrap acquisition exceeds the source catalog limit".to_string(),
);
}
workflow_args
.get_mut("input")
.and_then(Value::as_object_mut)
.ok_or_else(|| "DeepResearch workflow args have no input object".to_string())?
.insert("bootstrap_acquisition".to_string(), acquisition);
Ok(())
}
fn bounded_fallback_text(value: &str, maximum_chars: usize) -> String {
value.chars().take(maximum_chars).collect()
}
fn normalize_planner_budget(mut plan: Value) -> Result<Value, String> {
let Some(budget) = plan.get_mut("budget").and_then(Value::as_object_mut) else {
return Ok(plan);
};
let Some(seconds) = budget.remove("retrieval_timeout_secs") else {
return Ok(plan);
};
let seconds = seconds
.as_u64()
.filter(|seconds| *seconds > 0)
.ok_or_else(|| {
"DeepResearch plan budget `retrieval_timeout_secs` must be a positive integer"
.to_string()
})?;
let milliseconds = seconds.checked_mul(1_000).ok_or_else(|| {
"DeepResearch plan budget `retrieval_timeout_secs` exceeds millisecond range".to_string()
})?;
budget.insert(
"retrieval_timeout_ms".to_string(),
Value::from(milliseconds),
);
Ok(plan)
}
pub(super) fn bound_workflow_timeout(args: &mut Value, timeout_ms: u64) -> Result<(), String> {
args.get_mut("limits")
.and_then(Value::as_object_mut)
.ok_or_else(|| "DeepResearch workflow args have no limits object".to_string())?
.insert("timeoutMs".to_string(), Value::from(timeout_ms));
Ok(())
}
fn string_array(
value: Option<&Value>,
resource: &str,
maximum: usize,
) -> Result<Vec<String>, String> {
let values = value
.and_then(Value::as_array)
.ok_or_else(|| format!("DeepResearch plan {resource} is not an array"))?;
if values.len() > maximum {
return Err(format!(
"DeepResearch plan {resource} has {} items; maximum is {maximum}",
values.len()
));
}
values
.iter()
.map(|value| {
value
.as_str()
.map(str::trim)
.filter(|value| !value.is_empty())
.map(str::to_string)
.ok_or_else(|| format!("DeepResearch plan {resource} contains a blank item"))
})
.collect()
}
pub(super) fn exact_string_array(
value: Option<&Value>,
resource: &str,
maximum: usize,
) -> Result<Vec<String>, String> {
let values = value
.and_then(Value::as_array)
.ok_or_else(|| format!("DeepResearch plan {resource} is not an array"))?;
if values.len() > maximum {
return Err(format!(
"DeepResearch plan {resource} has {} items; maximum is {maximum}",
values.len()
));
}
values
.iter()
.map(|value| {
let value = value.as_str().ok_or_else(|| {
format!("DeepResearch plan {resource} contains a non-string item")
})?;
if value.is_empty() || value.trim().is_empty() {
return Err(format!(
"DeepResearch plan {resource} contains a blank item"
));
}
if value.trim() != value {
return Err(format!(
"DeepResearch plan {resource} contains an item with surrounding whitespace"
));
}
Ok(value.to_string())
})
.collect()
}
fn required_text<'a>(object: &'a Map<String, Value>, key: &str) -> Result<&'a str, String> {
object
.get(key)
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.ok_or_else(|| format!("DeepResearch plan omitted non-empty `{key}`"))
}