chore(wip): checkpoint P8/C avant chantier Codex inter-agents
Sauvegarde de l'arbre de travail en cours (persistance P8, conversations C-series, write-portal frontend, médiation d'entrée) avant d'attaquer le support de la délégation inter-agents pour les profils Codex. Le round-trip inter-agent question/réponse est couvert sans tokens par les tests loopback existants (state::mcp_e2e_loopback_tests). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -325,6 +325,28 @@ pub struct AgentProfile {
|
||||
/// un profil sans motif sérialise exactement comme avant.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub prompt_ready_pattern: Option<String>,
|
||||
/// Séquence de soumission écrite **après** le texte d'une délégation pour la
|
||||
/// faire valider par la CLI (§20.3, fix Bug 1). Le portail d'écriture (front)
|
||||
/// écrit d'abord le texte (sans `\n`, pour esquiver la détection de paste de
|
||||
/// la TUI), puis **cette séquence seule** après un court délai.
|
||||
///
|
||||
/// `None` ⇒ défaut `"\r"` appliqué **au point d'usage** (front), jamais codé
|
||||
/// en dur dans le domaine (model-agnostic, déclaratif). Une autre TUI peut
|
||||
/// exiger une séquence différente → c'est précisément pourquoi c'est donnée.
|
||||
///
|
||||
/// `skip_serializing_if = Option::is_none` ⇒ **zéro régression** : un profil
|
||||
/// sans cette clé sérialise exactement comme avant.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub submit_sequence: Option<String>,
|
||||
/// Délai (ms) entre l'écriture du texte et celle de [`Self::submit_sequence`]
|
||||
/// (§20.3, fix Bug 1). Évite que la TUI absorbe la soumission comme un paste.
|
||||
///
|
||||
/// `None` ⇒ défaut (~60 ms) appliqué **au point d'usage** (front), jamais codé
|
||||
/// en dur dans le domaine.
|
||||
///
|
||||
/// `skip_serializing_if = Option::is_none` ⇒ **zéro régression** de sérialisation.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub submit_delay_ms: Option<u32>,
|
||||
}
|
||||
|
||||
/// Embedding strategy of an [`EmbedderProfile`] (LOT C, étage 2 vectoriel).
|
||||
@ -461,6 +483,8 @@ impl AgentProfile {
|
||||
structured_adapter: None,
|
||||
mcp: None,
|
||||
prompt_ready_pattern: None,
|
||||
submit_sequence: None,
|
||||
submit_delay_ms: None,
|
||||
})
|
||||
}
|
||||
|
||||
@ -491,6 +515,23 @@ impl AgentProfile {
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder : fixe la [`Self::submit_sequence`] (§20.3, fix Bug 1) et renvoie le
|
||||
/// profil. Laisse [`AgentProfile::new`] stable (zéro régression d'appel) : les
|
||||
/// profils qui s'en remettent au défaut `"\r"` ne l'appellent simplement pas.
|
||||
#[must_use]
|
||||
pub fn with_submit_sequence(mut self, sequence: impl Into<String>) -> Self {
|
||||
self.submit_sequence = Some(sequence.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder : fixe le [`Self::submit_delay_ms`] (§20.3, fix Bug 1) et renvoie le
|
||||
/// profil. Laisse [`AgentProfile::new`] stable (zéro régression d'appel).
|
||||
#[must_use]
|
||||
pub const fn with_submit_delay_ms(mut self, delay_ms: u32) -> Self {
|
||||
self.submit_delay_ms = Some(delay_ms);
|
||||
self
|
||||
}
|
||||
|
||||
/// Indique si ce profil peut être **proposé à la sélection/création** d'un
|
||||
/// agent (§17.3, lot D7). Source **unique** de vérité : un profil n'est
|
||||
/// sélectionnable que s'il porte un [`StructuredAdapter`], c'est-à-dire s'il
|
||||
@ -611,10 +652,9 @@ mod mcp_tests {
|
||||
#[test]
|
||||
fn mcp_config_strategy_uses_tagged_camel_case() {
|
||||
// The `strategy` tag and camelCase rename are part of the wire contract.
|
||||
let json = serde_json::to_string(
|
||||
&McpConfigStrategy::config_file(".mcp.json").expect("valid"),
|
||||
)
|
||||
.expect("serialise");
|
||||
let json =
|
||||
serde_json::to_string(&McpConfigStrategy::config_file(".mcp.json").expect("valid"))
|
||||
.expect("serialise");
|
||||
assert!(json.contains("\"strategy\":\"configFile\""), "got: {json}");
|
||||
}
|
||||
|
||||
@ -651,7 +691,12 @@ mod mcp_tests {
|
||||
#[test]
|
||||
fn config_file_accepts_safe_relative_target() {
|
||||
let s = McpConfigStrategy::config_file(".mcp.json").expect("safe relative");
|
||||
assert_eq!(s, McpConfigStrategy::ConfigFile { target: ".mcp.json".to_owned() });
|
||||
assert_eq!(
|
||||
s,
|
||||
McpConfigStrategy::ConfigFile {
|
||||
target: ".mcp.json".to_owned()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -663,7 +708,12 @@ mod mcp_tests {
|
||||
#[test]
|
||||
fn flag_accepts_non_empty() {
|
||||
let s = McpConfigStrategy::flag("--mcp-config {path}").expect("non-empty");
|
||||
assert_eq!(s, McpConfigStrategy::Flag { flag: "--mcp-config {path}".to_owned() });
|
||||
assert_eq!(
|
||||
s,
|
||||
McpConfigStrategy::Flag {
|
||||
flag: "--mcp-config {path}".to_owned()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -675,7 +725,12 @@ mod mcp_tests {
|
||||
#[test]
|
||||
fn env_accepts_valid_name() {
|
||||
let s = McpConfigStrategy::env("IDEA_MCP_SERVER").expect("valid");
|
||||
assert_eq!(s, McpConfigStrategy::Env { var: "IDEA_MCP_SERVER".to_owned() });
|
||||
assert_eq!(
|
||||
s,
|
||||
McpConfigStrategy::Env {
|
||||
var: "IDEA_MCP_SERVER".to_owned()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// -- Lot C5 : prompt_ready_pattern (détection retour-de-prompt) --------------
|
||||
@ -721,4 +776,60 @@ mod mcp_tests {
|
||||
assert_eq!(profile, back);
|
||||
assert_eq!(back.prompt_ready_pattern.as_deref(), Some("\n> "));
|
||||
}
|
||||
|
||||
// -- §20 : submit_sequence / submit_delay_ms (portail d'écriture) ------------
|
||||
|
||||
#[test]
|
||||
fn profile_default_has_no_submit_fields() {
|
||||
let p = profile_without_mcp();
|
||||
assert!(p.submit_sequence.is_none());
|
||||
assert!(p.submit_delay_ms.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn profile_without_submit_fields_omits_keys_in_json() {
|
||||
let json = serde_json::to_string(&profile_without_mcp()).expect("serialise");
|
||||
assert!(
|
||||
!json.contains("submitSequence"),
|
||||
"no submitSequence key when None (zero regression); got: {json}"
|
||||
);
|
||||
assert!(
|
||||
!json.contains("submitDelayMs"),
|
||||
"no submitDelayMs key when None (zero regression); got: {json}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_json_without_submit_fields_deserialises_to_none() {
|
||||
let legacy = r#"{
|
||||
"id": "00000000-0000-0000-0000-000000000000",
|
||||
"name": "Dev",
|
||||
"command": "claude",
|
||||
"args": [],
|
||||
"contextInjection": { "strategy": "conventionFile", "target": "CLAUDE.md" },
|
||||
"detect": null,
|
||||
"cwdTemplate": "{agentRunDir}"
|
||||
}"#;
|
||||
let p: AgentProfile = serde_json::from_str(legacy).expect("legacy deserialise");
|
||||
assert!(p.submit_sequence.is_none());
|
||||
assert!(p.submit_delay_ms.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_submit_fields_set_and_round_trip_camel_case() {
|
||||
let p = profile_without_mcp()
|
||||
.with_submit_sequence("\r")
|
||||
.with_submit_delay_ms(60);
|
||||
assert_eq!(p.submit_sequence.as_deref(), Some("\r"));
|
||||
assert_eq!(p.submit_delay_ms, Some(60));
|
||||
|
||||
let json = serde_json::to_string(&p).expect("serialise");
|
||||
assert!(json.contains("submitSequence"), "key present: {json}");
|
||||
assert!(json.contains("submitDelayMs"), "key present: {json}");
|
||||
|
||||
let back: AgentProfile = serde_json::from_str(&json).expect("deserialise");
|
||||
assert_eq!(p, back);
|
||||
assert_eq!(back.submit_sequence.as_deref(), Some("\r"));
|
||||
assert_eq!(back.submit_delay_ms, Some(60));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user