@josephyan/qingflow-app-builder-mcp 0.2.0-beta.4 → 0.2.0-beta.6

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.
package/README.md CHANGED
@@ -3,13 +3,13 @@
3
3
  Install:
4
4
 
5
5
  ```bash
6
- npm install @josephyan/qingflow-app-builder-mcp@0.2.0-beta.4
6
+ npm install @josephyan/qingflow-app-builder-mcp@0.2.0-beta.6
7
7
  ```
8
8
 
9
9
  Run:
10
10
 
11
11
  ```bash
12
- npx -y -p @josephyan/qingflow-app-builder-mcp@0.2.0-beta.4 qingflow-app-builder-mcp
12
+ npx -y -p @josephyan/qingflow-app-builder-mcp@0.2.0-beta.6 qingflow-app-builder-mcp
13
13
  ```
14
14
 
15
15
  Environment:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@josephyan/qingflow-app-builder-mcp",
3
- "version": "0.2.0-beta.4",
3
+ "version": "0.2.0-beta.6",
4
4
  "description": "Builder MCP for Qingflow app/package/system design and staged solution workflows.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "qingflow-mcp"
7
- version = "0.2.0b4"
7
+ version = "0.2.0b6"
8
8
  description = "User-authenticated MCP server for Qingflow"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -2,4 +2,4 @@ from __future__ import annotations
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "0.2.0b4"
5
+ __version__ = "0.2.0b6"
@@ -221,8 +221,14 @@ class AiBuilderFacade:
221
221
  if app_key:
222
222
  try:
223
223
  base = self.apps.app_get_base(profile=profile, app_key=app_key, include_raw=True)
224
- except RuntimeError as exc:
225
- return _failed("APP_NOT_FOUND", f"failed to resolve app_key '{app_key}'", details={"error": str(exc)})
224
+ except (QingflowApiError, RuntimeError) as exc:
225
+ api_error = _coerce_api_error(exc)
226
+ return _failed_from_api_error(
227
+ "APP_NOT_FOUND" if api_error.http_status == 404 else "APP_RESOLVE_FAILED",
228
+ api_error,
229
+ details={"app_key": app_key},
230
+ suggested_next_call={"tool_name": "app_read_summary", "arguments": {"profile": profile, "app_key": app_key}},
231
+ )
226
232
  result = base.get("result") if isinstance(base.get("result"), dict) else {}
227
233
  return {
228
234
  "status": "success",
@@ -299,8 +305,30 @@ class AiBuilderFacade:
299
305
  }
300
306
 
301
307
  def app_read_summary(self, *, profile: str, app_key: str) -> JSONObject:
302
- state = self._load_app_state(profile=profile, app_key=app_key)
308
+ try:
309
+ state = self._load_base_schema_state(profile=profile, app_key=app_key)
310
+ except (QingflowApiError, RuntimeError) as error:
311
+ api_error = _coerce_api_error(error)
312
+ return _failed_from_api_error(
313
+ "APP_READ_FAILED",
314
+ api_error,
315
+ normalized_args={"app_key": app_key},
316
+ details={"app_key": app_key},
317
+ suggested_next_call={"tool_name": "app_resolve", "arguments": {"profile": profile, "app_key": app_key}},
318
+ )
319
+ views, views_unavailable = self._load_views_result(profile=profile, app_key=app_key, tolerate_404=True)
320
+ workflow, workflow_unavailable = self._load_workflow_result(profile=profile, app_key=app_key, tolerate_404=True)
303
321
  parsed = state["parsed"]
322
+ verification_hints = _build_verification_hints(
323
+ tag_ids=_coerce_int_list(state["base"].get("tagIds")),
324
+ fields=parsed["fields"],
325
+ layout=parsed["layout"],
326
+ views=_summarize_views(views),
327
+ )
328
+ if views_unavailable:
329
+ verification_hints.append("views_read_unavailable")
330
+ if workflow_unavailable:
331
+ verification_hints.append("workflow_read_unavailable")
304
332
  response = AppReadSummaryResponse(
305
333
  app_key=app_key,
306
334
  title=state["base"].get("formTitle"),
@@ -308,14 +336,9 @@ class AiBuilderFacade:
308
336
  publish_status=state["base"].get("appPublishStatus"),
309
337
  field_count=len(parsed["fields"]),
310
338
  layout_section_count=len(parsed["layout"].get("sections", [])),
311
- view_count=len(_summarize_views(state["views"])),
312
- workflow_enabled=bool(state["workflow"]),
313
- verification_hints=_build_verification_hints(
314
- tag_ids=_coerce_int_list(state["base"].get("tagIds")),
315
- fields=parsed["fields"],
316
- layout=parsed["layout"],
317
- views=_summarize_views(state["views"]),
318
- ),
339
+ view_count=len(_summarize_views(views)),
340
+ workflow_enabled=bool(workflow),
341
+ verification_hints=verification_hints,
319
342
  )
320
343
  return {
321
344
  "status": "success",
@@ -334,7 +357,17 @@ class AiBuilderFacade:
334
357
  }
335
358
 
336
359
  def app_read_fields(self, *, profile: str, app_key: str) -> JSONObject:
337
- state = self._load_app_state(profile=profile, app_key=app_key)
360
+ try:
361
+ state = self._load_base_schema_state(profile=profile, app_key=app_key)
362
+ except (QingflowApiError, RuntimeError) as error:
363
+ api_error = _coerce_api_error(error)
364
+ return _failed_from_api_error(
365
+ "FIELDS_READ_FAILED",
366
+ api_error,
367
+ normalized_args={"app_key": app_key},
368
+ details={"app_key": app_key},
369
+ suggested_next_call={"tool_name": "app_read_summary", "arguments": {"profile": profile, "app_key": app_key}},
370
+ )
338
371
  parsed = state["parsed"]
339
372
  response = AppFieldsReadResponse(
340
373
  app_key=app_key,
@@ -368,7 +401,17 @@ class AiBuilderFacade:
368
401
  }
369
402
 
370
403
  def app_read_layout_summary(self, *, profile: str, app_key: str) -> JSONObject:
371
- state = self._load_app_state(profile=profile, app_key=app_key)
404
+ try:
405
+ state = self._load_base_schema_state(profile=profile, app_key=app_key)
406
+ except (QingflowApiError, RuntimeError) as error:
407
+ api_error = _coerce_api_error(error)
408
+ return _failed_from_api_error(
409
+ "LAYOUT_READ_FAILED",
410
+ api_error,
411
+ normalized_args={"app_key": app_key},
412
+ details={"app_key": app_key},
413
+ suggested_next_call={"tool_name": "app_read_summary", "arguments": {"profile": profile, "app_key": app_key}},
414
+ )
372
415
  parsed = state["parsed"]
373
416
  layout = parsed["layout"]
374
417
  response = AppLayoutReadResponse(
@@ -394,10 +437,20 @@ class AiBuilderFacade:
394
437
  }
395
438
 
396
439
  def app_read_views_summary(self, *, profile: str, app_key: str) -> JSONObject:
397
- state = self._load_app_state(profile=profile, app_key=app_key)
440
+ try:
441
+ views, _ = self._load_views_result(profile=profile, app_key=app_key, tolerate_404=False)
442
+ except (QingflowApiError, RuntimeError) as error:
443
+ api_error = _coerce_api_error(error)
444
+ return _failed_from_api_error(
445
+ "VIEWS_READ_FAILED",
446
+ api_error,
447
+ normalized_args={"app_key": app_key},
448
+ details={"app_key": app_key},
449
+ suggested_next_call={"tool_name": "app_read_summary", "arguments": {"profile": profile, "app_key": app_key}},
450
+ )
398
451
  response = AppViewsReadResponse(
399
452
  app_key=app_key,
400
- views=_summarize_views(state["views"]),
453
+ views=_summarize_views(views),
401
454
  )
402
455
  return {
403
456
  "status": "success",
@@ -416,11 +469,21 @@ class AiBuilderFacade:
416
469
  }
417
470
 
418
471
  def app_read_flow_summary(self, *, profile: str, app_key: str) -> JSONObject:
419
- state = self._load_app_state(profile=profile, app_key=app_key)
472
+ try:
473
+ workflow, workflow_unavailable = self._load_workflow_result(profile=profile, app_key=app_key, tolerate_404=True)
474
+ except (QingflowApiError, RuntimeError) as error:
475
+ api_error = _coerce_api_error(error)
476
+ return _failed_from_api_error(
477
+ "FLOW_READ_FAILED",
478
+ api_error,
479
+ normalized_args={"app_key": app_key},
480
+ details={"app_key": app_key},
481
+ suggested_next_call={"tool_name": "app_read_summary", "arguments": {"profile": profile, "app_key": app_key}},
482
+ )
420
483
  response = AppFlowReadResponse(
421
484
  app_key=app_key,
422
- enabled=bool(state["workflow"]),
423
- nodes=_summarize_workflow_nodes(state["workflow"]),
485
+ enabled=bool(workflow),
486
+ nodes=_summarize_workflow_nodes(workflow),
424
487
  transitions=[],
425
488
  )
