Skip to content

Commit d61e355

Browse files
committed
feat(cli): host bindings for act:sessions/session-provider
Add host-side support for stateful components via the \`act:sessions/session-provider@0.1.0\` interface. Detection - The host's existing \`act-world\` (tool-provider only) bindings still work; components that additionally export session-provider are instantiated through the same world (extra exports are not a mismatch in wasmtime). - After instantiation, the host looks up session-provider exports manually via raw component-model APIs (\`Instance::get_export\` + \`Func::typed\`). Result is an \`Option<SessionProvider>\` carried alongside the actor. Runtime - \`runtime/sessions.rs\` — \`Session\` record (with ComponentType derives) and typed-func aliases. \`Error\` and \`LocalizedString\` are reused from the tool-provider bindings. - New \`ComponentRequest\` variants: \`GetOpenSessionArgsSchema\`, \`OpenSession\`, \`CloseSession\`. Each errors gracefully if the component does not export session-provider. - Actor tracks open session-ids and closes any still-open ones on channel-shutdown (ACT-SESSIONS §2.5). CLI - \`act session open-args-schema <component>\` - \`act session open <component> --args '{...}'\` — prints the session record (id + metadata) as JSON. - \`act session close <component> <session-id>\` Verified end-to-end against \`tests/fixtures/sessions-canary.wasm\`. ACT-HTTP \`/sessions\` endpoints and MCP virtual session tools land in follow-ups.
1 parent 661f0ae commit d61e355

3 files changed

Lines changed: 448 additions & 10 deletions

File tree

act-cli/src/main.rs

Lines changed: 155 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,35 @@ enum Command {
146146
#[arg(short = 'O', conflicts_with = "output")]
147147
output_from_ref: bool,
148148
},
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+
},
149178
}
150179

151180
#[tokio::main]
@@ -168,6 +197,11 @@ async fn main() -> Result<()> {
168197
opts.config.as_deref()
169198
}
170199
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+
},
171205
};
172206
let log_level = config::load_config(config_path)
173207
.ok()
@@ -210,6 +244,21 @@ async fn main() -> Result<()> {
210244
output,
211245
output_from_ref,
212246
} => 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+
},
213262
}
214263
}
215264

@@ -314,10 +363,10 @@ async fn prepare_component(
314363
let engine = runtime::create_engine()?;
315364
let wasm = runtime::load_component(&engine, &component_path)?;
316365
let linker = runtime::create_linker(&engine)?;
317-
let (instance, store) =
366+
let (instance, session_provider, store) =
318367
runtime::instantiate_component(&engine, &wasm, &linker, &preopens, &http, &fs, &info)
319368
.await?;
320-
let handle = runtime::spawn_component_actor(instance, store);
369+
let handle = runtime::spawn_component_actor(instance, session_provider, store);
321370

322371
tracing::debug!(name = %info.std.name, version = %info.std.version, "Component ready");
323372

@@ -458,6 +507,110 @@ async fn cmd_call(
458507
}
459508
}
460509

510+
// ── Session subcommands ────────────────────────────────────────────────────
511+
512+
async fn cmd_session_open_args_schema(component: ComponentRef, opts: CommonOpts) -> Result<()> {
513+
let pc = prepare_component(&component, &opts).await?;
514+
let (reply_tx, reply_rx) = tokio::sync::oneshot::channel();
515+
pc.handle
516+
.send(runtime::ComponentRequest::GetOpenSessionArgsSchema {
517+
metadata: pc.metadata.clone().into(),
518+
reply: reply_tx,
519+
})
520+
.await
521+
.map_err(|_| anyhow::anyhow!("component actor unavailable"))?;
522+
match reply_rx.await? {
523+
Ok(schema) => {
524+
// Pretty-print if it's valid JSON; otherwise print as-is.
525+
match serde_json::from_str::<serde_json::Value>(&schema) {
526+
Ok(v) => println!("{}", serde_json::to_string_pretty(&v)?),
527+
Err(_) => println!("{schema}"),
528+
}
529+
Ok(())
530+
}
531+
Err(runtime::ComponentError::Tool(te)) => {
532+
let ls = act_types::types::LocalizedString::from(&te.message);
533+
anyhow::bail!("{}: {}", te.kind, ls.any_text());
534+
}
535+
Err(runtime::ComponentError::Internal(e)) => Err(e),
536+
}
537+
}
538+
539+
async fn cmd_session_open(component: ComponentRef, args: String, opts: CommonOpts) -> Result<()> {
540+
let pc = prepare_component(&component, &opts).await?;
541+
542+
// Args are a JSON object; convert to metadata-shaped (key, cbor) pairs.
543+
let args_value: serde_json::Value =
544+
serde_json::from_str(&args).context("invalid --args JSON")?;
545+
let serde_json::Value::Object(args_obj) = args_value else {
546+
anyhow::bail!("--args must be a JSON object");
547+
};
548+
let mut wit_args: Vec<(String, Vec<u8>)> = Vec::with_capacity(args_obj.len());
549+
for (key, value) in args_obj {
550+
let cbor = act_types::cbor::json_to_cbor(&value).context("encoding arg as CBOR")?;
551+
wit_args.push((key, cbor));
552+
}
553+
554+
let (reply_tx, reply_rx) = tokio::sync::oneshot::channel();
555+
pc.handle
556+
.send(runtime::ComponentRequest::OpenSession {
557+
args: wit_args,
558+
metadata: pc.metadata.clone().into(),
559+
reply: reply_tx,
560+
})
561+
.await
562+
.map_err(|_| anyhow::anyhow!("component actor unavailable"))?;
563+
564+
match reply_rx.await? {
565+
Ok(session) => {
566+
// Re-emit metadata as JSON object for human consumption.
567+
let metadata_json: serde_json::Map<String, serde_json::Value> = session
568+
.metadata
569+
.iter()
570+
.filter_map(|(k, v)| {
571+
let val = act_types::cbor::cbor_to_json(v).ok()?;
572+
Some((k.clone(), val))
573+
})
574+
.collect();
575+
let out = serde_json::json!({
576+
"id": session.id,
577+
"metadata": metadata_json,
578+
});
579+
println!("{}", serde_json::to_string_pretty(&out)?);
580+
Ok(())
581+
}
582+
Err(runtime::ComponentError::Tool(te)) => {
583+
let ls = act_types::types::LocalizedString::from(&te.message);
584+
anyhow::bail!("{}: {}", te.kind, ls.any_text());
585+
}
586+
Err(runtime::ComponentError::Internal(e)) => Err(e),
587+
}
588+
}
589+
590+
async fn cmd_session_close(
591+
component: ComponentRef,
592+
session_id: String,
593+
opts: CommonOpts,
594+
) -> Result<()> {
595+
let pc = prepare_component(&component, &opts).await?;
596+
let (reply_tx, reply_rx) = tokio::sync::oneshot::channel();
597+
pc.handle
598+
.send(runtime::ComponentRequest::CloseSession {
599+
session_id,
600+
reply: reply_tx,
601+
})
602+
.await
603+
.map_err(|_| anyhow::anyhow!("component actor unavailable"))?;
604+
match reply_rx.await? {
605+
Ok(()) => Ok(()),
606+
Err(runtime::ComponentError::Tool(te)) => {
607+
let ls = act_types::types::LocalizedString::from(&te.message);
608+
anyhow::bail!("{}: {}", te.kind, ls.any_text());
609+
}
610+
Err(runtime::ComponentError::Internal(e)) => Err(e),
611+
}
612+
}
613+
461614
async fn cmd_info(
462615
component: ComponentRef,
463616
show_tools: bool,

0 commit comments

Comments
 (0)