mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-15 06:04:43 +00:00
Merge pull request #153 from soilSpoon/feature/ampcode
feature(ampcode): Enhances ampcode schema with new message types and fields
This commit is contained in:
commit
87a4e81d31
5 changed files with 167 additions and 33 deletions
|
|
@ -9,6 +9,10 @@
|
||||||
"type": {
|
"type": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
|
"system",
|
||||||
|
"user",
|
||||||
|
"assistant",
|
||||||
|
"result",
|
||||||
"message",
|
"message",
|
||||||
"tool_call",
|
"tool_call",
|
||||||
"tool_result",
|
"tool_result",
|
||||||
|
|
@ -27,6 +31,45 @@
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"subtype": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"cwd": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"session_id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"tools": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mcp_servers": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"parent_tool_use_id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"duration_ms": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"is_error": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"num_turns": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"result": {
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
|
|
|
||||||
|
|
@ -204,12 +204,27 @@ function createFallbackSchema(): NormalizedSchema {
|
||||||
properties: {
|
properties: {
|
||||||
type: {
|
type: {
|
||||||
type: "string",
|
type: "string",
|
||||||
enum: ["message", "tool_call", "tool_result", "error", "done"],
|
enum: ["system", "user", "assistant", "result", "message", "tool_call", "tool_result", "error", "done"],
|
||||||
},
|
},
|
||||||
|
// Common fields
|
||||||
id: { type: "string" },
|
id: { type: "string" },
|
||||||
content: { type: "string" },
|
content: { type: "string" },
|
||||||
tool_call: { $ref: "#/definitions/ToolCall" },
|
tool_call: { $ref: "#/definitions/ToolCall" },
|
||||||
error: { type: "string" },
|
error: { type: "string" },
|
||||||
|
// System message fields
|
||||||
|
subtype: { type: "string" },
|
||||||
|
cwd: { type: "string" },
|
||||||
|
session_id: { type: "string" },
|
||||||
|
tools: { type: "array", items: { type: "string" } },
|
||||||
|
mcp_servers: { type: "array", items: { type: "object" } },
|
||||||
|
// User/Assistant message fields
|
||||||
|
message: { type: "object" },
|
||||||
|
parent_tool_use_id: { type: "string" },
|
||||||
|
// Result fields
|
||||||
|
duration_ms: { type: "number" },
|
||||||
|
is_error: { type: "boolean" },
|
||||||
|
num_turns: { type: "number" },
|
||||||
|
result: { type: "string" },
|
||||||
},
|
},
|
||||||
required: ["type"],
|
required: ["type"],
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1058,26 +1058,21 @@ fn spawn_amp(
|
||||||
let mut args: Vec<&str> = Vec::new();
|
let mut args: Vec<&str> = Vec::new();
|
||||||
if flags.execute {
|
if flags.execute {
|
||||||
args.push("--execute");
|
args.push("--execute");
|
||||||
} else if flags.print {
|
args.push(&options.prompt);
|
||||||
args.push("--print");
|
|
||||||
}
|
}
|
||||||
if flags.output_format {
|
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") {
|
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);
|
let mut command = Command::new(path);
|
||||||
command.current_dir(working_dir);
|
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() {
|
if let Some(session_id) = options.session_id.as_deref() {
|
||||||
command.arg("--continue").arg(session_id);
|
command.arg("--continue").arg(session_id);
|
||||||
}
|
}
|
||||||
command.args(&args).arg(&options.prompt);
|
command.args(&args);
|
||||||
for (key, value) in &options.env {
|
for (key, value) in &options.env {
|
||||||
command.env(key, value);
|
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 flags = detect_amp_flags(path, working_dir).unwrap_or_default();
|
||||||
let mut command = Command::new(path);
|
let mut command = Command::new(path);
|
||||||
command.current_dir(working_dir);
|
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() {
|
if let Some(session_id) = options.session_id.as_deref() {
|
||||||
command.arg("--continue").arg(session_id);
|
command.arg("--continue").arg(session_id);
|
||||||
}
|
}
|
||||||
if flags.execute {
|
if flags.execute {
|
||||||
command.arg("--execute");
|
command.arg("--execute");
|
||||||
} else if flags.print {
|
command.arg(&options.prompt);
|
||||||
command.arg("--print");
|
|
||||||
}
|
}
|
||||||
if flags.output_format {
|
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") {
|
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 {
|
for (key, value) in &options.env {
|
||||||
command.env(key, value);
|
command.env(key, value);
|
||||||
}
|
}
|
||||||
|
|
@ -1128,7 +1118,6 @@ fn build_amp_command(path: &Path, working_dir: &Path, options: &SpawnOptions) ->
|
||||||
#[derive(Debug, Default, Clone, Copy)]
|
#[derive(Debug, Default, Clone, Copy)]
|
||||||
struct AmpFlags {
|
struct AmpFlags {
|
||||||
execute: bool,
|
execute: bool,
|
||||||
print: bool,
|
|
||||||
output_format: bool,
|
output_format: bool,
|
||||||
dangerously_skip_permissions: bool,
|
dangerously_skip_permissions: bool,
|
||||||
}
|
}
|
||||||
|
|
@ -1146,9 +1135,8 @@ fn detect_amp_flags(path: &Path, working_dir: &Path) -> Option<AmpFlags> {
|
||||||
);
|
);
|
||||||
Some(AmpFlags {
|
Some(AmpFlags {
|
||||||
execute: text.contains("--execute"),
|
execute: text.contains("--execute"),
|
||||||
print: text.contains("--print"),
|
output_format: text.contains("--stream-json"),
|
||||||
output_format: text.contains("--output-format"),
|
dangerously_skip_permissions: text.contains("--dangerously-allow-all"),
|
||||||
dangerously_skip_permissions: text.contains("--dangerously-skip-permissions"),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1157,23 +1145,19 @@ fn spawn_amp_fallback(
|
||||||
working_dir: &Path,
|
working_dir: &Path,
|
||||||
options: &SpawnOptions,
|
options: &SpawnOptions,
|
||||||
) -> Result<std::process::Output, AgentError> {
|
) -> Result<std::process::Output, AgentError> {
|
||||||
let mut attempts = vec![
|
let mut attempts: Vec<Vec<&str>> = vec![
|
||||||
vec!["--execute"],
|
vec!["--execute"],
|
||||||
vec!["--print", "--output-format", "stream-json"],
|
vec!["stream-json"],
|
||||||
vec!["--output-format", "stream-json"],
|
vec!["--dangerously-allow-all"],
|
||||||
vec!["--dangerously-skip-permissions"],
|
|
||||||
vec![],
|
vec![],
|
||||||
];
|
];
|
||||||
if options.permission_mode.as_deref() != Some("bypass") {
|
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 {
|
for args in attempts {
|
||||||
let mut command = Command::new(path);
|
let mut command = Command::new(path);
|
||||||
command.current_dir(working_dir);
|
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() {
|
if let Some(session_id) = options.session_id.as_deref() {
|
||||||
command.arg("--continue").arg(session_id);
|
command.arg("--continue").arg(session_id);
|
||||||
}
|
}
|
||||||
|
|
@ -1192,9 +1176,6 @@ fn spawn_amp_fallback(
|
||||||
|
|
||||||
let mut command = Command::new(path);
|
let mut command = Command::new(path);
|
||||||
command.current_dir(working_dir);
|
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() {
|
if let Some(session_id) = options.session_id.as_deref() {
|
||||||
command.arg("--continue").arg(session_id);
|
command.arg("--continue").arg(session_id);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -73,3 +73,32 @@ fn test_amp_message() {
|
||||||
assert!(json.contains("user"));
|
assert!(json.contains("user"));
|
||||||
assert!(json.contains("Hello"));
|
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));
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,72 @@ pub fn event_to_universal(
|
||||||
) -> Result<Vec<EventConversion>, String> {
|
) -> Result<Vec<EventConversion>, String> {
|
||||||
let mut events = Vec::new();
|
let mut events = Vec::new();
|
||||||
match event.type_ {
|
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 => {
|
schema::StreamJsonMessageType::Message => {
|
||||||
let text = event.content.clone().unwrap_or_default();
|
let text = event.content.clone().unwrap_or_default();
|
||||||
let item = UniversalItem {
|
let item = UniversalItem {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue