act-cli 0.6.0__tar.gz → 0.7.0__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.
Files changed (28) hide show
  1. {act_cli-0.6.0 → act_cli-0.7.0}/Cargo.lock +4 -4
  2. {act_cli-0.6.0 → act_cli-0.7.0}/Cargo.toml +2 -2
  3. {act_cli-0.6.0 → act_cli-0.7.0}/PKG-INFO +1 -1
  4. {act_cli-0.6.0 → act_cli-0.7.0}/act-cli/src/http.rs +137 -0
  5. {act_cli-0.6.0 → act_cli-0.7.0}/act-cli/src/main.rs +160 -3
  6. {act_cli-0.6.0 → act_cli-0.7.0}/act-cli/src/rmcp_bridge.rs +249 -16
  7. {act_cli-0.6.0 → act_cli-0.7.0}/act-cli/src/runtime/mod.rs +181 -8
  8. act_cli-0.7.0/act-cli/src/runtime/sessions.rs +112 -0
  9. {act_cli-0.6.0 → act_cli-0.7.0}/README.md +0 -0
  10. {act_cli-0.6.0 → act_cli-0.7.0}/act-cli/Cargo.toml +0 -0
  11. {act_cli-0.6.0 → act_cli-0.7.0}/act-cli/README.md +0 -0
  12. {act_cli-0.6.0 → act_cli-0.7.0}/act-cli/build.rs +0 -0
  13. {act_cli-0.6.0 → act_cli-0.7.0}/act-cli/src/config.rs +0 -0
  14. {act_cli-0.6.0 → act_cli-0.7.0}/act-cli/src/format.rs +0 -0
  15. {act_cli-0.6.0 → act_cli-0.7.0}/act-cli/src/resolve.rs +0 -0
  16. {act_cli-0.6.0 → act_cli-0.7.0}/act-cli/src/runtime/bindings/mod.rs +0 -0
  17. {act_cli-0.6.0 → act_cli-0.7.0}/act-cli/src/runtime/effective.rs +0 -0
  18. {act_cli-0.6.0 → act_cli-0.7.0}/act-cli/src/runtime/fs_matcher.rs +0 -0
  19. {act_cli-0.6.0 → act_cli-0.7.0}/act-cli/src/runtime/fs_policy.rs +0 -0
  20. {act_cli-0.6.0 → act_cli-0.7.0}/act-cli/src/runtime/http_client.rs +0 -0
  21. {act_cli-0.6.0 → act_cli-0.7.0}/act-cli/src/runtime/http_policy.rs +0 -0
  22. {act_cli-0.6.0 → act_cli-0.7.0}/act-cli/src/runtime/network.rs +0 -0
  23. {act_cli-0.6.0 → act_cli-0.7.0}/act-cli/wit/deps/act-core/act-core.wit +0 -0
  24. {act_cli-0.6.0 → act_cli-0.7.0}/act-cli/wit/deps/act-tools/act-tools.wit +0 -0
  25. {act_cli-0.6.0 → act_cli-0.7.0}/act-cli/wit/deps.lock +0 -0
  26. {act_cli-0.6.0 → act_cli-0.7.0}/act-cli/wit/deps.toml +0 -0
  27. {act_cli-0.6.0 → act_cli-0.7.0}/act-cli/wit/world.wit +0 -0
  28. {act_cli-0.6.0 → act_cli-0.7.0}/pyproject.toml +0 -0
@@ -4,7 +4,7 @@ version = 4
4
4
 
5
5
  [[package]]
6
6
  name = "act-build"
7
- version = "0.6.0"
7
+ version = "0.7.0"
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.6.0"
28
+ version = "0.7.0"
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.5.0"
73
+ version = "0.7.0"
74
74
  source = "registry+https://github.com/rust-lang/crates.io-index"
75
- checksum = "8061ce793866b6430317d2f06f9be9a0db3d08127ad398f133bbd0537824e5ca"
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.0"
6
+ version = "0.7.0"
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.5"
14
+ act-types = "0.7"
15
15
  wasmparser = "0.247.0"
16
16
 
17
17
  [profile.release]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: act-cli
3
- Version: 0.6.0
3
+ Version: 0.7.0
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Environment :: Console
6
6
  Classifier: Intended Audience :: Developers
@@ -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,35 @@ enum Command {
146
146
  #[arg(short = 'O', conflicts_with = "output")]
147
147
  output_from_ref: bool,
148
148
  },
149
+ /// Manage component sessions (`act:sessions/session-provider`).
150
+ #[command(subcommand)]
151
+ Session(SessionCommand),
152
+ }
153
+
154
+ #[derive(clap::Subcommand)]
155
+ enum SessionCommand {
156
+ /// Print the JSON Schema for `open-session` args.
157
+ OpenArgsSchema {
158
+ component: ComponentRef,
159
+ #[command(flatten)]
160
+ opts: CommonOpts,
161
+ },
162
+ /// Open a new session, print the session record (id + metadata) as JSON.
163
+ Open {
164
+ component: ComponentRef,
165
+ /// JSON object with session-args.
166
+ #[arg(long, default_value = "{}")]
167
+ args: String,
168
+ #[command(flatten)]
169
+ opts: CommonOpts,
170
+ },
171
+ /// Close a session by id.
172
+ Close {
173
+ component: ComponentRef,
174
+ session_id: String,
175
+ #[command(flatten)]
176
+ opts: CommonOpts,
177
+ },
149
178
  }
