steerable-agent-runtime-native 0.6.34__tar.gz → 0.6.36__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. {steerable_agent_runtime_native-0.6.34 → steerable_agent_runtime_native-0.6.36}/Cargo.lock +1 -1
  2. {steerable_agent_runtime_native-0.6.34 → steerable_agent_runtime_native-0.6.36}/Cargo.toml +1 -1
  3. {steerable_agent_runtime_native-0.6.34 → steerable_agent_runtime_native-0.6.36}/PKG-INFO +1 -1
  4. {steerable_agent_runtime_native-0.6.34 → steerable_agent_runtime_native-0.6.36}/pyproject.toml +1 -1
  5. {steerable_agent_runtime_native-0.6.34 → steerable_agent_runtime_native-0.6.36}/src/engine.rs +87 -70
  6. {steerable_agent_runtime_native-0.6.34 → steerable_agent_runtime_native-0.6.36}/src/hooks.rs +6 -0
  7. {steerable_agent_runtime_native-0.6.34 → steerable_agent_runtime_native-0.6.36}/src/python.rs +13 -0
  8. {steerable_agent_runtime_native-0.6.34 → steerable_agent_runtime_native-0.6.36}/tests/test_loop.rs +81 -0
  9. {steerable_agent_runtime_native-0.6.34 → steerable_agent_runtime_native-0.6.36}/.gitignore +0 -0
  10. {steerable_agent_runtime_native-0.6.34 → steerable_agent_runtime_native-0.6.36}/README.md +0 -0
  11. {steerable_agent_runtime_native-0.6.34 → steerable_agent_runtime_native-0.6.36}/src/anthropic_http.rs +0 -0
  12. {steerable_agent_runtime_native-0.6.34 → steerable_agent_runtime_native-0.6.36}/src/anthropic_wire.rs +0 -0
  13. {steerable_agent_runtime_native-0.6.34 → steerable_agent_runtime_native-0.6.36}/src/budget.rs +0 -0
  14. {steerable_agent_runtime_native-0.6.34 → steerable_agent_runtime_native-0.6.36}/src/compat.rs +0 -0
  15. {steerable_agent_runtime_native-0.6.34 → steerable_agent_runtime_native-0.6.36}/src/errors.rs +0 -0
  16. {steerable_agent_runtime_native-0.6.34 → steerable_agent_runtime_native-0.6.36}/src/executor.rs +0 -0
  17. {steerable_agent_runtime_native-0.6.34 → steerable_agent_runtime_native-0.6.36}/src/gemini_http.rs +0 -0
  18. {steerable_agent_runtime_native-0.6.34 → steerable_agent_runtime_native-0.6.36}/src/gemini_wire.rs +0 -0
  19. {steerable_agent_runtime_native-0.6.34 → steerable_agent_runtime_native-0.6.36}/src/json_py.rs +0 -0
  20. {steerable_agent_runtime_native-0.6.34 → steerable_agent_runtime_native-0.6.36}/src/lib.rs +0 -0
  21. {steerable_agent_runtime_native-0.6.34 → steerable_agent_runtime_native-0.6.36}/src/model_catalog.json +0 -0
  22. {steerable_agent_runtime_native-0.6.34 → steerable_agent_runtime_native-0.6.36}/src/model_resolve.rs +0 -0
  23. {steerable_agent_runtime_native-0.6.34 → steerable_agent_runtime_native-0.6.36}/src/openai_http.rs +0 -0
  24. {steerable_agent_runtime_native-0.6.34 → steerable_agent_runtime_native-0.6.36}/src/openai_wire.rs +0 -0
  25. {steerable_agent_runtime_native-0.6.34 → steerable_agent_runtime_native-0.6.36}/src/presets.rs +0 -0
  26. {steerable_agent_runtime_native-0.6.34 → steerable_agent_runtime_native-0.6.36}/src/pricing.rs +0 -0
  27. {steerable_agent_runtime_native-0.6.34 → steerable_agent_runtime_native-0.6.36}/src/provider.rs +0 -0
  28. {steerable_agent_runtime_native-0.6.34 → steerable_agent_runtime_native-0.6.36}/src/ptc_js.rs +0 -0
  29. {steerable_agent_runtime_native-0.6.34 → steerable_agent_runtime_native-0.6.36}/src/responses_http.rs +0 -0
  30. {steerable_agent_runtime_native-0.6.34 → steerable_agent_runtime_native-0.6.36}/src/responses_wire.rs +0 -0
  31. {steerable_agent_runtime_native-0.6.34 → steerable_agent_runtime_native-0.6.36}/src/run_code.rs +0 -0
  32. {steerable_agent_runtime_native-0.6.34 → steerable_agent_runtime_native-0.6.36}/src/skills.rs +0 -0
  33. {steerable_agent_runtime_native-0.6.34 → steerable_agent_runtime_native-0.6.36}/src/sse_http.rs +0 -0
  34. {steerable_agent_runtime_native-0.6.34 → steerable_agent_runtime_native-0.6.36}/src/todo.rs +0 -0
  35. {steerable_agent_runtime_native-0.6.34 → steerable_agent_runtime_native-0.6.36}/src/tokens.rs +0 -0
  36. {steerable_agent_runtime_native-0.6.34 → steerable_agent_runtime_native-0.6.36}/src/types.rs +0 -0
  37. {steerable_agent_runtime_native-0.6.34 → steerable_agent_runtime_native-0.6.36}/src/web.rs +0 -0
  38. {steerable_agent_runtime_native-0.6.34 → steerable_agent_runtime_native-0.6.36}/src/web_html.rs +0 -0
  39. {steerable_agent_runtime_native-0.6.34 → steerable_agent_runtime_native-0.6.36}/src/web_search.rs +0 -0
  40. {steerable_agent_runtime_native-0.6.34 → steerable_agent_runtime_native-0.6.36}/src/wire.rs +0 -0
  41. {steerable_agent_runtime_native-0.6.34 → steerable_agent_runtime_native-0.6.36}/src/write_lease.rs +0 -0
  42. {steerable_agent_runtime_native-0.6.34 → steerable_agent_runtime_native-0.6.36}/tests/test_llm_wire.rs +0 -0
  43. {steerable_agent_runtime_native-0.6.34 → steerable_agent_runtime_native-0.6.36}/tests/test_ptc_js.rs +0 -0
  44. {steerable_agent_runtime_native-0.6.34 → steerable_agent_runtime_native-0.6.36}/tests/test_run_code.rs +0 -0
  45. {steerable_agent_runtime_native-0.6.34 → steerable_agent_runtime_native-0.6.36}/tests/test_todo.rs +0 -0
  46. {steerable_agent_runtime_native-0.6.34 → steerable_agent_runtime_native-0.6.36}/tests/test_web.rs +0 -0
  47. {steerable_agent_runtime_native-0.6.34 → steerable_agent_runtime_native-0.6.36}/tests/test_write_lease.rs +0 -0
  48. {steerable_agent_runtime_native-0.6.34 → steerable_agent_runtime_native-0.6.36}/uv.lock +0 -0