426
489
  return {
@@ -435,7 +498,7 @@ class AiBuilderFacade:
435
498
  "request_id": None,
436
499
  "suggested_next_call": None,
437
500
  "noop": False,
438
- "verification": {"app_exists": True},
501
+ "verification": {"app_exists": True, "workflow_read_unavailable": workflow_unavailable},
439
502
  **response.model_dump(mode="json"),
440
503
  }
441
504
 
@@ -453,7 +516,11 @@ class AiBuilderFacade:
453
516
  return target
454
517
  current_fields: list[dict[str, Any]] = []
455
518
  if not bool(target.get("would_create")):
456
- current_fields = self.app_read_fields(profile=profile, app_key=str(target["app_key"])).get("fields", [])
519
+ fields_result = self.app_read_fields(profile=profile, app_key=str(target["app_key"]))
520
+ if fields_result.get("status") == "failed":
521
+ fields_result.setdefault("normalized_args", normalized_args)
522
+ return fields_result
523
+ current_fields = fields_result.get("fields", [])
457
524
  current_by_name = {str(field.get("name") or ""): field for field in current_fields}
458
525
  blocking_issues: list[dict[str, Any]] = []
459
526
  preview_added: list[str] = []
@@ -511,8 +578,12 @@ class AiBuilderFacade:
511
578
 
512
579
  def app_layout_plan(self, *, profile: str, request: LayoutPlanRequest) -> JSONObject:
513
580
  read_fields = self.app_read_fields(profile=profile, app_key=request.app_key)
581
+ if read_fields.get("status") == "failed":
582
+ return read_fields
514
583
  current_names = [str(field.get("name") or "") for field in read_fields.get("fields", []) if field.get("name")]
515
584
  current_layout = self.app_read_layout_summary(profile=profile, app_key=request.app_key)
585
+ if current_layout.get("status") == "failed":
586
+ return current_layout
516
587
  requested_sections = [section.model_dump(mode="json") for section in request.sections]
517
588
  if request.preset is not None:
518
589
  requested_sections = _build_layout_preset_sections(preset=request.preset, field_names=current_names)
@@ -575,7 +646,10 @@ class AiBuilderFacade:
575
646
  transitions = [transition.model_dump(mode="json", by_alias=True) for transition in request.transitions]
576
647
  if request.preset is not None:
577
648
  nodes, transitions = _build_flow_preset(request.preset)
578
- current_fields = self.app_read_fields(profile=profile, app_key=request.app_key).get("fields", [])
649
+ fields_result = self.app_read_fields(profile=profile, app_key=request.app_key)
650
+ if fields_result.get("status") == "failed":
651
+ return fields_result
652
+ current_fields = fields_result.get("fields", [])
579
653
  status_field_present = _infer_status_field_id(current_fields) is not None
580
654
  node_types = {str(node.get("type") or "") for node in nodes}
581
655
  if ("approve" in node_types or request.preset in {FlowPreset.basic_approval, FlowPreset.basic_fill_then_approve}) and not status_field_present:
@@ -645,7 +719,10 @@ class AiBuilderFacade:
645
719
  }
646
720
 
647
721
  def app_views_plan(self, *, profile: str, request: ViewsPlanRequest) -> JSONObject:
648
- current_fields = self.app_read_fields(profile=profile, app_key=request.app_key).get("fields", [])
722
+ fields_result = self.app_read_fields(profile=profile, app_key=request.app_key)
723
+ if fields_result.get("status") == "failed":
724
+ return fields_result
725
+ current_fields = fields_result.get("fields", [])
649
726
  field_names = {str(field.get("name") or "") for field in current_fields}
650
727
  upsert_views = [view.model_dump(mode="json") for view in request.upsert_views]
651
728
  if request.preset is not None:
@@ -762,7 +839,23 @@ class AiBuilderFacade:
762
839
  app_name=str(resolved["app_name"]),
763
840
  tag_ids=_coerce_int_list(resolved.get("tag_ids")),
764
841
  )
765
- schema_result, _schema_source = self._read_schema_with_fallback(profile=profile, app_key=target.app_key)
842
+ schema_readback_delayed = False
843
+ try:
844
+ schema_result, _schema_source = self._read_schema_with_fallback(profile=profile, app_key=target.app_key)
845
+ except (QingflowApiError, RuntimeError) as error:
846
+ api_error = _coerce_api_error(error)
847
+ if not bool(resolved.get("created")) or api_error.http_status != 404:
848
+ return _failed_from_api_error(
849
+ "SCHEMA_READBACK_FAILED",
850
+ api_error,
851
+ normalized_args=normalized_args,
852
+ allowed_values={"field_types": [item.value for item in PublicFieldType]},
853
+ details={"app_key": target.app_key},
854
+ suggested_next_call={"tool_name": "app_read_summary", "arguments": {"profile": profile, "app_key": target.app_key}},
855
+ )
856
+ schema_result = _empty_schema_result(target.app_name)
857
+ _schema_source = "synthetic_new_app"
858
+ schema_readback_delayed = True
766
859
  parsed = _parse_schema(schema_result)
767
860
  current_fields = parsed["fields"]
768
861
  layout = parsed["layout"]
@@ -870,13 +963,8 @@ class AiBuilderFacade:
870
963
  },
871
964
  suggested_next_call={"tool_name": "app_read_fields", "arguments": {"profile": profile, "app_key": target.app_key}},
872
965
  )
873
- verified = self.app_read(profile=profile, app_key=target.app_key, include_raw=False)
874
- verified_field_names = {field["name"] for field in verified["schema"]["fields"]}
875
- tag_ids_after = _coerce_int_list((self.apps.app_get_base(profile=profile, app_key=target.app_key, include_raw=True).get("result") or {}).get("tagIds"))
876
- verification_ok = all(name in verified_field_names for name in added + updated) and all(name not in verified_field_names for name in removed)
877
- package_attached = None if package_tag_id is None else package_tag_id in tag_ids_after
878
966
  response = {
879
- "status": "success" if verification_ok and (package_attached is not False) else "partial_success",
967
+ "status": "success",
880
968
  "error_code": None,
881
969
  "recoverable": False,
882
970
  "message": "applied schema patch",
@@ -885,21 +973,11 @@ class AiBuilderFacade:
885
973
  "allowed_values": {"field_types": [item.value for item in PublicFieldType]},
886
974
  "details": {},
887
975
  "request_id": None,
888
- "suggested_next_call": None
889
- if package_attached is not False
890
- else {
891
- "tool_name": "package_attach_app",
892
- "arguments": {
893
- "profile": profile,
894
- "tag_id": package_tag_id,
895
- "app_key": target.app_key,
896
- "app_title": app_name or target.app_name,
897
- },
898
- },
976
+ "suggested_next_call": None,
899
977
  "noop": False,
900
978
  "verification": {
901
- "fields_verified": verification_ok,
902
- "package_attached": package_attached,
979
+ "fields_verified": False,
980
+ "package_attached": None,
903
981
  },
904
982
  "app_key": target.app_key,
905
983
  "created": bool(resolved.get("created")),
@@ -908,11 +986,71 @@ class AiBuilderFacade:
908
986
  "updated": updated,
909
987
  "removed": removed,
910
988
  },
911
- "verified": verification_ok,
912
- "tag_ids_after": tag_ids_after,
913
- "package_attached": package_attached,
989
+ "verified": False,
990
+ "tag_ids_after": [],
991
+ "package_attached": None,
914
992
  }
