Skip to main content

runx_runtime/execution/
skill_front.rs

1// rust-style-allow: large-file - the skill front owns source-type dispatch,
2// domain-act frame construction, and shared sealed-output projection for all
3// first-class skill runners.
4//! The skill front: compiles a skill-run request into an execution (cli-tool,
5//! agent, or graph runner) and seals it through the shared act engine. This is
6//! one of the source-type "fronts" from `plans/governed-execution-layer.md`;
7//! the act engine (`execution::runner`) owns admit -> execute -> seal.
8
9use std::fs;
10use std::path::Path;
11
12use runx_contracts::{ClosureDisposition, JsonNumber, JsonObject, JsonValue};
13use runx_parser::{ActDeclaration, SkillRunnerDefinition, SkillRunnerManifest};
14use serde::Serialize;
15use thiserror::Error;
16
17use crate::RuntimeError;
18use crate::adapter::{InvocationStatus, SkillInvocation, SkillOutput};
19use crate::agent_invocation::{AgentActInvocationSourceType, agent_act_resolution_request};
20use crate::effects::RuntimeEffectRegistry;
21use crate::execution::disposition::agent_answer_disposition_or_closed;
22use crate::execution::orchestrator::SkillRunRequest;
23use crate::execution::output_projection::project_step_output;
24use crate::receipts::signing::strip_receipt_signing_env;
25use crate::receipts::store::ReceiptStoreError;
26use crate::receipts::{
27    DomainActFrame, RuntimeReceiptSignatureConfig, StepSeal, StepSealClosure, seal_step,
28};
29use crate::services::{ReceiptServices, WorkspaceEnv};
30
31mod agent;
32mod graph;
33mod graph_state;
34mod inline_harness;
35mod runner_manifest;
36
37#[cfg(feature = "cli-tool")]
38pub(crate) use self::graph::SkillRunGraphAdapter;
39pub(crate) use self::graph::graph_domain_act_receipt;
40pub(crate) use self::inline_harness::run_inline_harness_with_effects;
41
42use self::agent::execute_agent_skill_run;
43use self::graph::execute_graph_skill_run;
44use self::runner_manifest::{
45    execute_cli_tool_skill_run, load_runner_manifest, resolve_skill_dir, runner_invocation,
46    selected_runner,
47};
48
49// The run-result envelope schema. The string keeps the `skill_run` name, a stable
50// wire contract consumed by the CLI/SDK/cloud, even though the module is now
51// `skill_front`; renaming the wire schema is a separate, versioned change.
52const SKILL_RUN_SCHEMA: &str = "runx.skill_run.v1";
53const GRAPH_SKILL_STATE_SCHEMA: &str = "runx.graph_skill_state.v1";
54
55#[derive(Debug, Error)]
56pub enum SkillRunError {
57    #[error("skill run failed: {0}")]
58    Invalid(String),
59    #[error(transparent)]
60    Runtime(#[from] RuntimeError),
61    #[error(transparent)]
62    ReceiptStore(#[from] ReceiptStoreError),
63}
64
65/// Optional, non-default knobs for a single skill run.
66///
67/// `execute_skill_run` keeps today's behavior (default runner, file-based
68/// answers). The inline harness needs two extra capabilities without touching
69/// the 35+ `SkillRunRequest` construction sites: select a named runner, and
70/// seed answers inline for a single fresh pass (distinct from the `answers_path`
71/// resume channel). Both default to "off", so `execute_skill_run` and every CLI
72/// path are unchanged.
73#[derive(Clone, Debug, Default)]
74pub(crate) struct SkillRunOverrides {
75    /// Select a runner by name instead of the manifest default.
76    pub(crate) runner: Option<String>,
77    /// Answers seeded for a single fresh run, keyed by resolution request id.
78    /// Drives agent/graph runs to completion in one pass; `None` keeps the
79    /// `answers_path` (resume-from-checkpoint) behavior.
80    pub(crate) seeded_answers: Option<JsonObject>,
81}
82
83pub(crate) fn execute_skill_run_with_effects(
84    request: &SkillRunRequest,
85    effects: &RuntimeEffectRegistry,
86) -> Result<JsonValue, SkillRunError> {
87    execute_skill_run_with_overrides(request, &SkillRunOverrides::default(), effects)
88}
89
90pub(crate) fn execute_skill_run_with_overrides(
91    request: &SkillRunRequest,
92    overrides: &SkillRunOverrides,
93    effects: &RuntimeEffectRegistry,
94) -> Result<JsonValue, SkillRunError> {
95    let raw_workspace = WorkspaceEnv::new(request.env.clone(), request.cwd.clone());
96    let receipts = ReceiptServices::from_env_or_local_development(raw_workspace.env())
97        .map_err(|error| SkillRunError::Invalid(error.to_string()))?;
98    let mut runtime_env = request.env.clone();
99    strip_receipt_signing_env(&mut runtime_env);
100    let workspace = WorkspaceEnv::new(runtime_env, request.cwd.clone());
101    let skill_dir = resolve_skill_dir(&request.skill_path)?;
102    let manifest = load_runner_manifest(&skill_dir)?;
103    let runner = selected_runner(&manifest, overrides.runner.as_deref())?;
104    let skill_env = workspace.skill_env_for_skill(&skill_dir);
105    if runner.source.source_type == runx_parser::SourceKind::CliTool
106        && request.local_credential.is_some()
107    {
108        return Err(invalid(
109            "local credential process-env delivery is not supported for cli-tool runners",
110        ));
111    }
112    let invocation = runner_invocation(
113        &skill_dir,
114        runner,
115        &request.inputs,
116        &skill_env,
117        request.local_credential.as_ref(),
118    )?;
119    if runner.source.source_type == runx_parser::SourceKind::CliTool {
120        return execute_cli_tool_skill_run(
121            request, &workspace, &receipts, &manifest, runner, invocation,
122        );
123    }
124    if runner.source.source_type == runx_parser::SourceKind::Graph {
125        return execute_graph_skill_run(
126            request, overrides, effects, &workspace, &receipts, &manifest, runner,
127        );
128    }
129
130    execute_agent_skill_run(
131        request, overrides, &workspace, &receipts, &manifest, runner, invocation,
132    )
133}
134
135/// Aggregate result of running a skill's declared inline harness (the
136/// `harness.cases` in its runner manifest). Mirrors the publish-harness summary
137/// the registry publish flow records: a status, counts, the per-case assertion
138/// failures, the case names, the receipts each case sealed, and how many cases
139/// exercised a graph (the stable-maturity graph-integration signal).
140#[derive(Clone, Debug, Serialize)]
141pub struct InlineHarnessReport {
142    pub status: &'static str,
143    pub case_count: usize,
144    pub assertion_error_count: usize,
145    pub assertion_errors: Vec<String>,
146    pub case_names: Vec<String>,
147    pub receipt_ids: Vec<String>,
148    pub graph_case_count: usize,
149}
150
151impl InlineHarnessReport {
152    fn not_declared() -> Self {
153        Self {
154            status: "not_declared",
155            case_count: 0,
156            assertion_error_count: 0,
157            assertion_errors: Vec::new(),
158            case_names: Vec::new(),
159            receipt_ids: Vec::new(),
160            graph_case_count: 0,
161        }
162    }
163}
164
165fn agent_invocation_source_type(
166    value: &str,
167) -> Result<AgentActInvocationSourceType, SkillRunError> {
168    AgentActInvocationSourceType::from_contract_value(value)
169        .ok_or_else(|| invalid(format!("unsupported agent source type {value}")))
170}
171
172fn agent_request(
173    invocation: &SkillInvocation,
174    source_type: AgentActInvocationSourceType,
175) -> Result<JsonValue, SkillRunError> {
176    contract_json_value(&agent_act_resolution_request(invocation, source_type)?)
177}
178
179fn needs_agent_output(run_id: &str, request_id: &str, request: JsonValue) -> JsonObject {
180    let mut output = JsonObject::new();
181    output.insert(
182        "schema".to_owned(),
183        JsonValue::String(SKILL_RUN_SCHEMA.to_owned()),
184    );
185    output.insert(
186        "status".to_owned(),
187        JsonValue::String("needs_agent".to_owned()),
188    );
189    output.insert("run_id".to_owned(), JsonValue::String(run_id.to_owned()));
190    output.insert(
191        "requests".to_owned(),
192        JsonValue::Array(vec![request_for_public_loop(request_id, request)]),
193    );
194    output
195}
196
197fn request_for_public_loop(request_id: &str, request: JsonValue) -> JsonValue {
198    let mut object = match request {
199        JsonValue::Object(object) => object,
200        _ => JsonObject::new(),
201    };
202    object.insert("id".to_owned(), JsonValue::String(request_id.to_owned()));
203    object
204        .entry("kind".to_owned())
205        .or_insert_with(|| JsonValue::String("agent_act".to_owned()));
206    JsonValue::Object(object)
207}
208
209fn read_answer(path: &Path, request_id: &str) -> Result<JsonValue, SkillRunError> {
210    let raw = fs::read_to_string(path)
211        .map_err(|source| RuntimeError::io(format!("reading {}", path.display()), source))?;
212    let value = serde_json::from_str::<JsonValue>(&raw).map_err(|source| {
213        RuntimeError::json(format!("parsing answers file {}", path.display()), source)
214    })?;
215    let answers = match &value {
216        JsonValue::Object(object) => match object.get("answers") {
217            Some(JsonValue::Object(nested)) => nested,
218            _ => object,
219        },
220        _ => return Err(invalid("answers file must be a JSON object")),
221    };
222    answers
223        .get(request_id)
224        .cloned()
225        .ok_or_else(|| invalid(format!("answers file did not include {request_id}")))
226}
227
228fn seal_skill_answer(
229    run_id: &str,
230    runner: &SkillRunnerDefinition,
231    stdout: &str,
232    disposition: ClosureDisposition,
233    signature_config: &RuntimeReceiptSignatureConfig,
234) -> Result<runx_contracts::Receipt, SkillRunError> {
235    let disposition_label = disposition.label();
236    let succeeded = disposition == ClosureDisposition::Closed;
237    let status = if succeeded {
238        InvocationStatus::Success
239    } else {
240        InvocationStatus::Failure
241    };
242    let skill_output = SkillOutput {
243        status,
244        stdout: stdout.to_owned(),
245        stderr: if succeeded {
246            String::new()
247        } else {
248            format!("agent act closed with {disposition_label}")
249        },
250        exit_code: succeeded.then_some(0),
251        duration_ms: 0,
252        metadata: JsonObject::new(),
253    };
254    seal_skill_output(
255        run_id,
256        runner,
257        &skill_output,
258        disposition,
259        format!("agent_act_{disposition_label}"),
260        format!("agent act closed with {disposition_label}"),
261        signature_config,
262    )
263}
264
265/// Build the domain act frame for a governed turn when its runner declares an
266/// `act:` block: the trusted mapping from the driver's pinned beat inputs and the
267/// model's reason text to the receipt's act, decision, and authority. Returns
268/// `None` for runners without an `act:` block (sealed generically, exactly as
269/// before). The model supplies only the reason prose; every structural field is
270/// read from the runner declaration and the trusted inputs, never the model.
271fn domain_act_frame(
272    invocation: &SkillInvocation,
273    answer: &JsonValue,
274    governed_effect: Option<&JsonValue>,
275) -> Option<DomainActFrame> {
276    let act = invocation.source.act.as_ref()?;
277    // Promote the delivered credential into the act's held authority: a governed
278    // turn's receipt records the grants it actually carried, not just the
279    // declared scope.
280    let authority_grant_refs = invocation
281        .credential_delivery
282        .public_observation()
283        .map(|observation| observation.credential_refs.clone())
284        .unwrap_or_default();
285    build_domain_act_frame(
286        act,
287        &invocation.inputs,
288        answer,
289        governed_effect,
290        authority_grant_refs,
291    )
292}
293
294/// The core of [`domain_act_frame`], reusable by the graph path: build the domain
295/// act frame from a declared `act:` block, the trusted run inputs, the model's
296/// authored reason source, and the real governed effect.
297// rust-style-allow: long-function - act-frame construction is intentionally one
298// branch table so each declared field, input fallback, and governed-effect
299// reference is visible in one receipt-shaping pass.
300fn build_domain_act_frame(
301    act: &ActDeclaration,
302    inputs: &runx_contracts::JsonObject,
303    reason_source: &JsonValue,
304    governed_effect: Option<&JsonValue>,
305    authority_grant_refs: Vec<runx_contracts::Reference>,
306) -> Option<DomainActFrame> {
307    use runx_contracts::{
308        ActForm, AuthorityAttenuation, AuthoritySubsetProof, AuthorityTerm, DecisionChoice,
309        Reference, ReferenceType,
310    };
311
312    // A declared field may be a static literal (`form: review`) or driver-pinned
313    // from an input (`form_from: act_form` names the input key). The driver-pinned
314    // input wins, so one generic skill serves every beat.
315    let resolve = |from_key: Option<&str>, literal: Option<&str>| -> Option<String> {
316        from_key
317            .and_then(|key| inputs.get(key))
318            .and_then(JsonValue::as_str)
319            .or(literal)
320            .map(str::trim)
321            .filter(|value| !value.is_empty())
322            .map(str::to_owned)
323    };
324
325    let form = match resolve(act.form_from.as_deref(), act.form.as_deref())
326        .as_deref()
327        .unwrap_or("observation")
328    {
329        "revision" => ActForm::Revision,
330        "reply" => ActForm::Reply,
331        "review" => ActForm::Review,
332        "verification" => ActForm::Verification,
333        _ => ActForm::Observation,
334    };
335    let purpose = resolve(act.purpose_from.as_deref(), act.purpose.as_deref())?;
336    let legitimacy = resolve(act.legitimacy_from.as_deref(), act.legitimacy.as_deref())
337        .unwrap_or_else(|| "Held the declared authority for this act".to_owned());
338
339    // The single model-authored field: the human reason text.
340    let reason = act
341        .reason_from
342        .as_deref()
343        .and_then(|key| reason_source.as_object().and_then(|object| object.get(key)))
344        .and_then(JsonValue::as_str)
345        .map(str::trim)
346        .filter(|value| !value.is_empty())
347        .map_or_else(|| purpose.clone(), str::to_owned);
348
349    // Resolve a trusted input value (a uri) named by the act mapping into a ref.
350    let input_ref = |map_key: Option<&str>, reference_type: ReferenceType| -> Option<Reference> {
351        let uri = inputs.get(map_key?).and_then(JsonValue::as_str)?.trim();
352        (!uri.is_empty()).then(|| Reference::with_uri(reference_type, uri.to_owned()))
353    };
354
355    let decision_choice = act
356        .decision_from
357        .as_deref()
358        .and_then(|key| inputs.get(key))
359        .and_then(JsonValue::as_str)
360        .and_then(map_decision_choice)
361        .unwrap_or(DecisionChoice::Close);
362
363    let reference_type = match act.effect_type.as_deref().unwrap_or("artifact") {
364        "act" => ReferenceType::Act,
365        "tracking_item" => ReferenceType::TrackingItem,
366        "receipt" => ReferenceType::Receipt,
367        "provider" | "provider_event" => ReferenceType::ProviderEvent,
368        "provider_thread" => ReferenceType::ProviderThread,
369        "provider_comment" => ReferenceType::ProviderComment,
370        "github_issue" => ReferenceType::GithubIssue,
371        "external_url" => ReferenceType::ExternalUrl,
372        _ => ReferenceType::Artifact,
373    };
374    let prefix = resolve(
375        act.effect_prefix_from.as_deref(),
376        act.effect_prefix.as_deref(),
377    )
378    .unwrap_or_default();
379    let effect_ref = |id: &str| {
380        let id = id.trim();
381        (!id.is_empty())
382            .then(|| Reference::with_uri(reference_type.clone(), format!("{prefix}{id}")))
383    };
384    let mut artifact_refs = Vec::new();
385    if let Some(reference) = act
386        .effect_from_input
387        .as_deref()
388        .and_then(|key| inputs.get(key))
389        .and_then(JsonValue::as_str)
390        .and_then(effect_ref)
391    {
392        artifact_refs.push(reference);
393    }
394    if let Some(reference) = governed_effect
395        .and_then(|effect| {
396            let field = resolve(act.effect_field_from.as_deref(), act.effect_from.as_deref())?;
397            effect
398                .as_object()
399                .and_then(|object| object.get(field.as_str()))
400                .and_then(JsonValue::as_str)
401                .and_then(effect_ref)
402        })
403        .filter(|reference| !artifact_refs.contains(reference))
404    {
405        artifact_refs.push(reference);
406    }
407
408    // Charter attenuation, read from driver-pinned inputs (the model never sets
409    // authority). The member's child term, the parent charter reference, and the
410    // subset proof each ride a trusted input key named by the act declaration.
411    // Attenuation is recorded only when both a parent and a proof are present; a
412    // term without them is a root and carries no proof, as the receipt verifier
413    // requires.
414    let authority_terms = act
415        .authority_term_from
416        .as_deref()
417        .and_then(|key| inputs.get(key))
418        .and_then(|value| serde_json::to_value(value).ok())
419        .and_then(|value| serde_json::from_value::<AuthorityTerm>(value).ok())
420        .map(|term| vec![term])
421        .unwrap_or_default();
422    let parent_authority_ref = act
423        .authority_parent_from
424        .as_deref()
425        .and_then(|key| inputs.get(key))
426        .and_then(|value| serde_json::to_value(value).ok())
427        .and_then(|value| serde_json::from_value::<Reference>(value).ok());
428    let subset_proof = act
429        .authority_subset_proof_from
430        .as_deref()
431        .and_then(|key| inputs.get(key))
432        .and_then(|value| serde_json::to_value(value).ok())
433        .and_then(|value| serde_json::from_value::<AuthoritySubsetProof>(value).ok());
434    let authority_attenuation = match (parent_authority_ref, subset_proof) {
435        (Some(parent), Some(proof)) => Some(AuthorityAttenuation {
436            parent_authority_ref: Some(parent),
437            subset_proof: Some(proof),
438        }),
439        _ => None,
440    };
441
442    Some(DomainActFrame {
443        form,
444        purpose: purpose.into(),
445        legitimacy: legitimacy.into(),
446        summary: reason.clone().into(),
447        target_refs: input_ref(act.target_from.as_deref(), ReferenceType::TrackingItem)
448            .into_iter()
449            .collect(),
450        artifact_refs,
451        decision_choice,
452        decision_summary: reason.into(),
453        actor_ref: input_ref(act.actor_from.as_deref(), ReferenceType::Principal)
454            .unwrap_or_else(|| Reference::runx(ReferenceType::Principal, "local_runtime")),
455        authority_grant_refs,
456        authority_scope_refs: input_ref(act.authority_from.as_deref(), ReferenceType::Grant)
457            .into_iter()
458            .collect(),
459        authority_terms,
460        authority_attenuation,
461        previous: input_ref(act.previous_from.as_deref(), ReferenceType::Receipt),
462    })
463}
464
465/// Map a driver-pinned decision word onto the receipt's `DecisionChoice`.
466fn map_decision_choice(value: &str) -> Option<runx_contracts::DecisionChoice> {
467    use runx_contracts::DecisionChoice;
468    match value.trim().to_ascii_lowercase().as_str() {
469        "decline" | "reject" | "rejected" | "deny" | "denied" => Some(DecisionChoice::Decline),
470        "close" | "accept" | "accepted" | "approve" | "approved" | "paid" | "settle"
471        | "settled" => Some(DecisionChoice::Close),
472        "continue" | "claim" | "claimed" | "deliver" | "delivered" => {
473            Some(DecisionChoice::Continue)
474        }
475        "defer" | "deferred" => Some(DecisionChoice::Defer),
476        "escalate" | "escalated" => Some(DecisionChoice::Escalate),
477        "monitor" | "monitored" => Some(DecisionChoice::Monitor),
478        _ => None,
479    }
480}
481
482fn seal_skill_output(
483    run_id: &str,
484    runner: &SkillRunnerDefinition,
485    output: &SkillOutput,
486    disposition: ClosureDisposition,
487    reason_code: String,
488    summary: String,
489    signature_config: &RuntimeReceiptSignatureConfig,
490) -> Result<runx_contracts::Receipt, SkillRunError> {
491    let graph_name = identifier_segment(run_id);
492    let step_id = identifier_segment(&runner.name);
493    let projection = project_step_output(output);
494    Ok(seal_step(
495        StepSeal {
496            graph_name: &graph_name,
497            step_id: &step_id,
498            attempt: 1,
499            output,
500            projection: &projection,
501            created_at: &crate::time::now_iso8601(),
502            authority_grant_refs: Vec::new(),
503            closure: Some(StepSealClosure {
504                disposition,
505                reason_code,
506                summary,
507            }),
508        },
509        signature_config.signature_policy(),
510    )?)
511}
512
513fn answer_disposition(answer: &JsonValue) -> Result<ClosureDisposition, SkillRunError> {
514    agent_answer_disposition_or_closed(answer).map_err(|error| invalid(format!("{error}")))
515}
516
517fn sealed_output(
518    manifest: &SkillRunnerManifest,
519    run_id: &str,
520    skill_output: &SkillOutput,
521    payload: &JsonValue,
522    receipt: &runx_contracts::Receipt,
523    receipt_value: JsonValue,
524) -> JsonObject {
525    let mut execution = JsonObject::new();
526    execution.insert(
527        "stdout".to_owned(),
528        JsonValue::String(skill_output.stdout.clone()),
529    );
530    execution.insert(
531        "stderr".to_owned(),
532        JsonValue::String(skill_output.stderr.clone()),
533    );
534    execution.insert(
535        "exit_code".to_owned(),
536        skill_output.exit_code.map_or(JsonValue::Null, |exit_code| {
537            JsonValue::Number(JsonNumber::I64(i64::from(exit_code)))
538        }),
539    );
540    execution.insert("structured_output".to_owned(), payload.clone());
541    execution.insert("skill_claim".to_owned(), payload.clone());
542    if let Some(observations) = skill_output
543        .metadata
544        .get(crate::adapter::CREDENTIAL_DELIVERY_OBSERVATIONS_METADATA)
545    {
546        execution.insert(
547            crate::adapter::CREDENTIAL_DELIVERY_OBSERVATIONS_METADATA.to_owned(),
548            observations.clone(),
549        );
550    }
551
552    let mut output = JsonObject::new();
553    output.insert(
554        "schema".to_owned(),
555        JsonValue::String(SKILL_RUN_SCHEMA.to_owned()),
556    );
557    output.insert("status".to_owned(), JsonValue::String("sealed".to_owned()));
558    output.insert(
559        "skill_name".to_owned(),
560        JsonValue::String(manifest.skill.clone().unwrap_or_else(|| "skill".to_owned())),
561    );
562    output.insert("run_id".to_owned(), JsonValue::String(run_id.to_owned()));
563    output.insert(
564        "receipt_id".to_owned(),
565        JsonValue::String(receipt.id.to_string()),
566    );
567    output.insert(
568        "closure".to_owned(),
569        JsonValue::Object(closure_output(&receipt.seal)),
570    );
571    output.insert("receipt".to_owned(), receipt_value);
572    output.insert("execution".to_owned(), JsonValue::Object(execution));
573    output.insert("payload".to_owned(), payload.clone());
574    output
575}
576
577fn closure_output(seal: &runx_contracts::Seal) -> JsonObject {
578    let mut closure = JsonObject::new();
579    closure.insert(
580        "disposition".to_owned(),
581        JsonValue::String(seal.disposition.label().to_owned()),
582    );
583    closure.insert(
584        "reason_code".to_owned(),
585        JsonValue::String(seal.reason_code.to_string()),
586    );
587    closure.insert(
588        "summary".to_owned(),
589        JsonValue::String(seal.summary.to_string()),
590    );
591    closure.insert(
592        "closed_at".to_owned(),
593        JsonValue::String(seal.closed_at.to_string()),
594    );
595    closure
596}
597
598fn normalize_request_id(value: &str) -> String {
599    let mut normalized = String::new();
600    let mut replaced = false;
601    for character in value.chars() {
602        if character.is_ascii_alphanumeric() || matches!(character, '_' | '.' | '-') {
603            normalized.push(character);
604            replaced = false;
605        } else if !replaced {
606            normalized.push('_');
607            replaced = true;
608        }
609    }
610    normalized
611}
612
613fn identifier_segment(value: &str) -> String {
614    normalize_request_id(value)
615        .trim_matches(['.', '_', '-'])
616        .replace('.', "-")
617}
618
619fn contract_json_value(value: &impl serde::Serialize) -> Result<JsonValue, SkillRunError> {
620    let value = serde_json::to_value(value)
621        .map_err(|source| RuntimeError::json("serializing native skill contract value", source))?;
622    serde_json::from_value(value).map_err(|source| {
623        RuntimeError::json("normalizing native skill contract value", source).into()
624    })
625}
626
627fn invalid(message: impl Into<String>) -> SkillRunError {
628    SkillRunError::Invalid(message.into())
629}