1use std::future::Future;
2use std::sync::Arc;
3
4use crate::runtime::AssembledTurn;
5use crate::{
6 MessageRole, ProtocolTurnOptions, SessionPolicy, ToolAvailability, ToolDefinition,
7 ToolManifest, ToolProvider, ToolResult, TurnInput,
8};
9
10pub use lash_sansio::{
11 CheckpointKind, PluginMessage, PluginRuntimeEvent, PromptContribution, ToolCatalogContribution,
12 ToolCatalogOverride,
13};
14
15mod actions;
16mod error;
17mod history;
18mod hooks;
19mod protocol;
20mod registrar;
21mod registry;
22pub mod runtime_host;
23mod runtime_impl;
24mod services;
25mod session_obj;
26mod session_types;
27mod snapshot;
28mod tool_catalog;
29mod trigger_registry;
30
31pub(crate) use actions::RegisteredPluginAction;
32pub use actions::{
33 PluginAction, PluginActionContext, PluginActionDef, PluginActionFailure, PluginActionFuture,
34 PluginActionHandler, PluginActionInvokeFuture, PluginActionKind, SessionParam,
35 plugin_action_def,
36};
37pub use error::PluginError;
38pub use history::{
39 CompactionContext, ContextCompaction, ContextCompactor, ContextError, SessionReadView,
40 TurnContextTransform, TurnTransformContext,
41};
42pub use hooks::{
43 AfterToolCallHook, AfterTurnHook, AssistantResponseHook, AssistantResponseHookContext,
44 AssistantResponseTransform, AssistantStreamHook, AssistantStreamHookContext,
45 AssistantStreamTransform, BeforeToolCallHook, BeforeTurnHook, CheckpointHook,
46 CheckpointHookContext, PluginFuture, PluginLifecycleEvent, PluginLifecycleEventHook,
47 PluginLifecycleFuture, PluginSessionTask, PromptContributor, PromptHookContext,
48 SessionConfigChangedContext, SessionConfigMutator, SessionStateChangedContext,
49 ToolCallHookContext, ToolCatalogContributor, ToolDiscoveryContributor, ToolResultHookContext,
50 ToolResultProjectionContext, ToolResultProjector, TurnHookContext, TurnResultHookContext,
51 TurnResultSummary,
52};
53pub use protocol::{
54 AssistantProseProjectorPlugin, CodeExecutorPlugin, PluginOptions, ProtocolBeforeLlmCallContext,
55 ProtocolDriverPlugin, ProtocolLlmCallAction, ProtocolRuntimeContext, ProtocolSessionContext,
56 ProtocolSessionPlugin,
57};
58pub use registrar::{
59 ContextRegistrations, ExecutionRegistrations, OutputRegistrations, PluginActionRegistrations,
60 PluginRegistrar, PromptRegistrations, ProtocolRegistrations, SessionRegistrations,
61 ToolCallRegistrations, ToolCatalogRegistrations, ToolRegistrations, ToolResultRegistrations,
62 TriggerEventRegistrations, TurnRegistrations,
63};
64pub(crate) use registrar::{PluginContributions, RegisteredHook};
65pub use registry::{
66 PluginExtensionContribution, PluginExtensions, PluginFactory, PluginSessionContext, PluginSpec,
67 PluginSpecBuilder, PluginSpecFactory, SessionPlugin, SessionReadyContext, StaticPluginFactory,
68};
69pub use runtime_host::{
70 AppendSessionNodesRequest, AppendSessionNodesResult, DirectCompletion, DirectLlmCompletion,
71 SessionGraphService, SessionLifecycleService, SessionStateService, SessionTurnInput,
72 SessionTurnRequest,
73};
74pub use runtime_impl::{PluginHost, SessionAuthorityContext};
75pub(crate) use services::NoopSessionManager;
76pub use services::{PersistentRuntimeServices, PluginActionInvokeError, RuntimeServices};
77pub use session_obj::PluginSession;
78pub use session_types::{
79 AgentFrameAssignment, AgentFrameId, AgentFrameReason, AgentFrameRecord, AgentFrameStatus,
80 OpenAgentFrameRequest, OpenAgentFrameResult, PluginOwned, SessionAppendNode,
81 SessionContextOverlay, SessionCreateRequest, SessionHandle, SessionPluginSource,
82 SessionRelation, SessionSnapshot, SessionStartPoint, SessionToolAccess, SubagentSessionContext,
83};
84pub(crate) use snapshot::{InMemorySnapshotReader, InMemorySnapshotWriter};
85pub use snapshot::{
86 PluginSessionSnapshot, PluginSnapshotArtifact, PluginSnapshotEntry, PluginSnapshotMeta,
87 SnapshotReader, SnapshotWriter,
88};
89pub use tool_catalog::{
90 CheckpointApplication, PluginAbort, PluginDirective, PrepareTurnRequest, ToolCatalogContext,
91 ToolDiscoveryContext, ToolDiscoveryContribution, ToolDiscoveryToolContribution,
92 TurnFinalization, TurnPreparation,
93};
94pub(crate) use tool_catalog::{emit_plugin_runtime_events, plugin_runtime_session_events};
95pub(crate) fn builtin_plugin_factories() -> Vec<Arc<dyn PluginFactory>> {
96 let factories: Vec<Arc<dyn PluginFactory>> =
100 vec![Arc::new(trigger_registry::TriggerResourcePluginFactory)];
101 #[cfg(not(test))]
102 return factories;
103
104 #[cfg(test)]
105 {
106 factories
107 .into_iter()
108 .chain(crate::testing::test_standard_protocol_factories())
109 .collect()
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use schemars::JsonSchema;
116 use serde::{Deserialize, Serialize};
117 use serde_json::json;
118
119 use super::*;
120 use crate::{SessionSnapshot, ToolDefinition};
121
122 struct MockToolProvider;
123
124 #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
125 struct TypedEchoArgs {
126 value: String,
127 }
128
129 #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
130 struct TypedEchoOutput {
131 value: String,
132 session_id: Option<String>,
133 }
134
135 struct TypedEchoOp;
136
137 impl PluginAction for TypedEchoOp {
138 const NAME: &'static str = "mock.typed_echo";
139 const DESCRIPTION: &'static str = "typed echo";
140 const KIND: PluginActionKind = PluginActionKind::Query;
141 const SESSION_PARAM: SessionParam = SessionParam::Optional;
142 type Args = TypedEchoArgs;
143 type Output = TypedEchoOutput;
144 }
145
146 #[async_trait::async_trait]
147 impl ToolProvider for MockToolProvider {
148 fn tool_manifests(&self) -> Vec<ToolManifest> {
149 self.tool_definitions()
150 .into_iter()
151 .map(|tool| tool.manifest())
152 .collect()
153 }
154
155 fn resolve_contract(&self, name: &str) -> Option<Arc<crate::ToolContract>> {
156 self.tool_definitions()
157 .into_iter()
158 .find(|tool| tool.name() == name)
159 .map(|tool| Arc::new(tool.contract()))
160 }
161
162 async fn execute(&self, call: crate::ToolCall<'_>) -> ToolResult {
163 ToolResult::ok(call.args.clone())
164 }
165 }
166
167 impl MockToolProvider {
168 fn tool_definitions(&self) -> Vec<ToolDefinition> {
169 vec![
170 ToolDefinition::raw(
171 "tool:mock_tool",
172 "mock_tool",
173 "",
174 json!({
175 "type": "object",
176 "properties": { "value": { "type": "string" } },
177 "required": ["value"],
178 "additionalProperties": false
179 }),
180 json!({ "type": "string" }),
181 )
182 .with_availability(crate::ToolAvailabilityConfig::callable()),
183 ]
184 }
185 }
186
187 struct MockPluginFactory;
188
189 impl PluginFactory for MockPluginFactory {
190 fn id(&self) -> &'static str {
191 "mock"
192 }
193
194 fn build(&self, ctx: &PluginSessionContext) -> Result<Arc<dyn SessionPlugin>, PluginError> {
195 Ok(Arc::new(MockPlugin {
196 session_id: ctx.session_id.clone(),
197 }))
198 }
199 }
200
201 const TEST_EXTENSION_ID: &str = "test.extension";
202
203 struct ExtensionPluginFactory;
204
205 impl PluginFactory for ExtensionPluginFactory {
206 fn id(&self) -> &'static str {
207 "extension_resource"
208 }
209
210 fn extension_contributions(&self) -> Vec<PluginExtensionContribution> {
211 vec![PluginExtensionContribution::from_value(
212 TEST_EXTENSION_ID,
213 json!({ "resource": "clock.alarm" }),
214 )]
215 }
216
217 fn build(
218 &self,
219 _ctx: &PluginSessionContext,
220 ) -> Result<Arc<dyn SessionPlugin>, PluginError> {
221 Ok(Arc::new(ExtensionPlugin))
222 }
223 }
224
225 struct ExtensionPlugin;
226
227 impl SessionPlugin for ExtensionPlugin {
228 fn id(&self) -> &'static str {
229 "extension_resource"
230 }
231
232 fn register(&self, _reg: &mut PluginRegistrar) -> Result<(), PluginError> {
233 Ok(())
234 }
235 }
236
237 struct MockPlugin {
238 session_id: String,
239 }
240
241 use crate::testing::MockSessionManager;
242
243 impl SessionPlugin for MockPlugin {
244 fn id(&self) -> &'static str {
245 "mock"
246 }
247
248 fn register(&self, reg: &mut PluginRegistrar) -> Result<(), PluginError> {
249 reg.tools().provider(Arc::new(MockToolProvider))?;
250 reg.prompt().contribute(Arc::new(|_ctx| {
251 Box::pin(async move {
252 Ok(vec![
253 PromptContribution::guidance("Plugin Prompt", "Structured plugin prompt"),
254 PromptContribution::guidance("Dynamic Note", "dynamic note")
255 .with_priority(1),
256 ])
257 })
258 }));
259 let session_id = self.session_id.clone();
260 reg.actions().op(
261 PluginActionDef {
262 name: "mock.echo".to_string(),
263 description: "echo".to_string(),
264 kind: PluginActionKind::Query,
265 session_param: SessionParam::Optional,
266 input_schema: json!({}),
267 output_schema: json!({}),
268 },
269 Arc::new(move |ctx, args| {
270 let session_id = session_id.clone();
271 Box::pin(async move {
272 ToolResult::ok(json!({
273 "session_id": ctx.session_id,
274 "plugin_session_id": session_id,
275 "args": args,
276 }))
277 })
278 }),
279 )?;
280 reg.actions()
281 .typed::<TypedEchoOp, _, _>(move |ctx, args| async move {
282 Ok(TypedEchoOutput {
283 value: args.value,
284 session_id: ctx.session_id,
285 })
286 })?;
287 Ok(())
288 }
289
290 fn snapshot(
291 &self,
292 _writer: &mut dyn SnapshotWriter,
293 ) -> Result<PluginSnapshotMeta, PluginError> {
294 Ok(PluginSnapshotMeta {
295 plugin_id: self.id().to_string(),
296 plugin_version: self.version().to_string(),
297 revision: self.snapshot_revision(),
298 state: Some(json!({"session_id": self.session_id})),
299 })
300 }
301 }
302
303 #[test]
304 fn plugin_host_collects_factory_extension_contributions() {
305 let host = PluginHost::new(vec![Arc::new(ExtensionPluginFactory)]);
306
307 assert_eq!(
308 host.extensions().payloads(TEST_EXTENSION_ID),
309 &[json!({ "resource": "clock.alarm" })]
310 );
311 let session = host.build_session("root", None).expect("session");
312 assert_eq!(
313 session.extensions().payloads(TEST_EXTENSION_ID),
314 &[json!({ "resource": "clock.alarm" })]
315 );
316 }
317
318 #[test]
319 fn declared_triggers_enter_session_catalog() {
320 struct TriggerEventOnlyFactory;
321
322 impl PluginFactory for TriggerEventOnlyFactory {
323 fn id(&self) -> &'static str {
324 "trigger_only"
325 }
326
327 fn build(
328 &self,
329 _ctx: &PluginSessionContext,
330 ) -> Result<Arc<dyn SessionPlugin>, PluginError> {
331 Ok(Arc::new(TriggerEventOnlyPlugin))
332 }
333 }
334
335 struct TriggerEventOnlyPlugin;
336
337 impl SessionPlugin for TriggerEventOnlyPlugin {
338 fn id(&self) -> &'static str {
339 "trigger_only"
340 }
341
342 fn register(&self, reg: &mut PluginRegistrar) -> Result<(), PluginError> {
343 reg.triggers().declare(crate::TriggerEvent::new(
344 "Button",
345 "ui.button",
346 "pressed",
347 crate::LashSchema::any(),
348 ))
349 }
350 }
351
352 let host = PluginHost::new(vec![Arc::new(TriggerEventOnlyFactory)]);
353
354 let session = host.build_session("root", None).expect("session");
355 assert!(
356 session
357 .triggers()
358 .get("Button", "ui.button", "pressed")
359 .is_some()
360 );
361 let event = session
362 .triggers()
363 .get("Button", "ui.button", "pressed")
364 .expect("button event");
365 assert_eq!(event.source_type(), "ui.button.pressed");
366 }
367
368 #[tokio::test]
369 async fn session_collects_tools_and_prompts() {
370 let host = PluginHost::new(vec![Arc::new(MockPluginFactory)]);
371 let session = host.build_session("root", None).expect("session");
372 let tool_names = session
373 .tools()
374 .tool_manifests()
375 .into_iter()
376 .map(|manifest| manifest.name)
377 .collect::<std::collections::BTreeSet<_>>();
378 assert!(tool_names.contains("mock_tool"));
379 assert!(tool_names.contains("batch"));
380 let contributions = session
381 .collect_prompt_contributions(PromptHookContext {
382 session_id: "root".to_string(),
383 sessions: Arc::new(MockSessionManager::default()),
384 state: SessionReadView::from_snapshot(&SessionSnapshot::default()),
385 protocol_turn_options: ProtocolTurnOptions::default(),
386 turn_context: crate::TurnContext::default(),
387 })
388 .await
389 .expect("prompt contributions");
390 assert_eq!(
391 contributions,
392 vec![
393 PromptContribution::guidance("Plugin Prompt", "Structured plugin prompt"),
394 PromptContribution::guidance("Dynamic Note", "dynamic note").with_priority(1),
395 ]
396 );
397 }
398
399 #[tokio::test]
400 async fn external_invoke_defaults_to_current_session_when_requested() {
401 let host = PluginHost::new(vec![Arc::new(MockPluginFactory)]);
402 let session = host.build_session("root", None).expect("session");
403 let result = session
404 .invoke_plugin_action(
405 "mock.echo",
406 json!({"ok":true}),
407 None,
408 true,
409 Arc::new(MockSessionManager::default()),
410 Arc::new(MockSessionManager::default()),
411 Arc::new(MockSessionManager::default()),
412 Arc::new(crate::UnavailableProcessService),
413 )
414 .await
415 .expect("invoke");
416 assert!(result.is_success());
417 assert_eq!(
418 result
419 .value_for_projection()
420 .get("session_id")
421 .and_then(|v| v.as_str()),
422 Some("root")
423 );
424 }
425
426 #[tokio::test]
427 async fn plugin_action_generates_schema_and_invokes_typed_output() {
428 let host = PluginHost::new(vec![Arc::new(MockPluginFactory)]);
429 let session = host.build_session("root", None).expect("session");
430
431 let def = session
432 .plugin_actions()
433 .into_iter()
434 .find(|def| def.name == TypedEchoOp::NAME)
435 .expect("typed op definition");
436 assert_eq!(def.kind, PluginActionKind::Query);
437 assert_eq!(def.session_param, SessionParam::Optional);
438 let value_type = def
439 .input_schema
440 .pointer("/schema/properties/value/type")
441 .or_else(|| def.input_schema.pointer("/properties/value/type"))
442 .and_then(serde_json::Value::as_str);
443 assert_eq!(value_type, Some("string"));
444
445 let output = session
446 .call_plugin_action::<TypedEchoOp>(
447 TypedEchoArgs {
448 value: "hello".to_string(),
449 },
450 None,
451 true,
452 Arc::new(MockSessionManager::default()),
453 Arc::new(MockSessionManager::default()),
454 Arc::new(MockSessionManager::default()),
455 Arc::new(crate::UnavailableProcessService),
456 )
457 .await
458 .expect("typed invoke");
459 assert_eq!(output.value, "hello");
460 assert_eq!(output.session_id.as_deref(), Some("root"));
461 }
462
463 #[test]
464 fn plugin_action_rejects_duplicate_names() {
465 struct DuplicatePlugin;
466
467 impl SessionPlugin for DuplicatePlugin {
468 fn id(&self) -> &'static str {
469 "duplicate"
470 }
471
472 fn register(&self, reg: &mut PluginRegistrar) -> Result<(), PluginError> {
473 reg.actions()
474 .typed::<TypedEchoOp, _, _>(move |ctx, args| async move {
475 Ok(TypedEchoOutput {
476 value: args.value,
477 session_id: ctx.session_id,
478 })
479 })?;
480 reg.actions()
481 .typed::<TypedEchoOp, _, _>(move |ctx, args| async move {
482 Ok(TypedEchoOutput {
483 value: args.value,
484 session_id: ctx.session_id,
485 })
486 })
487 }
488 }
489
490 struct DuplicateFactory;
491 impl PluginFactory for DuplicateFactory {
492 fn id(&self) -> &'static str {
493 "duplicate"
494 }
495
496 fn build(
497 &self,
498 _ctx: &PluginSessionContext,
499 ) -> Result<Arc<dyn SessionPlugin>, PluginError> {
500 Ok(Arc::new(DuplicatePlugin))
501 }
502 }
503
504 let err =
505 match PluginHost::new(vec![Arc::new(DuplicateFactory)]).build_session("root", None) {
506 Ok(_) => panic!("duplicate typed plugin action should fail"),
507 Err(err) => err,
508 };
509 assert!(err.to_string().contains("duplicate plugin action name"));
510 }
511
512 #[tokio::test]
513 async fn typed_external_invoke_errors_on_failed_or_invalid_output() {
514 struct BadOp;
515 impl PluginAction for BadOp {
516 const NAME: &'static str = "mock.echo";
517 const DESCRIPTION: &'static str = "bad typed projection over raw op";
518 const KIND: PluginActionKind = PluginActionKind::Query;
519 const SESSION_PARAM: SessionParam = SessionParam::Optional;
520 type Args = TypedEchoArgs;
521 type Output = TypedEchoOutput;
522 }
523
524 let host = PluginHost::new(vec![Arc::new(MockPluginFactory)]);
525 let session = host.build_session("root", None).expect("session");
526 let err = session
527 .call_plugin_action::<BadOp>(
528 TypedEchoArgs {
529 value: "hello".to_string(),
530 },
531 None,
532 true,
533 Arc::new(MockSessionManager::default()),
534 Arc::new(MockSessionManager::default()),
535 Arc::new(MockSessionManager::default()),
536 Arc::new(crate::UnavailableProcessService),
537 )
538 .await
539 .expect_err("raw output shape should not match typed output");
540 assert!(err.to_string().contains("invalid mock.echo output"));
541 }
542
543 #[tokio::test]
544 async fn plugin_host_can_invoke_plugin_action_for_registered_session() {
545 let host = PluginHost::new(vec![Arc::new(MockPluginFactory)]);
546 let _session = host.build_session("root", None).expect("session");
547
548 let result = host
549 .invoke_plugin_action_for_session(
550 "root",
551 "mock.echo",
552 json!({"ok":true}),
553 Arc::new(MockSessionManager::default()),
554 Arc::new(MockSessionManager::default()),
555 Arc::new(MockSessionManager::default()),
556 Arc::new(crate::UnavailableProcessService),
557 )
558 .await
559 .expect("invoke");
560 assert!(result.is_success());
561 assert_eq!(
562 result
563 .value_for_projection()
564 .get("session_id")
565 .and_then(|v| v.as_str()),
566 Some("root")
567 );
568 assert_eq!(
569 result
570 .value_for_projection()
571 .get("plugin_session_id")
572 .and_then(|v| v.as_str()),
573 Some("root")
574 );
575 }
576
577 #[tokio::test]
578 async fn plugin_host_can_invoke_plugin_action_for_forked_session() {
579 let host = PluginHost::new(vec![Arc::new(MockPluginFactory)]);
580 let root = host.build_session("root", None).expect("root");
581 let child = root.fork_for_session("child").expect("child");
582
583 let result = host
584 .invoke_plugin_action_for_session(
585 "child",
586 "mock.echo",
587 json!({"ok":true}),
588 Arc::new(MockSessionManager::default()),
589 Arc::new(MockSessionManager::default()),
590 Arc::new(MockSessionManager::default()),
591 Arc::new(crate::UnavailableProcessService),
592 )
593 .await
594 .expect("invoke");
595 assert!(result.is_success());
596 assert_eq!(
597 result
598 .value_for_projection()
599 .get("session_id")
600 .and_then(|v| v.as_str()),
601 Some("child")
602 );
603 assert_eq!(
604 result
605 .value_for_projection()
606 .get("plugin_session_id")
607 .and_then(|v| v.as_str()),
608 Some("child")
609 );
610
611 drop(child);
612 }
613
614 #[test]
615 fn plugin_host_unregisters_sessions() {
616 let host = PluginHost::new(vec![Arc::new(MockPluginFactory)]);
617 let _session = host.build_session("root", None).expect("session");
618 assert!(host.session("root").is_ok());
619 host.unregister_session("root").expect("unregister");
620 match host.session("root") {
621 Err(PluginActionInvokeError::UnknownSession(id)) => assert_eq!(id, "root"),
622 Ok(_) => panic!("expected missing session"),
623 Err(other) => panic!("unexpected error: {other}"),
624 }
625 }
626
627 #[test]
628 fn snapshot_round_trip_preserves_plugin_entries() {
629 let host = PluginHost::new(vec![Arc::new(MockPluginFactory)]);
630 let session = host.build_session("root", None).expect("session");
631 let snapshot = session.snapshot().expect("snapshot");
632 assert!(snapshot.plugins.contains_key("mock"));
633 let restored = host
634 .build_session("child", Some(&snapshot))
635 .expect("restored");
636 let restored_snapshot = restored.snapshot().expect("snapshot");
637 assert!(restored_snapshot.plugins.contains_key("mock"));
638 }
639
640 #[test]
641 fn runtime_services_are_backed_by_plugin_sessions() {
642 let host = PluginHost::new(vec![Arc::new(StaticPluginFactory::new(
643 "mock_tool",
644 PluginSpec::new()
645 .with_tool_provider(Arc::new(MockToolProvider) as Arc<dyn ToolProvider>),
646 ))]);
647 let services = RuntimeServices::new(host.build_session("root", None).expect("session"));
648 assert_eq!(services.plugins.session_id(), "root");
649 assert!(
650 services
651 .plugins
652 .tools()
653 .tool_manifests()
654 .iter()
655 .any(|tool| tool.name == "mock_tool")
656 );
657 }
658
659 struct ProjectorPluginFactory {
660 plugin_id: &'static str,
661 }
662
663 impl PluginFactory for ProjectorPluginFactory {
664 fn id(&self) -> &'static str {
665 self.plugin_id
666 }
667
668 fn build(
669 &self,
670 _ctx: &PluginSessionContext,
671 ) -> Result<Arc<dyn SessionPlugin>, PluginError> {
672 Ok(Arc::new(ProjectorPlugin {
673 plugin_id: self.plugin_id,
674 }))
675 }
676 }
677
678 struct ProjectorPlugin {
679 plugin_id: &'static str,
680 }
681
682 impl SessionPlugin for ProjectorPlugin {
683 fn id(&self) -> &'static str {
684 self.plugin_id
685 }
686
687 fn register(&self, reg: &mut PluginRegistrar) -> Result<(), PluginError> {
688 reg.tool_results().projector(Arc::new(|ctx| {
689 Box::pin(async move {
690 Ok(crate::ModelToolReturn::from_output(
691 ctx.call_id,
692 ctx.tool_name,
693 &ctx.output,
694 ))
695 })
696 }))
697 }
698 }
699
700 #[test]
701 fn duplicate_tool_result_projectors_are_rejected() {
702 let host = PluginHost::new(vec![
703 Arc::new(ProjectorPluginFactory {
704 plugin_id: "projector-a",
705 }),
706 Arc::new(ProjectorPluginFactory {
707 plugin_id: "projector-b",
708 }),
709 ]);
710 let err = match host.build_session("root", None) {
711 Ok(_) => panic!("duplicate projector"),
712 Err(err) => err,
713 };
714 assert!(err.to_string().contains("duplicate tool result projector"));
715 assert!(err.to_string().contains("projector-a"));
716 assert!(err.to_string().contains("projector-b"));
717 }
718}