915
- return self._append_publish_result(profile=profile, app_key=target.app_key, publish=publish, response=response)
993
+ if schema_readback_delayed:
994
+ response["verification"]["schema_readback_delayed"] = True
995
+ response = self._append_publish_result(profile=profile, app_key=target.app_key, publish=publish, response=response)
996
+ verification_ok = False
997
+ tag_ids_after: list[int] = []
998
+ package_attached: bool | None = None
999
+ verification_error: QingflowApiError | None = None
1000
+ try:
1001
+ verified = self.app_read(profile=profile, app_key=target.app_key, include_raw=False)
1002
+ verified_field_names = {field["name"] for field in verified["schema"]["fields"]}
1003
+ verification_ok = all(name in verified_field_names for name in added + updated) and all(name not in verified_field_names for name in removed)
1004
+ except (QingflowApiError, RuntimeError) as error:
1005
+ verification_error = _coerce_api_error(error)
1006
+ verification_ok = False
1007
+ try:
1008
+ base_info = self.apps.app_get_base(profile=profile, app_key=target.app_key, include_raw=True).get("result") or {}
1009
+ tag_ids_after = _coerce_int_list(base_info.get("tagIds"))
1010
+ package_attached = None if package_tag_id is None else package_tag_id in tag_ids_after
1011
+ except (QingflowApiError, RuntimeError) as error:
1012
+ base_error = _coerce_api_error(error)
1013
+ if verification_error is None:
1014
+ verification_error = base_error
1015
+ tag_ids_after = []
1016
+ package_attached = None if package_tag_id is None else False
1017
+ response["verification"]["fields_verified"] = verification_ok
1018
+ response["verification"]["package_attached"] = package_attached
1019
+ response["verified"] = verification_ok
1020
+ response["tag_ids_after"] = tag_ids_after
1021
+ response["package_attached"] = package_attached
1022
+ if package_attached is False:
1023
+ response["suggested_next_call"] = {
1024
+ "tool_name": "package_attach_app",
1025
+ "arguments": {
1026
+ "profile": profile,
1027
+ "tag_id": package_tag_id,
1028
+ "app_key": target.app_key,
1029
+ "app_title": app_name or target.app_name,
1030
+ },
1031
+ }
1032
+ publish_failed = bool(response.get("publish_requested")) and not bool(response.get("published"))
1033
+ if verification_ok and package_attached is not False and not publish_failed:
1034
+ response["status"] = "success"
1035
+ else:
1036
+ response["status"] = "partial_success"
1037
+ if verification_error is not None:
1038
+ response["recoverable"] = True
1039
+ response["error_code"] = response.get("error_code") or (
1040
+ "READBACK_PENDING" if verification_error.http_status == 404 else "READBACK_FAILED"
1041
+ )
1042
+ response["message"] = f"{response.get('message') or 'apply succeeded'}; readback pending"
1043
+ response["request_id"] = response.get("request_id") or verification_error.request_id
1044
+ details = response.get("details")
1045
+ if not isinstance(details, dict):
1046
+ details = {}
1047
+ response["details"] = details
1048
+ details["verification_error"] = {
1049
+ "message": verification_error.message,
1050
+ "http_status": verification_error.http_status,
1051
+ "backend_code": verification_error.backend_code,
1052
+ }
1053
+ return response
916
1054
 
917
1055
  def app_layout_apply(
918
1056
  self,
@@ -929,7 +1067,17 @@ class AiBuilderFacade:
929
1067
  "sections": [section.model_dump(mode="json") for section in sections],
930
1068
  "publish": publish,
931
1069
  }
932
- schema_result, _schema_source = self._read_schema_with_fallback(profile=profile, app_key=app_key)
1070
+ try:
1071
+ schema_result, _schema_source = self._read_schema_with_fallback(profile=profile, app_key=app_key)
1072
+ except (QingflowApiError, RuntimeError) as error:
1073
+ api_error = _coerce_api_error(error)
1074
+ return _failed_from_api_error(
1075
+ "LAYOUT_READ_FAILED",
1076
+ api_error,
1077
+ normalized_args=normalized_args,
1078
+ details={"app_key": app_key},
1079
+ suggested_next_call={"tool_name": "app_read_layout_summary", "arguments": {"profile": profile, "app_key": app_key}},
1080
+ )
933
1081
  parsed = _parse_schema(schema_result)
934
1082
  current_fields = parsed["fields"]
935
1083
  fields_by_name = {field["name"]: field for field in current_fields}
@@ -1060,7 +1208,32 @@ class AiBuilderFacade:
1060
1208
  },
1061
1209
  suggested_next_call={"tool_name": "app_layout_plan", "arguments": {"profile": profile, **normalized_args}},
1062
1210
  )
1063
- verified = self.app_read(profile=profile, app_key=app_key, include_raw=False)
1211
+ verified = self.app_read_layout_summary(profile=profile, app_key=app_key)
1212
+ if verified.get("status") == "failed":
1213
+ response = {
1214
+ "status": "partial_success",
1215
+ "error_code": "LAYOUT_READBACK_PENDING",
1216
+ "recoverable": True,
1217
+ "message": "applied app layout; layout readback pending",
1218
+ "normalized_args": normalized_args,
1219
+ "missing_fields": [],
1220
+ "allowed_values": {"modes": ["merge", "replace"]},
1221
+ "details": {},
1222
+ "request_id": verified.get("request_id"),
1223
+ "suggested_next_call": {"tool_name": "app_read_layout_summary", "arguments": {"profile": profile, "app_key": app_key}},
1224
+ "noop": False,
1225
+ "verification": {"layout_verified": False, "layout_read_unavailable": True},
1226
+ "app_key": app_key,
1227
+ "layout_diff": {
1228
+ "mode": mode.value,
1229
+ "replaced": mode == LayoutApplyMode.replace,
1230
+ "merged": mode == LayoutApplyMode.merge,
1231
+ "auto_added_fields": merged["auto_added_fields"] if mode == LayoutApplyMode.merge else [],
1232
+ "fallback_applied": fallback_applied,
1233
+ },
1234
+ "verified": False,
1235
+ }
1236
+ return self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response)
1064
1237
  response = {
1065
1238
  "status": "partial_success" if fallback_applied else "success",
1066
1239
  "error_code": None,
@@ -1073,7 +1246,7 @@ class AiBuilderFacade:
1073
1246
  "request_id": None,
1074
1247
  "suggested_next_call": None,
1075
1248
  "noop": False,
1076
- "verification": {"layout_verified": verified["layout"] == applied_layout},
1249
+ "verification": {"layout_verified": verified["sections"] == applied_layout.get("sections", [])},
1077
1250
  "app_key": app_key,
1078
1251
  "layout_diff": {
1079
1252
  "mode": mode.value,
@@ -1082,7 +1255,7 @@ class AiBuilderFacade:
1082
1255
  "auto_added_fields": merged["auto_added_fields"] if mode == LayoutApplyMode.merge else [],
1083
1256
  "fallback_applied": fallback_applied,
1084
1257
  },
1085
- "verified": verified["layout"] == applied_layout,
1258
+ "verified": verified["sections"] == applied_layout.get("sections", []),
1086
1259
  }
1087
1260
  return self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response)
1088
1261
 
@@ -1111,8 +1284,18 @@ class AiBuilderFacade:
1111
1284
  allowed_values={"modes": ["replace"]},
1112
1285
  suggested_next_call={"tool_name": "app_flow_plan", "arguments": {"profile": profile, **normalized_args}},
1113
1286
  )
1114
- base = self.apps.app_get_base(profile=profile, app_key=app_key, include_raw=True).get("result") or {}
1115
- schema, _schema_source = self._read_schema_with_fallback(profile=profile, app_key=app_key)
1287
+ try:
1288
+ base = self.apps.app_get_base(profile=profile, app_key=app_key, include_raw=True).get("result") or {}
1289
+ schema, _schema_source = self._read_schema_with_fallback(profile=profile, app_key=app_key)
1290
+ except (QingflowApiError, RuntimeError) as error:
1291
+ api_error = _coerce_api_error(error)
1292
+ return _failed_from_api_error(
1293
+ "FLOW_READ_FAILED",
1294
+ api_error,
1295
+ normalized_args=normalized_args,
1296
+ details={"app_key": app_key},
1297
+ suggested_next_call={"tool_name": "app_read_flow_summary", "arguments": {"profile": profile, "app_key": app_key}},
1298
+ )
1116
1299
  entity = _entity_spec_from_app(base_info=base, schema=schema, views=None)
1117
1300
  workflow_spec = _build_public_workflow_spec(nodes=nodes, transitions=transitions)
1118
1301
  if workflow_spec.get("status") == "failed":
@@ -1121,7 +1304,8 @@ class AiBuilderFacade:
1121
1304
  workflow_spec["suggested_next_call"] = {"tool_name": "app_flow_plan", "arguments": {"profile": profile, **normalized_args}}
1122
1305
  return workflow_spec
1123
1306
  desired_node_count = len([node for node in nodes if node.get("type") != "end"])
1124
- current_node_count = len(_summarize_workflow_nodes(self.workflows.workflow_list_nodes(profile=profile, app_key=app_key).get("result")))
1307
+ current_workflow, _workflow_unavailable = self._load_workflow_result(profile=profile, app_key=app_key, tolerate_404=True)
1308
+ current_node_count = len(_summarize_workflow_nodes(current_workflow))
1125
1309
  if current_node_count == desired_node_count and desired_node_count > 0:
