@josephyan/qingflow-app-builder-mcp 1.1.25 → 1.1.26

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 (98) hide show
  1. package/README.md +5 -6
  2. package/docs/local-agent-install.md +6 -57
  3. package/entry_point.py +1 -1
  4. package/npm/bin/qingflow-app-builder-mcp.mjs +33 -2
  5. package/npm/lib/runtime.mjs +101 -21
  6. package/npm/scripts/postinstall.mjs +10 -1
  7. package/package.json +2 -3
  8. package/pyproject.toml +1 -1
  9. package/skills/qingflow-app-builder/SKILL.md +42 -104
  10. package/skills/qingflow-app-builder/references/build-complete-system.md +428 -0
  11. package/skills/qingflow-app-builder/references/build-single-app.md +530 -0
  12. package/skills/qingflow-app-builder/references/create-app.md +11 -30
  13. package/skills/qingflow-app-builder/references/environments.md +1 -1
  14. package/skills/qingflow-app-builder/references/gotchas.md +9 -31
  15. package/skills/qingflow-app-builder/references/match-rules.md +0 -15
  16. package/skills/qingflow-app-builder/references/solution-playbooks.md +0 -10
  17. package/skills/qingflow-app-builder/references/tool-selection.md +23 -38
  18. package/skills/qingflow-app-builder/references/update-flow.md +38 -22
  19. package/skills/qingflow-app-builder/references/update-schema.md +0 -14
  20. package/skills/qingflow-app-builder/references/update-views.md +14 -83
  21. package/skills/qingflow-app-builder-code-integrations/SKILL.md +3 -5
  22. package/skills/qingflow-app-builder-code-integrations/references/code-block.md +1 -1
  23. package/skills/qingflow-app-builder-code-integrations/references/q-linker.md +1 -1
  24. package/skills/qingflow-mcp-setup/SKILL.md +111 -0
  25. package/skills/qingflow-mcp-setup/agents/openai.yaml +4 -0
  26. package/skills/qingflow-mcp-setup/references/claude-desktop.md +34 -0
  27. package/skills/qingflow-mcp-setup/references/environments.md +61 -0
  28. package/skills/qingflow-mcp-setup/references/generic-stdio.md +31 -0
  29. package/skills/qingflow-mcp-setup/scripts/check_local_server.sh +38 -0
  30. package/skills/qingflow-workflow-builder/SKILL.md +96 -0
  31. package/skills/qingflow-workflow-builder/manifest.yaml +8 -0
  32. package/skills/qingflow-workflow-builder/references/01-overview.md +45 -0
  33. package/skills/qingflow-workflow-builder/references/02-update-mode.md +53 -0
  34. package/skills/qingflow-workflow-builder/references/03-flow-patterns.md +57 -0
  35. package/skills/qingflow-workflow-builder/references/04-stage1-business-modeling.md +131 -0
  36. package/skills/qingflow-workflow-builder/references/05-stage2-members-roles.md +29 -0
  37. package/skills/qingflow-workflow-builder/references/06-stage3-build-spec.md +165 -0
  38. package/skills/qingflow-workflow-builder/references/07-stage4-validate-spec.md +33 -0
  39. package/skills/qingflow-workflow-builder/references/08-stage5-apply-verify.md +51 -0
  40. package/skills/qingflow-workflow-builder/references/09-stage6-summary.md +88 -0
  41. package/skills/qingflow-workflow-builder/references/10-node-config-reference.md +93 -0
  42. package/skills/qingflow-workflow-builder/references/11-troubleshooting.md +15 -0
  43. package/skills/qingflow-workflow-builder/scripts/diff_flow_spec.py +275 -0
  44. package/skills/qingflow-workflow-builder/scripts/validate_flow_spec.py +605 -0
  45. package/src/qingflow_mcp/__init__.py +1 -1
  46. package/src/qingflow_mcp/__main__.py +2 -6
  47. package/src/qingflow_mcp/builder_facade/models.py +108 -485
  48. package/src/qingflow_mcp/builder_facade/service.py +1686 -6674
  49. package/src/qingflow_mcp/cli/commands/builder.py +364 -322
  50. package/src/qingflow_mcp/cli/commands/chart.py +1 -1
  51. package/src/qingflow_mcp/cli/commands/common.py +3 -12
  52. package/src/qingflow_mcp/cli/commands/exports.py +2 -2
  53. package/src/qingflow_mcp/cli/commands/imports.py +3 -3
  54. package/src/qingflow_mcp/cli/commands/portal.py +2 -2
  55. package/src/qingflow_mcp/cli/commands/record.py +27 -101
  56. package/src/qingflow_mcp/cli/commands/task.py +47 -28
  57. package/src/qingflow_mcp/cli/commands/view.py +1 -1
  58. package/src/qingflow_mcp/cli/context.py +3 -0
  59. package/src/qingflow_mcp/cli/formatters.py +16 -784
  60. package/src/qingflow_mcp/cli/main.py +33 -117
  61. package/src/qingflow_mcp/errors.py +2 -43
  62. package/src/qingflow_mcp/public_surface.py +17 -26
  63. package/src/qingflow_mcp/response_trim.py +17 -81
  64. package/src/qingflow_mcp/server.py +12 -14
  65. package/src/qingflow_mcp/server_app_builder.py +36 -79
  66. package/src/qingflow_mcp/server_app_user.py +16 -22
  67. package/src/qingflow_mcp/session_store.py +7 -11
  68. package/src/qingflow_mcp/solution/compiler/__init__.py +1 -3
  69. package/src/qingflow_mcp/solution/executor.py +18 -245
  70. package/src/qingflow_mcp/tools/ai_builder_tools.py +1396 -3103
  71. package/src/qingflow_mcp/tools/app_tools.py +43 -184
  72. package/src/qingflow_mcp/tools/approval_tools.py +35 -197
  73. package/src/qingflow_mcp/tools/auth_tools.py +16 -92
  74. package/src/qingflow_mcp/tools/code_block_tools.py +40 -298
  75. package/src/qingflow_mcp/tools/custom_button_tools.py +10 -64
  76. package/src/qingflow_mcp/tools/directory_tools.py +72 -236
  77. package/src/qingflow_mcp/tools/export_tools.py +34 -244
  78. package/src/qingflow_mcp/tools/feedback_tools.py +0 -9
  79. package/src/qingflow_mcp/tools/file_tools.py +3 -9
  80. package/src/qingflow_mcp/tools/import_tools.py +49 -336
  81. package/src/qingflow_mcp/tools/navigation_tools.py +12 -91
  82. package/src/qingflow_mcp/tools/package_tools.py +6 -118
  83. package/src/qingflow_mcp/tools/portal_tools.py +3 -39
  84. package/src/qingflow_mcp/tools/qingbi_report_tools.py +7 -116
  85. package/src/qingflow_mcp/tools/record_tools.py +360 -1145
  86. package/src/qingflow_mcp/tools/resource_read_tools.py +39 -188
  87. package/src/qingflow_mcp/tools/role_tools.py +9 -80
  88. package/src/qingflow_mcp/tools/solution_tools.py +45 -59
  89. package/src/qingflow_mcp/tools/task_context_tools.py +158 -662
  90. package/src/qingflow_mcp/tools/task_tools.py +29 -113
  91. package/src/qingflow_mcp/tools/view_tools.py +3 -106
  92. package/src/qingflow_mcp/tools/workflow_tools.py +4 -48
  93. package/src/qingflow_mcp/tools/workspace_tools.py +3 -71
  94. package/npm/bin/qingflow-skills.mjs +0 -5
  95. package/skills/qingflow-app-builder/references/complete-system-development-guide.md +0 -172
  96. package/skills/qingflow-app-builder/references/single-app-development-guide.md +0 -87
  97. package/skills/qingflow-app-builder/scripts/validate_system_build_summary.py +0 -124
  98. package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +0 -173
