fix: fix some displays and features
This commit is contained in:
@ -15,11 +15,11 @@ use std::sync::Arc;
|
||||
|
||||
use domain::ports::{
|
||||
AgentContextStore, AgentRuntime, ContextInjectionPlan, EventBus, FileSystem, PreparedContext,
|
||||
ProfileStore, PtyPort, RemotePath, SpawnSpec,
|
||||
ProfileStore, PtyPort, RemotePath, SkillStore, SpawnSpec, StoreError,
|
||||
};
|
||||
use domain::{
|
||||
Agent, AgentId, AgentManifest, AgentOrigin, DomainEvent, ManifestEntry, MarkdownDoc, NodeId,
|
||||
Project, ProfileId, ProjectPath, PtySize, SessionKind, SessionStatus, TerminalSession,
|
||||
Project, ProfileId, ProjectPath, PtySize, SessionKind, SessionStatus, Skill, TerminalSession,
|
||||
};
|
||||
|
||||
use crate::error::AppError;
|
||||
@ -350,6 +350,7 @@ pub struct LaunchAgent {
|
||||
runtime: Arc<dyn AgentRuntime>,
|
||||
fs: Arc<dyn FileSystem>,
|
||||
pty: Arc<dyn PtyPort>,
|
||||
skills: Arc<dyn SkillStore>,
|
||||
sessions: Arc<TerminalSessions>,
|
||||
events: Arc<dyn EventBus>,
|
||||
}
|
||||
@ -364,6 +365,7 @@ impl LaunchAgent {
|
||||
runtime: Arc<dyn AgentRuntime>,
|
||||
fs: Arc<dyn FileSystem>,
|
||||
pty: Arc<dyn PtyPort>,
|
||||
skills: Arc<dyn SkillStore>,
|
||||
sessions: Arc<TerminalSessions>,
|
||||
events: Arc<dyn EventBus>,
|
||||
) -> Self {
|
||||
@ -373,11 +375,35 @@ impl LaunchAgent {
|
||||
runtime,
|
||||
fs,
|
||||
pty,
|
||||
skills,
|
||||
sessions,
|
||||
events,
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolves the Markdown bodies of an agent's assigned skills, in the
|
||||
/// **manifest order** (deterministic). A skill that no longer exists in its
|
||||
/// store (deleted out from under the assignment) is silently skipped — a
|
||||
/// dangling [`domain::SkillRef`] must not block a launch.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`AppError::Store`] on any store failure other than a missing skill.
|
||||
async fn resolve_skills(
|
||||
&self,
|
||||
agent: &Agent,
|
||||
root: &ProjectPath,
|
||||
) -> Result<Vec<Skill>, AppError> {
|
||||
let mut out = Vec::with_capacity(agent.skills.len());
|
||||
for skill_ref in &agent.skills {
|
||||
match self.skills.get(skill_ref.scope, root, skill_ref.skill_id).await {
|
||||
Ok(skill) => out.push(skill),
|
||||
Err(StoreError::NotFound) => {}
|
||||
Err(e) => return Err(e.into()),
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Executes the launch.
|
||||
///
|
||||
/// Step order is contractually significant (and unit-tested): resolve the
|
||||
@ -444,8 +470,10 @@ impl LaunchAgent {
|
||||
.runtime
|
||||
.prepare_invocation(&profile, &prepared, &run_dir)?;
|
||||
|
||||
// 5. Apply the injection plan side effects *before* spawning.
|
||||
self.apply_injection(&input.project, &agent.context_path, &content, &mut spec)
|
||||
// 5. Resolve the agent's assigned skills (their `.md` bodies), then apply
|
||||
// the injection plan side effects *before* spawning.
|
||||
let skills = self.resolve_skills(&agent, &input.project.root).await?;
|
||||
self.apply_injection(&input.project, &agent.context_path, &content, &skills, &mut spec)
|
||||
.await?;
|
||||
|
||||
// 6. Spawn the PTY at the resolved cwd; adopt its session id everywhere.
|
||||
@ -487,6 +515,7 @@ impl LaunchAgent {
|
||||
project: &Project,
|
||||
context_rel_path: &str,
|
||||
content: &MarkdownDoc,
|
||||
skills: &[Skill],
|
||||
spec: &mut SpawnSpec,
|
||||
) -> Result<(), AppError> {
|
||||
match spec.context_plan.clone() {
|
||||
@ -496,9 +525,10 @@ impl LaunchAgent {
|
||||
// run directory — `spec.cwd` is that run dir, never the project
|
||||
// root, so there is zero collision between agents. The document is
|
||||
// composed: an absolute project-root header (so the agent knows
|
||||
// where to operate, since its cwd is *not* the root) followed by
|
||||
// the agent's persona `.md`.
|
||||
let document = compose_convention_file(project.root.as_str(), content.as_str());
|
||||
// where to operate, since its cwd is *not* the root), the agent's
|
||||
// persona `.md`, then the bodies of its assigned skills (§14.2).
|
||||
let document =
|
||||
compose_convention_file(project.root.as_str(), content.as_str(), skills);
|
||||
let path = RemotePath::new(join(&spec.cwd, &target));
|
||||
self.fs.write(&path, document.as_bytes()).await?;
|
||||
}
|
||||
@ -537,13 +567,17 @@ fn agent_run_dir(root: &ProjectPath, agent_id: &AgentId) -> Result<ProjectPath,
|
||||
|
||||
/// Composes the convention file IdeA writes into an agent's run directory: an
|
||||
/// absolute project-root header (the agent's cwd is the run dir, *not* the root,
|
||||
/// so it must be told where to work) followed by the agent's persona `.md`.
|
||||
/// so it must be told where to work), the agent's persona `.md`, then the bodies
|
||||
/// of its assigned `skills` under a `# Skills` section (ARCHITECTURE §14.2).
|
||||
///
|
||||
/// Kept as a **pure** function (no I/O) so it is unit-testable in isolation, and
|
||||
/// deliberately structured so future blocks (assigned skills, shared project
|
||||
/// context — ARCHITECTURE §14.2) can be appended without touching the launcher.
|
||||
/// Skills are emitted in the order given (the caller passes them in manifest
|
||||
/// order, making the output deterministic); each is introduced by a `##` header
|
||||
/// carrying its name. When `skills` is empty the section is omitted entirely, so
|
||||
/// an agent with no skills gets exactly the previous document.
|
||||
///
|
||||
/// Kept as a **pure** function (no I/O) so it is unit-testable in isolation.
|
||||
#[must_use]
|
||||
pub(crate) fn compose_convention_file(project_root: &str, agent_md: &str) -> String {
|
||||
pub(crate) fn compose_convention_file(project_root: &str, agent_md: &str, skills: &[Skill]) -> String {
|
||||
let mut out = String::new();
|
||||
out.push_str("# Project root\n\n");
|
||||
out.push_str(project_root);
|
||||
@ -554,6 +588,17 @@ pub(crate) fn compose_convention_file(project_root: &str, agent_md: &str) -> Str
|
||||
);
|
||||
out.push_str("---\n\n");
|
||||
out.push_str(agent_md);
|
||||
|
||||
if !skills.is_empty() {
|
||||
out.push_str("\n\n---\n\n# Skills\n");
|
||||
for skill in skills {
|
||||
out.push_str("\n## ");
|
||||
out.push_str(&skill.name);
|
||||
out.push_str("\n\n");
|
||||
out.push_str(skill.content_md.as_str());
|
||||
out.push('\n');
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
@ -610,7 +655,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn compose_convention_file_carries_root_then_persona() {
|
||||
let doc = compose_convention_file("/abs/project/root", "# Persona\n\nDo things.");
|
||||
let doc = compose_convention_file("/abs/project/root", "# Persona\n\nDo things.", &[]);
|
||||
|
||||
// Absolute project root present.
|
||||
assert!(doc.contains("/abs/project/root"));
|
||||
@ -621,5 +666,38 @@ mod tests {
|
||||
let root_at = doc.find("/abs/project/root").unwrap();
|
||||
let persona_at = doc.find("# Persona").unwrap();
|
||||
assert!(root_at < persona_at, "root header must precede the persona");
|
||||
// No skills ⇒ no Skills section.
|
||||
assert!(!doc.contains("# Skills"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compose_convention_file_appends_assigned_skills_in_order() {
|
||||
let s = |n: u128, name: &str, body: &str| {
|
||||
Skill::new(
|
||||
domain::SkillId::from_uuid(uuid::Uuid::from_u128(n)),
|
||||
name,
|
||||
MarkdownDoc::new(body),
|
||||
domain::SkillScope::Global,
|
||||
)
|
||||
.unwrap()
|
||||
};
|
||||
let doc = compose_convention_file(
|
||||
"/root",
|
||||
"# Persona",
|
||||
&[s(1, "refactor", "REFAC_BODY"), s(2, "review", "REVIEW_BODY")],
|
||||
);
|
||||
|
||||
// Both skill bodies present, after the persona.
|
||||
assert!(doc.contains("REFAC_BODY"));
|
||||
assert!(doc.contains("REVIEW_BODY"));
|
||||
let persona_at = doc.find("# Persona").unwrap();
|
||||
let refac_at = doc.find("REFAC_BODY").unwrap();
|
||||
let review_at = doc.find("REVIEW_BODY").unwrap();
|
||||
assert!(persona_at < refac_at, "skills come after the persona");
|
||||
// Deterministic order: first assigned skill precedes the second.
|
||||
assert!(refac_at < review_at, "skills emitted in the given order");
|
||||
// Skill names surface as sub-headers.
|
||||
assert!(doc.contains("## refactor"));
|
||||
assert!(doc.contains("## review"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user