1126
1310
  # Lightweight idempotency check for repeat submissions of same simple graph.
1127
1311
  pass
@@ -1165,12 +1349,12 @@ class AiBuilderFacade:
1165
1349
  failed["normalized_args"] = normalized_args
1166
1350
  failed["suggested_next_call"] = failed.get("suggested_next_call") or {"tool_name": "app_flow_plan", "arguments": {"profile": profile, **normalized_args}}
1167
1351
  return failed
1168
- verified_nodes = self.workflows.workflow_list_nodes(profile=profile, app_key=app_key).get("result")
1352
+ verified_nodes, verified_nodes_unavailable = self._load_workflow_result(profile=profile, app_key=app_key, tolerate_404=True)
1169
1353
  response = {
1170
- "status": "success",
1354
+ "status": "success" if bool(verified_nodes) or not verified_nodes_unavailable else "partial_success",
1171
1355
  "error_code": None,
1172
- "recoverable": False,
1173
- "message": "applied workflow patch",
1356
+ "recoverable": bool(verified_nodes_unavailable),
1357
+ "message": "applied workflow patch" if not verified_nodes_unavailable else "applied workflow patch; flow readback pending",
1174
1358
  "normalized_args": normalized_args,
1175
1359
  "missing_fields": [],
1176
1360
  "allowed_values": {"modes": ["replace"]},
@@ -1178,11 +1362,14 @@ class AiBuilderFacade:
1178
1362
  "request_id": None,
1179
1363
  "suggested_next_call": None,
1180
1364
  "noop": False,
1181
- "verification": {"workflow_verified": bool(verified_nodes)},
1365
+ "verification": {"workflow_verified": bool(verified_nodes), "workflow_read_unavailable": verified_nodes_unavailable},
1182
1366
  "app_key": app_key,
1183
1367
  "flow_diff": {"mode": "replace", "node_count": desired_node_count},
1184
1368
  "verified": bool(verified_nodes),
1185
1369
  }
1370
+ if verified_nodes_unavailable:
1371
+ response["error_code"] = "FLOW_READBACK_PENDING"
1372
+ response["suggested_next_call"] = {"tool_name": "app_read_flow_summary", "arguments": {"profile": profile, "app_key": app_key}}
1186
1373
  return self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response)
1187
1374
 
1188
1375
  def app_views_apply(
@@ -1219,9 +1406,20 @@ class AiBuilderFacade:
1219
1406
  "verified": True,
1220
1407
  }
1221
1408
  return self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response)
1222
- base = self.apps.app_get_base(profile=profile, app_key=app_key, include_raw=True).get("result") or {}
1223
- schema, _schema_source = self._read_schema_with_fallback(profile=profile, app_key=app_key)
1224
- existing_views = self.views.view_list_flat(profile=profile, app_key=app_key).get("result") or []
1409
+ try:
1410
+ base = self.apps.app_get_base(profile=profile, app_key=app_key, include_raw=True).get("result") or {}
1411
+ schema, _schema_source = self._read_schema_with_fallback(profile=profile, app_key=app_key)
1412
+ existing_views, _views_unavailable = self._load_views_result(profile=profile, app_key=app_key, tolerate_404=False)
1413
+ except (QingflowApiError, RuntimeError) as error:
1414
+ api_error = _coerce_api_error(error)
1415
+ return _failed_from_api_error(
1416
+ "VIEWS_READ_FAILED",
1417
+ api_error,
1418
+ normalized_args=normalized_args,
1419
+ details={"app_key": app_key},
1420
+ suggested_next_call={"tool_name": "app_read_views_summary", "arguments": {"profile": profile, "app_key": app_key}},
1421
+ )
1422
+ existing_views = existing_views or []
1225
1423
  existing_by_name = {}
1226
1424
  for view in existing_views if isinstance(existing_views, list) else []:
1227
1425
  if not isinstance(view, dict):
@@ -1334,22 +1532,37 @@ class AiBuilderFacade:
1334
1532
  "arguments": {"profile": profile, "app_key": app_key},
1335
1533
  },
1336
1534
  )
1337
- verified_names = {item.get("viewgraphName") or item.get("viewName") or item.get("title") for item in (self.views.view_list_flat(profile=profile, app_key=app_key).get("result") or []) if isinstance(item, dict)}
1338
- verified = all(name in verified_names for name in created + updated) and all(name not in verified_names for name in removed)
1535
+ try:
1536
+ verified_view_result, verified_views_unavailable = self._load_views_result(profile=profile, app_key=app_key, tolerate_404=True)
1537
+ except (QingflowApiError, RuntimeError) as error:
1538
+ api_error = _coerce_api_error(error)
1539
+ return _failed_from_api_error(
1540
+ "VIEWS_READ_FAILED",
1541
+ api_error,
1542
+ normalized_args=normalized_args,
1543
+ details={"app_key": app_key},
1544
+ suggested_next_call={"tool_name": "app_read_views_summary", "arguments": {"profile": profile, "app_key": app_key}},
1545
+ )
1546
+ verified_names = {
1547
+ item.get("viewgraphName") or item.get("viewName") or item.get("title")
1548
+ for item in (verified_view_result or [])
1549
+ if isinstance(item, dict)
1550
+ }
1551
+ verified = (not verified_views_unavailable) and all(name in verified_names for name in created + updated) and all(name not in verified_names for name in removed)
1339
1552
  noop = not created and not updated and not removed
1340
1553
  response = {
1341
1554
  "status": "success" if verified else "partial_success",
1342
- "error_code": None,
1343
- "recoverable": False,
1344
- "message": "applied view patch",
1555
+ "error_code": None if not verified_views_unavailable else "VIEWS_READBACK_PENDING",
1556
+ "recoverable": bool(verified_views_unavailable),
1557
+ "message": "applied view patch" if not verified_views_unavailable else "applied view patch; views readback pending",
1345
1558
  "normalized_args": normalized_args,
1346
1559
  "missing_fields": [],
1347
1560
  "allowed_values": {"view_types": ["table", "card", "board"]},
1348
1561
  "details": {},
1349
1562
  "request_id": None,
1350
- "suggested_next_call": None,
1563
+ "suggested_next_call": None if not verified_views_unavailable else {"tool_name": "app_read_views_summary", "arguments": {"profile": profile, "app_key": app_key}},
1351
1564
  "noop": noop,
1352
- "verification": {"views_verified": verified},
1565
+ "verification": {"views_verified": verified, "views_read_unavailable": verified_views_unavailable},
1353
1566
  "app_key": app_key,
1354
1567
  "views_diff": {"created": created, "updated": updated, "removed": removed},
1355
1568
  "verified": verified,
@@ -1364,12 +1577,33 @@ class AiBuilderFacade:
1364
1577
  expected_package_tag_id: int | None = None,
1365
1578
  ) -> JSONObject:
1366
1579
  normalized_args = {"app_key": app_key, "expected_package_tag_id": expected_package_tag_id}
1367
- base_before = self.apps.app_get_base(profile=profile, app_key=app_key, include_raw=True).get("result") or {}
1580
+ try:
1581
+ base_before = self.apps.app_get_base(profile=profile, app_key=app_key, include_raw=True).get("result") or {}
1582
+ except (QingflowApiError, RuntimeError) as error:
1583
+ api_error = _coerce_api_error(error)
1584
+ return _failed_from_api_error(
1585
+ "APP_READ_FAILED",
1586
+ api_error,
1587
+ normalized_args=normalized_args,
1588
+ details={"app_key": app_key},
1589
+ suggested_next_call={"tool_name": "app_read_summary", "arguments": {"profile": profile, "app_key": app_key}},
1590
+ )
1368
1591
  tag_ids_before = _coerce_int_list(base_before.get("tagIds"))
1369
1592
  already_published = bool(base_before.get("appPublishStatus") in {1, 2})
1370
1593
  package_already_attached = None if not expected_package_tag_id else expected_package_tag_id in tag_ids_before
