@team-agent/installer 0.5.69 → 0.5.71
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/mod.rs +42 -8
- package/crates/team-agent/src/compiler.rs +27 -37
- package/crates/team-agent/src/leader/start.rs +1 -1
- package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +130 -95
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +2 -12
- package/crates/team-agent/src/lifecycle/restart/common.rs +2 -12
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +23 -7
- package/crates/team-agent/src/lifecycle/restart.rs +2 -1
- package/crates/team-agent/src/lifecycle/tests/pi_catalog_red.rs +2 -2
- package/crates/team-agent/src/lifecycle/tests/pi_command_red.rs +36 -19
- package/crates/team-agent/src/lifecycle/tests/pi_compiler_red.rs +58 -22
- package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +27 -9
- package/crates/team-agent/src/lifecycle/tests/pi_executable_mcp_red.rs +212 -26
- package/crates/team-agent/src/lifecycle/tests/pi_session_delivery_red.rs +2 -2
- package/crates/team-agent/src/provider/adapters/pi.rs +22 -28
- package/package.json +4 -4
- package/skills/team-agent/SKILL.md +7 -1
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
|
@@ -3393,6 +3393,40 @@ pub mod lifecycle_port {
|
|
|
3393
3393
|
Some(crate::cli::QUICK_START_REMINDER)
|
|
3394
3394
|
);
|
|
3395
3395
|
}
|
|
3396
|
+
|
|
3397
|
+
#[test]
|
|
3398
|
+
fn spawn_failure_without_session_does_not_claim_a_worker_pane_exists() {
|
|
3399
|
+
let value = restart_value(
|
|
3400
|
+
crate::lifecycle::RestartReport::Failed {
|
|
3401
|
+
session_name: crate::transport::SessionName::new("team-current"),
|
|
3402
|
+
failed_agents: vec![crate::lifecycle::RestartFailedAgent {
|
|
3403
|
+
agent_id: crate::model::ids::AgentId::new("coder"),
|
|
3404
|
+
restart_mode: crate::lifecycle::StartMode::Fresh,
|
|
3405
|
+
decision: crate::lifecycle::ResumeDecision::FreshStart,
|
|
3406
|
+
session_id: None,
|
|
3407
|
+
phase: "spawn".to_string(),
|
|
3408
|
+
error: "provider io error".to_string(),
|
|
3409
|
+
}],
|
|
3410
|
+
next_actions: Vec::new(),
|
|
3411
|
+
attach_commands: Vec::new(),
|
|
3412
|
+
},
|
|
3413
|
+
None,
|
|
3414
|
+
);
|
|
3415
|
+
let action = value
|
|
3416
|
+
.pointer("/failed_agents/0/action")
|
|
3417
|
+
.and_then(Value::as_str)
|
|
3418
|
+
.expect("spawn failure action");
|
|
3419
|
+
|
|
3420
|
+
assert!(
|
|
3421
|
+
action.contains("before a pane was created"),
|
|
3422
|
+
"action={action}"
|
|
3423
|
+
);
|
|
3424
|
+
assert!(
|
|
3425
|
+
!action.contains("inspect worker coder output"),
|
|
3426
|
+
"action={action}"
|
|
3427
|
+
);
|
|
3428
|
+
assert!(!action.contains("restart-agent"), "action={action}");
|
|
3429
|
+
}
|
|
3396
3430
|
}
|
|
3397
3431
|
|
|
3398
3432
|
fn restart_value(report: crate::lifecycle::RestartReport, team: Option<&str>) -> Value {
|
|
@@ -3436,10 +3470,10 @@ pub mod lifecycle_port {
|
|
|
3436
3470
|
"session_id": failure.session_id.as_ref().map(|session| session.as_str()),
|
|
3437
3471
|
"phase": failure.phase,
|
|
3438
3472
|
"error": failure.error,
|
|
3439
|
-
"action":
|
|
3440
|
-
|
|
3441
|
-
failure.
|
|
3442
|
-
failure.
|
|
3473
|
+
"action": crate::lifecycle::restart_failure_action(
|
|
3474
|
+
&failure.agent_id,
|
|
3475
|
+
&failure.phase,
|
|
3476
|
+
failure.session_id.as_ref(),
|
|
3443
3477
|
),
|
|
3444
3478
|
"log": format!(
|
|
3445
3479
|
".team/logs/coordinator.log and .team/runtime/state.json agent={}",
|
|
@@ -3470,10 +3504,10 @@ pub mod lifecycle_port {
|
|
|
3470
3504
|
"session_id": failure.session_id.as_ref().map(|session| session.as_str()),
|
|
3471
3505
|
"phase": failure.phase,
|
|
3472
3506
|
"error": failure.error,
|
|
3473
|
-
"action":
|
|
3474
|
-
|
|
3475
|
-
failure.
|
|
3476
|
-
failure.
|
|
3507
|
+
"action": crate::lifecycle::restart_failure_action(
|
|
3508
|
+
&failure.agent_id,
|
|
3509
|
+
&failure.phase,
|
|
3510
|
+
failure.session_id.as_ref(),
|
|
3477
3511
|
),
|
|
3478
3512
|
"log": format!(
|
|
3479
3513
|
".team/logs/coordinator.log and .team/runtime/state.json agent={}",
|
|
@@ -392,12 +392,20 @@ fn compile_role_agent_with_mode(
|
|
|
392
392
|
let provider = required_string(&meta, role_path, "provider")?;
|
|
393
393
|
require_explicit_grok_role_model(&meta, role_path, &provider)?;
|
|
394
394
|
require_explicit_cursor_role_model(&meta, role_path, &provider)?;
|
|
395
|
-
|
|
396
|
-
let
|
|
395
|
+
validate_pi_role_fields(&meta, role_path, &provider)?;
|
|
396
|
+
let is_pi = parse_canonical_provider(&provider) == Some(Provider::Pi);
|
|
397
|
+
let model = if is_pi {
|
|
398
|
+
string_field(&meta, "model")
|
|
399
|
+
.filter(|value| !value.trim().is_empty())
|
|
400
|
+
.map(Value::Str)
|
|
401
|
+
.unwrap_or(Value::Null)
|
|
402
|
+
} else {
|
|
403
|
+
resolve_model(&meta, team_meta, &provider)
|
|
404
|
+
};
|
|
397
405
|
let auth_mode = string_field(&meta, "auth_mode")
|
|
398
406
|
.or_else(|| string_field(team_meta, "default_auth_mode"))
|
|
399
407
|
.unwrap_or_else(|| "subscription".to_string());
|
|
400
|
-
if
|
|
408
|
+
if is_pi && auth_mode != "subscription" {
|
|
401
409
|
return Err(ModelError::Validation(format!(
|
|
402
410
|
"{}: Pi roles support subscription auth_mode only",
|
|
403
411
|
role_path.display()
|
|
@@ -473,7 +481,11 @@ fn compile_role_agent_with_mode(
|
|
|
473
481
|
Some(raw) if !raw.trim().is_empty() => ProviderEffort::parse(raw.trim()),
|
|
474
482
|
_ => None,
|
|
475
483
|
};
|
|
476
|
-
let resolved_effort =
|
|
484
|
+
let resolved_effort = if is_pi {
|
|
485
|
+
role_effort
|
|
486
|
+
} else {
|
|
487
|
+
role_effort.or(team_effort)
|
|
488
|
+
};
|
|
477
489
|
if let Some(effort) = resolved_effort {
|
|
478
490
|
// Reject max + non-Claude at compile time.
|
|
479
491
|
let provider_str = agent_items
|
|
@@ -619,39 +631,23 @@ model: sonnet-4-thinking",
|
|
|
619
631
|
)))
|
|
620
632
|
}
|
|
621
633
|
|
|
622
|
-
fn
|
|
623
|
-
meta: &Value,
|
|
624
|
-
path: &Path,
|
|
625
|
-
provider: &str,
|
|
626
|
-
) -> Result<(), ModelError> {
|
|
634
|
+
fn validate_pi_role_fields(meta: &Value, path: &Path, provider: &str) -> Result<(), ModelError> {
|
|
627
635
|
if parse_canonical_provider(provider) != Some(Provider::Pi) {
|
|
628
636
|
return Ok(());
|
|
629
637
|
}
|
|
630
|
-
let model = string_field(meta, "model")
|
|
631
|
-
|
|
632
|
-
.ok_or_else(|| {
|
|
638
|
+
if let Some(model) = string_field(meta, "model").filter(|value| !value.trim().is_empty()) {
|
|
639
|
+
let (catalog, name) = model.trim().split_once('/').ok_or_else(|| {
|
|
633
640
|
ModelError::Validation(format!(
|
|
634
|
-
"{}: Pi roles require
|
|
641
|
+
"{}: Pi roles require a qualified exact model such as provider/model",
|
|
635
642
|
path.display()
|
|
636
643
|
))
|
|
637
644
|
})?;
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
if catalog.is_empty() || name.is_empty() || model.contains('*') {
|
|
645
|
-
return Err(ModelError::Validation(format!(
|
|
646
|
-
"{}: Pi roles require a qualified exact model",
|
|
647
|
-
path.display()
|
|
648
|
-
)));
|
|
649
|
-
}
|
|
650
|
-
if !string_field(meta, "effort").is_some_and(|value| !value.trim().is_empty()) {
|
|
651
|
-
return Err(ModelError::Validation(format!(
|
|
652
|
-
"{}: Pi roles require explicit effort",
|
|
653
|
-
path.display()
|
|
654
|
-
)));
|
|
645
|
+
if catalog.is_empty() || name.is_empty() || model.contains('*') {
|
|
646
|
+
return Err(ModelError::Validation(format!(
|
|
647
|
+
"{}: Pi roles require a qualified exact model",
|
|
648
|
+
path.display()
|
|
649
|
+
)));
|
|
650
|
+
}
|
|
655
651
|
}
|
|
656
652
|
let tools = required_tools(meta, path)?;
|
|
657
653
|
if !tools.iter().any(|tool| tool == "mcp_team") {
|
|
@@ -660,13 +656,7 @@ fn require_explicit_pi_role_fields(
|
|
|
660
656
|
path.display()
|
|
661
657
|
)));
|
|
662
658
|
}
|
|
663
|
-
|
|
664
|
-
Some(Value::Bool(true)) => Ok(()),
|
|
665
|
-
_ => Err(ModelError::Validation(format!(
|
|
666
|
-
"{}: Pi roles require dangerously_skip_permissions: true",
|
|
667
|
-
path.display()
|
|
668
|
-
))),
|
|
669
|
-
}
|
|
659
|
+
Ok(())
|
|
670
660
|
}
|
|
671
661
|
|
|
672
662
|
/// 0.5.66 bypass 单源:角色 md 的 `dangerously_skip_permissions` 必填 bool。
|
|
@@ -318,7 +318,7 @@ fn leader_start_plan_with_ambient_authority(
|
|
|
318
318
|
workspace,
|
|
319
319
|
team_id: identity.team_id.as_str(),
|
|
320
320
|
agent_id: "leader",
|
|
321
|
-
model:
|
|
321
|
+
model: parsed.model.as_deref(),
|
|
322
322
|
effort: parsed.effort,
|
|
323
323
|
system_prompt: &prompt,
|
|
324
324
|
tool_categories: &tools,
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
//! - name: materialize_pi_plan
|
|
6
6
|
//! what: 为 leader 与 TeamMate 生成同一来源的 Pi CommandPlan
|
|
7
7
|
//! - name: write_pi_wrapper
|
|
8
|
-
//! what: 原子写入 0600
|
|
8
|
+
//! what: 原子写入 0600 per-seat runtime MCP registration extension
|
|
9
9
|
//! boundary:
|
|
10
10
|
//! - 不枚举 Pi session;resume 只重验调用方给出的 exact backing
|
|
11
11
|
//! - 不投递消息、不执行 cleanup 或 doctor 判定
|
|
@@ -270,7 +270,10 @@ pub(crate) fn validate_pi_executable_chain(chain: &PiExecutableChain) -> Result<
|
|
|
270
270
|
if !chain.path_entry.is_absolute()
|
|
271
271
|
|| !chain.launch_executable.is_absolute()
|
|
272
272
|
|| !chain.real_binary.is_absolute()
|
|
273
|
-
||
|
|
273
|
+
|| !matches!(
|
|
274
|
+
chain.path_entry_type,
|
|
275
|
+
PiExecutableFileType::Wrapper | PiExecutableFileType::Symlink
|
|
276
|
+
)
|
|
274
277
|
|| chain.launch_executable != chain.path_entry
|
|
275
278
|
|| chain.real_binary == chain.launch_executable
|
|
276
279
|
|| chain.catalog_sha256.len() != 64
|
|
@@ -349,15 +352,6 @@ fn render_pi_wrapper(request: &PiWrapperRequest<'_>) -> Result<String, ProviderE
|
|
|
349
352
|
"command".to_string(),
|
|
350
353
|
serde_json::Value::String(expected.to_string_lossy().into_owned()),
|
|
351
354
|
);
|
|
352
|
-
server.insert(
|
|
353
|
-
"lifecycle".to_string(),
|
|
354
|
-
serde_json::Value::String("lazy".to_string()),
|
|
355
|
-
);
|
|
356
|
-
server.insert("directTools".to_string(), serde_json::Value::Bool(false));
|
|
357
|
-
server.insert(
|
|
358
|
-
"toolPrefix".to_string(),
|
|
359
|
-
serde_json::Value::String("server".to_string()),
|
|
360
|
-
);
|
|
361
355
|
server.insert(
|
|
362
356
|
"includeTools".to_string(),
|
|
363
357
|
serde_json::Value::Array(
|
|
@@ -389,66 +383,91 @@ fn render_pi_wrapper(request: &PiWrapperRequest<'_>) -> Result<String, ProviderE
|
|
|
389
383
|
"TEAM_AGENT_OWNER_TEAM_ID".to_string(),
|
|
390
384
|
serde_json::Value::String(request.team_id.to_string()),
|
|
391
385
|
);
|
|
392
|
-
let
|
|
393
|
-
"
|
|
394
|
-
"team_orchestrator": serde_json::Value::Object(server)
|
|
395
|
-
}
|
|
396
|
-
});
|
|
397
|
-
let import = serde_json::to_string(&request.adapter.index_ts)
|
|
398
|
-
.map_err(|error| ProviderError::Command(format!("serialize Pi adapter path: {error}")))?;
|
|
399
|
-
let config = serde_json::to_string_pretty(&config)
|
|
400
|
-
.map_err(|error| ProviderError::Command(format!("serialize Pi MCP config: {error}")))?;
|
|
386
|
+
let definition = serde_json::to_string_pretty(&serde_json::Value::Object(server))
|
|
387
|
+
.map_err(|error| ProviderError::Command(format!("serialize Pi MCP server: {error}")))?;
|
|
401
388
|
Ok(format!(
|
|
402
|
-
"
|
|
389
|
+
r#"const MCP_RUNTIME_REGISTER_EVENT = "pi-mcp-adapter:runtime-register:v1";
|
|
390
|
+
|
|
391
|
+
export default function teamAgentMcp(pi: any) {{
|
|
392
|
+
let registration: {{ dispose(): Promise<void> }} | undefined;
|
|
393
|
+
|
|
394
|
+
pi.on("session_start", () => {{
|
|
395
|
+
if (registration) return;
|
|
396
|
+
const request: any = {{
|
|
397
|
+
version: 1,
|
|
398
|
+
name: "team_orchestrator",
|
|
399
|
+
definition: {definition},
|
|
400
|
+
}};
|
|
401
|
+
pi.events.emit(MCP_RUNTIME_REGISTER_EVENT, request);
|
|
402
|
+
if (!request.result) throw new Error("pi-mcp-adapter is not enabled");
|
|
403
|
+
if (!request.result.ok) throw request.result.error;
|
|
404
|
+
registration = request.result.registration;
|
|
405
|
+
}});
|
|
406
|
+
|
|
407
|
+
pi.on("session_shutdown", async () => {{
|
|
408
|
+
const current = registration;
|
|
409
|
+
registration = undefined;
|
|
410
|
+
await current?.dispose();
|
|
411
|
+
}});
|
|
412
|
+
}}
|
|
413
|
+
"#
|
|
403
414
|
))
|
|
404
415
|
}
|
|
405
416
|
|
|
406
417
|
/// ---
|
|
407
|
-
/// purpose: 静态核对 wrapper
|
|
408
|
-
/// returns: wrapper
|
|
409
|
-
/// errors: identity
|
|
418
|
+
/// purpose: 静态核对 wrapper 只做 per-seat runtime MCP registration
|
|
419
|
+
/// returns: wrapper 保留 direct Pi 配置且冻结 seat identity 时成功
|
|
420
|
+
/// errors: identity、runtime registration 或 candidate 缺失时返回 ProviderError
|
|
410
421
|
/// ---
|
|
411
422
|
pub(crate) fn validate_pi_wrapper_source(
|
|
412
423
|
source: &str,
|
|
413
424
|
adapter: &PiAdapterIdentity,
|
|
414
425
|
candidate_executable: &Path,
|
|
415
426
|
) -> Result<(), ProviderError> {
|
|
416
|
-
|
|
427
|
+
validate_pi_adapter_identity(adapter)?;
|
|
417
428
|
let candidate = candidate_executable.to_string_lossy();
|
|
418
|
-
if !source.contains(
|
|
419
|
-
|| !source.contains(
|
|
420
|
-
|| !source.contains("
|
|
421
|
-
|| !source.contains("
|
|
429
|
+
if !source.contains(candidate.as_ref())
|
|
430
|
+
|| !source.contains("pi-mcp-adapter:runtime-register:v1")
|
|
431
|
+
|| !source.contains("session_start")
|
|
432
|
+
|| !source.contains("session_shutdown")
|
|
422
433
|
|| !source.contains("team_orchestrator")
|
|
423
|
-
|| !source.contains("lazy")
|
|
424
|
-
|| !source.contains("directTools")
|
|
425
|
-
|| !source.contains("false")
|
|
426
|
-
|| !source.contains("toolPrefix")
|
|
427
|
-
|| !source.contains("server")
|
|
428
434
|
|| !source.contains("includeTools")
|
|
435
|
+
|| source.contains("createMcpAdapter")
|
|
436
|
+
|| source.contains("directTools")
|
|
437
|
+
|| source.contains("toolPrefix")
|
|
429
438
|
|| source.contains("--mcp-config")
|
|
430
439
|
{
|
|
431
440
|
return Err(ProviderError::Command(
|
|
432
|
-
"Pi wrapper source does not preserve
|
|
433
|
-
.to_string(),
|
|
441
|
+
"Pi wrapper source does not preserve direct Pi plus per-seat runtime MCP".to_string(),
|
|
434
442
|
));
|
|
435
443
|
}
|
|
436
444
|
Ok(())
|
|
437
445
|
}
|
|
438
446
|
|
|
439
447
|
/// ---
|
|
440
|
-
/// purpose: 在 seat root 原子写入 mode 0600 的
|
|
448
|
+
/// purpose: 在 seat root 原子写入 mode 0600 的 runtime MCP registration extension
|
|
441
449
|
/// returns: 最终 wrapper 路径
|
|
442
450
|
/// errors: identity、序列化、目录、写盘或 rename 失败时返回 ProviderError
|
|
443
451
|
/// ---
|
|
444
452
|
pub(crate) fn write_pi_wrapper(request: PiWrapperRequest<'_>) -> Result<PathBuf, ProviderError> {
|
|
453
|
+
write_pi_wrapper_with_publish(request, |from, to| std::fs::rename(from, to))
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
pub(crate) fn write_pi_wrapper_with_publish(
|
|
457
|
+
request: PiWrapperRequest<'_>,
|
|
458
|
+
publish: impl FnOnce(&Path, &Path) -> std::io::Result<()>,
|
|
459
|
+
) -> Result<PathBuf, ProviderError> {
|
|
445
460
|
let source = render_pi_wrapper(&request)?;
|
|
446
461
|
validate_pi_wrapper_source(&source, request.adapter, request.candidate_executable)?;
|
|
447
462
|
let parent = request.destination.parent().ok_or_else(|| {
|
|
448
463
|
ProviderError::Io("Pi wrapper destination has no parent directory".to_string())
|
|
449
464
|
})?;
|
|
450
|
-
std::fs::create_dir_all(parent)
|
|
451
|
-
|
|
465
|
+
std::fs::create_dir_all(parent).map_err(|error| {
|
|
466
|
+
ProviderError::Io(format!(
|
|
467
|
+
"Pi wrapper atomic write stage=create_parent path={}: {error}",
|
|
468
|
+
parent.display()
|
|
469
|
+
))
|
|
470
|
+
})?;
|
|
452
471
|
let temp = parent.join(format!(".team-mcp.ts.{}.tmp", std::process::id()));
|
|
453
472
|
let write_result = (|| {
|
|
454
473
|
let mut file = OpenOptions::new()
|
|
@@ -456,22 +475,38 @@ pub(crate) fn write_pi_wrapper(request: PiWrapperRequest<'_>) -> Result<PathBuf,
|
|
|
456
475
|
.write(true)
|
|
457
476
|
.mode(0o600)
|
|
458
477
|
.open(&temp)
|
|
459
|
-
.map_err(|error|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
478
|
+
.map_err(|error| {
|
|
479
|
+
ProviderError::Io(format!(
|
|
480
|
+
"Pi wrapper atomic write stage=open_temp path={}: {error}",
|
|
481
|
+
temp.display()
|
|
482
|
+
))
|
|
483
|
+
})?;
|
|
484
|
+
file.write_all(source.as_bytes()).map_err(|error| {
|
|
465
485
|
ProviderError::Io(format!(
|
|
466
|
-
"
|
|
467
|
-
temp.display()
|
|
468
|
-
request.destination.display()
|
|
486
|
+
"Pi wrapper atomic write stage=write_temp path={}: {error}",
|
|
487
|
+
temp.display()
|
|
469
488
|
))
|
|
470
489
|
})?;
|
|
471
|
-
|
|
490
|
+
file.set_permissions(std::fs::Permissions::from_mode(0o600))
|
|
472
491
|
.map_err(|error| {
|
|
473
|
-
ProviderError::Io(format!(
|
|
492
|
+
ProviderError::Io(format!(
|
|
493
|
+
"Pi wrapper atomic write stage=chmod_temp_fd path={}: {error}",
|
|
494
|
+
temp.display()
|
|
495
|
+
))
|
|
474
496
|
})?;
|
|
497
|
+
file.sync_all().map_err(|error| {
|
|
498
|
+
ProviderError::Io(format!(
|
|
499
|
+
"Pi wrapper atomic write stage=fsync_temp path={}: {error}",
|
|
500
|
+
temp.display()
|
|
501
|
+
))
|
|
502
|
+
})?;
|
|
503
|
+
publish(&temp, request.destination).map_err(|error| {
|
|
504
|
+
ProviderError::Io(format!(
|
|
505
|
+
"Pi wrapper atomic write stage=rename_publish from={} to={}: {error}",
|
|
506
|
+
temp.display(),
|
|
507
|
+
request.destination.display()
|
|
508
|
+
))
|
|
509
|
+
})?;
|
|
475
510
|
Ok(request.destination.to_path_buf())
|
|
476
511
|
})();
|
|
477
512
|
if write_result.is_err() {
|
|
@@ -482,14 +517,14 @@ pub(crate) fn write_pi_wrapper(request: PiWrapperRequest<'_>) -> Result<PathBuf,
|
|
|
482
517
|
|
|
483
518
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
484
519
|
pub(crate) struct PiLeaderArgs {
|
|
485
|
-
pub model: String
|
|
486
|
-
pub effort: ProviderEffort
|
|
520
|
+
pub model: Option<String>,
|
|
521
|
+
pub effort: Option<ProviderEffort>,
|
|
487
522
|
}
|
|
488
523
|
|
|
489
524
|
/// ---
|
|
490
525
|
/// purpose: 解析 Pi leader 唯一允许的 model/thinking 输入
|
|
491
|
-
/// returns:
|
|
492
|
-
/// errors:
|
|
526
|
+
/// returns: 仅保留调用方显式给出的 qualified model 与 effort
|
|
527
|
+
/// errors: 重复、未知或 materializer-owned flag 时返回 ProviderError
|
|
493
528
|
/// ---
|
|
494
529
|
pub(crate) fn parse_pi_leader_args(args: &[String]) -> Result<PiLeaderArgs, ProviderError> {
|
|
495
530
|
let args = args.strip_prefix(&["--".to_string()]).unwrap_or(args);
|
|
@@ -519,24 +554,17 @@ pub(crate) fn parse_pi_leader_args(args: &[String]) -> Result<PiLeaderArgs, Prov
|
|
|
519
554
|
}
|
|
520
555
|
index += 2;
|
|
521
556
|
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
.is_some_and(|(provider, name)| !provider.is_empty() && !name.is_empty())
|
|
529
|
-
{
|
|
557
|
+
if model.as_ref().is_some_and(|model| {
|
|
558
|
+
model.contains('*')
|
|
559
|
+
|| !model
|
|
560
|
+
.split_once('/')
|
|
561
|
+
.is_some_and(|(provider, name)| !provider.is_empty() && !name.is_empty())
|
|
562
|
+
}) {
|
|
530
563
|
return Err(ProviderError::Command(
|
|
531
564
|
"Pi leader --model must be a qualified exact id".to_string(),
|
|
532
565
|
));
|
|
533
566
|
}
|
|
534
|
-
Ok(PiLeaderArgs {
|
|
535
|
-
model,
|
|
536
|
-
effort: effort.ok_or_else(|| {
|
|
537
|
-
ProviderError::Command("Pi leader requires an explicit --thinking".to_string())
|
|
538
|
-
})?,
|
|
539
|
-
})
|
|
567
|
+
Ok(PiLeaderArgs { model, effort })
|
|
540
568
|
}
|
|
541
569
|
|
|
542
570
|
fn sha256(bytes: &[u8]) -> String {
|
|
@@ -598,7 +626,7 @@ fn command_stdout(executable: &Path, args: &[&str]) -> Result<Vec<u8>, ProviderE
|
|
|
598
626
|
}
|
|
599
627
|
|
|
600
628
|
fn discover_pi_adapter(launch_executable: &Path) -> Result<PiAdapterIdentity, ProviderError> {
|
|
601
|
-
let listing = command_stdout(launch_executable, &["list"
|
|
629
|
+
let listing = command_stdout(launch_executable, &["list"])?;
|
|
602
630
|
let listing = std::str::from_utf8(&listing).map_err(|error| {
|
|
603
631
|
ProviderError::Command(format!("Pi package list is not UTF-8: {error}"))
|
|
604
632
|
})?;
|
|
@@ -660,16 +688,16 @@ fn discover_pi_adapter(launch_executable: &Path) -> Result<PiAdapterIdentity, Pr
|
|
|
660
688
|
pub(crate) fn resolve_pi_executable_chain(
|
|
661
689
|
) -> Result<(PiExecutableChain, Vec<String>), ProviderError> {
|
|
662
690
|
let path_entries = pi_path_entries()?;
|
|
663
|
-
let path_entry = path_entries
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
691
|
+
let path_entry = path_entries[0].clone();
|
|
692
|
+
let path_entry_type = executable_file_type(&path_entry)?;
|
|
693
|
+
if !matches!(
|
|
694
|
+
path_entry_type,
|
|
695
|
+
PiExecutableFileType::Wrapper | PiExecutableFileType::Symlink
|
|
696
|
+
) {
|
|
697
|
+
return Err(ProviderError::Command(
|
|
698
|
+
"the direct Pi PATH entry is not a verified wrapper or npm symlink".to_string(),
|
|
699
|
+
));
|
|
700
|
+
}
|
|
673
701
|
let version =
|
|
674
702
|
String::from_utf8(command_stdout(&path_entry, &["--version"])?).map_err(|error| {
|
|
675
703
|
ProviderError::Command(format!("Pi version output is not UTF-8: {error}"))
|
|
@@ -677,17 +705,21 @@ pub(crate) fn resolve_pi_executable_chain(
|
|
|
677
705
|
let version = version.trim().to_string();
|
|
678
706
|
let launch_identity = std::fs::canonicalize(&path_entry)
|
|
679
707
|
.map_err(|error| ProviderError::Io(format!("{}: {error}", path_entry.display())))?;
|
|
680
|
-
let real_binary =
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
708
|
+
let real_binary = if path_entry_type == PiExecutableFileType::Symlink {
|
|
709
|
+
launch_identity
|
|
710
|
+
} else {
|
|
711
|
+
path_entries
|
|
712
|
+
.iter()
|
|
713
|
+
.find_map(|candidate| {
|
|
714
|
+
let real = std::fs::canonicalize(candidate).ok()?;
|
|
715
|
+
(real != launch_identity).then_some(real)
|
|
716
|
+
})
|
|
717
|
+
.ok_or_else(|| {
|
|
718
|
+
ProviderError::Command(
|
|
719
|
+
"Pi PATH wrapper has no independently resolved real executable".to_string(),
|
|
720
|
+
)
|
|
721
|
+
})?
|
|
722
|
+
};
|
|
691
723
|
let _real_version =
|
|
692
724
|
String::from_utf8(command_stdout(&real_binary, &["--version"])?).map_err(|error| {
|
|
693
725
|
ProviderError::Command(format!(
|
|
@@ -714,8 +746,8 @@ pub(crate) struct PiMaterializeRequest<'a> {
|
|
|
714
746
|
pub workspace: &'a Path,
|
|
715
747
|
pub team_id: &'a str,
|
|
716
748
|
pub agent_id: &'a str,
|
|
717
|
-
pub model:
|
|
718
|
-
pub effort: ProviderEffort
|
|
749
|
+
pub model: Option<&'a str>,
|
|
750
|
+
pub effort: Option<ProviderEffort>,
|
|
719
751
|
pub system_prompt: &'a str,
|
|
720
752
|
pub tool_categories: &'a [&'a str],
|
|
721
753
|
pub team_mcp_tools: &'a [&'a str],
|
|
@@ -752,7 +784,10 @@ fn materialize_pi_plan_with_session(
|
|
|
752
784
|
resume: Option<(&SessionId, &Path, &Path)>,
|
|
753
785
|
) -> Result<CommandPlan, ProviderError> {
|
|
754
786
|
let (chain, catalog) = resolve_pi_executable_chain()?;
|
|
755
|
-
let model =
|
|
787
|
+
let model = request
|
|
788
|
+
.model
|
|
789
|
+
.map(|model| select_exact_pi_model(&catalog, model))
|
|
790
|
+
.transpose()?;
|
|
756
791
|
let paths = pi_seat_paths(request.workspace, request.team_id, request.agent_id);
|
|
757
792
|
let resume = if let Some((session_id, session_path, spawn_cwd)) = resume {
|
|
758
793
|
let session_root = std::fs::canonicalize(&paths.sessions).map_err(|error| {
|
|
@@ -795,7 +830,7 @@ fn materialize_pi_plan_with_session(
|
|
|
795
830
|
let argv = build_pi_command_argv(PiCommandRequest {
|
|
796
831
|
executable: &chain.launch_executable,
|
|
797
832
|
extension: &wrapper,
|
|
798
|
-
model:
|
|
833
|
+
model: model.as_deref(),
|
|
799
834
|
effort: request.effort,
|
|
800
835
|
system_prompt: request.system_prompt,
|
|
801
836
|
tool_categories: request.tool_categories,
|
|
@@ -809,7 +844,7 @@ fn materialize_pi_plan_with_session(
|
|
|
809
844
|
let argv = build_pi_command_argv(PiCommandRequest {
|
|
810
845
|
executable: &chain.launch_executable,
|
|
811
846
|
extension: &wrapper,
|
|
812
|
-
model:
|
|
847
|
+
model: model.as_deref(),
|
|
813
848
|
effort: request.effort,
|
|
814
849
|
system_prompt: request.system_prompt,
|
|
815
850
|
tool_categories: request.tool_categories,
|
|
@@ -178,23 +178,13 @@ pub(super) fn spawn_agents(
|
|
|
178
178
|
effort: agent_effort,
|
|
179
179
|
};
|
|
180
180
|
let mut plan = if provider == Provider::Pi {
|
|
181
|
-
let model = command_model.ok_or_else(|| {
|
|
182
|
-
LifecycleError::RequirementUnmet(
|
|
183
|
-
"Pi launch requires an explicit qualified model".to_string(),
|
|
184
|
-
)
|
|
185
|
-
})?;
|
|
186
|
-
let effort = agent_effort.ok_or_else(|| {
|
|
187
|
-
LifecycleError::RequirementUnmet(
|
|
188
|
-
"Pi launch requires an explicit thinking effort".to_string(),
|
|
189
|
-
)
|
|
190
|
-
})?;
|
|
191
181
|
crate::lifecycle::launch::pi_mcp::materialize_pi_plan(
|
|
192
182
|
crate::lifecycle::launch::pi_mcp::PiMaterializeRequest {
|
|
193
183
|
workspace,
|
|
194
184
|
team_id: &mcp_team_id,
|
|
195
185
|
agent_id: agent_id_raw,
|
|
196
|
-
model,
|
|
197
|
-
effort,
|
|
186
|
+
model: command_model,
|
|
187
|
+
effort: agent_effort,
|
|
198
188
|
system_prompt: &system_prompt,
|
|
199
189
|
tool_categories: &resolved_tool_refs,
|
|
200
190
|
team_mcp_tools: &["send_message", "report_result"],
|
|
@@ -392,22 +392,12 @@ pub(super) fn spawn_agent_window(
|
|
|
392
392
|
.or(pi_spawn_cwd.as_deref())
|
|
393
393
|
.unwrap_or(workspace);
|
|
394
394
|
let mut plan = if provider == Provider::Pi {
|
|
395
|
-
let model = command_model.ok_or_else(|| {
|
|
396
|
-
LifecycleError::RequirementUnmet(
|
|
397
|
-
"Pi restart requires an explicit qualified model".to_string(),
|
|
398
|
-
)
|
|
399
|
-
})?;
|
|
400
|
-
let effort = restart_effort.ok_or_else(|| {
|
|
401
|
-
LifecycleError::RequirementUnmet(
|
|
402
|
-
"Pi restart requires an explicit thinking effort".to_string(),
|
|
403
|
-
)
|
|
404
|
-
})?;
|
|
405
395
|
let request = crate::lifecycle::launch::pi_mcp::PiMaterializeRequest {
|
|
406
396
|
workspace,
|
|
407
397
|
team_id: team_id.as_deref().unwrap_or(""),
|
|
408
398
|
agent_id: agent_id.as_str(),
|
|
409
|
-
model,
|
|
410
|
-
effort,
|
|
399
|
+
model: command_model,
|
|
400
|
+
effort: restart_effort,
|
|
411
401
|
system_prompt: &system_prompt,
|
|
412
402
|
tool_categories: &resolved_tool_refs,
|
|
413
403
|
team_mcp_tools: &["send_message", "report_result"],
|