@team-agent/installer 0.5.72 → 0.5.74
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.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/adapters.rs +19 -13
- package/crates/team-agent/src/cli/emit.rs +6 -3
- package/crates/team-agent/src/cli/mod.rs +151 -0
- package/crates/team-agent/src/cli/tests/leader_watch.rs +28 -0
- package/crates/team-agent/src/cli/tests/run_delegation.rs +3 -0
- package/crates/team-agent/src/cli/types.rs +6 -0
- package/crates/team-agent/src/compiler.rs +14 -1
- package/crates/team-agent/src/leader/lease.rs +106 -21
- package/crates/team-agent/src/leader/registry.rs +0 -89
- package/crates/team-agent/src/leader/start.rs +8 -0
- package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +43 -7
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +457 -73
- package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +16 -6
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +1 -0
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +5 -3
- package/crates/team-agent/src/lifecycle/restart/common.rs +1 -11
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +60 -0
- package/crates/team-agent/src/lifecycle/tests/pi_catalog_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/pi_command_red.rs +71 -1
- package/crates/team-agent/src/lifecycle/tests/pi_compiler_red.rs +34 -1
- package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +25 -23
- package/crates/team-agent/src/lifecycle/tests/pi_executable_mcp_red.rs +396 -3
- package/crates/team-agent/src/lifecycle/tests/pi_session_delivery_red.rs +3 -1
- package/crates/team-agent/src/messaging/delivery.rs +26 -8
- package/crates/team-agent/src/messaging/tests/runtime.rs +234 -0
- package/crates/team-agent/src/model/spec.rs +81 -7
- package/crates/team-agent/src/provider/adapters/pi.rs +26 -6
- package/crates/team-agent/src/state/persist.rs +272 -3
- package/crates/team-agent/src/state/repository.rs +9 -2
- package/crates/team-agent/src/tmux_backend/tests.rs +64 -0
- package/crates/team-agent/src/tmux_backend.rs +57 -0
- package/package.json +4 -4
- package/skills/team-agent/SKILL.md +4 -1
- package/skills/team-agent/command-coverage.json +21 -0
- package/skills/team-agent/references/team-agent-operator.md +9 -2
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
use super::*;
|
|
2
2
|
use crate::transport::test_support::OfflineTransport;
|
|
3
|
+
use crate::transport::{PaneId, Target};
|
|
3
4
|
|
|
4
5
|
fn pane_info(pane_id: &str, session: &str, window: &str) -> PaneInfo {
|
|
5
6
|
PaneInfo {
|
|
@@ -3421,3 +3422,236 @@ fn gate054_fallback_pane_grep_guard_no_cross_server_chain_when_socket_recorded()
|
|
|
3421
3422
|
"fallback injection must consume the shared resolver's typed channel"
|
|
3422
3423
|
);
|
|
3423
3424
|
}
|
|
3425
|
+
|
|
3426
|
+
fn delivery_blocked_channel_reasons(ws: &std::path::Path) -> Vec<String> {
|
|
3427
|
+
EventLog::new(ws)
|
|
3428
|
+
.tail(80)
|
|
3429
|
+
.unwrap()
|
|
3430
|
+
.into_iter()
|
|
3431
|
+
.filter(|event| event.get("event") == Some(&serde_json::json!("leader_receiver.delivery_blocked")))
|
|
3432
|
+
.filter_map(|event| {
|
|
3433
|
+
event
|
|
3434
|
+
.get("channel_reason")
|
|
3435
|
+
.and_then(serde_json::Value::as_str)
|
|
3436
|
+
.map(str::to_string)
|
|
3437
|
+
})
|
|
3438
|
+
.collect()
|
|
3439
|
+
}
|
|
3440
|
+
|
|
3441
|
+
fn live_leader_pane(ws: &std::path::Path, pane_id: &str) -> PaneInfo {
|
|
3442
|
+
let mut pane = pane_info(pane_id, "team-current", "leader");
|
|
3443
|
+
pane.current_path = Some(ws.to_path_buf());
|
|
3444
|
+
pane
|
|
3445
|
+
}
|
|
3446
|
+
|
|
3447
|
+
#[test]
|
|
3448
|
+
fn owner_projection_null_fallback_projects_nested_current_receiver() {
|
|
3449
|
+
let ws = tmp_ws("proj-null-current");
|
|
3450
|
+
let state = serde_json::json!({
|
|
3451
|
+
"active_team_key": "current",
|
|
3452
|
+
"team_key": "current",
|
|
3453
|
+
"teams": {
|
|
3454
|
+
"current": {
|
|
3455
|
+
"team_key": "current",
|
|
3456
|
+
"leader_receiver": {
|
|
3457
|
+
"status": "attached",
|
|
3458
|
+
"mode": "direct_tmux",
|
|
3459
|
+
"pane_id": "%9"
|
|
3460
|
+
},
|
|
3461
|
+
"agents": {}
|
|
3462
|
+
}
|
|
3463
|
+
},
|
|
3464
|
+
"agents": {}
|
|
3465
|
+
});
|
|
3466
|
+
crate::state::persist::save_runtime_state(&ws, &state).unwrap();
|
|
3467
|
+
let store = store_for(&ws);
|
|
3468
|
+
let log = EventLog::new(&ws);
|
|
3469
|
+
let message_id = store
|
|
3470
|
+
.create_message(None, "w1", "leader", "nested current", None, false, Some("current"))
|
|
3471
|
+
.unwrap();
|
|
3472
|
+
let transport = OfflineTransport::new().with_targets(vec![live_leader_pane(&ws, "%9")]);
|
|
3473
|
+
|
|
3474
|
+
let out = deliver_pending_message(&ws, &store, &transport, &message_id, &log, &serde_json::json!({}))
|
|
3475
|
+
.unwrap();
|
|
3476
|
+
|
|
3477
|
+
assert!(
|
|
3478
|
+
!delivery_blocked_channel_reasons(&ws)
|
|
3479
|
+
.iter()
|
|
3480
|
+
.any(|reason| reason == "receiver_missing"),
|
|
3481
|
+
"fresh Null fallback must project teams.current, not return Null; out={out:?} reasons={:?}",
|
|
3482
|
+
delivery_blocked_channel_reasons(&ws)
|
|
3483
|
+
);
|
|
3484
|
+
assert_eq!(
|
|
3485
|
+
transport.inject_targets(),
|
|
3486
|
+
vec![Target::Pane(PaneId::new("%9"))],
|
|
3487
|
+
"nested current receiver must select pane %9; out={out:?}"
|
|
3488
|
+
);
|
|
3489
|
+
}
|
|
3490
|
+
|
|
3491
|
+
#[test]
|
|
3492
|
+
fn owner_projection_non_object_does_not_match_current() {
|
|
3493
|
+
assert!(
|
|
3494
|
+
!crate::messaging::delivery::top_level_state_matches_owner_team(
|
|
3495
|
+
&serde_json::Value::Null,
|
|
3496
|
+
"current"
|
|
3497
|
+
),
|
|
3498
|
+
"Null must not match current through team_state_key default"
|
|
3499
|
+
);
|
|
3500
|
+
assert!(
|
|
3501
|
+
!crate::messaging::delivery::top_level_state_matches_owner_team(
|
|
3502
|
+
&serde_json::json!([]),
|
|
3503
|
+
"current"
|
|
3504
|
+
),
|
|
3505
|
+
"non-object must not match current"
|
|
3506
|
+
);
|
|
3507
|
+
}
|
|
3508
|
+
|
|
3509
|
+
#[test]
|
|
3510
|
+
fn owner_projection_legacy_top_level_receiver_survives_null_fallback() {
|
|
3511
|
+
let ws = tmp_ws("proj-legacy-top");
|
|
3512
|
+
let state = serde_json::json!({
|
|
3513
|
+
"active_team_key": "current",
|
|
3514
|
+
"leader_receiver": {
|
|
3515
|
+
"status": "attached",
|
|
3516
|
+
"mode": "direct_tmux",
|
|
3517
|
+
"pane_id": "%3"
|
|
3518
|
+
},
|
|
3519
|
+
"agents": {}
|
|
3520
|
+
});
|
|
3521
|
+
crate::state::persist::save_runtime_state(&ws, &state).unwrap();
|
|
3522
|
+
let store = store_for(&ws);
|
|
3523
|
+
let log = EventLog::new(&ws);
|
|
3524
|
+
let message_id = store
|
|
3525
|
+
.create_message(None, "w1", "leader", "legacy top", None, false, Some("current"))
|
|
3526
|
+
.unwrap();
|
|
3527
|
+
let transport = OfflineTransport::new().with_targets(vec![live_leader_pane(&ws, "%3")]);
|
|
3528
|
+
|
|
3529
|
+
let out = deliver_pending_message(&ws, &store, &transport, &message_id, &log, &serde_json::json!({}))
|
|
3530
|
+
.unwrap();
|
|
3531
|
+
|
|
3532
|
+
assert!(
|
|
3533
|
+
!delivery_blocked_channel_reasons(&ws)
|
|
3534
|
+
.iter()
|
|
3535
|
+
.any(|reason| reason == "receiver_missing"),
|
|
3536
|
+
"legacy top-level receiver must remain usable; out={out:?}"
|
|
3537
|
+
);
|
|
3538
|
+
assert_eq!(
|
|
3539
|
+
transport.inject_targets(),
|
|
3540
|
+
vec![Target::Pane(PaneId::new("%3"))],
|
|
3541
|
+
"legacy top-level receiver must select pane %3; out={out:?}"
|
|
3542
|
+
);
|
|
3543
|
+
}
|
|
3544
|
+
|
|
3545
|
+
#[test]
|
|
3546
|
+
fn owner_projection_missing_selected_does_not_borrow_sibling() {
|
|
3547
|
+
let ws = tmp_ws("proj-no-sibling");
|
|
3548
|
+
let state = serde_json::json!({
|
|
3549
|
+
"active_team_key": "alpha",
|
|
3550
|
+
"team_key": "alpha",
|
|
3551
|
+
"teams": {
|
|
3552
|
+
"alpha": {
|
|
3553
|
+
"team_key": "alpha",
|
|
3554
|
+
"agents": {}
|
|
3555
|
+
},
|
|
3556
|
+
"beta": {
|
|
3557
|
+
"team_key": "beta",
|
|
3558
|
+
"leader_receiver": {
|
|
3559
|
+
"status": "attached",
|
|
3560
|
+
"mode": "direct_tmux",
|
|
3561
|
+
"pane_id": "%beta"
|
|
3562
|
+
},
|
|
3563
|
+
"agents": {}
|
|
3564
|
+
}
|
|
3565
|
+
},
|
|
3566
|
+
"agents": {}
|
|
3567
|
+
});
|
|
3568
|
+
crate::state::persist::save_runtime_state(&ws, &state).unwrap();
|
|
3569
|
+
let store = store_for(&ws);
|
|
3570
|
+
let log = EventLog::new(&ws);
|
|
3571
|
+
let message_id = store
|
|
3572
|
+
.create_message(None, "w1", "leader", "alpha has no receiver", None, false, Some("alpha"))
|
|
3573
|
+
.unwrap();
|
|
3574
|
+
let transport = OfflineTransport::new().with_targets(vec![live_leader_pane(&ws, "%beta")]);
|
|
3575
|
+
|
|
3576
|
+
let out = deliver_pending_message(&ws, &store, &transport, &message_id, &log, &serde_json::json!({}))
|
|
3577
|
+
.unwrap();
|
|
3578
|
+
|
|
3579
|
+
assert!(
|
|
3580
|
+
delivery_blocked_channel_reasons(&ws)
|
|
3581
|
+
.iter()
|
|
3582
|
+
.any(|reason| reason == "receiver_missing"),
|
|
3583
|
+
"alpha must not borrow beta's receiver; out={out:?} reasons={:?}",
|
|
3584
|
+
delivery_blocked_channel_reasons(&ws)
|
|
3585
|
+
);
|
|
3586
|
+
assert_eq!(transport.inject_targets().len(), 0, "must not inject into the sibling pane");
|
|
3587
|
+
}
|
|
3588
|
+
|
|
3589
|
+
#[test]
|
|
3590
|
+
fn owner_projection_peer_inject_uses_nested_worker_pane_not_top_level() {
|
|
3591
|
+
let ws = tmp_ws("proj-peer-nested");
|
|
3592
|
+
let state = serde_json::json!({
|
|
3593
|
+
"active_team_key": "teamA",
|
|
3594
|
+
"session_name": "team-top",
|
|
3595
|
+
"agents": {
|
|
3596
|
+
"worker_b": {
|
|
3597
|
+
"provider": "fake",
|
|
3598
|
+
"status": "running",
|
|
3599
|
+
"window": "worker_b",
|
|
3600
|
+
"pane_id": "%top-b"
|
|
3601
|
+
}
|
|
3602
|
+
},
|
|
3603
|
+
"teams": {
|
|
3604
|
+
"teamA": {
|
|
3605
|
+
"session_name": "team-nested",
|
|
3606
|
+
"agents": {
|
|
3607
|
+
"worker_b": {
|
|
3608
|
+
"provider": "fake",
|
|
3609
|
+
"status": "running",
|
|
3610
|
+
"window": "worker_b",
|
|
3611
|
+
"pane_id": "%nested-b"
|
|
3612
|
+
}
|
|
3613
|
+
}
|
|
3614
|
+
}
|
|
3615
|
+
}
|
|
3616
|
+
});
|
|
3617
|
+
crate::state::persist::save_runtime_state(&ws, &state).unwrap();
|
|
3618
|
+
let store = store_for(&ws);
|
|
3619
|
+
let log = EventLog::new(&ws);
|
|
3620
|
+
let message_id = store
|
|
3621
|
+
.create_message(
|
|
3622
|
+
None,
|
|
3623
|
+
"worker_a",
|
|
3624
|
+
"worker_b",
|
|
3625
|
+
"MCP_SIM_BROADCAST_CANARY",
|
|
3626
|
+
None,
|
|
3627
|
+
false,
|
|
3628
|
+
Some("teamA"),
|
|
3629
|
+
)
|
|
3630
|
+
.unwrap();
|
|
3631
|
+
let transport = OfflineTransport::new().with_targets(vec![
|
|
3632
|
+
pane_info("%top-b", "team-top", "worker_b"),
|
|
3633
|
+
pane_info("%nested-b", "team-nested", "worker_b"),
|
|
3634
|
+
]);
|
|
3635
|
+
|
|
3636
|
+
let out = deliver_pending_message(
|
|
3637
|
+
&ws,
|
|
3638
|
+
&store,
|
|
3639
|
+
&transport,
|
|
3640
|
+
&message_id,
|
|
3641
|
+
&log,
|
|
3642
|
+
&serde_json::json!({}),
|
|
3643
|
+
)
|
|
3644
|
+
.unwrap();
|
|
3645
|
+
|
|
3646
|
+
let targets = transport.inject_targets();
|
|
3647
|
+
assert!(
|
|
3648
|
+
!targets.is_empty(),
|
|
3649
|
+
"owner teamA peer inject must attempt the nested worker pane; out={out:?}"
|
|
3650
|
+
);
|
|
3651
|
+
assert!(
|
|
3652
|
+
targets
|
|
3653
|
+
.iter()
|
|
3654
|
+
.all(|target| target == &Target::Pane(PaneId::new("%nested-b"))),
|
|
3655
|
+
"owner teamA peer inject must select nested worker pane %nested-b, not top-level %top-b; out={out:?} targets={targets:?}"
|
|
3656
|
+
);
|
|
3657
|
+
}
|
|
@@ -13,12 +13,13 @@ use std::path::Path;
|
|
|
13
13
|
|
|
14
14
|
use serde_json::Value;
|
|
15
15
|
|
|
16
|
-
use crate::model::enums::TaskStatus;
|
|
16
|
+
use crate::model::enums::{Provider, TaskStatus};
|
|
17
17
|
use crate::model::errors::ModelError;
|
|
18
18
|
use crate::model::ids::TaskId;
|
|
19
19
|
use crate::model::task_graph::{find_dependency_cycle, TaskNode};
|
|
20
20
|
use crate::model::yaml::Value as Yaml;
|
|
21
21
|
use crate::model::{permissions, yaml};
|
|
22
|
+
use crate::provider::adapters::pi::first_unsupported_pi_tool_category;
|
|
22
23
|
use crate::provider::wire::{is_claude_family, parse_canonical_provider};
|
|
23
24
|
|
|
24
25
|
/// result_envelope_v1 顶层 required(= allowed)。
|
|
@@ -793,11 +794,26 @@ fn semantic_errors(spec: &Yaml, base_dir: &Path) -> Vec<String> {
|
|
|
793
794
|
.iter()
|
|
794
795
|
.filter_map(Yaml::as_str)
|
|
795
796
|
.collect();
|
|
796
|
-
|
|
797
|
-
|
|
797
|
+
let expanded_tools = permissions::expand_tool_strings(tools);
|
|
798
|
+
for tool in &expanded_tools {
|
|
799
|
+
if !permissions::is_canonical_tool(tool) {
|
|
798
800
|
e.push(format!(
|
|
799
801
|
"/agents/{idx}/tools: unknown tool {}",
|
|
800
|
-
py_repr_str(
|
|
802
|
+
py_repr_str(tool)
|
|
803
|
+
));
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
if parse_canonical_provider(provider.and_then(Yaml::as_str).unwrap_or(""))
|
|
807
|
+
== Some(Provider::Pi)
|
|
808
|
+
{
|
|
809
|
+
if let Some(category) = first_unsupported_pi_tool_category(
|
|
810
|
+
expanded_tools
|
|
811
|
+
.iter()
|
|
812
|
+
.filter(|tool| permissions::is_canonical_tool(tool))
|
|
813
|
+
.map(String::as_str),
|
|
814
|
+
) {
|
|
815
|
+
e.push(format!(
|
|
816
|
+
"/agents/{idx}/tools: Pi does not support Team Agent tool category {category:?}; remove it from this agent's tools"
|
|
801
817
|
));
|
|
802
818
|
}
|
|
803
819
|
}
|
|
@@ -810,11 +826,26 @@ fn semantic_errors(spec: &Yaml, base_dir: &Path) -> Vec<String> {
|
|
|
810
826
|
.iter()
|
|
811
827
|
.filter_map(Yaml::as_str)
|
|
812
828
|
.collect();
|
|
813
|
-
|
|
814
|
-
|
|
829
|
+
let expanded_leader_tools = permissions::expand_tool_strings(leader_tools);
|
|
830
|
+
for tool in &expanded_leader_tools {
|
|
831
|
+
if !permissions::is_canonical_tool(tool) {
|
|
815
832
|
e.push(format!(
|
|
816
833
|
"/leader/tools: unknown tool {}",
|
|
817
|
-
py_repr_str(
|
|
834
|
+
py_repr_str(tool)
|
|
835
|
+
));
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
if parse_canonical_provider(leader_provider.and_then(Yaml::as_str).unwrap_or(""))
|
|
839
|
+
== Some(Provider::Pi)
|
|
840
|
+
{
|
|
841
|
+
if let Some(category) = first_unsupported_pi_tool_category(
|
|
842
|
+
expanded_leader_tools
|
|
843
|
+
.iter()
|
|
844
|
+
.filter(|tool| permissions::is_canonical_tool(tool))
|
|
845
|
+
.map(String::as_str),
|
|
846
|
+
) {
|
|
847
|
+
e.push(format!(
|
|
848
|
+
"/leader/tools: Pi does not support Team Agent tool category {category:?}; remove it from the leader's tools"
|
|
818
849
|
));
|
|
819
850
|
}
|
|
820
851
|
}
|
|
@@ -1038,6 +1069,49 @@ mod tests {
|
|
|
1038
1069
|
assert!(r.is_ok(), "expected valid, got: {r:?}");
|
|
1039
1070
|
}
|
|
1040
1071
|
|
|
1072
|
+
#[test]
|
|
1073
|
+
fn pi_spec_accepts_native_categories_and_rejects_provider_builtin() {
|
|
1074
|
+
let pi_text = include_str!("testdata/team.spec.yaml")
|
|
1075
|
+
.lines()
|
|
1076
|
+
.filter(|line| {
|
|
1077
|
+
!line.contains("provider_builtin")
|
|
1078
|
+
&& !line.contains("git_diff")
|
|
1079
|
+
&& !line.contains("network")
|
|
1080
|
+
})
|
|
1081
|
+
.collect::<Vec<_>>()
|
|
1082
|
+
.join("\n");
|
|
1083
|
+
let pi_text = pi_text.replace("provider: codex", "provider: pi");
|
|
1084
|
+
let pi_spec = yaml::loads(&pi_text).unwrap();
|
|
1085
|
+
assert!(
|
|
1086
|
+
validate_spec(&pi_spec, Path::new(TD)).is_ok(),
|
|
1087
|
+
"Pi must accept the native Team Agent categories"
|
|
1088
|
+
);
|
|
1089
|
+
|
|
1090
|
+
let bad_agent_text = pi_text.replacen(
|
|
1091
|
+
" - mcp_team\n",
|
|
1092
|
+
" - mcp_team\n - provider_builtin\n",
|
|
1093
|
+
1,
|
|
1094
|
+
);
|
|
1095
|
+
let bad_agent = yaml::loads(&bad_agent_text).unwrap();
|
|
1096
|
+
let error = validate_spec(&bad_agent, Path::new(TD)).unwrap_err().to_string();
|
|
1097
|
+
assert!(
|
|
1098
|
+
error.contains("/agents/0/tools: Pi does not support Team Agent tool category \"provider_builtin\""),
|
|
1099
|
+
"spec validation must reject the Pi agent category: {error}"
|
|
1100
|
+
);
|
|
1101
|
+
|
|
1102
|
+
let bad_leader_text = pi_text.replacen(
|
|
1103
|
+
" - mcp_team\n",
|
|
1104
|
+
" - mcp_team\n - provider_builtin\n",
|
|
1105
|
+
1,
|
|
1106
|
+
);
|
|
1107
|
+
let bad_leader = yaml::loads(&bad_leader_text).unwrap();
|
|
1108
|
+
let error = validate_spec(&bad_leader, Path::new(TD)).unwrap_err().to_string();
|
|
1109
|
+
assert!(
|
|
1110
|
+
error.contains("/leader/tools: Pi does not support Team Agent tool category \"provider_builtin\""),
|
|
1111
|
+
"spec validation must reject the Pi leader category: {error}"
|
|
1112
|
+
);
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1041
1115
|
#[test]
|
|
1042
1116
|
fn invalid_spec_a_matches_python_golden() {
|
|
1043
1117
|
let spec = yaml::loads(include_str!("testdata/spec_invalid_a.yaml")).unwrap();
|
|
@@ -37,6 +37,20 @@ pub(crate) fn pi_tool_mapping(category: &str) -> PiToolMapping {
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
/// Return the first canonical category that Pi cannot map. Callers own alias
|
|
41
|
+
/// expansion and generic unknown-category validation; this keeps the provider
|
|
42
|
+
/// mapping itself as the only Pi capability contract.
|
|
43
|
+
pub(crate) fn first_unsupported_pi_tool_category<'a, I>(
|
|
44
|
+
categories: I,
|
|
45
|
+
) -> Option<&'a str>
|
|
46
|
+
where
|
|
47
|
+
I: IntoIterator<Item = &'a str>,
|
|
48
|
+
{
|
|
49
|
+
categories
|
|
50
|
+
.into_iter()
|
|
51
|
+
.find(|category| matches!(pi_tool_mapping(category), PiToolMapping::Unsupported))
|
|
52
|
+
}
|
|
53
|
+
|
|
40
54
|
#[derive(Debug, Clone, Copy)]
|
|
41
55
|
pub(crate) enum PiSessionSelector<'a> {
|
|
42
56
|
Fresh { session_id: &'a str },
|
|
@@ -50,7 +64,8 @@ pub(crate) struct PiCommandRequest<'a> {
|
|
|
50
64
|
pub effort: Option<ProviderEffort>,
|
|
51
65
|
pub system_prompt: &'a str,
|
|
52
66
|
pub tool_categories: &'a [&'a str],
|
|
53
|
-
|
|
67
|
+
/// Team-owned session root for isolated seats; `None` preserves Pi's native default.
|
|
68
|
+
pub session_dir: Option<&'a Path>,
|
|
54
69
|
pub session: PiSessionSelector<'a>,
|
|
55
70
|
pub agent_id: &'a str,
|
|
56
71
|
}
|
|
@@ -65,14 +80,15 @@ pub(crate) fn build_pi_command_argv(
|
|
|
65
80
|
) -> Result<Vec<String>, ProviderError> {
|
|
66
81
|
if request.executable.as_os_str().is_empty()
|
|
67
82
|
|| request.extension.as_os_str().is_empty()
|
|
68
|
-
|| request
|
|
83
|
+
|| request
|
|
84
|
+
.session_dir
|
|
85
|
+
.is_some_and(|session_dir| session_dir.as_os_str().is_empty())
|
|
69
86
|
|| request.model.is_some_and(|model| model.trim().is_empty())
|
|
70
87
|
|| request.system_prompt.trim().is_empty()
|
|
71
88
|
|| request.agent_id.trim().is_empty()
|
|
72
89
|
{
|
|
73
90
|
return Err(ProviderError::Command(
|
|
74
|
-
"Pi command requires executable, extension, prompt,
|
|
75
|
-
.to_string(),
|
|
91
|
+
"Pi command requires executable, extension, prompt, and agent id".to_string(),
|
|
76
92
|
));
|
|
77
93
|
}
|
|
78
94
|
|
|
@@ -110,9 +126,13 @@ pub(crate) fn build_pi_command_argv(
|
|
|
110
126
|
argv.extend([
|
|
111
127
|
"--append-system-prompt".to_string(),
|
|
112
128
|
request.system_prompt.to_string(),
|
|
113
|
-
"--session-dir".to_string(),
|
|
114
|
-
request.session_dir.to_string_lossy().into_owned(),
|
|
115
129
|
]);
|
|
130
|
+
if let Some(session_dir) = request.session_dir {
|
|
131
|
+
argv.extend([
|
|
132
|
+
"--session-dir".to_string(),
|
|
133
|
+
session_dir.to_string_lossy().into_owned(),
|
|
134
|
+
]);
|
|
135
|
+
}
|
|
116
136
|
// The shared lifecycle materializer always revalidates resume root/path/header.
|
|
117
137
|
// Keep the pure argv builder usable before a fixture root is materialized, while
|
|
118
138
|
// refusing a missing exact file once the recorded path's parent exists.
|