1371
- views_before = self.views.view_list_flat(profile=profile, app_key=app_key).get("result") or []
1372
- if already_published and package_already_attached is not False and isinstance(views_before, list):
1594
+ try:
1595
+ views_before, views_before_unavailable = self._load_views_result(profile=profile, app_key=app_key, tolerate_404=True)
1596
+ except (QingflowApiError, RuntimeError) as error:
1597
+ api_error = _coerce_api_error(error)
1598
+ return _failed_from_api_error(
1599
+ "VIEWS_READ_FAILED",
1600
+ api_error,
1601
+ normalized_args=normalized_args,
1602
+ details={"app_key": app_key},
1603
+ suggested_next_call={"tool_name": "app_read_views_summary", "arguments": {"profile": profile, "app_key": app_key}},
1604
+ )
1605
+ views_before = views_before or []
1606
+ if already_published and package_already_attached is not False and isinstance(views_before, list) and not views_before_unavailable:
1373
1607
  return {
1374
1608
  "status": "success",
1375
1609
  "error_code": None,
@@ -1404,17 +1638,38 @@ class AiBuilderFacade:
1404
1638
  details={"app_key": app_key, "edit_version_no": edit_version_no},
1405
1639
  suggested_next_call={"tool_name": "app_read_summary", "arguments": {"profile": profile, "app_key": app_key}},
1406
1640
  )
1407
- base = self.apps.app_get_base(profile=profile, app_key=app_key, include_raw=True).get("result") or {}
1641
+ try:
1642
+ base = self.apps.app_get_base(profile=profile, app_key=app_key, include_raw=True).get("result") or {}
1643
+ except (QingflowApiError, RuntimeError) as error:
1644
+ api_error = _coerce_api_error(error)
1645
+ return _failed_from_api_error(
1646
+ "APP_READ_FAILED",
1647
+ api_error,
1648
+ normalized_args=normalized_args,
1649
+ details={"app_key": app_key},
1650
+ suggested_next_call={"tool_name": "app_read_summary", "arguments": {"profile": profile, "app_key": app_key}},
1651
+ )
1408
1652
  tag_ids_after = _coerce_int_list(base.get("tagIds"))
1409
1653
  package_attached = None if not expected_package_tag_id else expected_package_tag_id in tag_ids_after
1410
- views = self.views.view_list_flat(profile=profile, app_key=app_key).get("result") or []
1411
- views_ok = isinstance(views, list)
1654
+ try:
1655
+ views, views_unavailable = self._load_views_result(profile=profile, app_key=app_key, tolerate_404=True)
1656
+ except (QingflowApiError, RuntimeError) as error:
1657
+ api_error = _coerce_api_error(error)
1658
+ return _failed_from_api_error(
1659
+ "VIEWS_READ_FAILED",
1660
+ api_error,
1661
+ normalized_args=normalized_args,
1662
+ details={"app_key": app_key},
1663
+ suggested_next_call={"tool_name": "app_read_views_summary", "arguments": {"profile": profile, "app_key": app_key}},
1664
+ )
1665
+ views = views or []
1666
+ views_ok = isinstance(views, list) and not views_unavailable
1412
1667
  verified = bool(base.get("appPublishStatus") in {1, 2}) and (package_attached is not False) and views_ok
1413
1668
  return {
1414
1669
  "status": "success" if verified else "partial_success",
1415
- "error_code": None,
1416
- "recoverable": False,
1417
- "message": "published and verified app",
1670
+ "error_code": None if not views_unavailable else "VIEWS_READBACK_PENDING",
1671
+ "recoverable": bool(views_unavailable),
1672
+ "message": "published and verified app" if not views_unavailable else "published app; views readback pending",
1418
1673
  "normalized_args": normalized_args,
1419
1674
  "missing_fields": [],
1420
1675
  "allowed_values": {},
@@ -1427,7 +1682,7 @@ class AiBuilderFacade:
1427
1682
  "arguments": {"profile": profile, "tag_id": expected_package_tag_id, "app_key": app_key},
1428
1683
  },
1429
1684
  "noop": False,
1430
- "verification": {"published": bool(base.get("appPublishStatus") in {1, 2}), "package_attached": package_attached, "views_ok": views_ok},
1685
+ "verification": {"published": bool(base.get("appPublishStatus") in {1, 2}), "package_attached": package_attached, "views_ok": views_ok, "views_read_unavailable": views_unavailable},
1431
1686
  "app_key": app_key,
1432
1687
  "published": bool(base.get("appPublishStatus") in {1, 2}),
1433
1688
  "package_attached": package_attached,
@@ -1491,21 +1746,59 @@ class AiBuilderFacade:
1491
1746
  response["suggested_next_call"] = publish_result.get("suggested_next_call")
1492
1747
  return response
1493
1748
 
1494
- def _load_app_state(self, *, profile: str, app_key: str) -> dict[str, Any]:
1749
+ def _load_base_schema_state(self, *, profile: str, app_key: str) -> dict[str, Any]:
1495
1750
  base = self.apps.app_get_base(profile=profile, app_key=app_key, include_raw=True)
1496
1751
  schema_result, schema_source = self._read_schema_with_fallback(profile=profile, app_key=app_key)
1497
- views = self.views.view_list_flat(profile=profile, app_key=app_key)
1498
- workflow = self.workflows.workflow_list_nodes(profile=profile, app_key=app_key)
1499
1752
  base_result = base.get("result") if isinstance(base.get("result"), dict) else {}
1500
1753
  return {
1501
1754
  "base": base_result,
1502
1755
  "schema": schema_result,
1503
1756
  "parsed": _parse_schema(schema_result),
1504
- "views": views.get("result"),
1505
- "workflow": workflow.get("result"),
1506
1757
  "schema_source": schema_source,
1507
1758
  }
1508
1759
 
1760
+ def _load_views_result(self, *, profile: str, app_key: str, tolerate_404: bool) -> tuple[Any, bool]:
1761
+ try:
1762
+ views = self.views.view_list_flat(profile=profile, app_key=app_key)
1763
+ except (QingflowApiError, RuntimeError) as error:
1764
+ api_error = _coerce_api_error(error)
1765
+ if api_error.http_status == 404:
1766
+ try:
1767
+ legacy_views = self.views.view_list(profile=profile, app_key=app_key)
1768
+ except (QingflowApiError, RuntimeError) as legacy_error:
1769
+ legacy_api_error = _coerce_api_error(legacy_error)
1770
+ if tolerate_404 and legacy_api_error.http_status == 404:
1771
+ return [], True
1772
+ raise
1773
+ legacy_result = legacy_views.get("result")
1774
+ if _is_view_collection_shape(legacy_result):
1775
+ return _normalize_view_collection(legacy_result), False
1776
+ if tolerate_404:
1777
+ return [], True
1778
+ raise error
1779
+ raise
1780
+ return _normalize_view_collection(views.get("result")), False
1781
+
1782
+ def _load_workflow_result(self, *, profile: str, app_key: str, tolerate_404: bool) -> tuple[Any, bool]:
1783
+ try:
1784
+ workflow = self.workflows.workflow_list_nodes(profile=profile, app_key=app_key)
1785
+ except (QingflowApiError, RuntimeError) as error:
1786
+ api_error = _coerce_api_error(error)
1787
+ if tolerate_404 and api_error.http_status == 404:
1788
+ return [], True
1789
+ raise
1790
+ return workflow.get("result"), False
1791
+
1792
+ def _load_app_state(self, *, profile: str, app_key: str) -> dict[str, Any]:
1793
+ state = self._load_base_schema_state(profile=profile, app_key=app_key)
1794
+ views, views_unavailable = self._load_views_result(profile=profile, app_key=app_key, tolerate_404=True)
1795
+ workflow, workflow_unavailable = self._load_workflow_result(profile=profile, app_key=app_key, tolerate_404=True)
1796
+ state["views"] = views
1797
+ state["workflow"] = workflow
1798
+ state["views_unavailable"] = views_unavailable
1799
+ state["workflow_unavailable"] = workflow_unavailable
1800
+ return state
1801
+
1509
1802
  def _read_schema_with_fallback(self, *, profile: str, app_key: str) -> tuple[dict[str, Any], str]:
