Skip to content
Merged
2 changes: 2 additions & 0 deletions examples/agent_server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,7 @@ async fn chat_stream_handler(
}
} else if step.status == StepStatus::Done
|| step.status == StepStatus::Error
|| step.status == StepStatus::TerminalError
{
// If this tool was never seen as Active (e.g. auto-approved
// non-WRITE_TOOLS like LIST_DIR that go straight from
Expand Down Expand Up @@ -657,6 +658,7 @@ async fn chat_stream_handler(
StepStatus::Error => Some("ERROR"),
StepStatus::WaitingForUser => Some("WAITING_FOR_USER"),
StepStatus::Canceled => Some("CANCELED"),
StepStatus::TerminalError => Some("TERMINAL_ERROR"),
StepStatus::Unknown => None,
};
if let Some(status) = status_str {
Expand Down
2 changes: 1 addition & 1 deletion examples/leptos_ssr_axum/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ hydration_context = { version = "0.3.0" }
leptos = { version = "0.8.9" }
leptos_meta = { version = "0.8.5" }
leptos_router = { version = "0.8.7" }
leptos_wasi = { git = "https://github.com/leptos-rs/leptos_wasi", rev = "216acc484b0d6fe4b18876f1c96b68272498592b", default-features = false, features = ["wasip3"], optional = true }
leptos_wasi = { git = "https://github.com/leptos-rs/leptos_wasi", rev = "0ee7282c8115e5747cbd3d61a375d805601ca6ef", default-features = false, features = ["wasip3"], optional = true }
server_fn = { version = "0.8.7", features = ["axum-no-default"] }
spin-sdk = { version = "6.0.0", features = ["json"], optional = true }
wasip3 = { version = "0.6.0", features = ["http-compat"], optional = true }
Expand Down
11 changes: 11 additions & 0 deletions proto/localharness.proto
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,17 @@ enum NullValue {
NULL_VALUE = 0;
}

message ClientInfo {
optional string language = 1;
optional string version = 2;
optional string language_version = 3;
}

message InputConfig {
optional string storage_directory = 1;
optional uint32 port = 2;
optional string bind_address = 3;
optional ClientInfo client_info = 4;
}

message InitializeConversationEvent {
Expand Down Expand Up @@ -50,6 +57,9 @@ message GeminiConfig {
optional string thinking_level = 4;
optional bool enable_url_context = 5;
optional bool enable_google_search = 6;
optional bool use_vertex = 7;
optional string project = 8;
optional string location = 9;
}

message GemmaConfig {
Expand Down Expand Up @@ -173,6 +183,7 @@ message StepUpdate {
STATE_DONE = 2;
STATE_WAITING_FOR_USER = 3;
STATE_ERROR = 4;
STATE_TERMINAL_ERROR = 5;
}

enum Source {
Expand Down
2 changes: 1 addition & 1 deletion scripts/install_harness.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

set -euo pipefail

VERSION="0.1.0"
VERSION="0.1.1"
PLATFORM=""
case "$(uname -s)" in
Darwin)
Expand Down
12 changes: 10 additions & 2 deletions src/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ impl Agent<Unstarted> {
}

if !final_policies.is_empty() {
let enforcer = Arc::new(PolicyEnforcer::new(final_policies));
let enforcer = Arc::new(PolicyEnforcer::new(final_policies, Vec::new()));
self.hook_runner.register(enforcer).await;
}

Expand Down Expand Up @@ -603,7 +603,15 @@ fn get_default_binary_path() -> Option<String> {
if let Ok(path) = std::env::var("ANTIGRAVITY_HARNESS_PATH") {
return Some(path);
}
// Check if it is in standard PATH
// Check ./bin/localharness relative to the current working directory
// (this is where `just install` / `scripts/install_harness.sh` places the binary)
if let Ok(cwd) = std::env::current_dir() {
let local_bin = cwd.join("bin").join("localharness");
if local_bin.exists() {
return Some(local_bin.to_string_lossy().into_owned());
}
}
// Check if it is in standard PATH (e.g. via `pip install google-antigravity`)
if let Ok(paths) = std::env::var("PATH") {
for path in std::env::split_paths(&paths) {
let p = path.join("localharness");
Expand Down
118 changes: 82 additions & 36 deletions src/bin/mock_localharness.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#[cfg(not(target_arch = "wasm32"))]
use antigravity_sdk_rust::proto::localharness::OutputConfig;
use antigravity_sdk_rust::proto::localharness::{InputConfig, OutputConfig};
#[cfg(not(target_arch = "wasm32"))]
use futures_util::{SinkExt, StreamExt};
#[cfg(not(target_arch = "wasm32"))]
Expand Down Expand Up @@ -28,6 +28,15 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut input_buf = vec![0u8; length];
stdin.read_exact(&mut input_buf).await?;

let input_config = InputConfig::decode(&input_buf[..]).ok();
let client_info = input_config.as_ref().and_then(|c| c.client_info.as_ref());
let client_lang = client_info
.and_then(|ci| ci.language.as_deref())
.unwrap_or("unknown");
let client_ver = client_info
.and_then(|ci| ci.version.as_deref())
.unwrap_or("unknown");

// 2. Bind TCP listener to random port on localhost
let listener = TcpListener::bind("127.0.0.1:0").await?;
let port = listener.local_addr()?.port();
Expand All @@ -48,14 +57,33 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {

// 4. Accept a TCP connection and upgrade to WebSocket
let (stream, _) = listener.accept().await?;
let mut ws_stream = accept_async(stream).await?;
let ws_stream = accept_async(stream).await?;

// 5. Handle the WebSocket conversation
handle_ws_connection(ws_stream, client_lang, client_ver).await
}

// 5. Read client config message (InitializeConversationEvent / HarnessConfig)
#[cfg(not(target_arch = "wasm32"))]
async fn handle_ws_connection(
mut ws_stream: tokio_tungstenite::WebSocketStream<tokio::net::TcpStream>,
client_lang: &str,
client_ver: &str,
) -> Result<(), Box<dyn std::error::Error>> {
// Read client config message (InitializeConversationEvent / HarnessConfig)
if let Some(msg_res) = ws_stream.next().await {
let _ = msg_res?;
}

// 6. Send trajectoryStateUpdate (RUNNING) to signal the turn is active
// Read client user prompt message
let mut prompt = String::new();
if let Some(msg_res) = ws_stream.next().await {
let msg = msg_res?;
if let WsMessage::Text(text) = msg {
prompt = text;
}
}

// Send trajectoryStateUpdate (RUNNING) to signal the turn is active
let traj_running = serde_json::json!({
"trajectoryStateUpdate": {
"trajectoryId": "test_traj",
Expand All @@ -66,41 +94,59 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.send(WsMessage::Text(traj_running.to_string()))
.await?;

// 7. Send the step updates
let step1 = serde_json::json!({
"stepUpdate": {
"stepIndex": 1,
"cascadeId": "test_traj",
"trajectoryId": "test_traj",
"text": "Hello from mock harness!",
"textDelta": "Hello from mock harness!",
"state": "STATE_ACTIVE",
"source": "SOURCE_MODEL",
"target": "TARGET_USER"
}
});
ws_stream.send(WsMessage::Text(step1.to_string())).await?;
if prompt.contains("trigger_terminal_error") {
let step_terminal = serde_json::json!({
"stepUpdate": {
"stepIndex": 1,
"cascadeId": "test_traj",
"trajectoryId": "test_traj",
"text": "Terminal error triggered",
"state": "STATE_TERMINAL_ERROR",
"source": "SOURCE_MODEL",
"target": "TARGET_USER",
"errorMessage": "Terminal error triggered by prompt"
}
});
ws_stream
.send(WsMessage::Text(step_terminal.to_string()))
.await?;
} else {
// Send the step updates including client language/version info in output
let step1 = serde_json::json!({
"stepUpdate": {
"stepIndex": 1,
"cascadeId": "test_traj",
"trajectoryId": "test_traj",
"text": format!("Client info language: {}, version: {}", client_lang, client_ver),
"textDelta": format!("Client info language: {}, version: {}", client_lang, client_ver),
"state": "STATE_ACTIVE",
"source": "SOURCE_MODEL",
"target": "TARGET_USER"
}
});
ws_stream.send(WsMessage::Text(step1.to_string())).await?;

tokio::time::sleep(Duration::from_millis(100)).await;
tokio::time::sleep(Duration::from_millis(100)).await;

let step2 = serde_json::json!({
"stepUpdate": {
"stepIndex": 2,
"cascadeId": "test_traj",
"trajectoryId": "test_traj",
"text": "Hello from mock harness!How can I help you today?",
"textDelta": "How can I help you today?",
"state": "STATE_DONE",
"source": "SOURCE_MODEL",
"target": "TARGET_USER",
"finish": {
"outputString": "\"done\""
let step2 = serde_json::json!({
"stepUpdate": {
"stepIndex": 2,
"cascadeId": "test_traj",
"trajectoryId": "test_traj",
"text": "Hello from mock harness!How can I help you today?",
"textDelta": "How can I help you today?",
"state": "STATE_DONE",
"source": "SOURCE_MODEL",
"target": "TARGET_USER",
"finish": {
"outputString": "\"done\""
}
}
}
});
ws_stream.send(WsMessage::Text(step2.to_string())).await?;
});
ws_stream.send(WsMessage::Text(step2.to_string())).await?;
}

// 8. Send trajectoryStateUpdate (IDLE) to signal the turn is complete
// Send trajectoryStateUpdate (IDLE) to signal the turn is complete
let traj_idle = serde_json::json!({
"trajectoryStateUpdate": {
"trajectoryId": "test_traj",
Expand All @@ -111,7 +157,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.send(WsMessage::Text(traj_idle.to_string()))
.await?;

// 9. Keep reading until client disconnects or we get terminated
// Keep reading until client disconnects or we get terminated
while let Some(msg_res) = ws_stream.next().await {
if msg_res.is_err() {
break;
Expand Down
1 change: 1 addition & 0 deletions src/bin/start_harness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
storage_directory: Some("target/harness_store".to_string()),
port: Some(8000),
bind_address: Some("127.0.0.1".to_string()),
client_info: None,
};

// Serialize InputConfig
Expand Down
Loading
Loading