@celestia-island/plana-types 0.1.0
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.toml +38 -0
- package/bindings/FileAnchor.ts +3 -0
- package/bindings/engine.ts +277 -0
- package/bindings/enums.ts +31 -0
- package/bindings/httpTypes.ts +197 -0
- package/bindings/index.ts +47 -0
- package/bindings/mcp/aporia.ts +61 -0
- package/bindings/mcp/eleos.ts +18 -0
- package/bindings/mcp/epieikeia.ts +48 -0
- package/bindings/mcp/haplotes.ts +47 -0
- package/bindings/mcp/hubris.ts +41 -0
- package/bindings/mcp/index.ts +13 -0
- package/bindings/mcp/kalos.ts +47 -0
- package/bindings/mcp/neikos.ts +76 -0
- package/bindings/mcp/orexis.ts +64 -0
- package/bindings/mcp/philia.ts +57 -0
- package/bindings/mcp/polemos.ts +55 -0
- package/bindings/mcp/skemma.ts +58 -0
- package/bindings/mcp/skopeo.ts +56 -0
- package/bindings/mcp/webAutomation.ts +31 -0
- package/bindings/model.ts +240 -0
- package/bindings/package.json +16 -0
- package/bindings/region.ts +3 -0
- package/bindings/serde_json/JsonValue.ts +3 -0
- package/bindings/ws/agentLifecycle.ts +41 -0
- package/bindings/ws/auth.ts +15 -0
- package/bindings/ws/baseMessages.ts +7 -0
- package/bindings/ws/bridgeNetwork.ts +71 -0
- package/bindings/ws/core.ts +51 -0
- package/bindings/ws/fileBrowsing.ts +57 -0
- package/bindings/ws/handshake.ts +23 -0
- package/bindings/ws/industrial.ts +72 -0
- package/bindings/ws/knowledgeBase.ts +10 -0
- package/bindings/ws/layer2.ts +25 -0
- package/bindings/ws/llmProvider.ts +74 -0
- package/bindings/ws/logs.ts +11 -0
- package/bindings/ws/malkuth.ts +62 -0
- package/bindings/ws/noa.ts +17 -0
- package/bindings/ws/stateSync.ts +19 -0
- package/bindings/ws/systemUi.ts +5 -0
- package/bindings/ws/tasks.ts +6 -0
- package/bindings/ws/views.ts +163 -0
- package/bindings/ws/workspace.ts +11 -0
- package/bindings/ws/yolo.ts +32 -0
- package/examples/schema_dump.rs +51 -0
- package/package.json +6 -0
- package/pnpm-workspace.yaml +2 -0
- package/src/engine.rs +602 -0
- package/src/enums.rs +334 -0
- package/src/external_mcp.rs +132 -0
- package/src/http.rs +1077 -0
- package/src/identity.rs +160 -0
- package/src/lib.rs +1215 -0
- package/src/malkuth.rs +145 -0
- package/src/mcp/aporia.rs +272 -0
- package/src/mcp/eleos.rs +210 -0
- package/src/mcp/epieikeia.rs +194 -0
- package/src/mcp/haplotes.rs +310 -0
- package/src/mcp/hubris.rs +377 -0
- package/src/mcp/kalos.rs +251 -0
- package/src/mcp/mod.rs +23 -0
- package/src/mcp/neikos.rs +533 -0
- package/src/mcp/orexis.rs +493 -0
- package/src/mcp/philia.rs +267 -0
- package/src/mcp/polemos.rs +241 -0
- package/src/mcp/skemma.rs +442 -0
- package/src/mcp/skopeo.rs +282 -0
- package/src/mcp/web_automation.rs +122 -0
- package/src/model.rs +421 -0
- package/src/protocol/base_messages.rs +125 -0
- package/src/protocol/handshake.rs +364 -0
- package/src/protocol/jsonrpc.rs +888 -0
- package/src/protocol/mod.rs +10 -0
- package/src/rbac.rs +786 -0
- package/src/region.rs +362 -0
- package/src/tracing_helpers.rs +9 -0
- package/src/ws/agent/agent_lifecycle.rs +221 -0
- package/src/ws/agent/layer2.rs +126 -0
- package/src/ws/agent/mod.rs +9 -0
- package/src/ws/agent/state_sync.rs +111 -0
- package/src/ws/agent/tasks.rs +43 -0
- package/src/ws/agent/yolo.rs +161 -0
- package/src/ws/mod.rs +10 -0
- package/src/ws/services/auth.rs +99 -0
- package/src/ws/services/industrial.rs +647 -0
- package/src/ws/services/knowledge_base.rs +59 -0
- package/src/ws/services/llm_provider.rs +371 -0
- package/src/ws/services/mod.rs +7 -0
- package/src/ws/ui/bridge_network.rs +96 -0
- package/src/ws/ui/file_browsing.rs +88 -0
- package/src/ws/ui/logs.rs +55 -0
- package/src/ws/ui/mod.rs +11 -0
- package/src/ws/ui/noa.rs +105 -0
- package/src/ws/ui/system_ui.rs +27 -0
- package/src/ws/ui/views.rs +159 -0
- package/src/ws/ui/workspace.rs +73 -0
package/src/lib.rs
ADDED
|
@@ -0,0 +1,1215 @@
|
|
|
1
|
+
//! Shared protocol types for the entelecheia multi-agent platform.
|
|
2
|
+
//!
|
|
3
|
+
//! Every type in this crate MUST be:
|
|
4
|
+
//! - Defined in entelecheia (canonical source of truth)
|
|
5
|
+
//! - Consumed by shittim-chest (core, mock_scepter, or webui)
|
|
6
|
+
//!
|
|
7
|
+
//! If a type is not paired on both sides, it does not belong here.
|
|
8
|
+
|
|
9
|
+
// ── Module tree ─────────────────────────────────────────────
|
|
10
|
+
// Foundational shared enums are defined directly in this file (below). The
|
|
11
|
+
// other type groups live under a small set of domain folders:
|
|
12
|
+
// protocol/ — JSON-RPC envelope, base messages, handshake (WS transport)
|
|
13
|
+
// ws/ — SyncMessage variant params (agent / ui / services sub-groups)
|
|
14
|
+
// mcp/ — per-agent MCP tool I/O structs
|
|
15
|
+
// and a few single-file modules at the root (enums, http, model,
|
|
16
|
+
// external_mcp). The glob re-exports at the bottom keep every type reachable
|
|
17
|
+
// at the crate root (`arona::TypeName`).
|
|
18
|
+
pub mod engine;
|
|
19
|
+
pub mod enums;
|
|
20
|
+
pub mod external_mcp;
|
|
21
|
+
pub mod http;
|
|
22
|
+
pub mod identity;
|
|
23
|
+
pub mod malkuth;
|
|
24
|
+
pub mod mcp;
|
|
25
|
+
pub mod model;
|
|
26
|
+
pub mod protocol;
|
|
27
|
+
pub mod rbac;
|
|
28
|
+
pub mod region;
|
|
29
|
+
pub mod ws;
|
|
30
|
+
|
|
31
|
+
pub use http::{BackendKind, HealthResponse, NetworkInfo, ServiceStatus};
|
|
32
|
+
|
|
33
|
+
#[cfg(feature = "tracing-helpers")]
|
|
34
|
+
pub mod tracing_helpers;
|
|
35
|
+
|
|
36
|
+
use schemars::JsonSchema;
|
|
37
|
+
use serde::{Deserialize, Serialize};
|
|
38
|
+
use ts_rs::TS;
|
|
39
|
+
|
|
40
|
+
/// Protocol version advertised by the platform.
|
|
41
|
+
pub const PROTOCOL_VERSION: &str = "1.0.0";
|
|
42
|
+
/// Default report type when none is specified.
|
|
43
|
+
pub const DEFAULT_REPORT_TYPE: &str = "general";
|
|
44
|
+
|
|
45
|
+
/// Serde default helper for `bool` fields that default to `true`.
|
|
46
|
+
pub(crate) fn default_true() -> bool {
|
|
47
|
+
true
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ═══════════════════════════════════════════════════════════════
|
|
51
|
+
// Core enums
|
|
52
|
+
// ═══════════════════════════════════════════════════════════════
|
|
53
|
+
|
|
54
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, TS, JsonSchema)]
|
|
55
|
+
#[ts(export, export_to = "ws/core.ts")]
|
|
56
|
+
pub enum Agent {
|
|
57
|
+
HapLotes,
|
|
58
|
+
SkoPeo,
|
|
59
|
+
HubRis,
|
|
60
|
+
KaLos,
|
|
61
|
+
NeiKos,
|
|
62
|
+
SkeMma,
|
|
63
|
+
ApoRia,
|
|
64
|
+
EleOs,
|
|
65
|
+
EpieiKeia,
|
|
66
|
+
OreXis,
|
|
67
|
+
PhiLia,
|
|
68
|
+
PoleMos,
|
|
69
|
+
WebAutomation,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
impl Agent {
|
|
73
|
+
pub fn all() -> &'static [Agent] {
|
|
74
|
+
&[
|
|
75
|
+
Agent::HapLotes,
|
|
76
|
+
Agent::SkoPeo,
|
|
77
|
+
Agent::HubRis,
|
|
78
|
+
Agent::KaLos,
|
|
79
|
+
Agent::NeiKos,
|
|
80
|
+
Agent::SkeMma,
|
|
81
|
+
Agent::ApoRia,
|
|
82
|
+
Agent::EleOs,
|
|
83
|
+
Agent::EpieiKeia,
|
|
84
|
+
Agent::OreXis,
|
|
85
|
+
Agent::PhiLia,
|
|
86
|
+
Agent::PoleMos,
|
|
87
|
+
Agent::WebAutomation,
|
|
88
|
+
]
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
#[derive(JsonSchema, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, TS)]
|
|
93
|
+
#[ts(export, export_to = "ws/core.ts")]
|
|
94
|
+
pub struct AgentBadge(pub String);
|
|
95
|
+
|
|
96
|
+
#[derive(JsonSchema, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
|
|
97
|
+
#[ts(export, export_to = "ws/core.ts")]
|
|
98
|
+
pub enum AgentStatus {
|
|
99
|
+
Initializing,
|
|
100
|
+
Online,
|
|
101
|
+
Busy,
|
|
102
|
+
Offline,
|
|
103
|
+
Error,
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS, JsonSchema)]
|
|
107
|
+
#[ts(export, export_to = "ws/core.ts")]
|
|
108
|
+
pub enum WorkStatus {
|
|
109
|
+
Thinking,
|
|
110
|
+
StreamingResponse,
|
|
111
|
+
Executing { skill_name: String },
|
|
112
|
+
Retrying { retry_count: u32, max_retries: u32 },
|
|
113
|
+
Nudging,
|
|
114
|
+
Completed,
|
|
115
|
+
RequestFailed,
|
|
116
|
+
Failed,
|
|
117
|
+
ToolLoopTerminated,
|
|
118
|
+
CallingTool,
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
#[derive(JsonSchema, Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, TS)]
|
|
122
|
+
#[ts(export, export_to = "ws/core.ts")]
|
|
123
|
+
pub enum RequestState {
|
|
124
|
+
#[default]
|
|
125
|
+
Idle,
|
|
126
|
+
Waiting,
|
|
127
|
+
Streaming,
|
|
128
|
+
Retrying,
|
|
129
|
+
WaitingTool,
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
#[derive(JsonSchema, Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, TS)]
|
|
133
|
+
#[ts(export, export_to = "ws/core.ts")]
|
|
134
|
+
pub enum CompletionOutcome {
|
|
135
|
+
#[default]
|
|
136
|
+
None,
|
|
137
|
+
Reported,
|
|
138
|
+
Failed,
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
#[derive(JsonSchema, Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, TS)]
|
|
142
|
+
#[ts(export, export_to = "ws/core.ts")]
|
|
143
|
+
pub enum ModelTier {
|
|
144
|
+
Deep,
|
|
145
|
+
Normal,
|
|
146
|
+
Basic,
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS, JsonSchema)]
|
|
150
|
+
#[ts(export, export_to = "ws/core.ts")]
|
|
151
|
+
pub enum SkillStage {
|
|
152
|
+
Started(String),
|
|
153
|
+
Done(String),
|
|
154
|
+
Complete(String),
|
|
155
|
+
Failed(String),
|
|
156
|
+
ToolCall(String),
|
|
157
|
+
Retrying(String, usize, usize, Option<RetryReason>),
|
|
158
|
+
TryingModel(String, String),
|
|
159
|
+
ModelFailed(String, String, String),
|
|
160
|
+
Nudging(String),
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS, JsonSchema)]
|
|
164
|
+
#[ts(export, export_to = "ws/core.ts")]
|
|
165
|
+
pub enum RetryReason {
|
|
166
|
+
EmptyOutput,
|
|
167
|
+
ReportNotCaptured,
|
|
168
|
+
LlmError { message: String },
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS, JsonSchema)]
|
|
172
|
+
#[ts(export, export_to = "ws/core.ts")]
|
|
173
|
+
#[serde(rename_all = "snake_case")]
|
|
174
|
+
pub enum ReportType {
|
|
175
|
+
Query,
|
|
176
|
+
Human,
|
|
177
|
+
Reply,
|
|
178
|
+
SkillTerminal,
|
|
179
|
+
SkillStep,
|
|
180
|
+
NextActionFallback,
|
|
181
|
+
ChainMaxDepth,
|
|
182
|
+
ChainCycle,
|
|
183
|
+
SkillFailed,
|
|
184
|
+
SkillEmptyOutput,
|
|
185
|
+
SkillMissingReport,
|
|
186
|
+
Error,
|
|
187
|
+
System,
|
|
188
|
+
/// Emitted when the server begins processing a user message. Acts as a
|
|
189
|
+
/// transient placeholder — the real `Reply`/`Error` report replaces it
|
|
190
|
+
/// once `task_decompose` (or a downstream skill) finishes. Tui renders
|
|
191
|
+
/// this as a status indicator rather than a resident card.
|
|
192
|
+
Pending,
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
impl ReportType {
|
|
196
|
+
pub fn is_query(&self) -> bool {
|
|
197
|
+
matches!(self, Self::Query)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
pub fn is_error(&self) -> bool {
|
|
201
|
+
matches!(
|
|
202
|
+
self,
|
|
203
|
+
Self::Error
|
|
204
|
+
| Self::ChainMaxDepth
|
|
205
|
+
| Self::ChainCycle
|
|
206
|
+
| Self::SkillFailed
|
|
207
|
+
| Self::SkillEmptyOutput
|
|
208
|
+
| Self::SkillMissingReport
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
pub fn is_pending(&self) -> bool {
|
|
213
|
+
matches!(self, Self::Pending)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
pub fn is_terminal(&self) -> bool {
|
|
217
|
+
matches!(
|
|
218
|
+
self,
|
|
219
|
+
Self::Reply
|
|
220
|
+
| Self::SkillTerminal
|
|
221
|
+
| Self::Error
|
|
222
|
+
| Self::System
|
|
223
|
+
| Self::NextActionFallback
|
|
224
|
+
)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/// Selection semantics for an inquiry (`report_type: "query"`) report's
|
|
229
|
+
/// `preset_options`. Defaults to `Single` when omitted.
|
|
230
|
+
#[derive(JsonSchema, Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, TS)]
|
|
231
|
+
#[ts(export, export_to = "ws/core.ts")]
|
|
232
|
+
#[serde(rename_all = "snake_case")]
|
|
233
|
+
pub enum ReportSelection {
|
|
234
|
+
#[default]
|
|
235
|
+
Single,
|
|
236
|
+
Multiple,
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
#[derive(JsonSchema, Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, TS)]
|
|
240
|
+
#[ts(export, export_to = "ws/core.ts")]
|
|
241
|
+
pub enum StreamChunkKind {
|
|
242
|
+
#[default]
|
|
243
|
+
Text,
|
|
244
|
+
Thinking,
|
|
245
|
+
DeepThinking,
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
#[derive(JsonSchema, Debug, Clone, Serialize, Deserialize, PartialEq, TS)]
|
|
249
|
+
#[ts(export, export_to = "ws/core.ts")]
|
|
250
|
+
pub enum StreamSegment {
|
|
251
|
+
Text {
|
|
252
|
+
text: String,
|
|
253
|
+
#[serde(default)]
|
|
254
|
+
#[ts(optional)]
|
|
255
|
+
#[ts(type = "string")]
|
|
256
|
+
message_id: Option<uuid::Uuid>,
|
|
257
|
+
},
|
|
258
|
+
Thinking {
|
|
259
|
+
text: String,
|
|
260
|
+
#[serde(default)]
|
|
261
|
+
#[ts(optional)]
|
|
262
|
+
#[ts(type = "string")]
|
|
263
|
+
message_id: Option<uuid::Uuid>,
|
|
264
|
+
},
|
|
265
|
+
DeepThinking {
|
|
266
|
+
text: String,
|
|
267
|
+
#[serde(default)]
|
|
268
|
+
#[ts(optional)]
|
|
269
|
+
#[ts(type = "string")]
|
|
270
|
+
message_id: Option<uuid::Uuid>,
|
|
271
|
+
},
|
|
272
|
+
McpCall {
|
|
273
|
+
tool_name: String,
|
|
274
|
+
call_id: String,
|
|
275
|
+
#[ts(type = "unknown")]
|
|
276
|
+
params: serde_json::Value,
|
|
277
|
+
#[serde(default)]
|
|
278
|
+
#[ts(optional)]
|
|
279
|
+
agent_type: Option<String>,
|
|
280
|
+
#[serde(default)]
|
|
281
|
+
#[ts(optional)]
|
|
282
|
+
#[ts(type = "string")]
|
|
283
|
+
message_id: Option<uuid::Uuid>,
|
|
284
|
+
},
|
|
285
|
+
McpResult {
|
|
286
|
+
tool_name: String,
|
|
287
|
+
call_id: String,
|
|
288
|
+
success: bool,
|
|
289
|
+
#[ts(type = "unknown")]
|
|
290
|
+
data: serde_json::Value,
|
|
291
|
+
#[serde(default)]
|
|
292
|
+
#[ts(optional)]
|
|
293
|
+
duration_ms: Option<u64>,
|
|
294
|
+
#[serde(default)]
|
|
295
|
+
#[ts(optional)]
|
|
296
|
+
agent_type: Option<String>,
|
|
297
|
+
#[serde(default)]
|
|
298
|
+
#[ts(optional)]
|
|
299
|
+
#[ts(type = "string")]
|
|
300
|
+
message_id: Option<uuid::Uuid>,
|
|
301
|
+
},
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
#[derive(JsonSchema, Debug, Clone, Serialize, Deserialize, TS)]
|
|
305
|
+
#[ts(export, export_to = "ws/core.ts")]
|
|
306
|
+
pub struct LlmStream {
|
|
307
|
+
#[serde(default)]
|
|
308
|
+
pub segments: Vec<StreamSegment>,
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
#[derive(JsonSchema, Debug, Clone, Serialize, Deserialize, TS)]
|
|
312
|
+
#[ts(export, export_to = "ws/core.ts")]
|
|
313
|
+
pub struct RouteInfo {
|
|
314
|
+
pub direction: String,
|
|
315
|
+
pub target: String,
|
|
316
|
+
#[serde(default)]
|
|
317
|
+
#[ts(optional)]
|
|
318
|
+
pub target_token: Option<String>,
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
#[derive(
|
|
322
|
+
JsonSchema, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, TS, thiserror::Error,
|
|
323
|
+
)]
|
|
324
|
+
#[ts(export, export_to = "ws/core.ts")]
|
|
325
|
+
#[serde(rename_all = "snake_case")]
|
|
326
|
+
pub enum AgentErrorCode {
|
|
327
|
+
#[error("model has no providers")]
|
|
328
|
+
ModelNoProviders,
|
|
329
|
+
#[error("model has no models")]
|
|
330
|
+
ModelNoModels,
|
|
331
|
+
#[error("model tier mismatch")]
|
|
332
|
+
ModelTierMismatch,
|
|
333
|
+
#[error("all models excluded")]
|
|
334
|
+
ModelAllExcluded,
|
|
335
|
+
#[error("model env incomplete")]
|
|
336
|
+
ModelEnvIncomplete,
|
|
337
|
+
#[error("model selection retry exhausted")]
|
|
338
|
+
ModelSelectionRetryExhausted,
|
|
339
|
+
#[error("LLM call failed")]
|
|
340
|
+
LlmCallFailed,
|
|
341
|
+
#[error("LLM empty response")]
|
|
342
|
+
LlmEmptyResponse,
|
|
343
|
+
#[error("LLM rate limited")]
|
|
344
|
+
LlmRateLimited,
|
|
345
|
+
#[error("LLM auth failed")]
|
|
346
|
+
LlmAuthFailed,
|
|
347
|
+
#[error("LLM timeout")]
|
|
348
|
+
LlmTimeout,
|
|
349
|
+
#[error("cosmos no connection")]
|
|
350
|
+
CosmosNoConnection,
|
|
351
|
+
#[error("cosmos tool failed")]
|
|
352
|
+
CosmosToolFailed,
|
|
353
|
+
#[error("cosmos local unavailable")]
|
|
354
|
+
CosmosLocalUnavailable,
|
|
355
|
+
#[error("chain max depth")]
|
|
356
|
+
ChainMaxDepth,
|
|
357
|
+
#[error("chain cycle detected")]
|
|
358
|
+
ChainCycle,
|
|
359
|
+
#[error("chain failed")]
|
|
360
|
+
ChainFailed,
|
|
361
|
+
#[error("skill failed")]
|
|
362
|
+
SkillFailed,
|
|
363
|
+
#[error("skill empty output")]
|
|
364
|
+
SkillEmptyOutput,
|
|
365
|
+
#[error("skill missing report")]
|
|
366
|
+
SkillMissingReport,
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
impl AgentErrorCode {
|
|
370
|
+
pub fn is_llm_error(&self) -> bool {
|
|
371
|
+
matches!(
|
|
372
|
+
self,
|
|
373
|
+
Self::LlmCallFailed
|
|
374
|
+
| Self::LlmEmptyResponse
|
|
375
|
+
| Self::LlmRateLimited
|
|
376
|
+
| Self::LlmAuthFailed
|
|
377
|
+
| Self::LlmTimeout
|
|
378
|
+
)
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
pub fn is_cosmos_error(&self) -> bool {
|
|
382
|
+
matches!(
|
|
383
|
+
self,
|
|
384
|
+
Self::CosmosNoConnection | Self::CosmosToolFailed | Self::CosmosLocalUnavailable
|
|
385
|
+
)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
pub fn is_chain_error(&self) -> bool {
|
|
389
|
+
matches!(
|
|
390
|
+
self,
|
|
391
|
+
Self::ChainMaxDepth | Self::ChainCycle | Self::ChainFailed
|
|
392
|
+
)
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
pub fn is_skill_error(&self) -> bool {
|
|
396
|
+
matches!(
|
|
397
|
+
self,
|
|
398
|
+
Self::SkillFailed | Self::SkillEmptyOutput | Self::SkillMissingReport
|
|
399
|
+
)
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
pub fn is_model_selection_error(&self) -> bool {
|
|
403
|
+
matches!(
|
|
404
|
+
self,
|
|
405
|
+
Self::ModelNoProviders
|
|
406
|
+
| Self::ModelNoModels
|
|
407
|
+
| Self::ModelTierMismatch
|
|
408
|
+
| Self::ModelAllExcluded
|
|
409
|
+
| Self::ModelEnvIncomplete
|
|
410
|
+
| Self::ModelSelectionRetryExhausted
|
|
411
|
+
)
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
#[derive(JsonSchema, Debug, Clone, Serialize, Deserialize, TS)]
|
|
416
|
+
#[ts(export, export_to = "ws/core.ts")]
|
|
417
|
+
pub struct StructuredAgentError {
|
|
418
|
+
pub code: AgentErrorCode,
|
|
419
|
+
#[serde(default)]
|
|
420
|
+
#[ts(optional)]
|
|
421
|
+
pub detail: Option<String>,
|
|
422
|
+
#[serde(default)]
|
|
423
|
+
pub context: std::collections::HashMap<String, String>,
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
#[derive(JsonSchema, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, TS)]
|
|
427
|
+
#[ts(export, export_to = "ws/core.ts")]
|
|
428
|
+
#[serde(rename_all = "snake_case")]
|
|
429
|
+
pub enum TaskStatus {
|
|
430
|
+
NotStarted,
|
|
431
|
+
InProgress,
|
|
432
|
+
Paused,
|
|
433
|
+
Completed,
|
|
434
|
+
Failed,
|
|
435
|
+
Warning,
|
|
436
|
+
Waiting { deadline: String, handle: String },
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
#[derive(JsonSchema, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
|
|
440
|
+
#[ts(export, export_to = "ws/core.ts")]
|
|
441
|
+
#[serde(rename_all = "snake_case")]
|
|
442
|
+
pub enum ContainerStatus {
|
|
443
|
+
Created,
|
|
444
|
+
Running,
|
|
445
|
+
Paused,
|
|
446
|
+
Restarting,
|
|
447
|
+
Removing,
|
|
448
|
+
Exited,
|
|
449
|
+
Dead,
|
|
450
|
+
Unknown,
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
#[derive(JsonSchema, Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, TS)]
|
|
454
|
+
#[ts(export, export_to = "ws/core.ts")]
|
|
455
|
+
pub enum PeriodType {
|
|
456
|
+
Hour5,
|
|
457
|
+
Day7,
|
|
458
|
+
Month1,
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
#[derive(JsonSchema, Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, TS)]
|
|
462
|
+
#[ts(export, export_to = "ws/core.ts")]
|
|
463
|
+
pub enum KnowledgeBaseStatus {
|
|
464
|
+
#[default]
|
|
465
|
+
Uninitialized,
|
|
466
|
+
Indexing,
|
|
467
|
+
Ready,
|
|
468
|
+
Error,
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
#[derive(JsonSchema, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
|
|
472
|
+
#[ts(export, export_to = "ws/core.ts")]
|
|
473
|
+
pub enum EmbeddingModel {
|
|
474
|
+
OpenAiSmall,
|
|
475
|
+
OpenAiLarge,
|
|
476
|
+
OpenAiAda,
|
|
477
|
+
Custom,
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
#[derive(JsonSchema, Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, TS)]
|
|
481
|
+
#[ts(export, export_to = "ws/core.ts")]
|
|
482
|
+
#[serde(rename_all = "snake_case")]
|
|
483
|
+
pub enum YoloTaskTier {
|
|
484
|
+
Realtime,
|
|
485
|
+
Periodic,
|
|
486
|
+
Daily,
|
|
487
|
+
Strategic,
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// ═══════════════════════════════════════════════════════════════
|
|
491
|
+
// Root re-exports
|
|
492
|
+
//
|
|
493
|
+
// The foundational enums above stay defined here. The domain structs live in
|
|
494
|
+
// the folder modules (`protocol/`, `ws/{agent,ui,services}/`) but are
|
|
495
|
+
// re-exported at the crate root so the public surface is unchanged —
|
|
496
|
+
// `arona::TuiAgentInfo`, `arona::HandshakeAckParams`, etc. all still resolve.
|
|
497
|
+
//
|
|
498
|
+
// `jsonrpc` is re-exported *as a module* (not globbed) so its deep path
|
|
499
|
+
// `arona::jsonrpc::*` keeps working for the many consumers that use it.
|
|
500
|
+
// ═══════════════════════════════════════════════════════════════
|
|
501
|
+
|
|
502
|
+
// protocol/ — transport core
|
|
503
|
+
pub use protocol::base_messages::*;
|
|
504
|
+
pub use protocol::handshake::*;
|
|
505
|
+
pub use protocol::jsonrpc;
|
|
506
|
+
|
|
507
|
+
// enums/ — foundational shared enums (ConnectionType, Agent, WorkStatus, etc.)
|
|
508
|
+
pub use enums::*;
|
|
509
|
+
|
|
510
|
+
// malkuth/ — supervision protocol types (restart authorization gate)
|
|
511
|
+
pub use malkuth::*;
|
|
512
|
+
// region/ — regional compliance policy types
|
|
513
|
+
pub use region::*;
|
|
514
|
+
|
|
515
|
+
// model/ — unified model management (re-export key types to crate root
|
|
516
|
+
// for ergonomic access: `arona::ModelCapability` not `arona::model::…`)
|
|
517
|
+
pub use model::{GenerationTier, HardwareRequirements, ModelCapability};
|
|
518
|
+
|
|
519
|
+
// ws/ — SyncMessage variant params (types at crate root)
|
|
520
|
+
pub use ws::agent::{agent_lifecycle::*, layer2::*, state_sync::*, tasks::*, yolo::*};
|
|
521
|
+
pub use ws::services::{auth::*, industrial::*, knowledge_base::*, llm_provider::*};
|
|
522
|
+
pub use ws::ui::{
|
|
523
|
+
bridge_network::*, file_browsing::*, logs::*, noa::*, system_ui::*, views::*, workspace::*,
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
#[cfg(test)]
|
|
527
|
+
mod tests {
|
|
528
|
+
use super::*;
|
|
529
|
+
use serde_json::json;
|
|
530
|
+
|
|
531
|
+
// ── Agent enum ─────────────────────────────────────────────────
|
|
532
|
+
|
|
533
|
+
#[test]
|
|
534
|
+
fn agent_all_returns_thirteen_unique_variants() {
|
|
535
|
+
let all = Agent::all();
|
|
536
|
+
assert_eq!(
|
|
537
|
+
all.len(),
|
|
538
|
+
13,
|
|
539
|
+
"Agent::all() must return exactly 13 variants"
|
|
540
|
+
);
|
|
541
|
+
// Verify uniqueness.
|
|
542
|
+
let mut seen = std::collections::HashSet::new();
|
|
543
|
+
for a in all {
|
|
544
|
+
let s = format!("{a:?}");
|
|
545
|
+
assert!(seen.insert(s.clone()), "duplicate agent variant: {s}");
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
#[test]
|
|
550
|
+
fn agent_serde_round_trip_each_variant() {
|
|
551
|
+
for agent in Agent::all() {
|
|
552
|
+
let s = serde_json::to_string(agent).unwrap();
|
|
553
|
+
let back: Agent = serde_json::from_str(&s).unwrap();
|
|
554
|
+
assert_eq!(back, *agent);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
#[test]
|
|
559
|
+
fn agent_serializes_as_pascal_case() {
|
|
560
|
+
// No #[serde(rename_all)] on Agent → PascalCase variant names.
|
|
561
|
+
assert_eq!(
|
|
562
|
+
serde_json::to_string(&Agent::HapLotes).unwrap(),
|
|
563
|
+
r#""HapLotes""#
|
|
564
|
+
);
|
|
565
|
+
assert_eq!(
|
|
566
|
+
serde_json::to_string(&Agent::WebAutomation).unwrap(),
|
|
567
|
+
r#""WebAutomation""#
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// ── AgentBadge newtype ─────────────────────────────────────────
|
|
572
|
+
|
|
573
|
+
#[test]
|
|
574
|
+
fn agent_badge_round_trip() {
|
|
575
|
+
let badge = AgentBadge("haplotes-01".into());
|
|
576
|
+
let v = serde_json::to_value(&badge).unwrap();
|
|
577
|
+
assert_eq!(v, "haplotes-01");
|
|
578
|
+
let back: AgentBadge = serde_json::from_value(v).unwrap();
|
|
579
|
+
assert_eq!(back.0, "haplotes-01");
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// ── AgentStatus ────────────────────────────────────────────────
|
|
583
|
+
|
|
584
|
+
#[test]
|
|
585
|
+
fn agent_status_round_trip_all_variants() {
|
|
586
|
+
for s in [
|
|
587
|
+
AgentStatus::Initializing,
|
|
588
|
+
AgentStatus::Online,
|
|
589
|
+
AgentStatus::Busy,
|
|
590
|
+
AgentStatus::Offline,
|
|
591
|
+
AgentStatus::Error,
|
|
592
|
+
] {
|
|
593
|
+
let json = serde_json::to_string(&s).unwrap();
|
|
594
|
+
let back: AgentStatus = serde_json::from_str(&json).unwrap();
|
|
595
|
+
assert_eq!(back, s);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// ── RequestState default ───────────────────────────────────────
|
|
600
|
+
|
|
601
|
+
#[test]
|
|
602
|
+
fn request_state_default_is_idle() {
|
|
603
|
+
assert_eq!(RequestState::default(), RequestState::Idle);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
#[test]
|
|
607
|
+
fn completion_outcome_default_is_none() {
|
|
608
|
+
assert_eq!(CompletionOutcome::default(), CompletionOutcome::None);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// ── ReportType classification ──────────────────────────────────
|
|
612
|
+
|
|
613
|
+
#[test]
|
|
614
|
+
fn report_type_is_query() {
|
|
615
|
+
assert!(ReportType::Query.is_query());
|
|
616
|
+
assert!(!ReportType::Reply.is_query());
|
|
617
|
+
assert!(!ReportType::Error.is_query());
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
#[test]
|
|
621
|
+
fn report_type_is_error_covers_all_error_variants() {
|
|
622
|
+
let error_variants = [
|
|
623
|
+
ReportType::Error,
|
|
624
|
+
ReportType::ChainMaxDepth,
|
|
625
|
+
ReportType::ChainCycle,
|
|
626
|
+
ReportType::SkillFailed,
|
|
627
|
+
ReportType::SkillEmptyOutput,
|
|
628
|
+
ReportType::SkillMissingReport,
|
|
629
|
+
];
|
|
630
|
+
for v in &error_variants {
|
|
631
|
+
assert!(v.is_error(), "{v:?} should be classified as error");
|
|
632
|
+
}
|
|
633
|
+
// Non-error variants.
|
|
634
|
+
assert!(!ReportType::Query.is_error());
|
|
635
|
+
assert!(!ReportType::Reply.is_error());
|
|
636
|
+
assert!(!ReportType::Human.is_error());
|
|
637
|
+
assert!(!ReportType::System.is_error());
|
|
638
|
+
assert!(!ReportType::Pending.is_error());
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
#[test]
|
|
642
|
+
fn report_type_is_pending() {
|
|
643
|
+
assert!(ReportType::Pending.is_pending());
|
|
644
|
+
assert!(!ReportType::Reply.is_pending());
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
#[test]
|
|
648
|
+
fn report_type_is_terminal() {
|
|
649
|
+
let terminal = [
|
|
650
|
+
ReportType::Reply,
|
|
651
|
+
ReportType::SkillTerminal,
|
|
652
|
+
ReportType::Error,
|
|
653
|
+
ReportType::System,
|
|
654
|
+
ReportType::NextActionFallback,
|
|
655
|
+
];
|
|
656
|
+
for v in &terminal {
|
|
657
|
+
assert!(v.is_terminal(), "{v:?} should be terminal");
|
|
658
|
+
}
|
|
659
|
+
// Non-terminal.
|
|
660
|
+
assert!(!ReportType::Query.is_terminal());
|
|
661
|
+
assert!(!ReportType::Pending.is_terminal());
|
|
662
|
+
assert!(!ReportType::SkillStep.is_terminal());
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
#[test]
|
|
666
|
+
fn report_type_serde_uses_snake_case() {
|
|
667
|
+
// ReportType has #[serde(rename_all = "snake_case")].
|
|
668
|
+
assert_eq!(
|
|
669
|
+
serde_json::to_string(&ReportType::SkillTerminal).unwrap(),
|
|
670
|
+
r#""skill_terminal""#
|
|
671
|
+
);
|
|
672
|
+
assert_eq!(
|
|
673
|
+
serde_json::to_string(&ReportType::NextActionFallback).unwrap(),
|
|
674
|
+
r#""next_action_fallback""#
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// ── AgentErrorCode classification ──────────────────────────────
|
|
679
|
+
|
|
680
|
+
#[test]
|
|
681
|
+
fn agent_error_code_is_llm_error() {
|
|
682
|
+
let llm_errors = [
|
|
683
|
+
AgentErrorCode::LlmCallFailed,
|
|
684
|
+
AgentErrorCode::LlmEmptyResponse,
|
|
685
|
+
AgentErrorCode::LlmRateLimited,
|
|
686
|
+
AgentErrorCode::LlmAuthFailed,
|
|
687
|
+
AgentErrorCode::LlmTimeout,
|
|
688
|
+
];
|
|
689
|
+
for e in &llm_errors {
|
|
690
|
+
assert!(e.is_llm_error(), "{e:?} should be LLM error");
|
|
691
|
+
}
|
|
692
|
+
assert!(!AgentErrorCode::CosmosNoConnection.is_llm_error());
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
#[test]
|
|
696
|
+
fn agent_error_code_is_cosmos_error() {
|
|
697
|
+
let cosmos = [
|
|
698
|
+
AgentErrorCode::CosmosNoConnection,
|
|
699
|
+
AgentErrorCode::CosmosToolFailed,
|
|
700
|
+
AgentErrorCode::CosmosLocalUnavailable,
|
|
701
|
+
];
|
|
702
|
+
for e in &cosmos {
|
|
703
|
+
assert!(e.is_cosmos_error(), "{e:?} should be cosmos error");
|
|
704
|
+
}
|
|
705
|
+
assert!(!AgentErrorCode::LlmTimeout.is_cosmos_error());
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
#[test]
|
|
709
|
+
fn agent_error_code_is_chain_error() {
|
|
710
|
+
assert!(AgentErrorCode::ChainMaxDepth.is_chain_error());
|
|
711
|
+
assert!(AgentErrorCode::ChainCycle.is_chain_error());
|
|
712
|
+
assert!(AgentErrorCode::ChainFailed.is_chain_error());
|
|
713
|
+
assert!(!AgentErrorCode::SkillFailed.is_chain_error());
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
#[test]
|
|
717
|
+
fn agent_error_code_is_skill_error() {
|
|
718
|
+
assert!(AgentErrorCode::SkillFailed.is_skill_error());
|
|
719
|
+
assert!(AgentErrorCode::SkillEmptyOutput.is_skill_error());
|
|
720
|
+
assert!(AgentErrorCode::SkillMissingReport.is_skill_error());
|
|
721
|
+
assert!(!AgentErrorCode::ChainFailed.is_skill_error());
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
#[test]
|
|
725
|
+
fn agent_error_code_is_model_selection_error() {
|
|
726
|
+
let model_errors = [
|
|
727
|
+
AgentErrorCode::ModelNoProviders,
|
|
728
|
+
AgentErrorCode::ModelNoModels,
|
|
729
|
+
AgentErrorCode::ModelTierMismatch,
|
|
730
|
+
AgentErrorCode::ModelAllExcluded,
|
|
731
|
+
AgentErrorCode::ModelEnvIncomplete,
|
|
732
|
+
AgentErrorCode::ModelSelectionRetryExhausted,
|
|
733
|
+
];
|
|
734
|
+
for e in &model_errors {
|
|
735
|
+
assert!(
|
|
736
|
+
e.is_model_selection_error(),
|
|
737
|
+
"{e:?} should be model selection error"
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
assert!(!AgentErrorCode::LlmTimeout.is_model_selection_error());
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
#[test]
|
|
744
|
+
fn agent_error_code_categories_are_mutually_exclusive() {
|
|
745
|
+
// Every variant belongs to at most one category.
|
|
746
|
+
for code in [
|
|
747
|
+
AgentErrorCode::ModelNoProviders,
|
|
748
|
+
AgentErrorCode::ModelNoModels,
|
|
749
|
+
AgentErrorCode::ModelTierMismatch,
|
|
750
|
+
AgentErrorCode::ModelAllExcluded,
|
|
751
|
+
AgentErrorCode::ModelEnvIncomplete,
|
|
752
|
+
AgentErrorCode::ModelSelectionRetryExhausted,
|
|
753
|
+
AgentErrorCode::LlmCallFailed,
|
|
754
|
+
AgentErrorCode::LlmEmptyResponse,
|
|
755
|
+
AgentErrorCode::LlmRateLimited,
|
|
756
|
+
AgentErrorCode::LlmAuthFailed,
|
|
757
|
+
AgentErrorCode::LlmTimeout,
|
|
758
|
+
AgentErrorCode::CosmosNoConnection,
|
|
759
|
+
AgentErrorCode::CosmosToolFailed,
|
|
760
|
+
AgentErrorCode::CosmosLocalUnavailable,
|
|
761
|
+
AgentErrorCode::ChainMaxDepth,
|
|
762
|
+
AgentErrorCode::ChainCycle,
|
|
763
|
+
AgentErrorCode::ChainFailed,
|
|
764
|
+
AgentErrorCode::SkillFailed,
|
|
765
|
+
AgentErrorCode::SkillEmptyOutput,
|
|
766
|
+
AgentErrorCode::SkillMissingReport,
|
|
767
|
+
] {
|
|
768
|
+
let count = [
|
|
769
|
+
code.is_llm_error(),
|
|
770
|
+
code.is_cosmos_error(),
|
|
771
|
+
code.is_chain_error(),
|
|
772
|
+
code.is_skill_error(),
|
|
773
|
+
code.is_model_selection_error(),
|
|
774
|
+
]
|
|
775
|
+
.iter()
|
|
776
|
+
.filter(|&&b| b)
|
|
777
|
+
.count();
|
|
778
|
+
assert_eq!(
|
|
779
|
+
count, 1,
|
|
780
|
+
"{code:?} belongs to {count} categories, expected 1"
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
#[test]
|
|
786
|
+
fn agent_error_code_thiserror_display() {
|
|
787
|
+
// Each variant has a non-empty error message via thiserror.
|
|
788
|
+
assert_eq!(AgentErrorCode::LlmCallFailed.to_string(), "LLM call failed");
|
|
789
|
+
assert_eq!(
|
|
790
|
+
AgentErrorCode::ModelNoProviders.to_string(),
|
|
791
|
+
"model has no providers"
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// ── StreamSegment variants ─────────────────────────────────────
|
|
796
|
+
|
|
797
|
+
#[test]
|
|
798
|
+
fn stream_segment_text_round_trip() {
|
|
799
|
+
let seg = StreamSegment::Text {
|
|
800
|
+
text: "hello".into(),
|
|
801
|
+
message_id: None,
|
|
802
|
+
};
|
|
803
|
+
let v = serde_json::to_value(&seg).unwrap();
|
|
804
|
+
// Externally tagged enum: {"Text": {"text": "hello", "message_id": null}}
|
|
805
|
+
assert_eq!(v["Text"]["text"], "hello");
|
|
806
|
+
let back: StreamSegment = serde_json::from_value(v).unwrap();
|
|
807
|
+
match back {
|
|
808
|
+
StreamSegment::Text { text, .. } => assert_eq!(text, "hello"),
|
|
809
|
+
other => panic!("expected Text, got {other:?}"),
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
#[test]
|
|
814
|
+
fn stream_segment_mcp_call_round_trip() {
|
|
815
|
+
let seg = StreamSegment::McpCall {
|
|
816
|
+
tool_name: "kalos.file_read".into(),
|
|
817
|
+
call_id: "call-1".into(),
|
|
818
|
+
params: json!({"path": "/etc/hosts"}),
|
|
819
|
+
agent_type: None,
|
|
820
|
+
message_id: None,
|
|
821
|
+
};
|
|
822
|
+
let v = serde_json::to_value(&seg).unwrap();
|
|
823
|
+
let back: StreamSegment = serde_json::from_value(v).unwrap();
|
|
824
|
+
match back {
|
|
825
|
+
StreamSegment::McpCall {
|
|
826
|
+
tool_name, call_id, ..
|
|
827
|
+
} => {
|
|
828
|
+
assert_eq!(tool_name, "kalos.file_read");
|
|
829
|
+
assert_eq!(call_id, "call-1");
|
|
830
|
+
}
|
|
831
|
+
other => panic!("expected McpCall, got {other:?}"),
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
#[test]
|
|
836
|
+
fn stream_segment_mcp_result_round_trip() {
|
|
837
|
+
let seg = StreamSegment::McpResult {
|
|
838
|
+
tool_name: "kalos.file_read".into(),
|
|
839
|
+
call_id: "call-1".into(),
|
|
840
|
+
success: true,
|
|
841
|
+
data: json!({"content": "file data"}),
|
|
842
|
+
duration_ms: Some(42),
|
|
843
|
+
agent_type: Some("KaLos".into()),
|
|
844
|
+
message_id: None,
|
|
845
|
+
};
|
|
846
|
+
let v = serde_json::to_value(&seg).unwrap();
|
|
847
|
+
let back: StreamSegment = serde_json::from_value(v).unwrap();
|
|
848
|
+
match back {
|
|
849
|
+
StreamSegment::McpResult {
|
|
850
|
+
success,
|
|
851
|
+
duration_ms,
|
|
852
|
+
..
|
|
853
|
+
} => {
|
|
854
|
+
assert!(success);
|
|
855
|
+
assert_eq!(duration_ms, Some(42));
|
|
856
|
+
}
|
|
857
|
+
other => panic!("expected McpResult, got {other:?}"),
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// ── StructuredAgentError ───────────────────────────────────────
|
|
862
|
+
|
|
863
|
+
#[test]
|
|
864
|
+
fn structured_agent_error_round_trip() {
|
|
865
|
+
let mut ctx = std::collections::HashMap::new();
|
|
866
|
+
ctx.insert("agent".into(), "haplotes".into());
|
|
867
|
+
let e = StructuredAgentError {
|
|
868
|
+
code: AgentErrorCode::LlmTimeout,
|
|
869
|
+
detail: Some("provider timed out after 30s".into()),
|
|
870
|
+
context: ctx,
|
|
871
|
+
};
|
|
872
|
+
let v = serde_json::to_value(&e).unwrap();
|
|
873
|
+
assert_eq!(v["code"], "llm_timeout");
|
|
874
|
+
assert_eq!(v["detail"], "provider timed out after 30s");
|
|
875
|
+
assert_eq!(v["context"]["agent"], "haplotes");
|
|
876
|
+
let back: StructuredAgentError = serde_json::from_value(v).unwrap();
|
|
877
|
+
assert_eq!(back.code, AgentErrorCode::LlmTimeout);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
#[test]
|
|
881
|
+
fn structured_agent_error_minimal() {
|
|
882
|
+
let e = StructuredAgentError {
|
|
883
|
+
code: AgentErrorCode::ChainCycle,
|
|
884
|
+
detail: None,
|
|
885
|
+
context: std::collections::HashMap::new(),
|
|
886
|
+
};
|
|
887
|
+
let v = serde_json::to_value(&e).unwrap();
|
|
888
|
+
// detail is #[ts(optional)] without skip → null.
|
|
889
|
+
assert_eq!(v["detail"], serde_json::Value::Null);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// ── ContainerStatus ────────────────────────────────────────────
|
|
893
|
+
|
|
894
|
+
#[test]
|
|
895
|
+
fn container_status_round_trip_all_variants() {
|
|
896
|
+
for s in [
|
|
897
|
+
ContainerStatus::Created,
|
|
898
|
+
ContainerStatus::Running,
|
|
899
|
+
ContainerStatus::Paused,
|
|
900
|
+
ContainerStatus::Restarting,
|
|
901
|
+
ContainerStatus::Removing,
|
|
902
|
+
ContainerStatus::Exited,
|
|
903
|
+
ContainerStatus::Dead,
|
|
904
|
+
ContainerStatus::Unknown,
|
|
905
|
+
] {
|
|
906
|
+
let json = serde_json::to_string(&s).unwrap();
|
|
907
|
+
let back: ContainerStatus = serde_json::from_str(&json).unwrap();
|
|
908
|
+
assert_eq!(back, s);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// ── KnowledgeBaseStatus default ────────────────────────────────
|
|
913
|
+
|
|
914
|
+
#[test]
|
|
915
|
+
fn knowledge_base_status_default_is_uninitialized() {
|
|
916
|
+
assert_eq!(
|
|
917
|
+
KnowledgeBaseStatus::default(),
|
|
918
|
+
KnowledgeBaseStatus::Uninitialized
|
|
919
|
+
);
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// ── YoloTaskTier serde ─────────────────────────────────────────
|
|
923
|
+
|
|
924
|
+
#[test]
|
|
925
|
+
fn yolo_task_tier_serde_snake_case() {
|
|
926
|
+
assert_eq!(
|
|
927
|
+
serde_json::to_string(&YoloTaskTier::Realtime).unwrap(),
|
|
928
|
+
r#""realtime""#
|
|
929
|
+
);
|
|
930
|
+
assert_eq!(
|
|
931
|
+
serde_json::to_string(&YoloTaskTier::Strategic).unwrap(),
|
|
932
|
+
r#""strategic""#
|
|
933
|
+
);
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// ── ReportSelection / StreamChunkKind defaults ────────────────
|
|
937
|
+
|
|
938
|
+
#[test]
|
|
939
|
+
fn report_selection_default_is_single() {
|
|
940
|
+
assert_eq!(ReportSelection::default(), ReportSelection::Single);
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
#[test]
|
|
944
|
+
fn stream_chunk_kind_default_is_text() {
|
|
945
|
+
assert_eq!(StreamChunkKind::default(), StreamChunkKind::Text);
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// ── Constants ──────────────────────────────────────────────────
|
|
949
|
+
|
|
950
|
+
#[test]
|
|
951
|
+
fn protocol_version_is_one_point_zero() {
|
|
952
|
+
assert_eq!(PROTOCOL_VERSION, "1.0.0");
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
#[test]
|
|
956
|
+
fn default_report_type_is_general() {
|
|
957
|
+
assert_eq!(DEFAULT_REPORT_TYPE, "general");
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// ── CEP engine protocol ────────────────────────────────────────
|
|
961
|
+
|
|
962
|
+
#[test]
|
|
963
|
+
fn engine_protocol_version_is_three() {
|
|
964
|
+
assert_eq!(engine::ENGINE_PROTOCOL_VERSION, 3);
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
#[test]
|
|
968
|
+
fn engine_binary_max_frame_is_256k() {
|
|
969
|
+
assert_eq!(engine::ENGINE_BINARY_MAX_FRAME_BYTES, 256 * 1024);
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
#[test]
|
|
973
|
+
fn engine_binary_receive_timeout_is_sixty_seconds() {
|
|
974
|
+
assert_eq!(engine::ENGINE_BINARY_RECEIVE_TIMEOUT_SECS, 60);
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
#[test]
|
|
978
|
+
fn engine_handshake_round_trips_with_optional_fields() {
|
|
979
|
+
let params = engine::EngineHandshakeParams {
|
|
980
|
+
token: None,
|
|
981
|
+
engine: engine::EngineIdentity {
|
|
982
|
+
name: "llamacpp".into(),
|
|
983
|
+
version: "b1234".into(),
|
|
984
|
+
language: Some("cpp".into()),
|
|
985
|
+
vendor: Some("ggml-org".into()),
|
|
986
|
+
},
|
|
987
|
+
capabilities: engine::EngineCapabilities {
|
|
988
|
+
streaming: true,
|
|
989
|
+
embeddings: false,
|
|
990
|
+
max_context_length: 8192,
|
|
991
|
+
hardware: vec![engine::EngineGpuInfo {
|
|
992
|
+
name: "RTX 5880".into(),
|
|
993
|
+
vram_gb: 47,
|
|
994
|
+
}],
|
|
995
|
+
input_modalities: vec![
|
|
996
|
+
engine::EngineModality::Text,
|
|
997
|
+
engine::EngineModality::Audio,
|
|
998
|
+
engine::EngineModality::Sensor,
|
|
999
|
+
],
|
|
1000
|
+
output_modalities: vec![engine::EngineModality::Text],
|
|
1001
|
+
content_types: vec!["audio/wav".into(), "application/json".into()],
|
|
1002
|
+
methods: vec!["audio.transcribe".into(), "signal.classify".into()],
|
|
1003
|
+
},
|
|
1004
|
+
};
|
|
1005
|
+
let json = serde_json::to_value(¶ms).unwrap();
|
|
1006
|
+
assert_eq!(json["engine"]["language"], "cpp");
|
|
1007
|
+
assert!(json.get("token").is_none());
|
|
1008
|
+
let back: engine::EngineHandshakeParams = serde_json::from_value(json).unwrap();
|
|
1009
|
+
assert_eq!(back.capabilities.max_context_length, 8192);
|
|
1010
|
+
assert_eq!(back.capabilities.hardware[0].vram_gb, 47);
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
#[test]
|
|
1014
|
+
fn engine_capabilities_defaults_are_lenient() {
|
|
1015
|
+
let parsed: engine::EngineCapabilities =
|
|
1016
|
+
serde_json::from_str(r#"{"streaming":false}"#).unwrap();
|
|
1017
|
+
assert!(!parsed.streaming);
|
|
1018
|
+
assert!(!parsed.embeddings);
|
|
1019
|
+
assert_eq!(parsed.max_context_length, 128_000);
|
|
1020
|
+
assert!(parsed.hardware.is_empty());
|
|
1021
|
+
// v2: modality/capability fields default to empty (text-only).
|
|
1022
|
+
assert!(parsed.input_modalities.is_empty());
|
|
1023
|
+
assert!(parsed.output_modalities.is_empty());
|
|
1024
|
+
assert!(parsed.content_types.is_empty());
|
|
1025
|
+
assert!(parsed.methods.is_empty());
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
#[test]
|
|
1029
|
+
fn engine_chat_chunk_serializes_stream_shape() {
|
|
1030
|
+
let chunk = engine::EngineChatChunk {
|
|
1031
|
+
stream_id: "s1".into(),
|
|
1032
|
+
token: "hello".into(),
|
|
1033
|
+
is_complete: false,
|
|
1034
|
+
usage: None,
|
|
1035
|
+
};
|
|
1036
|
+
let json = serde_json::to_value(&chunk).unwrap();
|
|
1037
|
+
assert_eq!(json["stream_id"], "s1");
|
|
1038
|
+
assert_eq!(json["token"], "hello");
|
|
1039
|
+
assert_eq!(json["is_complete"], false);
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// ── CEP v2: multimodal content + generic invocation ────────────
|
|
1043
|
+
|
|
1044
|
+
#[test]
|
|
1045
|
+
fn engine_message_carries_multimodal_parts() {
|
|
1046
|
+
let msg = engine::EngineMessage {
|
|
1047
|
+
role: "user".into(),
|
|
1048
|
+
content: vec![
|
|
1049
|
+
engine::EngineContentPart::text("transcribe this"),
|
|
1050
|
+
engine::EngineContentPart::base64("audio/wav", "UklGRg=="),
|
|
1051
|
+
engine::EngineContentPart::json(
|
|
1052
|
+
"application/json",
|
|
1053
|
+
json!({
|
|
1054
|
+
"sensor": "accel-x", "samples": [1.0, -2.0, 3.5],
|
|
1055
|
+
}),
|
|
1056
|
+
),
|
|
1057
|
+
],
|
|
1058
|
+
};
|
|
1059
|
+
let json = serde_json::to_value(&msg).unwrap();
|
|
1060
|
+
let parts = json["content"].as_array().unwrap();
|
|
1061
|
+
assert_eq!(parts.len(), 3);
|
|
1062
|
+
assert_eq!(parts[0]["mime"], "text/plain");
|
|
1063
|
+
assert_eq!(parts[1]["encoding"], "base64");
|
|
1064
|
+
assert_eq!(parts[2]["data"]["sensor"], "accel-x");
|
|
1065
|
+
let back: engine::EngineMessage = serde_json::from_value(json).unwrap();
|
|
1066
|
+
assert_eq!(back.content[1].mime, "audio/wav");
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
#[test]
|
|
1070
|
+
fn engine_binary_frame_part_needs_no_data() {
|
|
1071
|
+
let part = engine::EngineContentPart::binary_frame("audio/wav");
|
|
1072
|
+
let json = serde_json::to_value(&part).unwrap();
|
|
1073
|
+
assert_eq!(json["encoding"], "binary-frame");
|
|
1074
|
+
assert!(json.get("data").is_none());
|
|
1075
|
+
let back: engine::EngineContentPart = serde_json::from_value(json).unwrap();
|
|
1076
|
+
assert_eq!(back.mime, "audio/wav");
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
#[test]
|
|
1080
|
+
fn engine_handshake_result_optional_capabilities_round_trips() {
|
|
1081
|
+
let caps = engine::EngineCapabilities {
|
|
1082
|
+
streaming: true,
|
|
1083
|
+
embeddings: true,
|
|
1084
|
+
max_context_length: 32_000,
|
|
1085
|
+
hardware: vec![],
|
|
1086
|
+
input_modalities: vec![engine::EngineModality::Audio],
|
|
1087
|
+
output_modalities: vec![engine::EngineModality::Text],
|
|
1088
|
+
content_types: vec!["audio/wav".into()],
|
|
1089
|
+
methods: vec!["audio.transcribe".into()],
|
|
1090
|
+
};
|
|
1091
|
+
let result = engine::EngineHandshakeResult {
|
|
1092
|
+
ok: true,
|
|
1093
|
+
error: None,
|
|
1094
|
+
protocol_version: 2,
|
|
1095
|
+
capabilities: Some(caps),
|
|
1096
|
+
};
|
|
1097
|
+
let json = serde_json::to_value(&result).unwrap();
|
|
1098
|
+
assert_eq!(json["capabilities"]["input_modalities"], json!(["Audio"]));
|
|
1099
|
+
assert_eq!(json["protocol_version"], 2);
|
|
1100
|
+
let back: engine::EngineHandshakeResult = serde_json::from_value(json).unwrap();
|
|
1101
|
+
assert_eq!(
|
|
1102
|
+
back.capabilities.unwrap().methods,
|
|
1103
|
+
vec!["audio.transcribe".to_string()]
|
|
1104
|
+
);
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
#[test]
|
|
1108
|
+
fn engine_handshake_result_without_capabilities_parses() {
|
|
1109
|
+
// Server that doesn't declare capabilities (old engines).
|
|
1110
|
+
let back: engine::EngineHandshakeResult =
|
|
1111
|
+
serde_json::from_str(r#"{"ok":true,"protocol_version":1}"#).unwrap();
|
|
1112
|
+
assert!(back.capabilities.is_none());
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
#[test]
|
|
1116
|
+
fn engine_invoke_round_trips_free_form_payload() {
|
|
1117
|
+
let params = engine::EngineInvokeParams {
|
|
1118
|
+
method: "signal.filter".into(),
|
|
1119
|
+
params: json!({
|
|
1120
|
+
"filter": "lowpass", "cutoff_hz": 800,
|
|
1121
|
+
"channel": 3,
|
|
1122
|
+
}),
|
|
1123
|
+
messages: Some(vec![engine::EngineMessage::text(
|
|
1124
|
+
"user",
|
|
1125
|
+
"keep the pump vibration band",
|
|
1126
|
+
)]),
|
|
1127
|
+
stream_id: None,
|
|
1128
|
+
};
|
|
1129
|
+
let json = serde_json::to_value(¶ms).unwrap();
|
|
1130
|
+
assert_eq!(json["method"], "signal.filter");
|
|
1131
|
+
assert_eq!(json["params"]["cutoff_hz"], 800);
|
|
1132
|
+
let back: engine::EngineInvokeParams = serde_json::from_value(json).unwrap();
|
|
1133
|
+
assert_eq!(
|
|
1134
|
+
back.messages.as_ref().unwrap()[0].content[0].mime,
|
|
1135
|
+
"text/plain"
|
|
1136
|
+
);
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
#[test]
|
|
1140
|
+
fn engine_stream_chunk_describes_audio_block() {
|
|
1141
|
+
let chunk = engine::EngineStreamChunk {
|
|
1142
|
+
stream_id: "s2".into(),
|
|
1143
|
+
mime: "audio/wav".into(),
|
|
1144
|
+
encoding: "base64".into(),
|
|
1145
|
+
data: Some(json!("UklGRgAAAA==")),
|
|
1146
|
+
shape: Some(vec![1, 16000]),
|
|
1147
|
+
is_complete: true,
|
|
1148
|
+
usage: None,
|
|
1149
|
+
};
|
|
1150
|
+
let json = serde_json::to_value(&chunk).unwrap();
|
|
1151
|
+
assert_eq!(json["mime"], "audio/wav");
|
|
1152
|
+
assert_eq!(json["shape"], json!([1, 16000]));
|
|
1153
|
+
assert_eq!(json["is_complete"], true);
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
#[test]
|
|
1157
|
+
fn engine_binary_start_announce_round_trips() {
|
|
1158
|
+
let params = engine::EngineBinaryStartParams {
|
|
1159
|
+
transfer_id: "t-42".into(),
|
|
1160
|
+
mime: "audio/wav".into(),
|
|
1161
|
+
total_bytes: 1_000_000,
|
|
1162
|
+
chunk_count: 4,
|
|
1163
|
+
checksum: Some("deadbeef".into()),
|
|
1164
|
+
stream_id: None,
|
|
1165
|
+
};
|
|
1166
|
+
let json = serde_json::to_value(¶ms).unwrap();
|
|
1167
|
+
assert_eq!(json["mime"], "audio/wav");
|
|
1168
|
+
assert_eq!(json["chunk_count"], 4);
|
|
1169
|
+
assert_eq!(json["checksum"], "deadbeef");
|
|
1170
|
+
let back: engine::EngineBinaryStartParams = serde_json::from_value(json).unwrap();
|
|
1171
|
+
assert_eq!(back.total_bytes, 1_000_000);
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
#[test]
|
|
1175
|
+
fn engine_binary_end_validates_receipt() {
|
|
1176
|
+
let end = engine::EngineBinaryEndParams {
|
|
1177
|
+
transfer_id: "t-42".into(),
|
|
1178
|
+
bytes_received: 1_000_000,
|
|
1179
|
+
checksum_ok: Some(true),
|
|
1180
|
+
};
|
|
1181
|
+
let json = serde_json::to_value(&end).unwrap();
|
|
1182
|
+
assert_eq!(json["bytes_received"], 1_000_000);
|
|
1183
|
+
assert_eq!(json["checksum_ok"], true);
|
|
1184
|
+
let abort = engine::EngineBinaryAbortParams {
|
|
1185
|
+
transfer_id: "t-42".into(),
|
|
1186
|
+
reason: "client cancelled".into(),
|
|
1187
|
+
};
|
|
1188
|
+
let j2 = serde_json::to_value(&abort).unwrap();
|
|
1189
|
+
assert_eq!(j2["reason"], "client cancelled");
|
|
1190
|
+
// checksum_ok is optional on the wire.
|
|
1191
|
+
let bare: engine::EngineBinaryEndParams =
|
|
1192
|
+
serde_json::from_str(r#"{"transfer_id":"t","bytes_received":0}"#).unwrap();
|
|
1193
|
+
assert!(bare.checksum_ok.is_none());
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
#[test]
|
|
1197
|
+
fn engine_binary_chunk_size_stays_under_frame_bound() {
|
|
1198
|
+
// A 1 MiB payload with the 256 KiB frame bound needs >= 4 frames.
|
|
1199
|
+
let payload: usize = 1024 * 1024;
|
|
1200
|
+
let frames = payload.div_ceil(engine::ENGINE_BINARY_MAX_FRAME_BYTES);
|
|
1201
|
+
assert_eq!(frames, 4);
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
#[test]
|
|
1205
|
+
fn engine_modality_serde_uses_pascal_case() {
|
|
1206
|
+
assert_eq!(
|
|
1207
|
+
serde_json::to_string(&engine::EngineModality::Sensor).unwrap(),
|
|
1208
|
+
r#""Sensor""#
|
|
1209
|
+
);
|
|
1210
|
+
assert_eq!(
|
|
1211
|
+
serde_json::to_string(&engine::EngineModality::Tensor).unwrap(),
|
|
1212
|
+
r#""Tensor""#
|
|
1213
|
+
);
|
|
1214
|
+
}
|
|
1215
|
+
}
|