@@ -955,7 +955,7 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
955
955
 
956
956
  [[package]]
957
957
  name = "steerable-agent-runtime"
958
- version = "0.6.34"
958
+ version = "0.6.36"
959
959
  dependencies = [
960
960
  "async-stream",
961
961
  "async-trait",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "steerable-agent-runtime"
3
- version = "0.6.34"
3
+ version = "0.6.36"
4
4
  edition = "2021"
5
5
  description = "Steerable CoreLoop in Rust: think → act → observe, yielding LoopEvents."
6
6
  license = "MIT"
@@ -1,5 +1,5 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: steerable-agent-runtime-native
3
- Version: 0.6.34
3
+ Version: 0.6.36
4
4
  Summary: PyO3 bindings for the Rust CoreLoop.
5
5
  Requires-Python: >=3.10
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "steerable-agent-runtime-native"
7
- version = "0.6.34"
7
+ version = "0.6.36"
8
8
  description = "PyO3 bindings for the Rust CoreLoop."
9
9
  requires-python = ">=3.10"
10
10
 
@@ -387,79 +387,96 @@ impl<P: LLMProvider, E: ToolExecutor, H: LoopHooks> CoreLoop<P, E, H> {
387
387
  }
388
388
  }
389
389
 
390
- let (step_tool_choice, step_tool_choice_honored) =
391
- match self.hooks.pre_step(&history).await {
392
- PreStepOutcome::Reject { reason } => {
393
- emit(completion_event(
394
- round_index,
395
- "stop",
396
- "",
397
- &[],
398
- ctx.consecutive_tool_errors,
399
- "failed",
400
- reason,
401
- 1.0,
402
- &ctx,
390
+ let (step_tool_choice, step_tool_choice_honored) = match self
391
+ .hooks
392
+ .pre_step(&history)
393
+ .await
394
+ {
395
+ PreStepOutcome::Reject { reason } => {
396
+ emit(completion_event(
397
+ round_index,
398
+ "stop",
399
+ "",
400
+ &[],
401
+ ctx.consecutive_tool_errors,
402
+ "failed",
403
+ reason,
404
+ 1.0,
405
+ &ctx,
406
+ ));
407
+ break;
408
+ }
409
+ PreStepOutcome::Proceed {
410
+ appends,
411
+ rewrite,
412
+ reason,
413
+ action,
414
+ append_reason,
415
+ append_action,
416
+ appended_count,
417
+ pre_tokens,
418
+ post_tokens,
419
+ tool_choice,
420
+ tool_choice_honored,
421
+ notes,
422
+ } => {
423
+ if let Some(replacement) = rewrite {
424
+ history = replacement;
425
+ emit(LoopEvent::new(
426
+ "hook_action",
427
+ json!({
428
+ "hook": "pre_step",
429
+ "action": action.clone(),
430
+ "reason": reason.clone(),
431
+ "round": round_index,
432
+ "pre_tokens": pre_tokens,
433
+ "post_tokens": post_tokens,
434
+ }),
403
435
  ));
404
- break;
405
436
  }
406
- PreStepOutcome::Proceed {
407
- appends,
408
- rewrite,
409
- reason,
410
- action,
411
- pre_tokens,
412
- post_tokens,
413
- tool_choice,
414
- tool_choice_honored,
415
- notes,
416
- } => {
417
- if let Some(replacement) = rewrite {
418
- history = replacement;
419
- emit(LoopEvent::new(
420
- "hook_action",
421
- json!({
422
- "hook": "pre_step",
423
- "action": action.clone(),
424
- "reason": reason.clone(),
425
- "round": round_index,
426
- "pre_tokens": pre_tokens,
427
- "post_tokens": post_tokens,
428
- }),
429
- ));
430
- }
431
- for message in appends {
432
- history.push(message);
433
- emit(LoopEvent::new(
434
- "hook_action",
435
- json!({
436
- "hook": "pre_step",
437
- "action": action.clone(),
438
- "reason": reason.clone(),
439
- "round": round_index,
440
- }),
441
- ));
442
- }
443
- // Verdicts that took no action. Without these a trace
444
- // shows only the rounds a hook intervened in, which is a
445
- // numerator with no denominator — "routing allowed a
446
- // no-tool turn" and "routing never ran" look identical.
447
- for note in notes {
448
- emit(LoopEvent::new(
449
- "hook_action",
450
- json!({
451
- "hook": "pre_step",
452
- "action": note.action,
453
- "reason": note.reason,
454
- "value": note.value,
455
- "probability": note.probability,
456
- "round": round_index,
457
- }),
458
- ));
459
- }
460
- (tool_choice, tool_choice_honored)
437
+ for message in appends {
438
+ history.push(message);
439
+ emit(LoopEvent::new(
440
+ "hook_action",
441
+ json!({
442
+ "hook": "pre_step",
443
+ "action": append_action.clone().unwrap_or_else(|| action.clone()),
444
+ "reason": append_reason.clone().or_else(|| reason.clone()),
445
+ "round": round_index,
446
+ }),
447
+ ));
461
448
  }
462
- };
449
+ for _ in 0..appended_count {
450
+ emit(LoopEvent::new(
451
+ "hook_action",
452
+ json!({
453
+ "hook": "pre_step",
454
+ "action": append_action.clone().unwrap_or_else(|| "append".to_string()),
455
+ "reason": append_reason.clone(),
456
+ "round": round_index,
457
+ }),
458
+ ));
459
+ }
460
+ // Verdicts that took no action. Without these a trace
461
+ // shows only the rounds a hook intervened in, which is a
462
+ // numerator with no denominator — "routing allowed a
463
+ // no-tool turn" and "routing never ran" look identical.
464
+ for note in notes {
465
+ emit(LoopEvent::new(
466
+ "hook_action",
467
+ json!({
468
+ "hook": "pre_step",
469
+ "action": note.action,
470
+ "reason": note.reason,
471
+ "value": note.value,
472
+ "probability": note.probability,
473
+ "round": round_index,
474
+ }),
475
+ ));
476
+ }
477
+ (tool_choice, tool_choice_honored)
478
+ }
479
+ };
463
480
 
464
481
  if let Some(tool_choice) = &step_tool_choice {
465
482
  emit(LoopEvent::new(
@@ -44,6 +44,9 @@ pub enum PreStepOutcome {
44
44
  rewrite: Option<Vec<LLMMessage>>,
45
45
  reason: Option<String>,
46
46
  action: String,
47
+ append_reason: Option<String>,
48
+ append_action: Option<String>,
49
+ appended_count: u32,
47
50
  pre_tokens: Option<i64>,
48
51
  post_tokens: Option<i64>,
49
52
  tool_choice: Option<String>,
@@ -77,6 +80,9 @@ impl Default for PreStepOutcome {
77
80
  rewrite: None,
78
81
  reason: None,
79
82
  action: "append".into(),
83
+ append_reason: None,
84
+ append_action: None,
85
+ appended_count: 0,
80
86
  pre_tokens: None,
81
87
  post_tokens: None,
82
88
  tool_choice: None,
@@ -358,6 +358,19 @@ impl LoopHooks for CallbackHooks {
358
358
  .and_then(Value::as_str)
359
359
  .unwrap_or("append")
360
360
  .to_string(),
361
+ append_reason: value
362
+ .get("append_reason")
363
+ .and_then(Value::as_str)
364
+ .map(str::to_string),
365
+ append_action: value
366
+ .get("append_action")
367
+ .and_then(Value::as_str)
368
+ .map(str::to_string),
369
+ appended_count: value
370
+ .get("appended_count")
371
+ .and_then(Value::as_u64)
372
+ .and_then(|count| u32::try_from(count).ok())
373
+ .unwrap_or(0),
361
374
  pre_tokens: value.get("pre_tokens").and_then(Value::as_i64),
362
375
  post_tokens: value.get("post_tokens").and_then(Value::as_i64),
363
376
  tool_choice: value
@@ -945,6 +945,9 @@ impl LoopHooks for RouteVerdict {
945
945
  rewrite: None,
946
946
  reason: None,
947
947
  action: "append".into(),
948
+ append_reason: None,
949
+ append_action: None,
950
+ appended_count: 0,
948
951
  pre_tokens: None,
949
952
  post_tokens: None,
950
953
  tool_choice: forcing.then(|| "required".to_string()),
@@ -959,6 +962,32 @@ impl LoopHooks for RouteVerdict {
959
962
  }
960
963
  }
961
964
 
965
+ struct RewriteWithFoldedAppend;
966
+
967
+ #[async_trait]
968
+ impl LoopHooks for RewriteWithFoldedAppend {
969
+ async fn pre_step(&self, _transcript: &[LLMMessage]) -> PreStepOutcome {
970
+ PreStepOutcome::Proceed {
971
+ appends: Vec::new(),
972
+ rewrite: Some(vec![
973
+ LLMMessage::text("system", "system"),
974
+ LLMMessage::text("user", "instruction"),
975
+ LLMMessage::text("user", "write now"),
976
+ ]),
977
+ reason: Some("required tool rounds produced no tool call".into()),
978
+ action: "livelock_reset".into(),
979
+ append_reason: Some("forced_empty_livelock_reset".into()),
980
+ append_action: Some("delivery_nudge".into()),
981
+ appended_count: 1,
982
+ pre_tokens: None,
983
+ post_tokens: None,
984
+ tool_choice: Some("required".into()),
985
+ tool_choice_honored: Some(false),
986
+ notes: Vec::new(),
987
+ }
988
+ }
989
+ }
990
+
962
991
  #[tokio::test]
963
992
  async fn test_pre_step_notes_record_a_verdict_that_changed_nothing() {
964
993
  let mut agent = CoreLoop::new(
@@ -1013,6 +1042,58 @@ async fn test_tool_choice_event_reports_a_vendor_downgrade() {
1013
1042
  assert_eq!(forced.data["honored"], json!(false));
1014
1043
  }
1015
1044
 
1045
+ #[tokio::test]
1046
+ async fn test_rewrite_with_folded_append_is_sent_once_and_attributed() {
1047
+ let mut agent = CoreLoop::new(
1048
+ provider(vec![text_turn("done")]),
1049
+ RouterToolExecutor::new(ToolRouter::new()),
1050
+ )
1051
+ .with_hooks(RewriteWithFoldedAppend);
1052
+
1053
+ let events = agent
1054
+ .run(vec![
1055
+ LLMMessage::text("system", "old-system"),
1056
+ LLMMessage::text("user", "old-instruction"),
1057
+ LLMMessage::text("assistant", "very long reasoning"),
1058
+ ])
1059
+ .await;
1060
+
1061
+ assert_eq!(
1062
+ agent.provider().calls[0]
1063
+ .iter()
1064
+ .map(LLMMessage::content_text)
1065
+ .collect::<Vec<_>>(),
1066
+ vec!["system", "instruction", "write now"]
1067
+ );
1068
+ assert_eq!(
1069
+ events
1070
+ .iter()
1071
+ .filter(|event| {
1072
+ event.kind == "hook_action" && event.data["action"] == "livelock_reset"
1073
+ })
1074
+ .count(),
1075
+ 1
1076
+ );
1077
+ assert_eq!(
1078
+ events
1079
+ .iter()
1080
+ .filter(|event| {
1081
+ event.kind == "hook_action" && event.data["action"] == "delivery_nudge"
1082
+ })
1083
+ .count(),
1084
+ 1
1085
+ );
1086
+ let forced = events
1087
+ .iter()
1088
+ .find(|event| {
1089
+ event.kind == "hook_action"
1090
+ && event.data["action"] == "tool_choice"
1091
+ && event.data["value"] == "required"
1092
+ })
1093
+ .unwrap();
1094
+ assert_eq!(forced.data["honored"], false);
1095
+ }
1096
+
1016
1097
  struct NarrateOnFailure;
1017
1098
 
1018
1099
  #[async_trait]