1510
1803
  attempts = (
1511
1804
  ("draft", True),
@@ -1623,7 +1916,28 @@ class AiBuilderFacade:
1623
1916
  new_app_key = str(result.get("appKey") or (result.get("appKeys")[0] if isinstance(result.get("appKeys"), list) and result.get("appKeys") else ""))
1624
1917
  if not new_app_key:
1625
1918
  return _failed("APP_CREATE_FAILED", "failed to create app shell", details={"result": result}, suggested_next_call=None)
1626
- base = self.apps.app_get_base(profile=profile, app_key=new_app_key, include_raw=True).get("result") or {}
1919
+ try:
1920
+ base = self.apps.app_get_base(profile=profile, app_key=new_app_key, include_raw=True).get("result") or {}
1921
+ except (QingflowApiError, RuntimeError) as error:
1922
+ api_error = _coerce_api_error(error)
1923
+ if api_error.http_status != 404:
1924
+ return _failed_from_api_error(
1925
+ "APP_CREATE_READBACK_FAILED",
1926
+ api_error,
1927
+ details={"app_key": new_app_key, "app_name": app_name, "package_tag_id": package_tag_id},
1928
+ suggested_next_call={"tool_name": "app_read_summary", "arguments": {"profile": profile, "app_key": new_app_key}},
1929
+ )
1930
+ return {
1931
+ "status": "success",
1932
+ "error_code": None,
1933
+ "recoverable": False,
1934
+ "message": "created app; base readback pending",
1935
+ "suggested_next_call": None,
1936
+ "app_key": new_app_key,
1937
+ "app_name": app_name or "未命名应用",
1938
+ "tag_ids": [],
1939
+ "created": True,
1940
+ }
1627
1941
  return {
1628
1942
  "status": "success",
1629
1943
  "error_code": None,
@@ -1718,18 +2032,30 @@ def _failed_from_api_error(
1718
2032
  suggested_next_call: JSONObject | None = None,
1719
2033
  recoverable: bool = True,
1720
2034
  ) -> JSONObject:
2035
+ public_message = _public_error_message(error_code, error)
2036
+ public_http_status = None if error.http_status == 404 else error.http_status
2037
+ merged_details = dict(details or {})
2038
+ if error.http_status is not None or error.backend_code is not None:
2039
+ merged_details.setdefault(
2040
+ "transport_error",
2041
+ {
2042
+ "http_status": error.http_status,
2043
+ "backend_code": error.backend_code,
2044
+ "category": error.category,
2045
+ },
2046
+ )
1721
2047
  return _failed(
1722
2048
  error_code,
1723
- error.message,
2049
+ public_message,
1724
2050
  recoverable=recoverable,
1725
2051
  normalized_args=normalized_args,
1726
2052
  missing_fields=missing_fields,
1727
2053
  allowed_values=allowed_values,
1728
- details=details,
2054
+ details=merged_details,
1729
2055
  suggested_next_call=suggested_next_call,
1730
2056
  request_id=error.request_id,
1731
2057
  backend_code=error.backend_code,
1732
- http_status=error.http_status,
2058
+ http_status=public_http_status,
1733
2059
  )
1734
2060
 
1735
2061
 
@@ -1768,6 +2094,27 @@ def _coerce_api_error(error: Exception) -> QingflowApiError:
1768
2094
  return QingflowApiError(category="runtime", message=str(error))
1769
2095
 
1770
2096
 
2097
+ def _public_error_message(error_code: str, error: QingflowApiError) -> str:
2098
+ if error.http_status != 404:
2099
+ return error.message
2100
+ mapping = {
2101
+ "APP_READ_FAILED": "app base or schema is unavailable in the current route",
2102
+ "FIELDS_READ_FAILED": "app fields are unavailable in the current route",
2103
+ "LAYOUT_READ_FAILED": "layout resource is unavailable for this app in the current route",
2104
+ "VIEWS_READ_FAILED": "views resource is unavailable for this app in the current route",
2105
+ "FLOW_READ_FAILED": "workflow resource is unavailable for this app in the current route",
2106
+ "SCHEMA_READBACK_FAILED": "schema was written but schema readback is unavailable in the current route",
2107
+ "CREATE_APP_ROUTE_NOT_FOUND": "create app route is unavailable in the current workspace route",
2108
+ "APP_CREATE_READBACK_FAILED": "app was created but base readback is unavailable in the current route",
2109
+ "PACKAGE_ATTACH_FAILED": "package attachment could not be verified in the current route",
2110
+ "PUBLISH_FAILED": "publish route is unavailable in the current route",
2111
+ "VIEW_APPLY_FAILED": "view resource rejected the operation or is unavailable in the current route",
2112
+ "LAYOUT_APPLY_FAILED": "layout resource rejected the operation or is unavailable in the current route",
2113
+ "SCHEMA_APPLY_FAILED": "schema resource rejected the operation or is unavailable in the current route",
2114
+ }
2115
+ return mapping.get(error_code, "requested builder resource is unavailable in the current route")
2116
+
2117
+
1771
2118
  def _coerce_positive_int(value: Any) -> int | None:
1772
2119
  if isinstance(value, bool) or value is None:
1773
2120
  return None
@@ -1795,6 +2142,34 @@ def _coerce_int_list(values: Any) -> list[int]:
1795
2142
  return result
1796
2143
 
1797
2144
 
2145
+ def _normalize_view_collection(values: Any) -> list[dict[str, Any]]:
2146
+ if isinstance(values, list):
2147
+ return [item for item in values if isinstance(item, dict)]
2148
+ if isinstance(values, dict):
2149
+ for key in ("list", "viewList", "views", "result"):
2150
+ candidate = values.get(key)
2151
+ if isinstance(candidate, list):
2152
+ return [item for item in candidate if isinstance(item, dict)]
2153
+ return []
2154
+
2155
+
2156
+ def _is_view_collection_shape(values: Any) -> bool:
2157
+ if isinstance(values, list):
2158
+ return True
2159
+ if isinstance(values, dict):
2160
+ return any(isinstance(values.get(key), list) for key in ("list", "viewList", "views", "result"))
2161
+ return False
2162
+
2163
+
2164
+ def _empty_schema_result(title: str) -> dict[str, Any]:
2165
+ return {
2166
+ "formTitle": title,
2167
+ "editVersionNo": 1,
2168
+ "formQues": [],
2169
+ "questionRelations": [],
2170
+ }
2171
+
2172
+
1798
2173
  def _slugify(text: str, *, default: str) -> str:
1799
2174
  normalized = "".join(ch.lower() if ch.isalnum() else "_" for ch in str(text or ""))
1800
2175
  collapsed = "_".join(part for part in normalized.split("_") if part)
@@ -1,8 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import json
4
+
3
5
  from pydantic import ValidationError
4
6
 
5
7
  from ..config import DEFAULT_PROFILE
8
+ from ..errors import QingflowApiError
6
9
  from ..json_types import JSONObject
7
10
  from ..builder_facade.models import (
8
11
  FieldPatch,
@@ -240,31 +243,85 @@ class AiBuilderTools(ToolBase):
240
243
  )
241
244
 
242
245
  def package_list(self, *, profile: str, trial_status: str = "all") -> JSONObject:
243
- return self._facade.package_list(profile=profile, trial_status=trial_status)
246
+ normalized_args = {"trial_status": trial_status}
247
+ return _safe_tool_call(
248
+ lambda: self._facade.package_list(profile=profile, trial_status=trial_status),
249
+ error_code="PACKAGE_LIST_FAILED",
250
+ normalized_args=normalized_args,
251
+ suggested_next_call={"tool_name": "package_list", "arguments": {"profile": profile, "trial_status": trial_status}},
252
+ )
244
253
 
245
254
  def package_resolve(self, *, profile: str, package_name: str) -> JSONObject:
246
- return self._facade.package_resolve(profile=profile, package_name=package_name)
255
+ normalized_args = {"package_name": package_name}
256
+ return _safe_tool_call(
257
+ lambda: self._facade.package_resolve(profile=profile, package_name=package_name),
258
+ error_code="PACKAGE_RESOLVE_FAILED",
259
+ normalized_args=normalized_args,
260
+ suggested_next_call={"tool_name": "package_resolve", "arguments": {"profile": profile, "package_name": package_name}},
261
+ )
247
262
 
248
263
  def package_attach_app(self, *, profile: str, tag_id: int, app_key: str, app_title: str = "") -> JSONObject:
249
- return self._facade.package_attach_app(profile=profile, tag_id=tag_id, app_key=app_key, app_title=app_title)
264
+ normalized_args = {"tag_id": tag_id, "app_key": app_key, "app_title": app_title}
265
+ return _safe_tool_call(
266
+ lambda: self._facade.package_attach_app(profile=profile, tag_id=tag_id, app_key=app_key, app_title=app_title),
267
+ error_code="PACKAGE_ATTACH_FAILED",
268
+ normalized_args=normalized_args,
269
+ suggested_next_call={"tool_name": "package_attach_app", "arguments": {"profile": profile, **normalized_args}},
270
+ )
250
271
 
251
272
  def app_resolve(self, *, profile: str, app_key: str = "", app_name: str = "", package_tag_id: int | None = None) -> JSONObject:
252
- return self._facade.app_resolve(profile=profile, app_key=app_key, app_name=app_name, package_tag_id=package_tag_id)
273
+ normalized_args = {"app_key": app_key, "app_name": app_name, "package_tag_id": package_tag_id}
274
+ return _safe_tool_call(
275
+ lambda: self._facade.app_resolve(profile=profile, app_key=app_key, app_name=app_name, package_tag_id=package_tag_id),
276
+ error_code="APP_RESOLVE_FAILED",
277
+ normalized_args=normalized_args,
278
+ suggested_next_call={"tool_name": "app_resolve", "arguments": {"profile": profile, **normalized_args}},
279
+ )
253
280
 
254
281
  def app_read_summary(self, *, profile: str, app_key: str) -> JSONObject:
255
- return self._facade.app_read_summary(profile=profile, app_key=app_key)
282
+ normalized_args = {"app_key": app_key}
283
+ return _safe_tool_call(
284
+ lambda: self._facade.app_read_summary(profile=profile, app_key=app_key),
285
+ error_code="APP_READ_FAILED",
286
+ normalized_args=normalized_args,
287
+ suggested_next_call={"tool_name": "app_read_summary", "arguments": {"profile": profile, "app_key": app_key}},
288
+ )
256
289
 
257
290
  def app_read_fields(self, *, profile: str, app_key: str) -> JSONObject:
258
- return self._facade.app_read_fields(profile=profile, app_key=app_key)
291
+ normalized_args = {"app_key": app_key}
292
+ return _safe_tool_call(
293
+ lambda: self._facade.app_read_fields(profile=profile, app_key=app_key),
294
+ error_code="FIELDS_READ_FAILED",
295
+ normalized_args=normalized_args,
296
+ suggested_next_call={"tool_name": "app_read_fields", "arguments": {"profile": profile, "app_key": app_key}},
297
+ )
259
298
 
260
299
  def app_read_layout_summary(self, *, profile: str, app_key: str) -> JSONObject:
261
- return self._facade.app_read_layout_summary(profile=profile, app_key=app_key)
300
+ normalized_args = {"app_key": app_key}
301
+ return _safe_tool_call(
302
+ lambda: self._facade.app_read_layout_summary(profile=profile, app_key=app_key),
303
+ error_code="LAYOUT_READ_FAILED",
304
+ normalized_args=normalized_args,
305
+ suggested_next_call={"tool_name": "app_read_layout_summary", "arguments": {"profile": profile, "app_key": app_key}},
306
+ )
262
307
 
263
308
  def app_read_views_summary(self, *, profile: str, app_key: str) -> JSONObject:
264
- return self._facade.app_read_views_summary(profile=profile, app_key=app_key)
309
+ normalized_args = {"app_key": app_key}
310
+ return _safe_tool_call(
311
+ lambda: self._facade.app_read_views_summary(profile=profile, app_key=app_key),
312
+ error_code="VIEWS_READ_FAILED",
313
+ normalized_args=normalized_args,
314
+ suggested_next_call={"tool_name": "app_read_views_summary", "arguments": {"profile": profile, "app_key": app_key}},
315
+ )
265
316
 
266
317
  def app_read_flow_summary(self, *, profile: str, app_key: str) -> JSONObject:
267
- return self._facade.app_read_flow_summary(profile=profile, app_key=app_key)
318
+ normalized_args = {"app_key": app_key}
319
+ return _safe_tool_call(
320
+ lambda: self._facade.app_read_flow_summary(profile=profile, app_key=app_key),
321
+ error_code="FLOW_READ_FAILED",
322
+ normalized_args=normalized_args,
323
+ suggested_next_call={"tool_name": "app_read_flow_summary", "arguments": {"profile": profile, "app_key": app_key}},
324
+ )
268
325
 
269
326
  def app_schema_plan(
270
327
  self,
@@ -307,7 +364,12 @@ class AiBuilderTools(ToolBase):
307
364
  },
308
365
  },
309
366
  )