150
179
 
151
180
  #[tokio::main]
@@ -168,6 +197,11 @@ async fn main() -> Result<()> {
168
197
  opts.config.as_deref()
169
198
  }
170
199
  Command::Skill { .. } | Command::Pull { .. } => None,
200
+ Command::Session(sub) => match sub {
201
+ SessionCommand::OpenArgsSchema { opts, .. }
202
+ | SessionCommand::Open { opts, .. }
203
+ | SessionCommand::Close { opts, .. } => opts.config.as_deref(),
204
+ },
171
205
  };
172
206
  let log_level = config::load_config(config_path)
173
207
  .ok()
@@ -210,6 +244,21 @@ async fn main() -> Result<()> {
210
244
  output,
211
245
  output_from_ref,
212
246
  } => cmd_pull(reference, output, output_from_ref).await,
247
+ Command::Session(sub) => match sub {
248
+ SessionCommand::OpenArgsSchema { component, opts } => {
249
+ cmd_session_open_args_schema(component, opts).await
250
+ }
251
+ SessionCommand::Open {
252
+ component,
253
+ args,
254
+ opts,
255
+ } => cmd_session_open(component, args, opts).await,
256
+ SessionCommand::Close {
257
+ component,
258
+ session_id,
259
+ opts,
260
+ } => cmd_session_close(component, session_id, opts).await,
261
+ },
213
262
  }
214
263
  }
215
264
 
@@ -278,6 +327,8 @@ struct PreparedComponent {
278
327
  info: runtime::ComponentInfo,
279
328
  handle: runtime::ComponentHandle,
280
329
  metadata: runtime::Metadata,
330
+ /// Whether the component exports `act:sessions/session-provider`.
331
+ has_sessions: bool,
281
332
  }
282
333
 
283
334
  /// Resolve, load, and instantiate a component. Returns a running actor handle.
@@ -314,10 +365,11 @@ async fn prepare_component(
314
365
  let engine = runtime::create_engine()?;
315
366
  let wasm = runtime::load_component(&engine, &component_path)?;
316
367
  let linker = runtime::create_linker(&engine)?;
317
- let (instance, store) =
368
+ let (instance, session_provider, store) =
318
369
  runtime::instantiate_component(&engine, &wasm, &linker, &preopens, &http, &fs, &info)
319
370
  .await?;
320
- let handle = runtime::spawn_component_actor(instance, store);
371
+ let has_sessions = session_provider.is_some();
372
+ let handle = runtime::spawn_component_actor(instance, session_provider, store);
321
373
 
322
374
  tracing::debug!(name = %info.std.name, version = %info.std.version, "Component ready");
323
375
 
@@ -325,6 +377,7 @@ async fn prepare_component(
325
377
  info,
326
378
  handle,
327
379
  metadata,
380
+ has_sessions,
328
381
  })
329
382
  }
330
383
 
