speccoder_runtime/substrate/
ask.rs1use std::path::Path;
5use std::sync::Arc;
6
7use agentplane::core::{CorrelationKey, Delivery, InboundEvent};
8use agentplane::manifest::{Budgets, Manifest, ModelRef, Models, ToolGrant};
9use agentplane::model::ModelProvider;
10use agentplane::prelude::*;
11use agentplane::runtime::{Agent, RunOutcome, RunStreamObserver, RunTerms};
12use serde_json::Value;
13
14use super::model_config::ModelConfig;
15use super::{Plane, PlaneError, RunClaim, open_or_replace};
16
17const CONVERSATION_TOKENS: u64 = 50_000_000;
20
21fn manifest(model: &ModelConfig) -> Result<Manifest, PlaneError> {
22 Manifest::builder("speccoder-ask", env!("CARGO_PKG_VERSION"))
23 .configure(|spec| {
24 spec.capabilities
25 .provides
26 .push(crate::ask::skill::CAPABILITY.to_owned());
27 spec.models = Some(Models {
28 privileged: Some(ModelRef {
29 max_tokens: Some(model.max_output_tokens),
30 ..ModelRef::new(model.provider.clone(), model.model.clone())
31 }),
32 ..Models::default()
33 });
34 spec.security.max_sensitivity_egress = Some(Sensitivity::Internal);
35 spec.budgets = Some(Budgets {
36 max_tokens: Some(CONVERSATION_TOKENS),
37 ..Budgets::default()
38 });
39 let workspace = crate::tools::catalog()
41 .into_iter()
42 .filter(|t| crate::ask::tools::WORKSPACE.contains(&t.name))
43 .map(|t| {
44 (
45 t.server.to_owned(),
46 t.name.to_owned(),
47 t.description.to_owned(),
48 )
49 });
50 let readers = crate::ask::tools::declarations()
51 .into_iter()
52 .filter(|d| d.name != crate::ask::PROPOSE)
53 .map(|d| (crate::ask::tools::SERVER.to_owned(), d.name, d.description));
54 for (server, name, description) in workspace.chain(readers) {
55 spec.tools.push(ToolGrant {
56 description: Some(description),
57 max_sensitivity: Some(Sensitivity::Internal),
58 mutates: false,
59 ..ToolGrant::new(agentplane::tools::ToolId::new(&server, &name).reference())
60 });
61 }
62 })
63 .build()
64 .map_err(|e| PlaneError(format!("the Ask manifest: {e}")))
65}
66
67pub struct AskWiring {
69 pub workspace: Arc<crate::tools::WorkspaceTools>,
71 pub readers: Arc<crate::ask::tools::AskTools>,
73 pub progress: Option<Arc<dyn crate::progress::Progress>>,
75 pub stream: Option<Arc<dyn RunStreamObserver>>,
77 pub halt: agentplane::model::Stop,
79}
80
81impl Plane {
82 pub fn open_ask(
88 dir: &Path,
89 model: &ModelConfig,
90 provider: Arc<dyn ModelProvider>,
91 wiring: AskWiring,
92 ) -> Result<Self, PlaneError> {
93 let claim = RunClaim::take(dir)?;
94 let (store, replaced_journal) = open_or_replace(dir)?;
95 let manifest = manifest(model)?;
96 let agent = Agent::new(&manifest).skill(crate::ask::skill::Ask {
97 style: model.prompt_style(),
98 progress: wiring.progress,
99 halt: wiring.halt,
100 });
101 let workspace: Arc<dyn agentplane::tools::ToolClient> = wiring.workspace;
102 let mut builder = Runtime::builder_on(store)
103 .agent(agent)
104 .provider(model.provider.clone(), provider)
105 .tool_server("fs", Arc::clone(&workspace))
106 .tool_server("git", workspace)
107 .tool_server(crate::ask::tools::SERVER, wiring.readers);
108 if let Some(stream) = wiring.stream {
109 builder = builder.observe_model_streams(stream);
110 }
111 let runtime = builder
112 .try_build()
113 .map_err(|e| PlaneError(format!("building the Ask plane: {e}")))?;
114 Ok(Self {
115 runtime,
116 replaced_journal,
117 _run: Some(claim),
118 })
119 }
120
121 pub async fn ask_start(
127 &self,
128 id: &str,
129 input: Tainted<Value>,
130 ) -> Result<RunOutcome, PlaneError> {
131 self.runtime
132 .start(crate::ask::skill::CAPABILITY, input)
133 .terms(
134 RunTerms::default().correlated("ask", &[CorrelationKey::new("ask", id.to_owned())]),
135 )
136 .run()
137 .await
138 .map_err(|e| PlaneError(e.to_string()))
139 }
140
141 pub async fn ask_deliver(
149 &self,
150 id: &str,
151 message_id: &str,
152 payload: Value,
153 ) -> Result<bool, PlaneError> {
154 let event = InboundEvent::new(
155 "urn:speccoder:person",
156 message_id,
157 crate::ask::skill::MESSAGE,
158 payload,
159 )
160 .correlate(CorrelationKey::new("ask", id.to_owned()));
161 match self.runtime.deliver(&event).await {
162 Ok(Delivery::Resumed { .. }) => Ok(true),
163 Ok(_) => Ok(false),
164 Err(e) => Err(PlaneError(e.to_string())),
165 }
166 }
167
168 pub async fn ask_stop(&self, run: &str, by: &str) -> Result<(), PlaneError> {
175 let run = agentplane::core::RunId::parse(run).map_err(|e| PlaneError(e.to_string()))?;
176 let by = agentplane::core::Operator::asserted(by).map_err(|e| PlaneError(e.to_string()))?;
177 self.runtime
178 .request_cancel(run, &by, "stopped by the person asking")
179 .await
180 .map(|_| ())
181 .map_err(|e| PlaneError(e.to_string()))
182 }
183}