|
1 | 1 | //! Tools a long-lived agent's sessions get from the daemon, added to the |
2 | 2 | //! engine through `HostExtensions`: `message_agent`, `timer`, `schedule`, |
3 | | -//! `brief`, `question` and `report`. |
| 3 | +//! `brief`, `question`, `report` and `requirements`. |
4 | 4 |
|
5 | 5 | use crate::{ |
6 | 6 | hub::Hub, |
@@ -29,11 +29,44 @@ pub fn extensions(hub: Arc<Hub>, agent: String, session: String) -> HostExtensio |
29 | 29 | Err(error) => (json!({ "error": error }).to_string(), true), |
30 | 30 | } |
31 | 31 | }), |
32 | | - prompt: Arc::new(move || prompt_hub.agents.prompt(&prompt_agent, &prompt_session)), |
| 32 | + prompt: Arc::new(move || { |
| 33 | + let mut prompt = prompt_hub.agents.prompt(&prompt_agent, &prompt_session); |
| 34 | + prompt.push_str(&project_prompt(&prompt_hub, &prompt_agent)); |
| 35 | + prompt |
| 36 | + }), |
33 | 37 | exclusive: false, |
34 | 38 | } |
35 | 39 | } |
36 | 40 |
|
| 41 | +/// The project the agent belongs to: its name and directories. |
| 42 | +fn project_prompt(hub: &Hub, agent: &str) -> String { |
| 43 | + let Some(project) = hub |
| 44 | + .agents |
| 45 | + .get(agent) |
| 46 | + .and_then(|agent| agent.project) |
| 47 | + .and_then(|id| crate::projects::project(hub, &id)) |
| 48 | + else { |
| 49 | + return String::new(); |
| 50 | + }; |
| 51 | + let dirs: Vec<&str> = project["dirs"] |
| 52 | + .as_array() |
| 53 | + .into_iter() |
| 54 | + .flatten() |
| 55 | + .filter_map(Value::as_str) |
| 56 | + .collect(); |
| 57 | + format!( |
| 58 | + "\n<project id=\"{}\" name=\"{}\">\nYou belong to this project; the `requirements` tool lists its requirements.\nMain directory: {}\nExtra directories: {}\n</project>", |
| 59 | + project["id"].as_str().unwrap_or_default(), |
| 60 | + project["name"].as_str().unwrap_or_default(), |
| 61 | + project["path"].as_str().unwrap_or_default(), |
| 62 | + if dirs.is_empty() { |
| 63 | + "none".to_string() |
| 64 | + } else { |
| 65 | + dirs.join(", ") |
| 66 | + } |
| 67 | + ) |
| 68 | +} |
| 69 | + |
37 | 70 | /// The dispatcher's tools (see `dispatch`), plus `question`. |
38 | 71 | fn dispatcher(hub: Arc<Hub>, session: String) -> HostExtensions { |
39 | 72 | let prompt_hub = Arc::clone(&hub); |
@@ -229,6 +262,10 @@ fn run( |
229 | 262 | hub.post_report(&report)?; |
230 | 263 | Ok(json!({ "report": report.id })) |
231 | 264 | } |
| 265 | + "requirements" => { |
| 266 | + let project = hub.agents.get(agent).and_then(|agent| agent.project); |
| 267 | + crate::requirements::tool(hub, agent, project.as_deref(), session, args) |
| 268 | + } |
232 | 269 | other => Err(format!("unknown tool {other}")), |
233 | 270 | } |
234 | 271 | } |
@@ -335,5 +372,30 @@ fn definitions() -> Vec<Value> { |
335 | 372 | "additionalProperties": false |
336 | 373 | } |
337 | 374 | }), |
| 375 | + json!({ |
| 376 | + "type": "function", |
| 377 | + "name": "requirements", |
| 378 | + "description": "The user's requirements (what they mean to get done). `list` shows those of your project (`project`: another project's id, or `none` for unassigned ones; `state` filters). `get` shows one in full: words, progress, start gate, pending proposal. `propose_create` and `propose_close` only propose: the user is asked, and nothing changes until they accept. Do not propose the same thing twice; a pending proposal shows in `list`. `progress` adds to a requirement's progress record directly: list items are appended once each.", |
| 379 | + "parameters": { |
| 380 | + "type": "object", |
| 381 | + "properties": { |
| 382 | + "action": { "type": "string", "enum": ["list", "get", "propose_create", "propose_close", "progress"] }, |
| 383 | + "requirement": { "type": "string", "description": "Requirement id (R-12), for get, propose_close and progress." }, |
| 384 | + "project": { "type": "string", "description": "For list: a project id, or none for unassigned requirements." }, |
| 385 | + "state": { "type": "string", "enum": ["idea", "open", "done", "parked", "proposed"], "description": "For list." }, |
| 386 | + "text": { "type": "string", "description": "For propose_create: the requirement in the user's language." }, |
| 387 | + "reason": { "type": "string", "description": "For propose_create and propose_close: why, in one line the user reads." }, |
| 388 | + "outcome": { "type": "string", "enum": ["done", "parked"], "description": "For propose_close." }, |
| 389 | + "decided": { "type": "array", "items": { "type": "string" } }, |
| 390 | + "done": { "type": "array", "items": { "type": "string" } }, |
| 391 | + "doing": { "type": "array", "items": { "type": "string" } }, |
| 392 | + "blocked": { "type": "array", "items": { "type": "string" } }, |
| 393 | + "next": { "type": "array", "items": { "type": "string" } }, |
| 394 | + "note": { "type": "string", "description": "For progress: a note that replaces the previous one." } |
| 395 | + }, |
| 396 | + "required": ["action"], |
| 397 | + "additionalProperties": false |
| 398 | + } |
| 399 | + }), |
338 | 400 | ] |
339 | 401 | } |
0 commit comments