Merge pull request #153 from soilSpoon/feature/ampcode

feature(ampcode): Enhances ampcode schema with new message types and fields
This commit is contained in:
Nathan Flurry 2026-02-10 22:12:08 -08:00 committed by GitHub
commit 87a4e81d31
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 167 additions and 33 deletions

View file

@ -1058,26 +1058,21 @@ fn spawn_amp(
let mut args: Vec<&str> = Vec::new();
if flags.execute {
args.push("--execute");
} else if flags.print {
args.push("--print");
args.push(&options.prompt);
}
if flags.output_format {
args.push("--output-format");
args.push("stream-json");
args.push("--stream-json");
}
if flags.dangerously_skip_permissions && options.permission_mode.as_deref() == Some("bypass") {
args.push("--dangerously-skip-permissions");
args.push("--dangerously-allow-all");
}
let mut command = Command::new(path);
command.current_dir(working_dir);
if let Some(model) = options.model.as_deref() {
command.arg("--model").arg(model);
}
if let Some(session_id) = options.session_id.as_deref() {
command.arg("--continue").arg(session_id);
}
command.args(&args).arg(&options.prompt);
command.args(&args);
for (key, value) in &options.env {
command.env(key, value);
}
@ -1101,24 +1096,19 @@ fn build_amp_command(path: &Path, working_dir: &Path, options: &SpawnOptions) ->
let flags = detect_amp_flags(path, working_dir).unwrap_or_default();
let mut command = Command::new(path);
command.current_dir(working_dir);
if let Some(model) = options.model.as_deref() {
command.arg("--model").arg(model);
}
if let Some(session_id) = options.session_id.as_deref() {
command.arg("--continue").arg(session_id);
}
if flags.execute {
command.arg("--execute");
} else if flags.print {
command.arg("--print");
command.arg(&options.prompt);
}
if flags.output_format {
command.arg("--output-format").arg("stream-json");
command.arg("--stream-json");
}
if flags.dangerously_skip_permissions && options.permission_mode.as_deref() == Some("bypass") {
command.arg("--dangerously-skip-permissions");
command.arg("--dangerously-allow-all");
}
command.arg(&options.prompt);
for (key, value) in &options.env {
command.env(key, value);
}
@ -1128,7 +1118,6 @@ fn build_amp_command(path: &Path, working_dir: &Path, options: &SpawnOptions) ->
#[derive(Debug, Default, Clone, Copy)]
struct AmpFlags {
execute: bool,
print: bool,
output_format: bool,
dangerously_skip_permissions: bool,
}
@ -1146,9 +1135,8 @@ fn detect_amp_flags(path: &Path, working_dir: &Path) -> Option<AmpFlags> {
);
Some(AmpFlags {
execute: text.contains("--execute"),
print: text.contains("--print"),
output_format: text.contains("--output-format"),
dangerously_skip_permissions: text.contains("--dangerously-skip-permissions"),
output_format: text.contains("--stream-json"),
dangerously_skip_permissions: text.contains("--dangerously-allow-all"),
})
}
@ -1157,23 +1145,19 @@ fn spawn_amp_fallback(
working_dir: &Path,
options: &SpawnOptions,
) -> Result<std::process::Output, AgentError> {
let mut attempts = vec![
let mut attempts: Vec<Vec<&str>> = vec![
vec!["--execute"],
vec!["--print", "--output-format", "stream-json"],
vec!["--output-format", "stream-json"],
vec!["--dangerously-skip-permissions"],
vec!["stream-json"],
vec!["--dangerously-allow-all"],
vec![],
];
if options.permission_mode.as_deref() != Some("bypass") {
attempts.retain(|args| !args.contains(&"--dangerously-skip-permissions"));
attempts.retain(|args| !args.contains(&"--dangerously-allow-all"));
}
for args in attempts {
let mut command = Command::new(path);
command.current_dir(working_dir);
if let Some(model) = options.model.as_deref() {
command.arg("--model").arg(model);
}
if let Some(session_id) = options.session_id.as_deref() {
command.arg("--continue").arg(session_id);
}
@ -1192,9 +1176,6 @@ fn spawn_amp_fallback(
let mut command = Command::new(path);
command.current_dir(working_dir);
if let Some(model) = options.model.as_deref() {
command.arg("--model").arg(model);
}
if let Some(session_id) = options.session_id.as_deref() {
command.arg("--continue").arg(session_id);
}

View file

@ -73,3 +73,32 @@ fn test_amp_message() {
assert!(json.contains("user"));
assert!(json.contains("Hello"));
}
#[test]
fn test_amp_stream_json_message_types() {
// Test that all new message types can be parsed
let system_msg = r#"{"type":"system","subtype":"init","cwd":"/tmp","session_id":"sess-1","tools":["Bash"],"mcp_servers":[]}"#;
let parsed: amp::StreamJsonMessage = serde_json::from_str(system_msg).unwrap();
assert!(matches!(parsed.type_, amp::StreamJsonMessageType::System));
let user_msg = r#"{"type":"user","message":{"role":"user","content":"Hello"},"session_id":"sess-1"}"#;
let parsed: amp::StreamJsonMessage = serde_json::from_str(user_msg).unwrap();
assert!(matches!(parsed.type_, amp::StreamJsonMessageType::User));
let assistant_msg = r#"{"type":"assistant","message":{"role":"assistant","content":"Hi there"},"session_id":"sess-1"}"#;
let parsed: amp::StreamJsonMessage = serde_json::from_str(assistant_msg).unwrap();
assert!(matches!(parsed.type_, amp::StreamJsonMessageType::Assistant));
let result_msg = r#"{"type":"result","subtype":"success","duration_ms":1000,"is_error":false,"num_turns":1,"result":"Done","session_id":"sess-1"}"#;
let parsed: amp::StreamJsonMessage = serde_json::from_str(result_msg).unwrap();
assert!(matches!(parsed.type_, amp::StreamJsonMessageType::Result));
// Test legacy types still work
let message_msg = r#"{"type":"message","id":"msg-1","content":"Hello"}"#;
let parsed: amp::StreamJsonMessage = serde_json::from_str(message_msg).unwrap();
assert!(matches!(parsed.type_, amp::StreamJsonMessageType::Message));
let done_msg = r#"{"type":"done"}"#;
let parsed: amp::StreamJsonMessage = serde_json::from_str(done_msg).unwrap();
assert!(matches!(parsed.type_, amp::StreamJsonMessageType::Done));
}

View file

@ -21,6 +21,72 @@ pub fn event_to_universal(
) -> Result<Vec<EventConversion>, String> {
let mut events = Vec::new();
match event.type_ {
// System init message - contains metadata like cwd, tools, session_id
// We skip this as it's not a user-facing event
schema::StreamJsonMessageType::System => {}
// User message - extract content from the nested message field
schema::StreamJsonMessageType::User => {
if !event.message.is_empty() {
let text = event
.message
.get("content")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let item = UniversalItem {
item_id: next_temp_id("tmp_amp_user"),
native_item_id: event.session_id.clone(),
parent_id: None,
kind: ItemKind::Message,
role: Some(ItemRole::User),
content: vec![ContentPart::Text { text: text.clone() }],
status: ItemStatus::Completed,
};
events.extend(message_events(item, text));
}
}
// Assistant message - extract content from the nested message field
schema::StreamJsonMessageType::Assistant => {
if !event.message.is_empty() {
let text = event
.message
.get("content")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let item = UniversalItem {
item_id: next_temp_id("tmp_amp_assistant"),
native_item_id: event.session_id.clone(),
parent_id: None,
kind: ItemKind::Message,
role: Some(ItemRole::Assistant),
content: vec![ContentPart::Text { text: text.clone() }],
status: ItemStatus::Completed,
};
events.extend(message_events(item, text));
}
}
// Result message - signals completion
schema::StreamJsonMessageType::Result => {
events.push(turn_ended_event(None, None).synthetic());
events.push(
EventConversion::new(
UniversalEventType::SessionEnded,
UniversalEventData::SessionEnded(SessionEndedData {
reason: if event.is_error.unwrap_or(false) {
SessionEndReason::Error
} else {
SessionEndReason::Completed
},
terminated_by: TerminatedBy::Agent,
message: event.result.clone(),
exit_code: None,
stderr: None,
}),
)
.with_raw(serde_json::to_value(event).ok()),
);
}
schema::StreamJsonMessageType::Message => {
let text = event.content.clone().unwrap_or_default();
let item = UniversalItem {