@@ -5,7 +5,7 @@ from copy import deepcopy
5
5
  from typing import Any
6
6
  from uuid import uuid4
7
7
 
8
- from ..errors import QingflowApiError, backend_code_int, is_auth_like_error
8
+ from ..errors import QingflowApiError
9
9
  from ..tools.app_tools import AppTools
10
10
  from ..tools.navigation_tools import NavigationTools
11
11
  from ..tools.package_tools import PackageTools
@@ -14,7 +14,6 @@ from ..tools.qingbi_report_tools import QingbiReportTools
14
14
  from ..tools.record_tools import RecordTools
15
15
  from ..tools.role_tools import RoleTools
16
16
  from ..tools.view_tools import ViewTools
17
- from ..tools.workflow_tools import WorkflowTools
18
17
  from ..tools.workspace_tools import WorkspaceTools
19
18
  from .compiler import CompiledEntity, CompiledRole, CompiledSolution
20
19
  from .compiler.form_compiler import QUESTION_TYPE_MAP
@@ -36,7 +35,6 @@ class SolutionExecutor:
36
35
  role_tools: RoleTools,
37
36
  app_tools: AppTools,
38
37
  record_tools: RecordTools,
39
- workflow_tools: WorkflowTools,
40
38
  view_tools: ViewTools,