@@ -356,7 +409,7 @@ async fn cmd_run(
356
409
 
357
410
  if mcp {
358
411
  let pc = prepare_component(&component, &opts).await?;
359
- return rmcp_bridge::run_stdio(pc.info, pc.handle, pc.metadata).await;
412
+ return rmcp_bridge::run_stdio(pc.info, pc.handle, pc.metadata, pc.has_sessions).await;
360
413
  }
361
414
 
362
415
  if http || listen.is_some() {
@@ -458,6 +511,110 @@ async fn cmd_call(
458
511
  }
459
512
  }
460
513
 
514
+ // ── Session subcommands ────────────────────────────────────────────────────
515
+
516
+ async fn cmd_session_open_args_schema(component: ComponentRef, opts: CommonOpts) -> Result<()> {
517
+ let pc = prepare_component(&component, &opts).await?;
518
+ let (reply_tx, reply_rx) = tokio::sync::oneshot::channel();
519
+ pc.handle
520
+ .send(runtime::ComponentRequest::GetOpenSessionArgsSchema {
521
+ metadata: pc.metadata.clone().into(),
522
+ reply: reply_tx,
523
+ })
524
+ .await
525
+ .map_err(|_| anyhow::anyhow!("component actor unavailable"))?;
526
+ match reply_rx.await? {
527
+ Ok(schema) => {
528
+ // Pretty-print if it's valid JSON; otherwise print as-is.
529
+ match serde_json::from_str::<serde_json::Value>(&schema) {
530
+ Ok(v) => println!("{}", serde_json::to_string_pretty(&v)?),
531
+ Err(_) => println!("{schema}"),
532
+ }
533
+ Ok(())
534
+ }
535
+ Err(runtime::ComponentError::Tool(te)) => {
536
+ let ls = act_types::types::LocalizedString::from(&te.message);
537
+ anyhow::bail!("{}: {}", te.kind, ls.any_text());
538
+ }
539
+ Err(runtime::ComponentError::Internal(e)) => Err(e),
540
+ }
541
+ }
542
+
543
+ async fn cmd_session_open(component: ComponentRef, args: String, opts: CommonOpts) -> Result<()> {
544
+ let pc = prepare_component(&component, &opts).await?;
545
+
546
+ // Args are a JSON object; convert to metadata-shaped (key, cbor) pairs.
547
+ let args_value: serde_json::Value =
548
+ serde_json::from_str(&args).context("invalid --args JSON")?;
549
+ let serde_json::Value::Object(args_obj) = args_value else {
550
+ anyhow::bail!("--args must be a JSON object");
551
+ };
552
+ let mut wit_args: Vec<(String, Vec<u8>)> = Vec::with_capacity(args_obj.len());
553
+ for (key, value) in args_obj {
554
+ let cbor = act_types::cbor::json_to_cbor(&value).context("encoding arg as CBOR")?;
555
+ wit_args.push((key, cbor));
556
+ }
557
+
558
+ let (reply_tx, reply_rx) = tokio::sync::oneshot::channel();
559
+ pc.handle
560
+ .send(runtime::ComponentRequest::OpenSession {
561
+ args: wit_args,
562
+ metadata: pc.metadata.clone().into(),
563
+ reply: reply_tx,
564
+ })
565
+ .await
566
+ .map_err(|_| anyhow::anyhow!("component actor unavailable"))?;
567
+
568
+ match reply_rx.await? {
569
+ Ok(session) => {
570
+ // Re-emit metadata as JSON object for human consumption.
571
+ let metadata_json: serde_json::Map<String, serde_json::Value> = session
572
+ .metadata
573
+ .iter()
574
+ .filter_map(|(k, v)| {
575
+ let val = act_types::cbor::cbor_to_json(v).ok()?;
576
+ Some((k.clone(), val))
577
+ })
578
+ .collect();
579
+ let out = serde_json::json!({
580
+ "id": session.id,
581
+ "metadata": metadata_json,
582
+ });
583
+ println!("{}", serde_json::to_string_pretty(&out)?);
584
+ Ok(())
585
+ }
586
+ Err(runtime::ComponentError::Tool(te)) => {
587
+ let ls = act_types::types::LocalizedString::from(&te.message);
588
+ anyhow::bail!("{}: {}", te.kind, ls.any_text());
589
+ }
590
+ Err(runtime::ComponentError::Internal(e)) => Err(e),
591
+ }
592
+ }
593
+
594
+ async fn cmd_session_close(
595
+ component: ComponentRef,
596
+ session_id: String,
597
+ opts: CommonOpts,
598
+ ) -> Result<()> {
599
+ let pc = prepare_component(&component, &opts).await?;
600
+ let (reply_tx, reply_rx) = tokio::sync::oneshot::channel();
601
+ pc.handle
602
+ .send(runtime::ComponentRequest::CloseSession {
603
+ session_id,
604
+ reply: reply_tx,
605
+ })
606
+ .await
607
+ .map_err(|_| anyhow::anyhow!("component actor unavailable"))?;
608
+ match reply_rx.await? {
609
+ Ok(()) => Ok(()),
610
+ Err(runtime::ComponentError::Tool(te)) => {
611
+ let ls = act_types::types::LocalizedString::from(&te.message);
612
+ anyhow::bail!("{}: {}", te.kind, ls.any_text());
613
+ }
614
+ Err(runtime::ComponentError::Internal(e)) => Err(e),
615
+ }
616
+ }
617
+
461
618
  async fn cmd_info(
462
619
  component: ComponentRef,
463
620
  show_tools: bool,
@@ -1,16 +1,27 @@
1
1
  use crate::runtime;
2
2
  use act_types::cbor;
3
- use act_types::constants::{ERR_CAPABILITY_DENIED, ERR_INVALID_ARGS, ERR_NOT_FOUND};
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
- let mut arguments = request
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
- _context: rmcp::service::RequestContext<rmcp::RoleServer>,
285
- ) -> impl std::future::Future<Output = Result<rmcp::model::CallToolResult, rmcp::ErrorData>>
286
- + Send
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 and the store.
304
- /// Component info is read from custom sections (no instantiation needed for that).
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<(ActWorld, Store<HostState>)> {
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
- let instance = ActWorld::instantiate_async(&mut store, component, linker)
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((instance, store))
362
+ Ok((act_world, session_provider, store))
320
363
  }
321
364
 
322
- /// Spawn the component actor task. Owns the Store and ActWorld.
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(instance: ActWorld, mut store: Store<HostState>) -> ComponentHandle {
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