Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,4 @@ hex = "0.4"
syntect = { version = "5", default-features = false, features = ["default-fancy"] }
two-face = { version = "0.5.1", default-features = false, features = ["syntect-fancy"] }
regex = "1"
indexmap = "2"
31 changes: 16 additions & 15 deletions crates/giskard-proto/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,8 +157,6 @@ pub struct LiveTurnSnapshot {
/// approvals at once (three commands proposed together, say), and a single field would name
/// only the most recently raised one and silently drop the rest.
pub accumulated: Vec<WireAgentEvent>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub pending_server_requests: Vec<ServerRequest>,
/// Approvals the user already answered during this in-flight turn, with the decision they made.
///
/// Approval resolution lives only in browser memory, so a reload would otherwise re-surface an
Expand Down Expand Up @@ -985,28 +983,32 @@ mod tests {
}

#[test]
fn live_turn_snapshot_includes_pending_server_requests() {
let tid = ThreadId::new();
fn live_turn_snapshot_replays_server_requests_from_accumulated() {
let turn = TurnId::new();
let thread = ThreadId::new();
let snapshot = LiveTurnSnapshot {
thread_id: tid,
thread_id: thread,
turn_id: turn,
user_input: None,
accumulated: vec![],
pending_server_requests: vec![ServerRequest {
id: giskard_core::ids::ServerRequestId("req_1".into()),
method: "item/tool/call".into(),
params: serde_json::json!({ "tool": "example" }),
received_at: chrono::Utc::now(),
accumulated: vec![WireAgentEvent::ServerRequestReceived {
thread,
turn: Some(turn),
request: ServerRequest {
id: giskard_core::ids::ServerRequestId("req_1".into()),
method: "item/tool/call".into(),
params: serde_json::json!({ "tool": "example" }),
received_at: chrono::Utc::now(),
},
}],
answered_approvals: vec![],
answered_server_requests: vec![],
};
let json = serde_json::to_value(&snapshot).unwrap();
assert_eq!(json["thread_id"], tid.to_string());
assert_eq!(json["pending_server_requests"][0]["id"], "req_1");
assert_eq!(json["thread_id"], thread.to_string());
assert_eq!(json["accumulated"][0]["kind"], "server_request_received");
assert_eq!(json["accumulated"][0]["request"]["id"], "req_1");
assert_eq!(
json["pending_server_requests"][0]["method"],
json["accumulated"][0]["request"]["method"],
"item/tool/call"
);
}
Expand All @@ -1018,7 +1020,6 @@ mod tests {
turn_id: TurnId::new(),
user_input: None,
accumulated: vec![],
pending_server_requests: vec![],
answered_approvals: vec![AnsweredApproval {
request_id: ApprovalId("ap_1".into()),
decision: ApprovalDecision::Accept,
Expand Down
1 change: 1 addition & 0 deletions crates/giskard-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ two-face = { workspace = true }
regex = { workspace = true }
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
pulldown-cmark = { version = "0.13.4", default-features = false }
indexmap = { workspace = true }

[build-dependencies]
sha2 = { workspace = true }
Expand Down
22 changes: 21 additions & 1 deletion crates/giskard-server/src/bin/giskard-server-replay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,13 @@ const SCRIPTED_APPROVAL_THEN_ERROR_MESSAGE: &str = "Scripted non-fatal harness e
const SCRIPTED_SERVER_REQUEST_TRIGGER: &str = "Trigger a scripted user input request.";
const SCRIPTED_SERVER_REQUEST_ID: &str = "scripted-server-request-1";
const SCRIPTED_SERVER_REQUEST_QUESTION: &str = "Which branch should I use?";
/// Raises a server request and then streams a harness error in the same still-open turn. The error
/// is the last activity-bearing event, so a reconnect that took the replayed events at face value
/// would land on "errored, no active turn" and lose the fact that the turn is still blocked on the
/// user. Re-asserting the outstanding set after the replay is what restores it (SR11b).
const SCRIPTED_SERVER_REQUEST_THEN_ERROR_TRIGGER: &str =
"Trigger a server request followed by an error.";
const SCRIPTED_SERVER_REQUEST_THEN_ERROR_MESSAGE: &str = "Scripted non-fatal harness error.";
/// How long a scripted turn waits for the server's event forwarder to subscribe before giving up.
const RECEIVER_WAIT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(2);
const RECEIVER_POLL_INTERVAL: std::time::Duration = std::time::Duration::from_millis(10);
Expand Down Expand Up @@ -439,7 +446,10 @@ impl AgentHarness for ScriptedHarness {
.insert(ApprovalId(SCRIPTED_APPROVAL_ID.into()), (thread_id, turn));
}

let raise_server_request = input_text == Some(SCRIPTED_SERVER_REQUEST_TRIGGER);
let raise_server_request_then_error =
input_text == Some(SCRIPTED_SERVER_REQUEST_THEN_ERROR_TRIGGER);
let raise_server_request =
input_text == Some(SCRIPTED_SERVER_REQUEST_TRIGGER) || raise_server_request_then_error;

// Stream the canned reply the way a real harness would: start, incremental deltas, then a
// completed item and a turn-completed with token usage. Emitted off-task with yields so the
Expand Down Expand Up @@ -475,6 +485,16 @@ impl AgentHarness for ScriptedHarness {
received_at: chrono::Utc::now(),
},
});
if raise_server_request_then_error {
tokio::task::yield_now().await;
let _ = sender.send(AgentEvent::Error {
thread: thread_id,
turn: Some(turn),
error: giskard_core::error::HarnessError::Protocol(
SCRIPTED_SERVER_REQUEST_THEN_ERROR_MESSAGE.into(),
),
});
}
Comment thread
marmeladema marked this conversation as resolved.
return;
}

Expand Down
Loading
Loading