310
- return self._facade.app_schema_plan(profile=profile, request=request)
367
+ return _safe_tool_call(
368
+ lambda: self._facade.app_schema_plan(profile=profile, request=request),
369
+ error_code="SCHEMA_PLAN_FAILED",
370
+ normalized_args=request.model_dump(mode="json"),
371
+ suggested_next_call={"tool_name": "app_schema_plan", "arguments": {"profile": profile, **request.model_dump(mode="json")}},
372
+ )
311
373
 
312
374
  def app_layout_plan(
313
375
  self,
@@ -341,7 +403,12 @@ class AiBuilderTools(ToolBase):
341
403
  },
342
404
  },
343
405
  )
344
- return self._facade.app_layout_plan(profile=profile, request=request)
406
+ return _safe_tool_call(
407
+ lambda: self._facade.app_layout_plan(profile=profile, request=request),
408
+ error_code="LAYOUT_PLAN_FAILED",
409
+ normalized_args=request.model_dump(mode="json"),
410
+ suggested_next_call={"tool_name": "app_layout_plan", "arguments": {"profile": profile, **request.model_dump(mode="json")}},
411
+ )
345
412
 
346
413
  def app_flow_plan(
347
414
  self,
@@ -378,7 +445,12 @@ class AiBuilderTools(ToolBase):
378
445
  },
379
446
  },
380
447
  )
381
- return self._facade.app_flow_plan(profile=profile, request=request)
448
+ return _safe_tool_call(
449
+ lambda: self._facade.app_flow_plan(profile=profile, request=request),
450
+ error_code="FLOW_PLAN_FAILED",
451
+ normalized_args=request.model_dump(mode="json"),
452
+ suggested_next_call={"tool_name": "app_flow_plan", "arguments": {"profile": profile, **request.model_dump(mode="json")}},
453
+ )
382
454
 
383
455
  def app_views_plan(
384
456
  self,
@@ -412,7 +484,12 @@ class AiBuilderTools(ToolBase):
412
484
  },
413
485
  },
414
486
  )
415
- return self._facade.app_views_plan(profile=profile, request=request)
487
+ return _safe_tool_call(
488
+ lambda: self._facade.app_views_plan(profile=profile, request=request),
489
+ error_code="VIEWS_PLAN_FAILED",
490
+ normalized_args=request.model_dump(mode="json"),
491
+ suggested_next_call={"tool_name": "app_views_plan", "arguments": {"profile": profile, **request.model_dump(mode="json")}},
492
+ )
416
493
 
417
494
  def app_schema_apply(
418
495
  self,
@@ -450,16 +527,31 @@ class AiBuilderTools(ToolBase):
450
527
  },
451
528
  },
452
529
  )
453
- return self._facade.app_schema_apply(
454
- profile=profile,
455
- app_key=app_key,
456
- package_tag_id=package_tag_id,
457
- app_name=effective_app_name,
458
- create_if_missing=create_if_missing,
459
- publish=publish,
460
- add_fields=parsed_add,
461
- update_fields=parsed_update,
462
- remove_fields=parsed_remove,
530
+ normalized_args = {
531
+ "app_key": app_key,
532
+ "package_tag_id": package_tag_id,
533
+ "app_name": effective_app_name,
534
+ "create_if_missing": create_if_missing,
535
+ "publish": publish,
536
+ "add_fields": [patch.model_dump(mode="json") for patch in parsed_add],
537
+ "update_fields": [patch.model_dump(mode="json") for patch in parsed_update],
538
+ "remove_fields": [patch.model_dump(mode="json") for patch in parsed_remove],
539
+ }
540
+ return _safe_tool_call(
541
+ lambda: self._facade.app_schema_apply(
542
+ profile=profile,
543
+ app_key=app_key,
544
+ package_tag_id=package_tag_id,
545
+ app_name=effective_app_name,
546
+ create_if_missing=create_if_missing,
547
+ publish=publish,
548
+ add_fields=parsed_add,
549
+ update_fields=parsed_update,
550
+ remove_fields=parsed_remove,
551
+ ),
552
+ error_code="SCHEMA_APPLY_FAILED",
553
+ normalized_args=normalized_args,
554
+ suggested_next_call={"tool_name": "app_schema_apply", "arguments": {"profile": profile, **normalized_args}},
463
555
  )
464
556
 
465
557
  def app_layout_apply(self, *, profile: str, app_key: str, mode: str = "merge", publish: bool = True, sections: list[JSONObject]) -> JSONObject:
@@ -495,7 +587,18 @@ class AiBuilderTools(ToolBase):
495
587
  },
496
588
  },
497
589
  )
498
- return self._facade.app_layout_apply(profile=profile, app_key=app_key, mode=parsed_mode, publish=publish, sections=parsed_sections)
590
+ normalized_args = {
591
+ "app_key": app_key,
592
+ "mode": parsed_mode.value,
593
+ "publish": publish,
594
+ "sections": [section.model_dump(mode="json") for section in parsed_sections],
595
+ }
596
+ return _safe_tool_call(
597
+ lambda: self._facade.app_layout_apply(profile=profile, app_key=app_key, mode=parsed_mode, publish=publish, sections=parsed_sections),
598
+ error_code="LAYOUT_APPLY_FAILED",
599
+ normalized_args=normalized_args,
600
+ suggested_next_call={"tool_name": "app_layout_apply", "arguments": {"profile": profile, **normalized_args}},
601
+ )
499
602
 
