act-cli 0.6.0__tar.gz → 0.7.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {act_cli-0.6.0 → act_cli-0.7.1}/Cargo.lock +4 -4
- {act_cli-0.6.0 → act_cli-0.7.1}/Cargo.toml +2 -2
- {act_cli-0.6.0 → act_cli-0.7.1}/PKG-INFO +1 -1
- {act_cli-0.6.0 → act_cli-0.7.1}/act-cli/src/http.rs +137 -0
- {act_cli-0.6.0 → act_cli-0.7.1}/act-cli/src/main.rs +62 -3
- {act_cli-0.6.0 → act_cli-0.7.1}/act-cli/src/rmcp_bridge.rs +249 -16
- {act_cli-0.6.0 → act_cli-0.7.1}/act-cli/src/runtime/mod.rs +181 -8
- act_cli-0.7.1/act-cli/src/runtime/sessions.rs +112 -0
- {act_cli-0.6.0 → act_cli-0.7.1}/README.md +0 -0
- {act_cli-0.6.0 → act_cli-0.7.1}/act-cli/Cargo.toml +0 -0
- {act_cli-0.6.0 → act_cli-0.7.1}/act-cli/README.md +0 -0
- {act_cli-0.6.0 → act_cli-0.7.1}/act-cli/build.rs +0 -0
- {act_cli-0.6.0 → act_cli-0.7.1}/act-cli/src/config.rs +0 -0
- {act_cli-0.6.0 → act_cli-0.7.1}/act-cli/src/format.rs +0 -0
- {act_cli-0.6.0 → act_cli-0.7.1}/act-cli/src/resolve.rs +0 -0
- {act_cli-0.6.0 → act_cli-0.7.1}/act-cli/src/runtime/bindings/mod.rs +0 -0
- {act_cli-0.6.0 → act_cli-0.7.1}/act-cli/src/runtime/effective.rs +0 -0
- {act_cli-0.6.0 → act_cli-0.7.1}/act-cli/src/runtime/fs_matcher.rs +0 -0
- {act_cli-0.6.0 → act_cli-0.7.1}/act-cli/src/runtime/fs_policy.rs +0 -0
- {act_cli-0.6.0 → act_cli-0.7.1}/act-cli/src/runtime/http_client.rs +0 -0
- {act_cli-0.6.0 → act_cli-0.7.1}/act-cli/src/runtime/http_policy.rs +0 -0
- {act_cli-0.6.0 → act_cli-0.7.1}/act-cli/src/runtime/network.rs +0 -0
- {act_cli-0.6.0 → act_cli-0.7.1}/act-cli/wit/deps/act-core/act-core.wit +0 -0
- {act_cli-0.6.0 → act_cli-0.7.1}/act-cli/wit/deps/act-tools/act-tools.wit +0 -0
- {act_cli-0.6.0 → act_cli-0.7.1}/act-cli/wit/deps.lock +0 -0
- {act_cli-0.6.0 → act_cli-0.7.1}/act-cli/wit/deps.toml +0 -0
- {act_cli-0.6.0 → act_cli-0.7.1}/act-cli/wit/world.wit +0 -0
- {act_cli-0.6.0 → act_cli-0.7.1}/pyproject.toml +0 -0
|
@@ -4,7 +4,7 @@ version = 4
|
|
|
4
4
|
|
|
5
5
|
[[package]]
|
|
6
6
|
name = "act-build"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.7.1"
|
|
8
8
|
dependencies = [
|
|
9
9
|
"act-types",
|
|
10
10
|
"anyhow",
|
|
@@ -25,7 +25,7 @@ dependencies = [
|
|
|
25
25
|
|
|
26
26
|
[[package]]
|
|
27
27
|
name = "act-cli"
|
|
28
|
-
version = "0.
|
|
28
|
+
version = "0.7.1"
|
|
29
29
|
dependencies = [
|
|
30
30
|
"act-types",
|
|
31
31
|
"anyhow",
|
|
@@ -70,9 +70,9 @@ dependencies = [
|
|
|
70
70
|
|
|
71
71
|
[[package]]
|
|
72
72
|
name = "act-types"
|
|
73
|
-
version = "0.
|
|
73
|
+
version = "0.7.0"
|
|
74
74
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
75
|
-
checksum = "
|
|
75
|
+
checksum = "c814130fd708c159f2b8ae45f24e9a7de770b4980f8c82f6a6edce3fc0bdcbbd"
|
|
76
76
|
dependencies = [
|
|
77
77
|
"base64",
|
|
78
78
|
"ciborium",
|
|
@@ -3,7 +3,7 @@ members = ["act-cli"]
|
|
|
3
3
|
resolver = "3"
|
|
4
4
|
|
|
5
5
|
[workspace.package]
|
|
6
|
-
version = "0.
|
|
6
|
+
version = "0.7.1"
|
|
7
7
|
edition = "2024"
|
|
8
8
|
license = "MIT OR Apache-2.0"
|
|
9
9
|
repository = "https://github.com/actcore/act-cli"
|
|
@@ -11,7 +11,7 @@ homepage = "https://actcore.dev"
|
|
|
11
11
|
readme = "README.md"
|
|
12
12
|
|
|
13
13
|
[workspace.dependencies]
|
|
14
|
-
act-types = "0.
|
|
14
|
+
act-types = "0.7"
|
|
15
15
|
wasmparser = "0.247.0"
|
|
16
16
|
|
|
17
17
|
[profile.release]
|
|
@@ -377,6 +377,137 @@ fn query_method() -> &'static Method {
|
|
|
377
377
|
&QUERY
|
|
378
378
|
}
|
|
379
379
|
|
|
380
|
+
// ── Session helpers ────────────────────────────────────────────────────────
|
|
381
|
+
//
|
|
382
|
+
// Wire types live in `act_types::http` (OpenSessionRequest, OpenSessionResponse).
|
|
383
|
+
// Per ACT-SESSIONS.md §6.2.
|
|
384
|
+
|
|
385
|
+
/// Convert `Metadata` (Vec<(String, CborBytes)>) to a JSON object, dropping
|
|
386
|
+
/// entries whose CBOR can't be decoded.
|
|
387
|
+
fn metadata_pairs_to_json(
|
|
388
|
+
pairs: &[(String, Vec<u8>)],
|
|
389
|
+
) -> serde_json::Map<String, serde_json::Value> {
|
|
390
|
+
pairs
|
|
391
|
+
.iter()
|
|
392
|
+
.filter_map(|(k, v)| Some((k.clone(), cbor::cbor_to_json(v).ok()?)))
|
|
393
|
+
.collect()
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// ── Session handlers ───────────────────────────────────────────────────────
|
|
397
|
+
|
|
398
|
+
async fn session_open_args_schema_dispatcher(
|
|
399
|
+
state: State<Arc<AppState>>,
|
|
400
|
+
request: Request,
|
|
401
|
+
) -> axum::response::Response {
|
|
402
|
+
if request.method() != Method::POST && request.method() != query_method() {
|
|
403
|
+
return StatusCode::METHOD_NOT_ALLOWED.into_response();
|
|
404
|
+
}
|
|
405
|
+
let metadata_value = match parse_metadata_body(request).await {
|
|
406
|
+
Ok(m) => m,
|
|
407
|
+
Err(status) => return status.into_response(),
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
let mut meta = state.metadata.clone();
|
|
411
|
+
if let Some(value) = metadata_value {
|
|
412
|
+
meta.extend(Metadata::from(value));
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
let (reply_tx, reply_rx) = tokio::sync::oneshot::channel();
|
|
416
|
+
let request = runtime::ComponentRequest::GetOpenSessionArgsSchema {
|
|
417
|
+
metadata: meta.into(),
|
|
418
|
+
reply: reply_tx,
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
if state.component.send(request).await.is_err() {
|
|
422
|
+
return internal_error_response("component actor unavailable");
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
match reply_rx.await {
|
|
426
|
+
Ok(Ok(schema)) => match serde_json::from_str::<serde_json::Value>(&schema) {
|
|
427
|
+
Ok(v) => Json(v).into_response(),
|
|
428
|
+
Err(_) => internal_error_response("component returned non-JSON schema"),
|
|
429
|
+
},
|
|
430
|
+
Ok(Err(e)) => component_error_response(e),
|
|
431
|
+
Err(_) => internal_error_response("component actor dropped reply"),
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
async fn session_open(
|
|
436
|
+
State(state): State<Arc<AppState>>,
|
|
437
|
+
Json(body): Json<act_http::OpenSessionRequest>,
|
|
438
|
+
) -> axum::response::Response {
|
|
439
|
+
let serde_json::Value::Object(args_obj) = body.arguments else {
|
|
440
|
+
return (
|
|
441
|
+
StatusCode::BAD_REQUEST,
|
|
442
|
+
Json(act_http::ErrorResponse {
|
|
443
|
+
error: act_http::ToolError {
|
|
444
|
+
kind: ERR_INVALID_ARGS.to_string(),
|
|
445
|
+
message: "arguments must be a JSON object".to_string(),
|
|
446
|
+
metadata: None,
|
|
447
|
+
},
|
|
448
|
+
}),
|
|
449
|
+
)
|
|
450
|
+
.into_response();
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
let mut wit_args: Vec<(String, Vec<u8>)> = Vec::with_capacity(args_obj.len());
|
|
454
|
+
for (key, value) in args_obj {
|
|
455
|
+
match cbor::json_to_cbor(&value) {
|
|
456
|
+
Ok(bytes) => wit_args.push((key, bytes)),
|
|
457
|
+
Err(_) => return StatusCode::BAD_REQUEST.into_response(),
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
let mut meta = state.metadata.clone();
|
|
462
|
+
if let Some(value) = body.metadata {
|
|
463
|
+
meta.extend(Metadata::from(value));
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
let (reply_tx, reply_rx) = tokio::sync::oneshot::channel();
|
|
467
|
+
let request = runtime::ComponentRequest::OpenSession {
|
|
468
|
+
args: wit_args,
|
|
469
|
+
metadata: meta.into(),
|
|
470
|
+
reply: reply_tx,
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
if state.component.send(request).await.is_err() {
|
|
474
|
+
return internal_error_response("component actor unavailable");
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
match reply_rx.await {
|
|
478
|
+
Ok(Ok(session)) => {
|
|
479
|
+
let resp = act_http::OpenSessionResponse {
|
|
480
|
+
id: session.id,
|
|
481
|
+
metadata: metadata_pairs_to_json(&session.metadata),
|
|
482
|
+
};
|
|
483
|
+
(StatusCode::CREATED, Json(resp)).into_response()
|
|
484
|
+
}
|
|
485
|
+
Ok(Err(e)) => component_error_response(e),
|
|
486
|
+
Err(_) => internal_error_response("component actor dropped reply"),
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
async fn session_close(
|
|
491
|
+
State(state): State<Arc<AppState>>,
|
|
492
|
+
Path(session_id): Path<String>,
|
|
493
|
+
) -> axum::response::Response {
|
|
494
|
+
let (reply_tx, reply_rx) = tokio::sync::oneshot::channel();
|
|
495
|
+
let request = runtime::ComponentRequest::CloseSession {
|
|
496
|
+
session_id,
|
|
497
|
+
reply: reply_tx,
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
if state.component.send(request).await.is_err() {
|
|
501
|
+
return internal_error_response("component actor unavailable");
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
match reply_rx.await {
|
|
505
|
+
Ok(Ok(())) => StatusCode::NO_CONTENT.into_response(),
|
|
506
|
+
Ok(Err(e)) => component_error_response(e),
|
|
507
|
+
Err(_) => internal_error_response("component actor dropped reply"),
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
380
511
|
// ── Protocol version middleware ──
|
|
381
512
|
|
|
382
513
|
async fn protocol_version_layer(request: Request, next: Next) -> axum::response::Response {
|
|
@@ -428,6 +559,12 @@ pub fn create_router(state: Arc<AppState>) -> Router {
|
|
|
428
559
|
.route("/info", get(get_info))
|
|
429
560
|
.route("/tools", axum::routing::any(tools_dispatcher))
|
|
430
561
|
.route("/tools/{name}", axum::routing::any(tool_call_dispatcher))
|
|
562
|
+
.route(
|
|
563
|
+
"/sessions/open-args-schema",
|
|
564
|
+
axum::routing::any(session_open_args_schema_dispatcher),
|
|
565
|
+
)
|
|
566
|
+
.route("/sessions", axum::routing::post(session_open))
|
|
567
|
+
.route("/sessions/{id}", axum::routing::delete(session_close))
|
|
431
568
|
.layer(middleware::from_fn(protocol_version_layer))
|
|
432
569
|
.with_state(state)
|
|
433
570
|
}
|
|
@@ -146,6 +146,24 @@ enum Command {
|
|
|
146
146
|
#[arg(short = 'O', conflicts_with = "output")]
|
|
147
147
|
output_from_ref: bool,
|
|
148
148
|
},
|
|
149
|
+
/// Inspect `act:sessions/session-provider` — currently only
|
|
150
|
+
/// `open-args-schema`, since opening or closing a session from a
|
|
151
|
+
/// one-shot CLI invocation cannot keep the underlying wasm state
|
|
152
|
+
/// alive. For real session work, use `act run --http` or
|
|
153
|
+
/// `act run --mcp` (the host process holds the wasm instance and
|
|
154
|
+
/// the session lives as long as the host).
|
|
155
|
+
#[command(subcommand)]
|
|
156
|
+
Session(SessionCommand),
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
#[derive(clap::Subcommand)]
|
|
160
|
+
enum SessionCommand {
|
|
161
|
+
/// Print the JSON Schema for `open-session` args.
|
|
162
|
+
OpenArgsSchema {
|
|
163
|
+
component: ComponentRef,
|
|
164
|
+
#[command(flatten)]
|
|
165
|
+
opts: CommonOpts,
|
|
166
|
+
},
|
|
149
167
|
}
|
|
150
168
|
|
|
151
169
|
#[tokio::main]
|
|
@@ -168,6 +186,9 @@ async fn main() -> Result<()> {
|
|
|
168
186
|
opts.config.as_deref()
|
|
169
187
|
}
|
|
170
188
|
Command::Skill { .. } | Command::Pull { .. } => None,
|
|
189
|
+
Command::Session(sub) => match sub {
|
|
190
|
+
SessionCommand::OpenArgsSchema { opts, .. } => opts.config.as_deref(),
|
|
191
|
+
},
|
|
171
192
|
};
|
|
172
193
|
let log_level = config::load_config(config_path)
|
|
173
194
|
.ok()
|
|
@@ -210,6 +231,11 @@ async fn main() -> Result<()> {
|
|
|
210
231
|
output,
|
|
211
232
|
output_from_ref,
|
|
212
233
|
} => cmd_pull(reference, output, output_from_ref).await,
|
|
234
|
+
Command::Session(sub) => match sub {
|
|
235
|
+
SessionCommand::OpenArgsSchema { component, opts } => {
|
|
236
|
+
cmd_session_open_args_schema(component, opts).await
|
|
237
|
+
}
|
|
238
|
+
},
|
|
213
239
|
}
|
|
214
240
|
}
|
|
215
241
|
|
|
@@ -278,6 +304,8 @@ struct PreparedComponent {
|
|
|
278
304
|
info: runtime::ComponentInfo,
|
|
279
305
|
handle: runtime::ComponentHandle,
|
|
280
306
|
metadata: runtime::Metadata,
|
|
307
|
+
/// Whether the component exports `act:sessions/session-provider`.
|
|
308
|
+
has_sessions: bool,
|
|
281
309
|
}
|
|
282
310
|
|
|
283
311
|
/// Resolve, load, and instantiate a component. Returns a running actor handle.
|
|
@@ -314,10 +342,11 @@ async fn prepare_component(
|
|
|
314
342
|
let engine = runtime::create_engine()?;
|
|
315
343
|
let wasm = runtime::load_component(&engine, &component_path)?;
|
|
316
344
|
let linker = runtime::create_linker(&engine)?;
|
|
317
|
-
let (instance, store) =
|
|
345
|
+
let (instance, session_provider, store) =
|
|
318
346
|
runtime::instantiate_component(&engine, &wasm, &linker, &preopens, &http, &fs, &info)
|
|
319
347
|
.await?;
|
|
320
|
-
let
|
|
348
|
+
let has_sessions = session_provider.is_some();
|
|
349
|
+
let handle = runtime::spawn_component_actor(instance, session_provider, store);
|
|
321
350
|
|
|
322
351
|
tracing::debug!(name = %info.std.name, version = %info.std.version, "Component ready");
|
|
323
352
|
|
|
@@ -325,6 +354,7 @@ async fn prepare_component(
|
|
|
325
354
|
info,
|
|
326
355
|
handle,
|
|
327
356
|
metadata,
|
|
357
|
+
has_sessions,
|
|
328
358
|
})
|
|
329
359
|
}
|
|
330
360
|
|
|
@@ -356,7 +386,7 @@ async fn cmd_run(
|
|
|
356
386
|
|
|
357
387
|
if mcp {
|
|
358
388
|
let pc = prepare_component(&component, &opts).await?;
|
|
359
|
-
return rmcp_bridge::run_stdio(pc.info, pc.handle, pc.metadata).await;
|
|
389
|
+
return rmcp_bridge::run_stdio(pc.info, pc.handle, pc.metadata, pc.has_sessions).await;
|
|
360
390
|
}
|
|
361
391
|
|
|
362
392
|
if http || listen.is_some() {
|
|
@@ -458,6 +488,35 @@ async fn cmd_call(
|
|
|
458
488
|
}
|
|
459
489
|
}
|
|
460
490
|
|
|
491
|
+
// ── Session subcommands ────────────────────────────────────────────────────
|
|
492
|
+
|
|
493
|
+
async fn cmd_session_open_args_schema(component: ComponentRef, opts: CommonOpts) -> Result<()> {
|
|
494
|
+
let pc = prepare_component(&component, &opts).await?;
|
|
495
|
+
let (reply_tx, reply_rx) = tokio::sync::oneshot::channel();
|
|
496
|
+
pc.handle
|
|
497
|
+
.send(runtime::ComponentRequest::GetOpenSessionArgsSchema {
|
|
498
|
+
metadata: pc.metadata.clone().into(),
|
|
499
|
+
reply: reply_tx,
|
|
500
|
+
})
|
|
501
|
+
.await
|
|
502
|
+
.map_err(|_| anyhow::anyhow!("component actor unavailable"))?;
|
|
503
|
+
match reply_rx.await? {
|
|
504
|
+
Ok(schema) => {
|
|
505
|
+
// Pretty-print if it's valid JSON; otherwise print as-is.
|
|
506
|
+
match serde_json::from_str::<serde_json::Value>(&schema) {
|
|
507
|
+
Ok(v) => println!("{}", serde_json::to_string_pretty(&v)?),
|
|
508
|
+
Err(_) => println!("{schema}"),
|
|
509
|
+
}
|
|
510
|
+
Ok(())
|
|
511
|
+
}
|
|
512
|
+
Err(runtime::ComponentError::Tool(te)) => {
|
|
513
|
+
let ls = act_types::types::LocalizedString::from(&te.message);
|
|
514
|
+
anyhow::bail!("{}: {}", te.kind, ls.any_text());
|
|
515
|
+
}
|
|
516
|
+
Err(runtime::ComponentError::Internal(e)) => Err(e),
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
461
520
|
async fn cmd_info(
|
|
462
521
|
component: ComponentRef,
|
|
463
522
|
show_tools: bool,
|
|
@@ -1,16 +1,27 @@
|
|
|
1
1
|
use crate::runtime;
|
|
2
2
|
use act_types::cbor;
|
|
3
|
-
use act_types::constants::{
|
|
3
|
+
use act_types::constants::{
|
|
4
|
+
ERR_CAPABILITY_DENIED, ERR_INVALID_ARGS, ERR_NOT_FOUND, META_SESSION_OP,
|
|
5
|
+
};
|
|
4
6
|
use rmcp::ErrorData;
|
|
5
7
|
use rmcp::model::{Content, ErrorCode, Tool};
|
|
6
8
|
use serde_json::Value;
|
|
7
9
|
use std::borrow::Cow;
|
|
8
10
|
use std::sync::Arc;
|
|
9
11
|
|
|
12
|
+
/// Synthetic MCP tool name that maps to `session-provider.open-session`.
|
|
13
|
+
/// Per ACT-SESSIONS §6.1 these names are reserved.
|
|
14
|
+
const VIRTUAL_OPEN_SESSION: &str = "open_session";
|
|
15
|
+
const VIRTUAL_CLOSE_SESSION: &str = "close_session";
|
|
16
|
+
|
|
10
17
|
pub struct ActRmcpBridge {
|
|
11
18
|
pub handle: runtime::ComponentHandle,
|
|
12
19
|
pub info: runtime::ComponentInfo,
|
|
13
20
|
pub metadata: runtime::Metadata,
|
|
21
|
+
/// Whether the underlying component exports
|
|
22
|
+
/// `act:sessions/session-provider`. Controls synthesis of virtual
|
|
23
|
+
/// `open_session`/`close_session` tools and routing of those calls.
|
|
24
|
+
pub has_sessions: bool,
|
|
14
25
|
}
|
|
15
26
|
|
|
16
27
|
fn map_content_part(part: &runtime::exports::act::tools::tool_provider::ContentPart) -> Content {
|
|
@@ -146,11 +157,13 @@ pub async fn run_stdio(
|
|
|
146
157
|
info: runtime::ComponentInfo,
|
|
147
158
|
handle: runtime::ComponentHandle,
|
|
148
159
|
metadata: runtime::Metadata,
|
|
160
|
+
has_sessions: bool,
|
|
149
161
|
) -> anyhow::Result<()> {
|
|
150
162
|
let bridge = ActRmcpBridge {
|
|
151
163
|
handle,
|
|
152
164
|
info,
|
|
153
165
|
metadata,
|
|
166
|
+
has_sessions,
|
|
154
167
|
};
|
|
155
168
|
|
|
156
169
|
let service = rmcp::serve_server(bridge, (tokio::io::stdin(), tokio::io::stdout()))
|
|
@@ -194,7 +207,14 @@ impl ActRmcpBridge {
|
|
|
194
207
|
})?
|
|
195
208
|
.map_err(component_error_to_mcp)?;
|
|
196
209
|
|
|
197
|
-
let tools = convert_tool_definitions(&list.tools);
|
|
210
|
+
let mut tools = convert_tool_definitions(&list.tools);
|
|
211
|
+
|
|
212
|
+
if self.has_sessions {
|
|
213
|
+
let open_schema = self.fetch_open_session_args_schema().await?;
|
|
214
|
+
tools.push(virtual_open_session_tool(open_schema));
|
|
215
|
+
tools.push(virtual_close_session_tool());
|
|
216
|
+
}
|
|
217
|
+
|
|
198
218
|
Ok(rmcp::model::ListToolsResult {
|
|
199
219
|
tools,
|
|
200
220
|
next_cursor: None,
|
|
@@ -202,24 +222,84 @@ impl ActRmcpBridge {
|
|
|
202
222
|
})
|
|
203
223
|
}
|
|
204
224
|
|
|
225
|
+
/// Ask the component for its `get-open-session-args-schema` JSON Schema.
|
|
226
|
+
/// Errors bubble up as MCP errors so the agent sees them at list_tools time.
|
|
227
|
+
async fn fetch_open_session_args_schema(&self) -> Result<Value, rmcp::ErrorData> {
|
|
228
|
+
let (reply_tx, reply_rx) = tokio::sync::oneshot::channel();
|
|
229
|
+
let req = runtime::ComponentRequest::GetOpenSessionArgsSchema {
|
|
230
|
+
metadata: self.metadata.clone().into(),
|
|
231
|
+
reply: reply_tx,
|
|
232
|
+
};
|
|
233
|
+
self.handle.send(req).await.map_err(|_| {
|
|
234
|
+
rmcp::ErrorData::new(
|
|
235
|
+
rmcp::model::ErrorCode::INTERNAL_ERROR,
|
|
236
|
+
"component actor unavailable",
|
|
237
|
+
None,
|
|
238
|
+
)
|
|
239
|
+
})?;
|
|
240
|
+
let schema = reply_rx
|
|
241
|
+
.await
|
|
242
|
+
.map_err(|_| {
|
|
243
|
+
rmcp::ErrorData::new(
|
|
244
|
+
rmcp::model::ErrorCode::INTERNAL_ERROR,
|
|
245
|
+
"component actor dropped reply",
|
|
246
|
+
None,
|
|
247
|
+
)
|
|
248
|
+
})?
|
|
249
|
+
.map_err(component_error_to_mcp)?;
|
|
250
|
+
serde_json::from_str::<Value>(&schema).map_err(|e| {
|
|
251
|
+
rmcp::ErrorData::new(
|
|
252
|
+
rmcp::model::ErrorCode::INTERNAL_ERROR,
|
|
253
|
+
format!("component returned non-JSON schema: {e}"),
|
|
254
|
+
None,
|
|
255
|
+
)
|
|
256
|
+
})
|
|
257
|
+
}
|
|
258
|
+
|
|
205
259
|
async fn call_tool_impl(
|
|
206
260
|
&self,
|
|
207
261
|
request: rmcp::model::CallToolRequestParams,
|
|
262
|
+
ctx_meta: &rmcp::model::Meta,
|
|
208
263
|
) -> Result<rmcp::model::CallToolResult, rmcp::ErrorData> {
|
|
209
264
|
use rmcp::model::ErrorCode;
|
|
210
265
|
|
|
211
|
-
|
|
266
|
+
// Merge protocol-level `_meta` (per MCP SEP-1319) into the WIT
|
|
267
|
+
// metadata before dispatching. Hosts MUST forward `std:session-id`
|
|
268
|
+
// here per ACT-SESSIONS §6.1.
|
|
269
|
+
//
|
|
270
|
+
// rmcp moves `_meta` off params into `RequestContext::meta` during
|
|
271
|
+
// request dispatch (see service.rs `std::mem::swap`), so that's
|
|
272
|
+
// where we read it from — not `params.meta` (always None) and not
|
|
273
|
+
// `extensions` (which holds non-meta extension values).
|
|
274
|
+
let mut call_metadata = self.metadata.clone();
|
|
275
|
+
if !ctx_meta.0.is_empty() {
|
|
276
|
+
call_metadata.extend(act_types::types::Metadata::from(Value::Object(
|
|
277
|
+
ctx_meta.0.clone(),
|
|
278
|
+
)));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Route reserved virtual tools (`open_session` / `close_session`).
|
|
282
|
+
if self.has_sessions {
|
|
283
|
+
match request.name.as_ref() {
|
|
284
|
+
VIRTUAL_OPEN_SESSION => {
|
|
285
|
+
return self
|
|
286
|
+
.virtual_open_session(request.arguments, call_metadata)
|
|
287
|
+
.await;
|
|
288
|
+
}
|
|
289
|
+
VIRTUAL_CLOSE_SESSION => {
|
|
290
|
+
return self
|
|
291
|
+
.virtual_close_session(request.arguments, call_metadata)
|
|
292
|
+
.await;
|
|
293
|
+
}
|
|
294
|
+
_ => {}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
let arguments = request
|
|
212
299
|
.arguments
|
|
213
300
|
.map(Value::Object)
|
|
214
301
|
.unwrap_or_else(|| serde_json::json!({}));
|
|
215
302
|
|
|
216
|
-
let mut call_metadata = self.metadata.clone();
|
|
217
|
-
if let Some(obj) = arguments.as_object_mut()
|
|
218
|
-
&& let Some(Value::Object(extra)) = obj.remove("_metadata")
|
|
219
|
-
{
|
|
220
|
-
call_metadata.extend(act_types::types::Metadata::from(Value::Object(extra)));
|
|
221
|
-
}
|
|
222
|
-
|
|
223
303
|
let cbor_args = act_types::cbor::json_to_cbor(&arguments).map_err(|_| {
|
|
224
304
|
rmcp::ErrorData::new(ErrorCode::INVALID_PARAMS, "invalid arguments", None)
|
|
225
305
|
})?;
|
|
@@ -253,6 +333,160 @@ impl ActRmcpBridge {
|
|
|
253
333
|
|
|
254
334
|
Ok(fold_events_to_result(result))
|
|
255
335
|
}
|
|
336
|
+
|
|
337
|
+
async fn virtual_open_session(
|
|
338
|
+
&self,
|
|
339
|
+
arguments: Option<rmcp::model::JsonObject>,
|
|
340
|
+
metadata: runtime::Metadata,
|
|
341
|
+
) -> Result<rmcp::model::CallToolResult, rmcp::ErrorData> {
|
|
342
|
+
let args_obj = arguments.unwrap_or_default();
|
|
343
|
+
let mut wit_args: Vec<(String, Vec<u8>)> = Vec::with_capacity(args_obj.len());
|
|
344
|
+
for (key, value) in args_obj {
|
|
345
|
+
let cbor_bytes = cbor::json_to_cbor(&value).map_err(|_| {
|
|
346
|
+
rmcp::ErrorData::new(
|
|
347
|
+
rmcp::model::ErrorCode::INVALID_PARAMS,
|
|
348
|
+
format!("encoding `{key}` as CBOR failed"),
|
|
349
|
+
None,
|
|
350
|
+
)
|
|
351
|
+
})?;
|
|
352
|
+
wit_args.push((key, cbor_bytes));
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
let (reply_tx, reply_rx) = tokio::sync::oneshot::channel();
|
|
356
|
+
let req = runtime::ComponentRequest::OpenSession {
|
|
357
|
+
args: wit_args,
|
|
358
|
+
metadata: metadata.into(),
|
|
359
|
+
reply: reply_tx,
|
|
360
|
+
};
|
|
361
|
+
self.handle.send(req).await.map_err(|_| {
|
|
362
|
+
rmcp::ErrorData::new(
|
|
363
|
+
rmcp::model::ErrorCode::INTERNAL_ERROR,
|
|
364
|
+
"component actor unavailable",
|
|
365
|
+
None,
|
|
366
|
+
)
|
|
367
|
+
})?;
|
|
368
|
+
let session = reply_rx
|
|
369
|
+
.await
|
|
370
|
+
.map_err(|_| {
|
|
371
|
+
rmcp::ErrorData::new(
|
|
372
|
+
rmcp::model::ErrorCode::INTERNAL_ERROR,
|
|
373
|
+
"component actor dropped reply",
|
|
374
|
+
None,
|
|
375
|
+
)
|
|
376
|
+
})?
|
|
377
|
+
.map_err(component_error_to_mcp)?;
|
|
378
|
+
|
|
379
|
+
let metadata_json: serde_json::Map<String, Value> = session
|
|
380
|
+
.metadata
|
|
381
|
+
.iter()
|
|
382
|
+
.filter_map(|(k, v)| Some((k.clone(), cbor::cbor_to_json(v).ok()?)))
|
|
383
|
+
.collect();
|
|
384
|
+
let payload = serde_json::json!({
|
|
385
|
+
"id": session.id,
|
|
386
|
+
"metadata": metadata_json,
|
|
387
|
+
});
|
|
388
|
+
let json_text = serde_json::to_string(&payload).unwrap_or_default();
|
|
389
|
+
|
|
390
|
+
Ok(rmcp::model::CallToolResult::success(vec![Content::text(
|
|
391
|
+
json_text,
|
|
392
|
+
)]))
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
async fn virtual_close_session(
|
|
396
|
+
&self,
|
|
397
|
+
arguments: Option<rmcp::model::JsonObject>,
|
|
398
|
+
_metadata: runtime::Metadata,
|
|
399
|
+
) -> Result<rmcp::model::CallToolResult, rmcp::ErrorData> {
|
|
400
|
+
let session_id = arguments
|
|
401
|
+
.as_ref()
|
|
402
|
+
.and_then(|obj| obj.get("session_id"))
|
|
403
|
+
.and_then(|v| v.as_str())
|
|
404
|
+
.ok_or_else(|| {
|
|
405
|
+
rmcp::ErrorData::new(
|
|
406
|
+
rmcp::model::ErrorCode::INVALID_PARAMS,
|
|
407
|
+
"close_session requires `session_id` (string)",
|
|
408
|
+
None,
|
|
409
|
+
)
|
|
410
|
+
})?
|
|
411
|
+
.to_string();
|
|
412
|
+
|
|
413
|
+
let (reply_tx, reply_rx) = tokio::sync::oneshot::channel();
|
|
414
|
+
let req = runtime::ComponentRequest::CloseSession {
|
|
415
|
+
session_id,
|
|
416
|
+
reply: reply_tx,
|
|
417
|
+
};
|
|
418
|
+
self.handle.send(req).await.map_err(|_| {
|
|
419
|
+
rmcp::ErrorData::new(
|
|
420
|
+
rmcp::model::ErrorCode::INTERNAL_ERROR,
|
|
421
|
+
"component actor unavailable",
|
|
422
|
+
None,
|
|
423
|
+
)
|
|
424
|
+
})?;
|
|
425
|
+
reply_rx
|
|
426
|
+
.await
|
|
427
|
+
.map_err(|_| {
|
|
428
|
+
rmcp::ErrorData::new(
|
|
429
|
+
rmcp::model::ErrorCode::INTERNAL_ERROR,
|
|
430
|
+
"component actor dropped reply",
|
|
431
|
+
None,
|
|
432
|
+
)
|
|
433
|
+
})?
|
|
434
|
+
.map_err(component_error_to_mcp)?;
|
|
435
|
+
Ok(rmcp::model::CallToolResult::success(vec![]))
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/// Build the synthetic `open_session` MCP tool. The args schema comes from
|
|
440
|
+
/// `get-open-session-args-schema`. `_meta.std:session-op = "open"`
|
|
441
|
+
/// per ACT-CONSTANTS so agents can recognize this is a session-lifecycle
|
|
442
|
+
/// tool, not an ordinary capability.
|
|
443
|
+
fn virtual_open_session_tool(args_schema: Value) -> Tool {
|
|
444
|
+
let mut schema_map: serde_json::Map<String, Value> =
|
|
445
|
+
args_schema.as_object().cloned().unwrap_or_default();
|
|
446
|
+
schema_map
|
|
447
|
+
.entry("type".to_string())
|
|
448
|
+
.or_insert(Value::String("object".into()));
|
|
449
|
+
|
|
450
|
+
let mut tool = Tool::new(
|
|
451
|
+
Cow::Borrowed(VIRTUAL_OPEN_SESSION),
|
|
452
|
+
Cow::Borrowed("Open a new session against this component."),
|
|
453
|
+
Arc::new(schema_map),
|
|
454
|
+
);
|
|
455
|
+
tool = tool.with_meta(session_op_meta("open"));
|
|
456
|
+
tool
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/// Build the synthetic `close_session` MCP tool. Args is fixed:
|
|
460
|
+
/// `{ session_id: string }`. `_meta.std:session-op = "close"`.
|
|
461
|
+
fn virtual_close_session_tool() -> Tool {
|
|
462
|
+
let schema_map: serde_json::Map<String, Value> = serde_json::json!({
|
|
463
|
+
"type": "object",
|
|
464
|
+
"properties": {
|
|
465
|
+
"session_id": {
|
|
466
|
+
"type": "string",
|
|
467
|
+
"description": "Session-id returned by `open_session`."
|
|
468
|
+
}
|
|
469
|
+
},
|
|
470
|
+
"required": ["session_id"],
|
|
471
|
+
"additionalProperties": false,
|
|
472
|
+
})
|
|
473
|
+
.as_object()
|
|
474
|
+
.cloned()
|
|
475
|
+
.unwrap_or_default();
|
|
476
|
+
|
|
477
|
+
let mut tool = Tool::new(
|
|
478
|
+
Cow::Borrowed(VIRTUAL_CLOSE_SESSION),
|
|
479
|
+
Cow::Borrowed("Close a session previously opened via `open_session`."),
|
|
480
|
+
Arc::new(schema_map),
|
|
481
|
+
);
|
|
482
|
+
tool = tool.with_meta(session_op_meta("close"));
|
|
483
|
+
tool
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
fn session_op_meta(op: &'static str) -> rmcp::model::Meta {
|
|
487
|
+
let mut map = serde_json::Map::new();
|
|
488
|
+
map.insert(META_SESSION_OP.to_string(), Value::String(op.to_string()));
|
|
489
|
+
rmcp::model::Meta(map)
|
|
256
490
|
}
|
|
257
491
|
|
|
258
492
|
impl rmcp::ServerHandler for ActRmcpBridge {
|
|
@@ -278,14 +512,12 @@ impl rmcp::ServerHandler for ActRmcpBridge {
|
|
|
278
512
|
self.list_tools_impl()
|
|
279
513
|
}
|
|
280
514
|
|
|
281
|
-
fn call_tool(
|
|
515
|
+
async fn call_tool(
|
|
282
516
|
&self,
|
|
283
517
|
request: rmcp::model::CallToolRequestParams,
|
|
284
|
-
|
|
285
|
-
) ->
|
|
286
|
-
|
|
287
|
-
+ '_ {
|
|
288
|
-
self.call_tool_impl(request)
|
|
518
|
+
context: rmcp::service::RequestContext<rmcp::RoleServer>,
|
|
519
|
+
) -> Result<rmcp::model::CallToolResult, rmcp::ErrorData> {
|
|
520
|
+
self.call_tool_impl(request, &context.meta).await
|
|
289
521
|
}
|
|
290
522
|
}
|
|
291
523
|
|
|
@@ -378,6 +610,7 @@ mod tests {
|
|
|
378
610
|
handle: fake_handle(),
|
|
379
611
|
info: fake_info(),
|
|
380
612
|
metadata: runtime::Metadata::default(),
|
|
613
|
+
has_sessions: false,
|
|
381
614
|
};
|
|
382
615
|
let info = rmcp::ServerHandler::get_info(&bridge);
|
|
383
616
|
assert_eq!(info.server_info.name, "example");
|
|
@@ -5,7 +5,7 @@ use std::pin::Pin;
|
|
|
5
5
|
use std::task::{Context, Poll};
|
|
6
6
|
use tokio::sync::{mpsc, oneshot};
|
|
7
7
|
use wasmtime::component::{Component, Linker, ResourceTable, Source, StreamConsumer, StreamResult};
|
|
8
|
-
use wasmtime::{Config, Engine, Store, StoreContextMut};
|
|
8
|
+
use wasmtime::{AsContextMut, Config, Engine, Store, StoreContextMut};
|
|
9
9
|
use wasmtime_wasi::{WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView};
|
|
10
10
|
use wasmtime_wasi_http::WasiHttpCtx;
|
|
11
11
|
use wasmtime_wasi_http::p3::WasiHttpCtxView;
|
|
@@ -16,6 +16,7 @@ pub mod fs_policy;
|
|
|
16
16
|
pub mod http_client;
|
|
17
17
|
pub mod http_policy;
|
|
18
18
|
pub mod network;
|
|
19
|
+
pub mod sessions;
|
|
19
20
|
|
|
20
21
|
// Generated bindings from WIT — fully auto-generated, no manual patching.
|
|
21
22
|
#[allow(unused_mut, unused_variables, dead_code)]
|
|
@@ -283,6 +284,26 @@ pub enum ComponentRequest {
|
|
|
283
284
|
metadata: Vec<(String, Vec<u8>)>,
|
|
284
285
|
event_tx: mpsc::Sender<SseEvent>,
|
|
285
286
|
},
|
|
287
|
+
/// Returns a JSON Schema string. Errors with `std:not-found` if the
|
|
288
|
+
/// component does not export `session-provider`.
|
|
289
|
+
GetOpenSessionArgsSchema {
|
|
290
|
+
metadata: Vec<(String, Vec<u8>)>,
|
|
291
|
+
reply: oneshot::Sender<Result<String, ComponentError>>,
|
|
292
|
+
},
|
|
293
|
+
/// Errors with `std:not-found` if the component does not export
|
|
294
|
+
/// `session-provider`.
|
|
295
|
+
OpenSession {
|
|
296
|
+
args: Vec<(String, Vec<u8>)>,
|
|
297
|
+
metadata: Vec<(String, Vec<u8>)>,
|
|
298
|
+
reply: oneshot::Sender<Result<sessions::Session, ComponentError>>,
|
|
299
|
+
},
|
|
300
|
+
/// Errors with `std:not-found` if the component does not export
|
|
301
|
+
/// `session-provider`. The reply carries `()` so callers can wait for
|
|
302
|
+
/// the close to complete.
|
|
303
|
+
CloseSession {
|
|
304
|
+
session_id: String,
|
|
305
|
+
reply: oneshot::Sender<Result<(), ComponentError>>,
|
|
306
|
+
},
|
|
286
307
|
}
|
|
287
308
|
|
|
288
309
|
/// Collected result from call-tool (stream already consumed).
|
|
@@ -300,8 +321,12 @@ pub enum SseEvent {
|
|
|
300
321
|
/// Handle to send requests to the component actor.
|
|
301
322
|
pub type ComponentHandle = mpsc::Sender<ComponentRequest>;
|
|
302
323
|
|
|
303
|
-
/// Instantiate the component. Returns the ActWorld
|
|
304
|
-
///
|
|
324
|
+
/// Instantiate the component. Returns the ActWorld, an optional
|
|
325
|
+
/// SessionProvider (present iff the component exports
|
|
326
|
+
/// `act:sessions/session-provider`), and the store.
|
|
327
|
+
///
|
|
328
|
+
/// Component info is read from custom sections (no instantiation needed
|
|
329
|
+
/// for that).
|
|
305
330
|
pub async fn instantiate_component(
|
|
306
331
|
engine: &Engine,
|
|
307
332
|
component: &Component,
|
|
@@ -310,20 +335,50 @@ pub async fn instantiate_component(
|
|
|
310
335
|
http: &crate::config::HttpConfig,
|
|
311
336
|
fs: &crate::config::FsConfig,
|
|
312
337
|
info: &ComponentInfo,
|
|
313
|
-
) -> Result<(
|
|
338
|
+
) -> Result<(
|
|
339
|
+
ActWorld,
|
|
340
|
+
Option<sessions::SessionProvider>,
|
|
341
|
+
Store<HostState>,
|
|
342
|
+
)> {
|
|
314
343
|
let mut store = create_store(engine, preopens, http, fs, info)?;
|
|
315
|
-
|
|
344
|
+
|
|
345
|
+
// Manual instantiation flow (replicates ActWorld::instantiate_async)
|
|
346
|
+
// so we keep access to the raw `Instance` for session-provider lookup.
|
|
347
|
+
let pre = linker
|
|
348
|
+
.instantiate_pre(component)
|
|
349
|
+
.map_err(|e| anyhow::anyhow!("failed to pre-instantiate component: {e}"))?;
|
|
350
|
+
let indices =
|
|
351
|
+
ActWorldIndices::new(&pre).map_err(|e| anyhow::anyhow!("ActWorld indices: {e}"))?;
|
|
352
|
+
let instance = pre
|
|
353
|
+
.instantiate_async(&mut store)
|
|
316
354
|
.await
|
|
317
355
|
.map_err(|e| anyhow::anyhow!("failed to instantiate component: {e}"))?;
|
|
356
|
+
let act_world = indices
|
|
357
|
+
.load(&mut store, &instance)
|
|
358
|
+
.map_err(|e| anyhow::anyhow!("failed to load ActWorld: {e}"))?;
|
|
359
|
+
|
|
360
|
+
let session_provider = sessions::SessionProvider::lookup(&instance, store.as_context_mut())?;
|
|
318
361
|
|
|
319
|
-
Ok((
|
|
362
|
+
Ok((act_world, session_provider, store))
|
|
320
363
|
}
|
|
321
364
|
|
|
322
|
-
/// Spawn the component actor task. Owns the Store and
|
|
365
|
+
/// Spawn the component actor task. Owns the Store, ActWorld, and the
|
|
366
|
+
/// optional SessionProvider (present iff the component supports
|
|
367
|
+
/// `act:sessions/session-provider`).
|
|
368
|
+
///
|
|
323
369
|
/// Returns a handle for sending requests.
|
|
324
|
-
pub fn spawn_component_actor(
|
|
370
|
+
pub fn spawn_component_actor(
|
|
371
|
+
instance: ActWorld,
|
|
372
|
+
session_provider: Option<sessions::SessionProvider>,
|
|
373
|
+
mut store: Store<HostState>,
|
|
374
|
+
) -> ComponentHandle {
|
|
325
375
|
let (tx, mut rx) = mpsc::channel::<ComponentRequest>(32);
|
|
326
376
|
|
|
377
|
+
// Session-ids opened through this actor. Closed on actor shutdown
|
|
378
|
+
// per ACT-SESSIONS §2.5 ("host MUST call close-session for every
|
|
379
|
+
// still-open session before deinit").
|
|
380
|
+
let mut tracked_sessions: Vec<String> = Vec::new();
|
|
381
|
+
|
|
327
382
|
tokio::spawn(async move {
|
|
328
383
|
while let Some(request) = rx.recv().await {
|
|
329
384
|
match request {
|
|
@@ -477,6 +532,97 @@ pub fn spawn_component_actor(instance: ActWorld, mut store: Store<HostState>) ->
|
|
|
477
532
|
};
|
|
478
533
|
let _ = event_tx.send(terminal).await;
|
|
479
534
|
}
|
|
535
|
+
|
|
536
|
+
ComponentRequest::GetOpenSessionArgsSchema { metadata, reply } => {
|
|
537
|
+
let response = match &session_provider {
|
|
538
|
+
Some(sp) => {
|
|
539
|
+
let sp = sp.clone();
|
|
540
|
+
let result = store
|
|
541
|
+
.run_concurrent(async |accessor| {
|
|
542
|
+
sp.get_open_session_args_schema
|
|
543
|
+
.call_concurrent(&accessor, (metadata,))
|
|
544
|
+
.await
|
|
545
|
+
})
|
|
546
|
+
.await;
|
|
547
|
+
session_call_to_response(result, |(r,)| r)
|
|
548
|
+
}
|
|
549
|
+
None => Err(ComponentError::Internal(anyhow::anyhow!(
|
|
550
|
+
"component does not export act:sessions/session-provider"
|
|
551
|
+
))),
|
|
552
|
+
};
|
|
553
|
+
let _ = reply.send(response);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
ComponentRequest::OpenSession {
|
|
557
|
+
args,
|
|
558
|
+
metadata,
|
|
559
|
+
reply,
|
|
560
|
+
} => {
|
|
561
|
+
let response = match &session_provider {
|
|
562
|
+
Some(sp) => {
|
|
563
|
+
let sp = sp.clone();
|
|
564
|
+
let result = store
|
|
565
|
+
.run_concurrent(async |accessor| {
|
|
566
|
+
sp.open_session
|
|
567
|
+
.call_concurrent(&accessor, (args, metadata))
|
|
568
|
+
.await
|
|
569
|
+
})
|
|
570
|
+
.await;
|
|
571
|
+
let inner = session_call_to_response(result, |(r,)| r);
|
|
572
|
+
// Track open id so we can close on deinit.
|
|
573
|
+
if let Ok(s) = &inner {
|
|
574
|
+
tracked_sessions.push(s.id.clone());
|
|
575
|
+
}
|
|
576
|
+
inner
|
|
577
|
+
}
|
|
578
|
+
None => Err(ComponentError::Internal(anyhow::anyhow!(
|
|
579
|
+
"component does not export act:sessions/session-provider"
|
|
580
|
+
))),
|
|
581
|
+
};
|
|
582
|
+
let _ = reply.send(response);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
ComponentRequest::CloseSession { session_id, reply } => {
|
|
586
|
+
let response: Result<(), ComponentError> = match &session_provider {
|
|
587
|
+
Some(sp) => {
|
|
588
|
+
let sp = sp.clone();
|
|
589
|
+
let id = session_id.clone();
|
|
590
|
+
let result = store
|
|
591
|
+
.run_concurrent(async |accessor| {
|
|
592
|
+
sp.close_session.call_concurrent(&accessor, (id,)).await
|
|
593
|
+
})
|
|
594
|
+
.await;
|
|
595
|
+
// Untrack regardless of error.
|
|
596
|
+
tracked_sessions.retain(|sid| sid != &session_id);
|
|
597
|
+
match result {
|
|
598
|
+
Ok(Ok(())) => Ok(()),
|
|
599
|
+
Ok(Err(e)) => Err(ComponentError::Internal(anyhow::anyhow!(
|
|
600
|
+
"close-session failed: {e}"
|
|
601
|
+
))),
|
|
602
|
+
Err(e) => Err(ComponentError::Internal(anyhow::anyhow!(
|
|
603
|
+
"run_concurrent failed: {e}"
|
|
604
|
+
))),
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
None => Err(ComponentError::Internal(anyhow::anyhow!(
|
|
608
|
+
"component does not export act:sessions/session-provider"
|
|
609
|
+
))),
|
|
610
|
+
};
|
|
611
|
+
let _ = reply.send(response);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Actor channel closed → component is shutting down. Close any
|
|
617
|
+
// sessions we still track, best-effort. ACT-SESSIONS §2.5.
|
|
618
|
+
if let Some(sp) = &session_provider {
|
|
619
|
+
for id in std::mem::take(&mut tracked_sessions) {
|
|
620
|
+
let sp = sp.clone();
|
|
621
|
+
let _ = store
|
|
622
|
+
.run_concurrent(async |accessor| {
|
|
623
|
+
sp.close_session.call_concurrent(&accessor, (id,)).await
|
|
624
|
+
})
|
|
625
|
+
.await;
|
|
480
626
|
}
|
|
481
627
|
}
|
|
482
628
|
});
|
|
@@ -484,6 +630,33 @@ pub fn spawn_component_actor(instance: ActWorld, mut store: Store<HostState>) ->
|
|
|
484
630
|
tx
|
|
485
631
|
}
|
|
486
632
|
|
|
633
|
+
/// Helper for unwrapping `result<R, error>` returns from session-provider
|
|
634
|
+
/// typed-func calls.
|
|
635
|
+
fn session_call_to_response<R, F>(
|
|
636
|
+
raw: wasmtime::Result<
|
|
637
|
+
wasmtime::Result<(Result<R, exports::act::tools::tool_provider::Error>,)>,
|
|
638
|
+
>,
|
|
639
|
+
extract: F,
|
|
640
|
+
) -> Result<R, ComponentError>
|
|
641
|
+
where
|
|
642
|
+
F: FnOnce(
|
|
643
|
+
(Result<R, exports::act::tools::tool_provider::Error>,),
|
|
644
|
+
) -> Result<R, exports::act::tools::tool_provider::Error>,
|
|
645
|
+
{
|
|
646
|
+
match raw {
|
|
647
|
+
Ok(Ok(tuple)) => match extract(tuple) {
|
|
648
|
+
Ok(r) => Ok(r),
|
|
649
|
+
Err(e) => Err(ComponentError::Tool(e)),
|
|
650
|
+
},
|
|
651
|
+
Ok(Err(e)) => Err(ComponentError::Internal(anyhow::anyhow!(
|
|
652
|
+
"session-provider call failed: {e}"
|
|
653
|
+
))),
|
|
654
|
+
Err(e) => Err(ComponentError::Internal(anyhow::anyhow!(
|
|
655
|
+
"run_concurrent failed: {e}"
|
|
656
|
+
))),
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
487
660
|
/// A StreamConsumer that collects all items into a Vec and signals completion.
|
|
488
661
|
struct CollectingConsumer {
|
|
489
662
|
collected: std::sync::Arc<std::sync::Mutex<Vec<exports::act::tools::tool_provider::ToolEvent>>>,
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
//! Host-side wrappers for the `act:sessions/session-provider` interface.
|
|
2
|
+
//!
|
|
3
|
+
//! The host's `bindgen!` only covers `act:tools/tool-provider`, so we
|
|
4
|
+
//! look up the session-provider exports manually via raw component-model
|
|
5
|
+
//! APIs. Components without session-provider exports are loaded fine —
|
|
6
|
+
//! `lookup` simply returns `None` for them.
|
|
7
|
+
//!
|
|
8
|
+
//! `Error` and `LocalizedString` types are reused from the tool-provider
|
|
9
|
+
//! bindings (they are the same `act:core/types` records via structural
|
|
10
|
+
//! typing in wasmtime).
|
|
11
|
+
|
|
12
|
+
use anyhow::Result;
|
|
13
|
+
use wasmtime::component::{ComponentNamedList, ComponentType, Instance, Lift, Lower, TypedFunc};
|
|
14
|
+
use wasmtime::{AsContext, AsContextMut, StoreContextMut};
|
|
15
|
+
|
|
16
|
+
use super::HostState;
|
|
17
|
+
use super::exports::act::tools::tool_provider::{Error, LocalizedString};
|
|
18
|
+
|
|
19
|
+
/// `act:sessions/session-provider.session` — the WIT record returned by
|
|
20
|
+
/// `open-session`.
|
|
21
|
+
#[derive(Debug, Clone, ComponentType, Lift, Lower)]
|
|
22
|
+
#[component(record)]
|
|
23
|
+
pub struct Session {
|
|
24
|
+
pub id: String,
|
|
25
|
+
pub metadata: Vec<(String, Vec<u8>)>,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ── Typed function aliases (matching the WIT signatures) ───────────────────
|
|
29
|
+
|
|
30
|
+
/// `get-open-session-args-schema(metadata: metadata) -> result<string, error>`
|
|
31
|
+
type GetOpenSessionArgsSchemaFn = TypedFunc<(Vec<(String, Vec<u8>)>,), (Result<String, Error>,)>;
|
|
32
|
+
|
|
33
|
+
/// `open-session(args: metadata, metadata: metadata) -> result<session, error>`
|
|
34
|
+
type OpenSessionFn =
|
|
35
|
+
TypedFunc<(Vec<(String, Vec<u8>)>, Vec<(String, Vec<u8>)>), (Result<Session, Error>,)>;
|
|
36
|
+
|
|
37
|
+
/// `close-session(session-id: string)`
|
|
38
|
+
type CloseSessionFn = TypedFunc<(String,), ()>;
|
|
39
|
+
|
|
40
|
+
const INTERFACE_NAME: &str = "act:sessions/session-provider@0.1.0";
|
|
41
|
+
|
|
42
|
+
/// Typed handles to the three session-provider functions of one component
|
|
43
|
+
/// instance.
|
|
44
|
+
#[derive(Clone)]
|
|
45
|
+
pub struct SessionProvider {
|
|
46
|
+
pub get_open_session_args_schema: GetOpenSessionArgsSchemaFn,
|
|
47
|
+
pub open_session: OpenSessionFn,
|
|
48
|
+
pub close_session: CloseSessionFn,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
impl SessionProvider {
|
|
52
|
+
/// Look up the session-provider exports of a component instance.
|
|
53
|
+
/// Returns `None` if the component doesn't export session-provider.
|
|
54
|
+
pub fn lookup(
|
|
55
|
+
instance: &Instance,
|
|
56
|
+
mut store: StoreContextMut<'_, HostState>,
|
|
57
|
+
) -> Result<Option<Self>> {
|
|
58
|
+
let Some((_, iface)) = instance.get_export(&mut store, None, INTERFACE_NAME) else {
|
|
59
|
+
return Ok(None);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
let get_schema = lookup_typed(
|
|
63
|
+
instance,
|
|
64
|
+
store.as_context_mut(),
|
|
65
|
+
Some(&iface),
|
|
66
|
+
"get-open-session-args-schema",
|
|
67
|
+
)?;
|
|
68
|
+
let open = lookup_typed(
|
|
69
|
+
instance,
|
|
70
|
+
store.as_context_mut(),
|
|
71
|
+
Some(&iface),
|
|
72
|
+
"open-session",
|
|
73
|
+
)?;
|
|
74
|
+
let close = lookup_typed(
|
|
75
|
+
instance,
|
|
76
|
+
store.as_context_mut(),
|
|
77
|
+
Some(&iface),
|
|
78
|
+
"close-session",
|
|
79
|
+
)?;
|
|
80
|
+
|
|
81
|
+
Ok(Some(Self {
|
|
82
|
+
get_open_session_args_schema: get_schema,
|
|
83
|
+
open_session: open,
|
|
84
|
+
close_session: close,
|
|
85
|
+
}))
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
fn lookup_typed<Params, Return>(
|
|
90
|
+
instance: &Instance,
|
|
91
|
+
mut store: StoreContextMut<'_, HostState>,
|
|
92
|
+
iface: Option<&wasmtime::component::ComponentExportIndex>,
|
|
93
|
+
name: &str,
|
|
94
|
+
) -> Result<TypedFunc<Params, Return>>
|
|
95
|
+
where
|
|
96
|
+
Params: ComponentNamedList + Lower + Send + Sync + 'static,
|
|
97
|
+
Return: ComponentNamedList + Lift + Send + Sync + 'static,
|
|
98
|
+
{
|
|
99
|
+
let (_, idx) = instance
|
|
100
|
+
.get_export(&mut store, iface, name)
|
|
101
|
+
.ok_or_else(|| anyhow::anyhow!("session-provider missing export `{name}`"))?;
|
|
102
|
+
let func = instance
|
|
103
|
+
.get_func(&mut store, idx)
|
|
104
|
+
.ok_or_else(|| anyhow::anyhow!("session-provider export `{name}` is not a function"))?;
|
|
105
|
+
func.typed::<Params, Return>(store.as_context())
|
|
106
|
+
.map_err(|e| anyhow::anyhow!("session-provider `{name}` typed lookup failed: {e}"))
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// LocalizedString is currently only referenced indirectly through Error;
|
|
110
|
+
// keep the import alive for callers that grow handling here.
|
|
111
|
+
#[allow(dead_code)]
|
|
112
|
+
fn _localized_string_in_scope(_: LocalizedString) {}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|