41
39
  chart_tools: QingbiReportTools,
42
40
  portal_tools: PortalTools,
@@ -47,7 +45,6 @@ class SolutionExecutor:
47
45
  self.role_tools = role_tools
48
46
  self.app_tools = app_tools
49
47
  self.record_tools = record_tools
50
- self.workflow_tools = workflow_tools
51
48
  self.view_tools = view_tools
52
49
  self.chart_tools = chart_tools
53
50
  self.portal_tools = portal_tools
@@ -76,8 +73,7 @@ class SolutionExecutor:
76
73
  except Exception as exc: # noqa: BLE001
77
74
  store.record_step_failed(step.step_name, str(exc), debug_context=debug_context)
78
75
  return store.summary()
79
- final_status = "partial_success" if _artifacts_have_post_write_readback_pending(store.data.get("artifacts", {})) else "success"
80
- store.mark_finished(status=final_status)
76
+ store.mark_finished(status="success")
81
77
  return store.summary()
82
78
 
83
79
  def _repair_start_index(self, compiled: CompiledSolution, store: RunArtifactStore) -> int:
@@ -204,11 +200,6 @@ class SolutionExecutor:
204
200
  dash_key = store.get_artifact("portal", "dash_key")
205
201
  if dash_key:
206
202
  self.portal_tools.portal_publish(profile=profile, dash_key=dash_key)
207
- store.set_artifact(
208
- "portal",
209
- "publish",
210
- {"published": True, "write_executed": True, "safe_to_retry": False},
211
- )
212
203
  self._refresh_portal_artifact(profile=profile, store=store, being_draft=False, artifact_key="published_result")
213
204
  return
214
205
  if step_name == "publish.navigation" and publish and compiled.normalized_spec.publish_policy.navigation:
@@ -353,32 +344,7 @@ class SolutionExecutor:
353
344
  updated_items.insert(insert_at, item)
354
345
  self.package_tools.package_sort_items(profile=profile, tag_id=tag_id, tag_items=updated_items)
355
346
 
356
- try:
357
- verified_detail = _verify_package_attachment(self.package_tools, profile=profile, tag_id=tag_id, app_key=app_key)
358
- except Exception as exc: # noqa: BLE001
359
- api_error = _coerce_qingflow_error(exc)
360
- if api_error is None or not _is_permission_restricted_error(api_error):
361
- raise
362
- store.set_artifact(
363
- "package",
364
- "attachment_readback",
365
- _post_write_readback_artifact(
366
- resource="package_attach",
367
- target={"tag_id": tag_id, "app_key": app_key},
368
- error=api_error,
369
- ),
370
- )
371
- self._record_package_attachment(
372
- store,
373
- entity.entity_id,
374
- app_artifact,
375
- tag_id=tag_id,
376
- attached=True,
377
- reused=False,
378
- readback_status="unavailable",
379
- readback_verified=False,
380
- )
381
- return
347
+ verified_detail = _verify_package_attachment(self.package_tools, profile=profile, tag_id=tag_id, app_key=app_key)
382
348
  verified_result = verified_detail.get("result") if isinstance(verified_detail.get("result"), dict) else {}
383
349
  verified_items = [deepcopy(existing) for existing in verified_result.get("tagItems", []) if isinstance(existing, dict)]
384
350
  if not any(_package_item_app_key(existing) == app_key for existing in verified_items):
@@ -400,18 +366,12 @@ class SolutionExecutor:
400
366
  tag_id: int,
401
367
  attached: bool,
402
368
  reused: bool,
403
- readback_status: str = "verified",
404
- readback_verified: bool = True,
405
369
  ) -> None:
406
370
  next_artifact = deepcopy(app_artifact)
407
371
  next_artifact["package_attachment"] = {
408
372
  "tag_id": tag_id,
409
373
  "attached": attached,
410
374
  "reused": reused,
411
- "readback_status": readback_status,
412
- "readback_verified": readback_verified,
413
- "write_executed": not reused,
414
- "safe_to_retry": reused or not attached,
415
375
  }