500
603
  def app_flow_apply(
501
604
  self,
@@ -531,13 +634,25 @@ class AiBuilderTools(ToolBase):
531
634
  },
532
635
  },
533
636
  )
534
- return self._facade.app_flow_apply(
535
- profile=profile,
536
- app_key=request.app_key,
537
- mode=request.mode,
538
- publish=publish,
539
- nodes=[node.model_dump(mode="json") for node in request.nodes],
540
- transitions=[transition.model_dump(mode="json", by_alias=True) for transition in request.transitions],
637
+ normalized_args = {
638
+ "app_key": request.app_key,
639
+ "mode": request.mode,
640
+ "publish": publish,
641
+ "nodes": [node.model_dump(mode="json") for node in request.nodes],
642
+ "transitions": [transition.model_dump(mode="json", by_alias=True) for transition in request.transitions],
643
+ }
644
+ return _safe_tool_call(
645
+ lambda: self._facade.app_flow_apply(
646
+ profile=profile,
647
+ app_key=request.app_key,
648
+ mode=request.mode,
649
+ publish=publish,
650
+ nodes=[node.model_dump(mode="json") for node in request.nodes],
651
+ transitions=[transition.model_dump(mode="json", by_alias=True) for transition in request.transitions],
652
+ ),
653
+ error_code="FLOW_APPLY_FAILED",
654
+ normalized_args=normalized_args,
655
+ suggested_next_call={"tool_name": "app_flow_apply", "arguments": {"profile": profile, **normalized_args}},
541
656
  )
542
657
 
543
658
  def app_views_apply(
@@ -564,10 +679,27 @@ class AiBuilderTools(ToolBase):
564
679
  },
565
680
  },
566
681
  )
567
- return self._facade.app_views_apply(profile=profile, app_key=app_key, publish=publish, upsert_views=parsed_views, remove_views=remove_views)
682
+ normalized_args = {
683
+ "app_key": app_key,
684
+ "publish": publish,
685
+ "upsert_views": [view.model_dump(mode="json") for view in parsed_views],
686
+ "remove_views": list(remove_views),
687
+ }
688
+ return _safe_tool_call(
689
+ lambda: self._facade.app_views_apply(profile=profile, app_key=app_key, publish=publish, upsert_views=parsed_views, remove_views=remove_views),
690
+ error_code="VIEWS_APPLY_FAILED",
691
+ normalized_args=normalized_args,
692
+ suggested_next_call={"tool_name": "app_views_apply", "arguments": {"profile": profile, **normalized_args}},
693
+ )
568
694
 
569
695
  def app_publish_verify(self, *, profile: str, app_key: str, expected_package_tag_id: int | None = None) -> JSONObject:
570
- return self._facade.app_publish_verify(profile=profile, app_key=app_key, expected_package_tag_id=expected_package_tag_id)
696
+ normalized_args = {"app_key": app_key, "expected_package_tag_id": expected_package_tag_id}
697
+ return _safe_tool_call(
698
+ lambda: self._facade.app_publish_verify(profile=profile, app_key=app_key, expected_package_tag_id=expected_package_tag_id),
699
+ error_code="PUBLISH_VERIFY_FAILED",
700
+ normalized_args=normalized_args,
701
+ suggested_next_call={"tool_name": "app_publish_verify", "arguments": {"profile": profile, **normalized_args}},
702
+ )
571
703
 
572
704
 
573
705
  def _validation_failure(detail: str, *, suggested_next_call: JSONObject | None = None) -> JSONObject:
@@ -587,3 +719,86 @@ def _validation_failure(detail: str, *, suggested_next_call: JSONObject | None =
587
719
  "noop": False,
588
720
  "verification": {},
589
721
  }
722
+
723
+
724
+ def _safe_tool_call(
725
+ call,
726
+ *,
727
+ error_code: str,
728
+ normalized_args: JSONObject,
729
+ suggested_next_call: JSONObject | None,
730
+ ) -> JSONObject:
731
+ try:
732
+ return call()
733
+ except (QingflowApiError, RuntimeError) as error:
734
+ api_error = _coerce_api_error(error)
735
+ public_http_status = None if api_error.http_status == 404 else api_error.http_status
736
+ return {
737
+ "status": "failed",
738
+ "error_code": error_code,
739
+ "recoverable": True,
740
+ "message": _public_error_message(error_code, api_error),
741
+ "normalized_args": normalized_args,
742
+ "missing_fields": [],
743
+ "allowed_values": {},
744
+ "details": {
745
+ "transport_error": {
746
+ "http_status": api_error.http_status,
747
+ "backend_code": api_error.backend_code,
748
+ "category": api_error.category,
749
+ }
750
+ },
751
+ "suggested_next_call": suggested_next_call,
752
+ "request_id": api_error.request_id,
753
+ "backend_code": api_error.backend_code,
754
+ "http_status": public_http_status,
755
+ "noop": False,
756
+ "verification": {},
757
+ }
758
+
759
+
760
+ def _coerce_api_error(error: Exception) -> QingflowApiError:
761
+ if isinstance(error, QingflowApiError):
762
+ return error
763
+ if isinstance(error, RuntimeError):
764
+ try:
765
+ payload = json.loads(str(error))
766
+ except json.JSONDecodeError:
767
+ payload = None
768
+ if isinstance(payload, dict) and payload.get("category") and payload.get("message"):
769
+ details = payload.get("details")
770
+ return QingflowApiError(
771
+ category=str(payload.get("category")),
772
+ message=str(payload.get("message")),
773
+ backend_code=payload.get("backend_code"),
774
+ request_id=payload.get("request_id"),
775
+ http_status=payload.get("http_status"),
776
+ details=details if isinstance(details, dict) else None,
777
+ )
778
+ return QingflowApiError(category="runtime", message=str(error))
779
+
780
+
781
+ def _public_error_message(error_code: str, error: QingflowApiError) -> str:
782
+ if error.http_status != 404:
783
+ return error.message
784
+ mapping = {
785
+ "PACKAGE_LIST_FAILED": "package list is unavailable in the current route",
786
+ "PACKAGE_RESOLVE_FAILED": "package resolution is unavailable in the current route",
787
+ "PACKAGE_ATTACH_FAILED": "package attachment could not be verified in the current route",
788
+ "APP_RESOLVE_FAILED": "app resolution is unavailable in the current route",
789
+ "APP_READ_FAILED": "app base or schema is unavailable in the current route",
790
+ "FIELDS_READ_FAILED": "app fields are unavailable in the current route",
791
+ "LAYOUT_READ_FAILED": "layout resource is unavailable for this app in the current route",
792
+ "VIEWS_READ_FAILED": "views resource is unavailable for this app in the current route",
793
+ "FLOW_READ_FAILED": "workflow resource is unavailable for this app in the current route",
794
+ "SCHEMA_PLAN_FAILED": "schema planning could not load the required app state in the current route",
795
+ "LAYOUT_PLAN_FAILED": "layout planning could not load the required app state in the current route",
796
+ "FLOW_PLAN_FAILED": "flow planning could not load the required app state in the current route",
797
+ "VIEWS_PLAN_FAILED": "views planning could not load the required app state in the current route",
798
+ "SCHEMA_APPLY_FAILED": "schema apply could not complete because the app route or readback is unavailable",
799
+ "LAYOUT_APPLY_FAILED": "layout apply could not complete because the layout route or readback is unavailable",
800
+ "FLOW_APPLY_FAILED": "flow apply could not complete because the workflow route or readback is unavailable",
801
+ "VIEWS_APPLY_FAILED": "views apply could not complete because the views route or readback is unavailable",
802
+ "PUBLISH_VERIFY_FAILED": "publish verification is unavailable in the current route",
803
+ }
804
+ return mapping.get(error_code, "requested builder resource is unavailable in the current route")
@@ -237,7 +237,7 @@ class AppTools(ToolBase):
237
237
 
238
238
  def runner(session_profile, context):
239
239
  attempted_contexts = [context]
240
- if context.qf_version is not None and (context.qf_version_source or "unset") != "explicit":
240
+ if context.qf_version is not None:
241
241
  attempted_contexts.append(
242
242
  BackendRequestContext(
243
243
  base_url=context.base_url,
@@ -270,7 +270,6 @@ class AppTools(ToolBase):
270
270
  is_retryable_404 = (
271
271
  error.http_status == 404
272
272
  and call_context.qf_version is not None
273
- and (context.qf_version_source or "unset") != "explicit"
274
273
  )
275
274
  if not is_retryable_404:
276
275
  raise