@team-agent/installer 0.5.113 → 0.5.114
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 +252 -84
- package/Cargo.toml +3 -1
- package/crates/team-agent/Cargo.toml +3 -0
- package/crates/team-agent/src/cli/adapters.rs +28 -15
- package/crates/team-agent/src/cli/diagnose.rs +36 -13
- package/crates/team-agent/src/cli/emit.rs +55 -20
- package/crates/team-agent/src/cli/leader.rs +16 -0
- package/crates/team-agent/src/cli/leader_prompt.rs +261 -0
- package/crates/team-agent/src/cli/mod.rs +152 -8
- package/crates/team-agent/src/cli/models.rs +68 -18
- package/crates/team-agent/src/cli/spec.rs +7 -1
- package/crates/team-agent/src/cli/status_port/brief.rs +268 -101
- package/crates/team-agent/src/cli/tests/base.rs +88 -0
- package/crates/team-agent/src/cli/tests/status_send.rs +2 -0
- package/crates/team-agent/src/compiler.rs +66 -18
- package/crates/team-agent/src/contract_runtime/backend.rs +646 -0
- package/crates/team-agent/src/contract_runtime/config.rs +309 -0
- package/crates/team-agent/src/contract_runtime/discovery.rs +237 -0
- package/crates/team-agent/src/contract_runtime/forward.rs +229 -0
- package/crates/team-agent/src/contract_runtime/framework.rs +261 -0
- package/crates/team-agent/src/contract_runtime/mcp.rs +396 -0
- package/crates/team-agent/src/contract_runtime/mod.rs +14 -0
- package/crates/team-agent/src/contract_runtime/prompt.rs +216 -0
- package/crates/team-agent/src/contract_runtime/pump.rs +207 -0
- package/crates/team-agent/src/contract_runtime/registry.rs +30 -0
- package/crates/team-agent/src/coordinator/tick.rs +26 -8
- package/crates/team-agent/src/leader/mod.rs +1 -0
- package/crates/team-agent/src/leader/prompt.rs +906 -0
- package/crates/team-agent/src/leader/start.rs +260 -16
- package/crates/team-agent/src/leader/tests/mod.rs +2 -0
- package/crates/team-agent/src/leader/tests/prompt_contract.rs +268 -0
- package/crates/team-agent/src/leader/types.rs +6 -0
- package/crates/team-agent/src/lib.rs +4 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +472 -388
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +38 -9
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +17 -0
- package/crates/team-agent/src/lifecycle/launch.rs +39 -28
- package/crates/team-agent/src/lifecycle/types.rs +4 -1
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +37 -27
- package/crates/team-agent/src/main.rs +6 -0
- package/crates/team-agent/src/messaging/delivery.rs +50 -1
- package/crates/team-agent/src/messaging/send.rs +10 -0
- package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +131 -0
- package/crates/team-agent/src/model/spec.rs +24 -12
- package/crates/team-agent/src/state/persist.rs +73 -0
- package/package.json +4 -4
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
//! New-runtime egress into the existing shared framework business APIs. A
|
|
2
|
+
//! forwarding intent is not a legacy physical queue, nor proof of presentation.
|
|
3
|
+
use serde_json::{json, Value};
|
|
4
|
+
use team_agent_contract::contract::{session::CwdIdentity, types::ScopeId};
|
|
5
|
+
use team_agent_contract::host::{digest, digest_hex, process::resolve_cwd};
|
|
6
|
+
use team_agent_contract::orchestration::{forward::*, store::ContractStore, Error};
|
|
7
|
+
|
|
8
|
+
use crate::messaging::{self, MessageTarget, SendOptions, SendOrigin, TrustedSender};
|
|
9
|
+
use crate::model::ids::{AgentId, TaskId, TeamKey};
|
|
10
|
+
|
|
11
|
+
/// Created from the selected framework Team, never from MCP tool arguments.
|
|
12
|
+
pub struct FrameworkContext {
|
|
13
|
+
pub workspace: CwdIdentity,
|
|
14
|
+
pub team: String,
|
|
15
|
+
pub native_scope: ScopeId,
|
|
16
|
+
}
|
|
17
|
+
impl FrameworkContext {
|
|
18
|
+
pub fn route(&self) -> Result<String, Error> {
|
|
19
|
+
Ok(format!(
|
|
20
|
+
"framework-{}",
|
|
21
|
+
digest_hex(digest(&serde_json::to_vec(&(
|
|
22
|
+
&self.workspace,
|
|
23
|
+
&self.team,
|
|
24
|
+
&self.native_scope
|
|
25
|
+
))?))
|
|
26
|
+
))
|
|
27
|
+
}
|
|
28
|
+
fn validate(&self, store: &ContractStore) -> Result<(), Error> {
|
|
29
|
+
if self.team.trim().is_empty()
|
|
30
|
+
|| self.team.chars().any(char::is_control)
|
|
31
|
+
|| store.scope() != &self.native_scope
|
|
32
|
+
|| resolve_cwd(&self.workspace.path).map_err(|_| Error::Fence)? != self.workspace
|
|
33
|
+
{
|
|
34
|
+
return Err(Error::Fence);
|
|
35
|
+
}
|
|
36
|
+
let selected = crate::state::selector::resolve_active_team_readonly(
|
|
37
|
+
&self.workspace.path,
|
|
38
|
+
Some(&self.team),
|
|
39
|
+
crate::state::selector::SelectorMode::RuntimeOnly,
|
|
40
|
+
)
|
|
41
|
+
.map_err(|_| Error::Fence)?;
|
|
42
|
+
if selected.team_key != self.team
|
|
43
|
+
|| resolve_cwd(&selected.run_workspace).map_err(|_| Error::Fence)? != self.workspace
|
|
44
|
+
{
|
|
45
|
+
return Err(Error::Fence);
|
|
46
|
+
}
|
|
47
|
+
Ok(())
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
fn framework_result(result_id: &str, source: &str, envelope: &Value) -> Result<Value, Error> {
|
|
52
|
+
if envelope.get("agent_id").and_then(Value::as_str) != Some(source)
|
|
53
|
+
|| envelope
|
|
54
|
+
.get("task_id")
|
|
55
|
+
.and_then(Value::as_str)
|
|
56
|
+
.is_none_or(|task| task.trim().is_empty())
|
|
57
|
+
{
|
|
58
|
+
return Err(Error::Fence);
|
|
59
|
+
}
|
|
60
|
+
// K3's compact wire (schema 1 / completed) is not the root business envelope.
|
|
61
|
+
// Reuse the shared normalizer rather than maintaining a second alias/schema
|
|
62
|
+
// implementation or claiming that a persisted K3 result was forwarded.
|
|
63
|
+
let normalized = crate::mcp_server::normalize::normalize_report_envelope(envelope);
|
|
64
|
+
if normalized.presentation_error.is_some() {
|
|
65
|
+
return Err(Error::Invalid("result presentation"));
|
|
66
|
+
}
|
|
67
|
+
let mut value = serde_json::to_value(normalized)?;
|
|
68
|
+
value["result_id"] = json!(result_id);
|
|
69
|
+
Ok(value)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/// Reuses the common root send/result operations. No provider implementation is
|
|
73
|
+
/// selected here; the common per-recipient dispatcher remains authoritative.
|
|
74
|
+
fn forward_one(context: &FrameworkContext, intent: &ForwardIntent) -> (ForwardState, Value) {
|
|
75
|
+
match &intent.payload {
|
|
76
|
+
ForwardPayload::Message {
|
|
77
|
+
message,
|
|
78
|
+
task,
|
|
79
|
+
sender,
|
|
80
|
+
recipient,
|
|
81
|
+
content,
|
|
82
|
+
mailbox,
|
|
83
|
+
} => {
|
|
84
|
+
if sender != intent.source.seat.as_str() {
|
|
85
|
+
return (
|
|
86
|
+
ForwardState::Refused,
|
|
87
|
+
json!({"code":"sender_binding_mismatch"}),
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
let mut options = SendOptions {
|
|
91
|
+
origin: SendOrigin::Mcp,
|
|
92
|
+
task_id: task.as_ref().map(|task| TaskId::new(task.clone())),
|
|
93
|
+
route_task_id: false,
|
|
94
|
+
sender: TrustedSender::from_runtime_identity(AgentId::new(sender)),
|
|
95
|
+
team: Some(TeamKey::new(&context.team)),
|
|
96
|
+
message_id: Some(message.as_str().into()),
|
|
97
|
+
wait_visible: false,
|
|
98
|
+
block_until_delivered: false,
|
|
99
|
+
timeout: 2.0,
|
|
100
|
+
..Default::default()
|
|
101
|
+
};
|
|
102
|
+
if *mailbox {
|
|
103
|
+
options.presentation.sink = messaging::presentation::PresentationSink::Casefile;
|
|
104
|
+
}
|
|
105
|
+
match messaging::send_message(
|
|
106
|
+
&context.workspace.path,
|
|
107
|
+
&MessageTarget::Single(recipient.clone()),
|
|
108
|
+
content,
|
|
109
|
+
&options,
|
|
110
|
+
) {
|
|
111
|
+
Ok(outcome) => {
|
|
112
|
+
let accepted = outcome.message_id.as_deref() == Some(message.as_str())
|
|
113
|
+
&& matches!(
|
|
114
|
+
outcome.status,
|
|
115
|
+
messaging::DeliveryStatus::Queued
|
|
116
|
+
| messaging::DeliveryStatus::StoredOnly
|
|
117
|
+
| messaging::DeliveryStatus::Delivered
|
|
118
|
+
| messaging::DeliveryStatus::AlreadyDelivered
|
|
119
|
+
| messaging::DeliveryStatus::RetryScheduled
|
|
120
|
+
| messaging::DeliveryStatus::FallbackLog
|
|
121
|
+
);
|
|
122
|
+
let state = if accepted {
|
|
123
|
+
ForwardState::Accepted
|
|
124
|
+
} else if outcome.message_id.is_none()
|
|
125
|
+
&& matches!(outcome.status, messaging::DeliveryStatus::Refused)
|
|
126
|
+
{
|
|
127
|
+
ForwardState::Refused
|
|
128
|
+
} else {
|
|
129
|
+
ForwardState::Unknown
|
|
130
|
+
};
|
|
131
|
+
(
|
|
132
|
+
state,
|
|
133
|
+
json!({"ok":outcome.ok,"message_id":outcome.message_id,
|
|
134
|
+
"status":outcome.status,"message_status":outcome.message_status.0,
|
|
135
|
+
"reason":outcome.reason,"verification":outcome.verification}),
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
// Error alone is not proof that no destination row was written.
|
|
139
|
+
Err(_) => (
|
|
140
|
+
ForwardState::Unknown,
|
|
141
|
+
json!({"code":"framework_send_outcome_unknown"}),
|
|
142
|
+
),
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
ForwardPayload::Result {
|
|
146
|
+
result_id,
|
|
147
|
+
envelope,
|
|
148
|
+
} => {
|
|
149
|
+
let envelope = match framework_result(result_id, intent.source.seat.as_str(), envelope)
|
|
150
|
+
{
|
|
151
|
+
Ok(envelope) => envelope,
|
|
152
|
+
Err(_) => {
|
|
153
|
+
return (
|
|
154
|
+
ForwardState::Refused,
|
|
155
|
+
json!({"code":"invalid_result_envelope"}),
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
match messaging::results::report_result_for_owner_team(
|
|
160
|
+
&context.workspace.path,
|
|
161
|
+
&envelope,
|
|
162
|
+
Some(&context.team),
|
|
163
|
+
) {
|
|
164
|
+
Ok(receipt) => (ForwardState::Accepted, receipt),
|
|
165
|
+
Err(_) => (
|
|
166
|
+
ForwardState::Unknown,
|
|
167
|
+
json!({"code":"framework_result_outcome_unknown"}),
|
|
168
|
+
),
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/// The root coordinator owns this consumer. Claim commits before the delegate
|
|
175
|
+
/// is called, so no K3 transaction is held across root framework locks/writes.
|
|
176
|
+
/// InFlight/Unknown items require explicit exact-ID reconciliation, not replay.
|
|
177
|
+
pub fn drain(
|
|
178
|
+
context: &FrameworkContext,
|
|
179
|
+
store: &mut ContractStore,
|
|
180
|
+
limit: usize,
|
|
181
|
+
) -> Result<usize, Error> {
|
|
182
|
+
context.validate(store)?;
|
|
183
|
+
let route = context.route()?;
|
|
184
|
+
let pending = store.pending_forwards(limit)?;
|
|
185
|
+
let mut completed = 0;
|
|
186
|
+
for next in pending {
|
|
187
|
+
if next.source.scope != context.native_scope {
|
|
188
|
+
return Err(Error::Fence);
|
|
189
|
+
}
|
|
190
|
+
let intent = match store.claim_forward(&next.id) {
|
|
191
|
+
Ok(intent) => intent,
|
|
192
|
+
Err(Error::Conflict) => continue,
|
|
193
|
+
Err(error) => return Err(error),
|
|
194
|
+
};
|
|
195
|
+
let (state, receipt) = if intent.route != route {
|
|
196
|
+
(
|
|
197
|
+
ForwardState::Refused,
|
|
198
|
+
json!({"code":"framework_route_changed"}),
|
|
199
|
+
)
|
|
200
|
+
} else {
|
|
201
|
+
// Recheck the selected directory before every external operation.
|
|
202
|
+
context.validate(store)?;
|
|
203
|
+
forward_one(context, &intent)
|
|
204
|
+
};
|
|
205
|
+
store.finish_forward(&intent.id, state, receipt)?;
|
|
206
|
+
completed += 1;
|
|
207
|
+
}
|
|
208
|
+
Ok(completed)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
#[cfg(test)]
|
|
212
|
+
mod tests {
|
|
213
|
+
use super::*;
|
|
214
|
+
#[test]
|
|
215
|
+
fn compact_native_result_becomes_the_existing_framework_business_schema() {
|
|
216
|
+
let compact = json!({"schema_version":1,"agent_id":"worker","task_id":"msg-bound","status":"completed","summary":"5535"});
|
|
217
|
+
let result = framework_result("result-fixed", "worker", &compact).unwrap();
|
|
218
|
+
assert_eq!(result["schema_version"], "result_envelope_v1");
|
|
219
|
+
assert_eq!(result["status"], "success");
|
|
220
|
+
assert_eq!(result["task_id"], "msg-bound");
|
|
221
|
+
assert_eq!(result["summary"], "5535");
|
|
222
|
+
assert_eq!(result["result_id"], "result-fixed");
|
|
223
|
+
for field in ["changes", "tests", "risks", "artifacts", "next_actions"] {
|
|
224
|
+
assert!(result[field].is_array());
|
|
225
|
+
}
|
|
226
|
+
crate::messaging::helpers::validate_result_envelope(&result).unwrap();
|
|
227
|
+
assert!(framework_result("result-fixed", "other", &compact).is_err());
|
|
228
|
+
}
|
|
229
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
//! Shared-framework seams. A contract seat is never projected as a legacy pane:
|
|
2
|
+
//! its private socket's `%0` must not be interpreted on the workspace tmux server.
|
|
3
|
+
use super::{
|
|
4
|
+
backend::{self, Backend, BackendError},
|
|
5
|
+
config,
|
|
6
|
+
};
|
|
7
|
+
use serde_json::{json, Value};
|
|
8
|
+
use std::path::Path;
|
|
9
|
+
use team_agent_contract::contract::types::SeatId;
|
|
10
|
+
use team_agent_contract::host::process::{sample_process, ProcessState};
|
|
11
|
+
use team_agent_contract::orchestration::{
|
|
12
|
+
operator::OperatorSend,
|
|
13
|
+
store::{Outcome, SeatStatus},
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
pub fn is_contract(agent: &Value) -> bool {
|
|
17
|
+
agent
|
|
18
|
+
.get("provider")
|
|
19
|
+
.and_then(Value::as_str)
|
|
20
|
+
.is_some_and(|provider| super::registry::descriptor(provider).is_some())
|
|
21
|
+
}
|
|
22
|
+
pub fn has_contract(state: &Value) -> bool {
|
|
23
|
+
state
|
|
24
|
+
.get("agents")
|
|
25
|
+
.and_then(Value::as_object)
|
|
26
|
+
.is_some_and(|agents| agents.values().any(is_contract))
|
|
27
|
+
}
|
|
28
|
+
pub fn only_contract(state: &Value) -> bool {
|
|
29
|
+
state
|
|
30
|
+
.get("agents")
|
|
31
|
+
.and_then(Value::as_object)
|
|
32
|
+
.is_some_and(|agents| !agents.is_empty() && agents.values().all(is_contract))
|
|
33
|
+
}
|
|
34
|
+
/// Unsupported lifecycle operations must stop before the legacy provider enum
|
|
35
|
+
/// can turn an unknown native seat into its historical Codex fallback.
|
|
36
|
+
pub fn require_legacy_lifecycle(
|
|
37
|
+
workspace: &Path,
|
|
38
|
+
team: Option<&str>,
|
|
39
|
+
agent: Option<&str>,
|
|
40
|
+
operation: &str,
|
|
41
|
+
) -> Result<(), crate::cli::CliError> {
|
|
42
|
+
let Ok(selected) = crate::state::selector::resolve_active_team_readonly(
|
|
43
|
+
workspace,
|
|
44
|
+
team,
|
|
45
|
+
crate::state::selector::SelectorMode::RuntimeOnly,
|
|
46
|
+
) else {
|
|
47
|
+
return Ok(());
|
|
48
|
+
};
|
|
49
|
+
let selected_native = match agent {
|
|
50
|
+
Some(id) => selected
|
|
51
|
+
.state
|
|
52
|
+
.get("agents")
|
|
53
|
+
.and_then(|agents| agents.get(id))
|
|
54
|
+
.is_some_and(is_contract),
|
|
55
|
+
None => has_contract(&selected.state),
|
|
56
|
+
};
|
|
57
|
+
let configured_native = selected
|
|
58
|
+
.state
|
|
59
|
+
.get("team_dir")
|
|
60
|
+
.and_then(Value::as_str)
|
|
61
|
+
.and_then(|directory| {
|
|
62
|
+
team_config(
|
|
63
|
+
&selected.run_workspace,
|
|
64
|
+
Path::new(directory),
|
|
65
|
+
&selected.team_key,
|
|
66
|
+
)
|
|
67
|
+
.ok()
|
|
68
|
+
.flatten()
|
|
69
|
+
})
|
|
70
|
+
.is_some_and(|config| match agent {
|
|
71
|
+
Some(id) => config.roles.iter().any(|role| role.id.as_str() == id),
|
|
72
|
+
None => !config.roles.is_empty(),
|
|
73
|
+
});
|
|
74
|
+
if selected_native || configured_native {
|
|
75
|
+
return Err(crate::cli::CliError::Runtime(format!("contract_capability_unverified: {operation} is not admitted for Kiro; no legacy fallback or fresh replacement was performed")));
|
|
76
|
+
}
|
|
77
|
+
Ok(())
|
|
78
|
+
}
|
|
79
|
+
pub fn require_legacy_role(
|
|
80
|
+
path: Option<&Path>,
|
|
81
|
+
provider: Option<&str>,
|
|
82
|
+
) -> Result<(), crate::cli::CliError> {
|
|
83
|
+
let declared = path
|
|
84
|
+
.and_then(|path| std::fs::read_to_string(path).ok())
|
|
85
|
+
.and_then(|raw| {
|
|
86
|
+
crate::compiler::split_front_matter(&raw)
|
|
87
|
+
.0
|
|
88
|
+
.get("provider")
|
|
89
|
+
.and_then(crate::model::yaml::Value::as_str)
|
|
90
|
+
.map(str::to_owned)
|
|
91
|
+
});
|
|
92
|
+
if provider
|
|
93
|
+
.or(declared.as_deref())
|
|
94
|
+
.is_some_and(super::registry::recognizes)
|
|
95
|
+
{
|
|
96
|
+
return Err(crate::cli::CliError::Runtime("contract_capability_unverified: mutable Kiro role lifecycle is not admitted; use initial quick-start; no role file was changed".into()));
|
|
97
|
+
}
|
|
98
|
+
Ok(())
|
|
99
|
+
}
|
|
100
|
+
pub fn team_config(
|
|
101
|
+
workspace: &Path,
|
|
102
|
+
directory: &Path,
|
|
103
|
+
team: &str,
|
|
104
|
+
) -> Result<Option<config::TeamConfig>, config::ConfigError> {
|
|
105
|
+
config::read_team(&crate::cli::QuickStartArgs {
|
|
106
|
+
workspace: workspace.into(),
|
|
107
|
+
agents_dir: directory.into(),
|
|
108
|
+
name: None,
|
|
109
|
+
team_id: Some(team.into()),
|
|
110
|
+
yes: false,
|
|
111
|
+
json: false,
|
|
112
|
+
detail: false,
|
|
113
|
+
backend: Some("tmux".into()),
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
pub fn start_role(
|
|
117
|
+
workspace: &Path,
|
|
118
|
+
directory: &Path,
|
|
119
|
+
team: &str,
|
|
120
|
+
id: &str,
|
|
121
|
+
) -> Result<(), crate::lifecycle::LifecycleError> {
|
|
122
|
+
let error = |error: String| crate::lifecycle::LifecycleError::Provider(error);
|
|
123
|
+
let config = team_config(workspace, directory, team)
|
|
124
|
+
.map_err(|e| error(e.to_string()))?
|
|
125
|
+
.ok_or_else(|| error("contract role no longer configured".into()))?;
|
|
126
|
+
let role = config
|
|
127
|
+
.roles
|
|
128
|
+
.iter()
|
|
129
|
+
.find(|role| role.id.as_str() == id)
|
|
130
|
+
.ok_or_else(|| error("contract role not found".into()))?;
|
|
131
|
+
let (_, record) =
|
|
132
|
+
backend::start(workspace, team, role, &config.members).map_err(|e| error(e.to_string()))?;
|
|
133
|
+
if record.outcome != Outcome::Committed {
|
|
134
|
+
return Err(error(format!(
|
|
135
|
+
"native startup {:?}: {}",
|
|
136
|
+
record.outcome,
|
|
137
|
+
record.failure.unwrap_or_default()
|
|
138
|
+
)));
|
|
139
|
+
}
|
|
140
|
+
Ok(())
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
pub fn project_seat(workspace: &Path, team: &str, id: &str) -> Result<Value, BackendError> {
|
|
144
|
+
let backend = Backend::open_readonly(workspace, team)?;
|
|
145
|
+
let seat = backend
|
|
146
|
+
.store
|
|
147
|
+
.seat(&SeatId::new(id)?)?
|
|
148
|
+
.ok_or(BackendError::Metadata)?;
|
|
149
|
+
let role = backend::read_role(&backend.binding, &seat.identity.seat)?;
|
|
150
|
+
let alive = seat
|
|
151
|
+
.physical
|
|
152
|
+
.as_ref()
|
|
153
|
+
.is_some_and(|target| sample_process(&target.process) == ProcessState::Alive);
|
|
154
|
+
let status = if seat.status == SeatStatus::Stopped {
|
|
155
|
+
"stopped"
|
|
156
|
+
} else if seat.status != SeatStatus::Unknown && alive {
|
|
157
|
+
"running"
|
|
158
|
+
} else {
|
|
159
|
+
"unknown"
|
|
160
|
+
};
|
|
161
|
+
Ok(
|
|
162
|
+
json!({"agent_id":id,"provider":seat.provider,"status":status,"model":role.model,"effort":role.effort,
|
|
163
|
+
"contract_prompt_sha256":team_agent_contract::host::digest_hex(role.prompt.sha256),
|
|
164
|
+
"runtime_family":"contract","contract_scope":seat.identity.scope,"contract_instance":seat.identity.instance,
|
|
165
|
+
"generation":seat.identity.generation,"owner_team_id":team,"team_key":team,
|
|
166
|
+
"session_id":seat.session.map(|session| session.native_session),
|
|
167
|
+
"native_endpoint":seat.endpoint,"native_pane":seat.pane,
|
|
168
|
+
"pane_id":null,"window":null,"session_name":null,
|
|
169
|
+
"mcp_binding":"unverified","bootstrap_used":seat.bootstrap_used}),
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
pub fn enqueue(
|
|
174
|
+
workspace: &Path,
|
|
175
|
+
state: &Value,
|
|
176
|
+
recipient: &str,
|
|
177
|
+
content: &str,
|
|
178
|
+
options: &crate::messaging::SendOptions,
|
|
179
|
+
) -> Result<crate::messaging::DeliveryOutcome, crate::messaging::MessagingError> {
|
|
180
|
+
use crate::messaging::{DeliveryOutcome, DeliveryStatus, MessagingError};
|
|
181
|
+
let error = |error: String| MessagingError::Validation(error);
|
|
182
|
+
let team = crate::state::projection::team_state_key(state);
|
|
183
|
+
let mut backend = Backend::open(workspace, &team).map_err(|e| error(e.to_string()))?;
|
|
184
|
+
let request = OperatorSend {
|
|
185
|
+
recipient: SeatId::new(recipient).map_err(|e| error(e.to_string()))?,
|
|
186
|
+
content: content.into(),
|
|
187
|
+
task: options.task_id.as_ref().map(|task| task.as_str().into()),
|
|
188
|
+
message: options
|
|
189
|
+
.message_id
|
|
190
|
+
.clone()
|
|
191
|
+
.map(team_agent_contract::contract::types::MessageId::new)
|
|
192
|
+
.transpose()
|
|
193
|
+
.map_err(|e| error(e.to_string()))?,
|
|
194
|
+
mailbox: options.presentation.sink
|
|
195
|
+
== crate::messaging::presentation::PresentationSink::Casefile,
|
|
196
|
+
};
|
|
197
|
+
let sender = SeatId::new(options.sender.as_str()).map_err(|e| error(e.to_string()))?;
|
|
198
|
+
let receipt = backend
|
|
199
|
+
.store
|
|
200
|
+
.send_from_framework(&request, &sender)
|
|
201
|
+
.map_err(|e| error(e.to_string()))?;
|
|
202
|
+
// Queued is durable admission only. The coordinator consumes this outbox;
|
|
203
|
+
// CLI send never performs its own paste or repeats an uncertain attempt.
|
|
204
|
+
Ok(DeliveryOutcome {
|
|
205
|
+
ok: true,
|
|
206
|
+
status: if receipt.mailbox {
|
|
207
|
+
DeliveryStatus::StoredOnly
|
|
208
|
+
} else {
|
|
209
|
+
DeliveryStatus::Queued
|
|
210
|
+
},
|
|
211
|
+
message_status: crate::messaging::helpers::MessageStatusShadow(
|
|
212
|
+
if receipt.mailbox {
|
|
213
|
+
"stored_only"
|
|
214
|
+
} else {
|
|
215
|
+
"accepted"
|
|
216
|
+
}
|
|
217
|
+
.into(),
|
|
218
|
+
),
|
|
219
|
+
message_id: Some(receipt.message.as_str().into()),
|
|
220
|
+
verification: Some(
|
|
221
|
+
json!({"runtime":"contract","task_id":receipt.task,
|
|
222
|
+
"target":receipt.target,"native_submission":"not_observed"})
|
|
223
|
+
.to_string(),
|
|
224
|
+
),
|
|
225
|
+
stage: None,
|
|
226
|
+
reason: None,
|
|
227
|
+
channel: None,
|
|
228
|
+
ack_forced_off: false,
|
|
229
|
+
turn_verification: None,
|
|
230
|
+
})
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
pub fn stop_selected(workspace: &Path, state: &Value) -> Result<Vec<Value>, BackendError> {
|
|
234
|
+
let team = crate::state::projection::team_state_key(state);
|
|
235
|
+
let mut receipts = Vec::new();
|
|
236
|
+
if let Some(agents) = state.get("agents").and_then(Value::as_object) {
|
|
237
|
+
for (id, agent) in agents.iter().filter(|(_, agent)| is_contract(agent)) {
|
|
238
|
+
let backend = Backend::open(workspace, &team)?;
|
|
239
|
+
let seat = backend
|
|
240
|
+
.store
|
|
241
|
+
.seat(&SeatId::new(id)?)?
|
|
242
|
+
.ok_or(BackendError::Metadata)?;
|
|
243
|
+
if seat.status == SeatStatus::Stopped {
|
|
244
|
+
receipts.push(json!({"agent_id":id,"status":"already_stopped"}));
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
let _ = agent;
|
|
248
|
+
let receipt = backend::stop(workspace, &team, &SeatId::new(id)?)?;
|
|
249
|
+
if receipt.outcome != Outcome::Committed {
|
|
250
|
+
return Err(BackendError::Shutdown {
|
|
251
|
+
operation: receipt.id,
|
|
252
|
+
phase: receipt.phase,
|
|
253
|
+
outcome: receipt.outcome,
|
|
254
|
+
failure: receipt.failure,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
receipts.push(json!({"agent_id":id,"status":"stopped","operation":receipt.id}));
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
Ok(receipts)
|
|
261
|
+
}
|