416
376
  store.set_artifact("apps", entity_id, next_artifact)
417
377
 
@@ -456,123 +416,10 @@ class SolutionExecutor:
456
416
  def _build_workflow(self, profile: str, entity: CompiledEntity, store: RunArtifactStore) -> None:
457
417
  if entity.workflow_plan is None:
458
418
  return
459
- app_key = self._get_app_key(store, entity.entity_id)
460
- workflow_edit_version_no = self._ensure_edit_version(
461
- profile,
462
- entity.entity_id,
463
- store,
464
- app_key=app_key,
465
- force_new=True,
466
- )
467
- node_artifacts = store.get_artifact("apps", entity.entity_id, {}).get("workflow_nodes", {})
468
- existing_nodes = self.workflow_tools.workflow_list_nodes(profile=profile, app_key=app_key).get("result") or {}
469
- current_nodes = _coerce_workflow_nodes(existing_nodes)
470
- existing_nodes_by_name = {
471
- node.get("auditNodeName"): int(node_id)
472
- for node_id, node in current_nodes.items()
473
- if isinstance(node, dict) and node.get("auditNodeName")
474
- }
475
- applicant_node_id = next(
476
- (
477
- int(node_id)
478
- for node_id, node in current_nodes.items()
479
- if isinstance(node, dict) and node.get("type") == 0 and node.get("dealType") == 3
480
- ),
481
- None,
419
+ raise RuntimeError(
420
+ "Legacy auditNode workflow execution was removed. "
421
+ "Pass {app_key, spec} to solution_build_flow or use qingflow builder flow apply."
482
422
  )
483
- if applicant_node_id is not None:
484
- node_artifacts.setdefault("__applicant__", applicant_node_id)
485
-
486
- desired_global_settings = deepcopy(entity.workflow_plan["global_settings"])
487
- explicit_global_settings = _has_explicit_workflow_global_settings(desired_global_settings)
488
- current_global_settings: dict[str, Any] = {}
489
- if explicit_global_settings:
490
- current_global_settings = self.workflow_tools.workflow_get_global_settings(profile=profile, app_key=app_key).get("result") or {}
491
- else:
492
- try:
493
- current_global_settings = self.workflow_tools.workflow_get_global_settings(profile=profile, app_key=app_key).get("result") or {}
494
- except (QingflowApiError, RuntimeError) as error:
495
- api_error = QingflowApiError(**_coerce_nested_error_payload(error))
496
- if api_error.http_status != 404:
497
- raise
498
- current_global_settings = {}
499
- if explicit_global_settings:
500
- global_settings = deepcopy(current_global_settings if isinstance(current_global_settings, dict) else {})
501
- global_settings.update(desired_global_settings)
502
- global_settings["editVersionNo"] = workflow_edit_version_no or global_settings.get("editVersionNo") or 1
503
- self.workflow_tools.workflow_update_global_settings(profile=profile, app_key=app_key, payload=global_settings)
504
- for action in entity.workflow_plan["actions"]:
505
- if action["action"] == "create_sub_branch" and node_artifacts.get(action["node_id"]) is not None:
506
- continue
507
- if action["action"] == "add_node":
508
- if action.get("node_type") == "branch":
509
- existing_branch_id = node_artifacts.get(action["node_id"])
510
- if existing_branch_id is not None and not _workflow_node_is_branch(current_nodes, existing_branch_id):
511
- existing_branch_id = None
512
- if existing_branch_id is not None:
513
- for branch_index, lane_id in enumerate(_find_branch_lane_ids(current_nodes, existing_branch_id), start=1):
514
- node_artifacts[_branch_lane_ref(action["node_id"], branch_index)] = lane_id
515
- apps_artifact = store.get_artifact("apps", entity.entity_id, {})
516
- apps_artifact["workflow_nodes"] = node_artifacts
517
- store.set_artifact("apps", entity.entity_id, apps_artifact)
518
- continue
519
- existing_node_id = node_artifacts.get(action["node_id"]) or existing_nodes_by_name.get(action.get("node_name"))
520
- if existing_node_id is not None:
521
- node_artifacts[action["node_id"]] = existing_node_id
522
- apps_artifact = store.get_artifact("apps", entity.entity_id, {})
523
- apps_artifact["workflow_nodes"] = node_artifacts
524
- store.set_artifact("apps", entity.entity_id, apps_artifact)
525
- continue
526
- before_node_ids = set(current_nodes)
527
- payload = self._resolve_workflow_payload(action["payload"], node_artifacts)
528
- if workflow_edit_version_no is not None:
529
- payload["editVersionNo"] = int(workflow_edit_version_no)
530
- if action["action"] == "create_sub_branch":
531
- result = self.workflow_tools.workflow_create_sub_branch(profile=profile, app_key=app_key, payload=payload)
532
- elif action["action"] == "update_node":
533
- target_node_id = node_artifacts.get(action["node_id"])
534
- if target_node_id is None:
535
- raise RuntimeError(f"workflow lane '{action['node_id']}' could not be resolved before update")
536
- result = self.workflow_tools.workflow_update_node(
537
- profile=profile,
538
- app_key=app_key,
539
- audit_node_id=target_node_id,
540
- payload=payload,
541
- )
542
- else:
543
- result = self.workflow_tools.workflow_add_node(profile=profile, app_key=app_key, payload=payload)
544
- expected_type = 1 if action.get("node_type") == "branch" else None
545
- audit_node_id = _extract_workflow_node_id(result.get("result"), expected_type=expected_type)
546
- if action.get("node_type") == "branch" or action["action"] == "create_sub_branch":
547
- current_nodes = _coerce_workflow_nodes(
548
- self.workflow_tools.workflow_list_nodes(profile=profile, app_key=app_key).get("result") or {}
549
- )
550
- if audit_node_id is not None:
551
- node_artifacts[action["node_id"]] = audit_node_id
552
- if action.get("node_type") == "branch":
553
- branch_node_id = node_artifacts.get(action["node_id"]) or _find_created_branch_node_id(
554
- current_nodes,
555
- before_node_ids=before_node_ids,
556
- prev_id=payload.get("prevId"),
557
- )
558
- if branch_node_id is not None:
559
- node_artifacts[action["node_id"]] = branch_node_id
560
- for branch_index, lane_id in enumerate(_find_branch_lane_ids(current_nodes, branch_node_id), start=1):
561
- node_artifacts[_branch_lane_ref(action["node_id"], branch_index)] = lane_id
562
- if action["action"] == "create_sub_branch" and node_artifacts.get(action["node_id"]) is None:
563
- created_lane_id = audit_node_id or _find_created_sub_branch_lane_id(
564
- current_nodes,
565
- before_node_ids=before_node_ids,
566
- branch_node_id=payload.get("auditNodeId"),
567
- )
568
- if created_lane_id is not None:
569
- node_artifacts[action["node_id"]] = created_lane_id
570
- apps_artifact = store.get_artifact("apps", entity.entity_id, {})
571
- apps_artifact["workflow_nodes"] = node_artifacts
572
- store.set_artifact("apps", entity.entity_id, apps_artifact)
573
- apps_artifact = store.get_artifact("apps", entity.entity_id, {})
574
- apps_artifact["workflow_nodes"] = node_artifacts
575
- store.set_artifact("apps", entity.entity_id, apps_artifact)
576
423
 
577
424
  def _build_views(self, profile: str, entity: CompiledEntity, store: RunArtifactStore) -> None:
578
425
  app_key = self._get_app_key(store, entity.entity_id)
@@ -735,23 +582,12 @@ class SolutionExecutor:
735
582
  api_error = _coerce_qingflow_error(exc)
736
583
  if api_error is None or not _is_permission_restricted_error(api_error):
737
584
  raise
738
- if not result:
739
- raise _required_state_read_blocked_error(
740
- resource="portal",
741
- message=f"portal update requires readable draft state for dash '{dash_key}'",
742
- error=api_error,
743
- details={"dash_key": dash_key},
744
- ) from exc
745
- base_payload = {}
746
- store.set_artifact(
747
- "portal",
748
- "draft_readback_before_update",
749
- _post_write_readback_artifact(
750
- resource="portal",
751
- target={"dash_key": dash_key, "phase": "created_portal_draft_readback"},
752
- error=api_error,
753
- ),
754
- )
585
+ raise _required_state_read_blocked_error(
586
+ resource="portal",
587
+ message=f"portal update requires readable draft state for dash '{dash_key}'",
588
+ error=api_error,
589
+ details={"dash_key": dash_key},
590
+ ) from exc
755
591
  update_payload = self._resolve_portal_payload(compiled.portal_plan["update_payload"], store, base_payload=base_payload)
756
592
  self.portal_tools.portal_update(profile=profile, dash_key=dash_key, payload=update_payload)
757
593
  self._refresh_portal_artifact(profile=profile, store=store, being_draft=True, artifact_key="draft_result")
@@ -792,19 +628,7 @@ class SolutionExecutor:
792
628
  return
793
629
  try:
794
630
  result = self.portal_tools.portal_get(profile=profile, dash_key=dash_key, being_draft=being_draft)
795
- except Exception as exc: # noqa: BLE001
796
- api_error = _coerce_qingflow_error(exc)
797
- if api_error is None:
798
- raise
799
- store.set_artifact(
800
- "portal",
801
- f"{artifact_key}_readback",
802
- _post_write_readback_artifact(
803
- resource="portal",
804
- target={"dash_key": dash_key, "being_draft": being_draft, "artifact_key": artifact_key},
805
- error=api_error,
806
- ),
807
- )
631
+ except Exception: # noqa: BLE001
808
632
  return
809
633
  store.set_artifact("portal", artifact_key, result)
810
634
  store.set_artifact("portal", "result", result)
@@ -2157,24 +1981,11 @@ def _find_created_sub_branch_lane_id(
2157
1981
  return candidates[0] if candidates else None
2158
1982
 
2159
1983
 
2160
- def _has_explicit_workflow_global_settings(global_settings: dict[str, Any] | None) -> bool:
2161
- if not isinstance(global_settings, dict):
2162
- return False
2163
- for key, value in global_settings.items():
2164
- if key == "editVersionNo":
2165
- continue
2166
- if value is None:
2167
- continue
2168
- if isinstance(value, (list, dict)) and not value:
2169
- continue
2170
- return True
2171
- return False
2172
-
2173
-
2174
1984
  def _is_navigation_plugin_unavailable(error: QingflowApiError) -> bool:
2175
- if is_auth_like_error(error):
2176
- return False
2177
- backend_code = backend_code_int(error)
1985
+ try:
1986
+ backend_code = int(error.backend_code)
1987
+ except (TypeError, ValueError):
1988
+ backend_code = None
2178
1989
  if backend_code != 50004:
2179
1990
  return False
2180
1991
  message = error.message or ""
@@ -2211,9 +2022,7 @@ def _coerce_qingflow_error(error: Exception) -> QingflowApiError | None:
2211
2022
 
2212
2023
 
2213
2024
  def _is_permission_restricted_error(error: QingflowApiError) -> bool:
2214
- if is_auth_like_error(error):
2215
- return False
2216
- return backend_code_int(error) in {40002, 40027}
2025
+ return error.backend_code in {40002, 40027}
2217
2026
 
2218
2027
 
2219
2028
  def _required_state_read_blocked_error(
@@ -2243,42 +2052,6 @@ def _required_state_read_blocked_error(
2243
2052
  )
2244
2053
 
2245
2054
 
2246
- def _post_write_readback_artifact(
2247
- *,
2248
- resource: str,
2249
- target: dict[str, Any],
2250
- error: QingflowApiError,
2251
- ) -> dict[str, Any]:
2252
- return {
2253
- "resource": resource,
2254
- "target": deepcopy(target),
2255
- "readback_status": "unavailable",
2256
- "readback_verified": False,
2257
- "write_executed": True,
2258
- "safe_to_retry": False,
2259
- "transport_error": {
2260
- "http_status": error.http_status,
2261
- "backend_code": error.backend_code,
2262
- "category": error.category,
2263
- "request_id": error.request_id,
2264
- },
2265
- }
2266
-
2267
-
2268
- def _artifacts_have_post_write_readback_pending(value: Any) -> bool:
2269
- if isinstance(value, dict):
2270
- if (
2271
- value.get("write_executed") is True
2272
- and value.get("safe_to_retry") is False
2273
- and value.get("readback_status") == "unavailable"
2274
- ):
2275
- return True
2276
- return any(_artifacts_have_post_write_readback_pending(item) for item in value.values())
2277
- if isinstance(value, list):
2278
- return any(_artifacts_have_post_write_readback_pending(item) for item in value)
2279
- return False
2280
-
2281
-
2282
2055
  def _portal_component_position(
2283
2056
  source_type: Any,
2284
2057
  *,