@josephyan/qingflow-app-builder-mcp 0.2.0-beta.5 → 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.5
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.5 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.5",
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.0b5"
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.0b5"
5
+ __version__ = "0.2.0b6"
@@ -305,8 +305,30 @@ class AiBuilderFacade:
305
305
  }
306
306
 
307
307
  def app_read_summary(self, *, profile: str, app_key: str) -> JSONObject:
308
- 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)
309
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")
310
332
  response = AppReadSummaryResponse(
311
333
  app_key=app_key,
312
334
  title=state["base"].get("formTitle"),
@@ -314,14 +336,9 @@ class AiBuilderFacade:
314
336
  publish_status=state["base"].get("appPublishStatus"),
315
337
  field_count=len(parsed["fields"]),
316
338
  layout_section_count=len(parsed["layout"].get("sections", [])),
317
- view_count=len(_summarize_views(state["views"])),
318
- workflow_enabled=bool(state["workflow"]),
319
- verification_hints=_build_verification_hints(
320
- tag_ids=_coerce_int_list(state["base"].get("tagIds")),
321
- fields=parsed["fields"],
322
- layout=parsed["layout"],
323
- views=_summarize_views(state["views"]),
324
- ),
339
+ view_count=len(_summarize_views(views)),
340
+ workflow_enabled=bool(workflow),
341
+ verification_hints=verification_hints,
325
342
  )
326
343
  return {
327
344
  "status": "success",
@@ -340,7 +357,17 @@ class AiBuilderFacade:
340
357
  }
341
358
 
342
359
  def app_read_fields(self, *, profile: str, app_key: str) -> JSONObject:
343
- 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
+ )
344
371
  parsed = state["parsed"]
345
372
  response = AppFieldsReadResponse(
346
373
  app_key=app_key,
@@ -374,7 +401,17 @@ class AiBuilderFacade:
374
401
  }
375
402
 
376
403
  def app_read_layout_summary(self, *, profile: str, app_key: str) -> JSONObject:
377
- 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
+ )
378
415
  parsed = state["parsed"]
379
416
  layout = parsed["layout"]
380
417
  response = AppLayoutReadResponse(
@@ -400,10 +437,20 @@ class AiBuilderFacade:
400
437
  }
401
438
 
402
439
  def app_read_views_summary(self, *, profile: str, app_key: str) -> JSONObject:
403
- 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
+ )
404
451
  response = AppViewsReadResponse(
405
452
  app_key=app_key,
406
- views=_summarize_views(state["views"]),
453
+ views=_summarize_views(views),
407
454
  )
408
455
  return {
409
456
  "status": "success",
@@ -422,11 +469,21 @@ class AiBuilderFacade:
422
469
  }
423
470
 
424
471
  def app_read_flow_summary(self, *, profile: str, app_key: str) -> JSONObject:
425
- 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
+ )
426
483
  response = AppFlowReadResponse(
427
484
  app_key=app_key,
428
- enabled=bool(state["workflow"]),
429
- nodes=_summarize_workflow_nodes(state["workflow"]),
485
+ enabled=bool(workflow),
486
+ nodes=_summarize_workflow_nodes(workflow),
430
487
  transitions=[],
431
488
  )
432
489
  return {
@@ -441,7 +498,7 @@ class AiBuilderFacade:
441
498
  "request_id": None,
442
499
  "suggested_next_call": None,
443
500
  "noop": False,
444
- "verification": {"app_exists": True},
501
+ "verification": {"app_exists": True, "workflow_read_unavailable": workflow_unavailable},
445
502
  **response.model_dump(mode="json"),
446
503
  }
447
504
 
@@ -459,7 +516,11 @@ class AiBuilderFacade:
459
516
  return target
460
517
  current_fields: list[dict[str, Any]] = []
461
518
  if not bool(target.get("would_create")):
462
- 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", [])
463
524
  current_by_name = {str(field.get("name") or ""): field for field in current_fields}
464
525
  blocking_issues: list[dict[str, Any]] = []
465
526
  preview_added: list[str] = []
@@ -517,8 +578,12 @@ class AiBuilderFacade:
517
578
 
518
579
  def app_layout_plan(self, *, profile: str, request: LayoutPlanRequest) -> JSONObject:
519
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
520
583
  current_names = [str(field.get("name") or "") for field in read_fields.get("fields", []) if field.get("name")]
521
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
522
587
  requested_sections = [section.model_dump(mode="json") for section in request.sections]
523
588
  if request.preset is not None:
524
589
  requested_sections = _build_layout_preset_sections(preset=request.preset, field_names=current_names)
@@ -581,7 +646,10 @@ class AiBuilderFacade:
581
646
  transitions = [transition.model_dump(mode="json", by_alias=True) for transition in request.transitions]
582
647
  if request.preset is not None:
583
648
  nodes, transitions = _build_flow_preset(request.preset)
584
- 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", [])
585
653
  status_field_present = _infer_status_field_id(current_fields) is not None
586
654
  node_types = {str(node.get("type") or "") for node in nodes}
587
655
  if ("approve" in node_types or request.preset in {FlowPreset.basic_approval, FlowPreset.basic_fill_then_approve}) and not status_field_present:
@@ -651,7 +719,10 @@ class AiBuilderFacade:
651
719
  }
652
720
 
653
721
  def app_views_plan(self, *, profile: str, request: ViewsPlanRequest) -> JSONObject:
654
- 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", [])
655
726
  field_names = {str(field.get("name") or "") for field in current_fields}
656
727
  upsert_views = [view.model_dump(mode="json") for view in request.upsert_views]
657
728
  if request.preset is not None:
@@ -996,7 +1067,17 @@ class AiBuilderFacade:
996
1067
  "sections": [section.model_dump(mode="json") for section in sections],
997
1068
  "publish": publish,
998
1069
  }
999
- 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
+ )
1000
1081
  parsed = _parse_schema(schema_result)
1001
1082
  current_fields = parsed["fields"]
1002
1083
  fields_by_name = {field["name"]: field for field in current_fields}
@@ -1127,7 +1208,32 @@ class AiBuilderFacade:
1127
1208
  },
1128
1209
  suggested_next_call={"tool_name": "app_layout_plan", "arguments": {"profile": profile, **normalized_args}},
1129
1210
  )
1130
- 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)
1131
1237
  response = {
1132
1238
  "status": "partial_success" if fallback_applied else "success",
1133
1239
  "error_code": None,
@@ -1140,7 +1246,7 @@ class AiBuilderFacade:
1140
1246
  "request_id": None,
1141
1247
  "suggested_next_call": None,
1142
1248
  "noop": False,
1143
- "verification": {"layout_verified": verified["layout"] == applied_layout},
1249
+ "verification": {"layout_verified": verified["sections"] == applied_layout.get("sections", [])},
1144
1250
  "app_key": app_key,
1145
1251
  "layout_diff": {
1146
1252
  "mode": mode.value,
@@ -1149,7 +1255,7 @@ class AiBuilderFacade:
1149
1255
  "auto_added_fields": merged["auto_added_fields"] if mode == LayoutApplyMode.merge else [],
1150
1256
  "fallback_applied": fallback_applied,
1151
1257
  },
1152
- "verified": verified["layout"] == applied_layout,
1258
+ "verified": verified["sections"] == applied_layout.get("sections", []),
1153
1259
  }
1154
1260
  return self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response)
1155
1261
 
@@ -1178,8 +1284,18 @@ class AiBuilderFacade:
1178
1284
  allowed_values={"modes": ["replace"]},
1179
1285
  suggested_next_call={"tool_name": "app_flow_plan", "arguments": {"profile": profile, **normalized_args}},
1180
1286
  )
1181
- base = self.apps.app_get_base(profile=profile, app_key=app_key, include_raw=True).get("result") or {}
1182
- 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
+ )
1183
1299
  entity = _entity_spec_from_app(base_info=base, schema=schema, views=None)
1184
1300
  workflow_spec = _build_public_workflow_spec(nodes=nodes, transitions=transitions)
1185
1301
  if workflow_spec.get("status") == "failed":
@@ -1188,7 +1304,8 @@ class AiBuilderFacade:
1188
1304
  workflow_spec["suggested_next_call"] = {"tool_name": "app_flow_plan", "arguments": {"profile": profile, **normalized_args}}
1189
1305
  return workflow_spec
1190
1306
  desired_node_count = len([node for node in nodes if node.get("type") != "end"])
1191
- 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))
1192
1309
  if current_node_count == desired_node_count and desired_node_count > 0:
1193
1310
  # Lightweight idempotency check for repeat submissions of same simple graph.
1194
1311
  pass
@@ -1232,12 +1349,12 @@ class AiBuilderFacade:
1232
1349
  failed["normalized_args"] = normalized_args
1233
1350
  failed["suggested_next_call"] = failed.get("suggested_next_call") or {"tool_name": "app_flow_plan", "arguments": {"profile": profile, **normalized_args}}
1234
1351
  return failed
1235
- 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)
1236
1353
  response = {
1237
- "status": "success",
1354
+ "status": "success" if bool(verified_nodes) or not verified_nodes_unavailable else "partial_success",
1238
1355
  "error_code": None,
1239
- "recoverable": False,
1240
- "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",
1241
1358
  "normalized_args": normalized_args,
1242
1359
  "missing_fields": [],
1243
1360
  "allowed_values": {"modes": ["replace"]},
@@ -1245,11 +1362,14 @@ class AiBuilderFacade:
1245
1362
  "request_id": None,
1246
1363
  "suggested_next_call": None,
1247
1364
  "noop": False,
1248
- "verification": {"workflow_verified": bool(verified_nodes)},
1365
+ "verification": {"workflow_verified": bool(verified_nodes), "workflow_read_unavailable": verified_nodes_unavailable},
1249
1366
  "app_key": app_key,
1250
1367
  "flow_diff": {"mode": "replace", "node_count": desired_node_count},
1251
1368
  "verified": bool(verified_nodes),
1252
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}}
1253
1373
  return self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response)
1254
1374
 
1255
1375
  def app_views_apply(
@@ -1286,9 +1406,20 @@ class AiBuilderFacade:
1286
1406
  "verified": True,
1287
1407
  }
1288
1408
  return self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response)
1289
- base = self.apps.app_get_base(profile=profile, app_key=app_key, include_raw=True).get("result") or {}
1290
- schema, _schema_source = self._read_schema_with_fallback(profile=profile, app_key=app_key)
1291
- 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 []
1292
1423
  existing_by_name = {}
1293
1424
  for view in existing_views if isinstance(existing_views, list) else []:
1294
1425
  if not isinstance(view, dict):
@@ -1401,22 +1532,37 @@ class AiBuilderFacade:
1401
1532
  "arguments": {"profile": profile, "app_key": app_key},
1402
1533
  },
1403
1534
  )
1404
- 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)}
1405
- 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)
1406
1552
  noop = not created and not updated and not removed
1407
1553
  response = {
1408
1554
  "status": "success" if verified else "partial_success",
1409
- "error_code": None,
1410
- "recoverable": False,
1411
- "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",
1412
1558
  "normalized_args": normalized_args,
1413
1559
  "missing_fields": [],
1414
1560
  "allowed_values": {"view_types": ["table", "card", "board"]},
1415
1561
  "details": {},
1416
1562
  "request_id": None,
1417
- "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}},
1418
1564
  "noop": noop,
1419
- "verification": {"views_verified": verified},
1565
+ "verification": {"views_verified": verified, "views_read_unavailable": verified_views_unavailable},
1420
1566
  "app_key": app_key,
1421
1567
  "views_diff": {"created": created, "updated": updated, "removed": removed},
1422
1568
  "verified": verified,
@@ -1431,12 +1577,33 @@ class AiBuilderFacade:
1431
1577
  expected_package_tag_id: int | None = None,
1432
1578
  ) -> JSONObject:
1433
1579
  normalized_args = {"app_key": app_key, "expected_package_tag_id": expected_package_tag_id}
1434
- 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
+ )
1435
1591
  tag_ids_before = _coerce_int_list(base_before.get("tagIds"))
1436
1592
  already_published = bool(base_before.get("appPublishStatus") in {1, 2})
1437
1593
  package_already_attached = None if not expected_package_tag_id else expected_package_tag_id in tag_ids_before
1438
- views_before = self.views.view_list_flat(profile=profile, app_key=app_key).get("result") or []
1439
- 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:
1440
1607
  return {
1441
1608
  "status": "success",
1442
1609
  "error_code": None,
@@ -1471,17 +1638,38 @@ class AiBuilderFacade:
1471
1638
  details={"app_key": app_key, "edit_version_no": edit_version_no},
1472
1639
  suggested_next_call={"tool_name": "app_read_summary", "arguments": {"profile": profile, "app_key": app_key}},
1473
1640
  )
1474
- 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
+ )
1475
1652
  tag_ids_after = _coerce_int_list(base.get("tagIds"))
1476
1653
  package_attached = None if not expected_package_tag_id else expected_package_tag_id in tag_ids_after
1477
- views = self.views.view_list_flat(profile=profile, app_key=app_key).get("result") or []
1478
- 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
1479
1667
  verified = bool(base.get("appPublishStatus") in {1, 2}) and (package_attached is not False) and views_ok
1480
1668
  return {
1481
1669
  "status": "success" if verified else "partial_success",
1482
- "error_code": None,
1483
- "recoverable": False,
1484
- "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",
1485
1673
  "normalized_args": normalized_args,
1486
1674
  "missing_fields": [],
1487
1675
  "allowed_values": {},
@@ -1494,7 +1682,7 @@ class AiBuilderFacade:
1494
1682
  "arguments": {"profile": profile, "tag_id": expected_package_tag_id, "app_key": app_key},
1495
1683
  },
1496
1684
  "noop": False,
1497
- "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},
1498
1686
  "app_key": app_key,
1499
1687
  "published": bool(base.get("appPublishStatus") in {1, 2}),
1500
1688
  "package_attached": package_attached,
@@ -1558,21 +1746,59 @@ class AiBuilderFacade:
1558
1746
  response["suggested_next_call"] = publish_result.get("suggested_next_call")
1559
1747
  return response
1560
1748
 
1561
- 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]:
1562
1750
  base = self.apps.app_get_base(profile=profile, app_key=app_key, include_raw=True)
1563
1751
  schema_result, schema_source = self._read_schema_with_fallback(profile=profile, app_key=app_key)
1564
- views = self.views.view_list_flat(profile=profile, app_key=app_key)
1565
- workflow = self.workflows.workflow_list_nodes(profile=profile, app_key=app_key)
1566
1752
  base_result = base.get("result") if isinstance(base.get("result"), dict) else {}
1567
1753
  return {
1568
1754
  "base": base_result,
1569
1755
  "schema": schema_result,
1570
1756
  "parsed": _parse_schema(schema_result),
1571
- "views": views.get("result"),
1572
- "workflow": workflow.get("result"),
1573
1757
  "schema_source": schema_source,
1574
1758
  }
1575
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
+
1576
1802
  def _read_schema_with_fallback(self, *, profile: str, app_key: str) -> tuple[dict[str, Any], str]:
1577
1803
  attempts = (
1578
1804
  ("draft", True),
@@ -1806,18 +2032,30 @@ def _failed_from_api_error(
1806
2032
  suggested_next_call: JSONObject | None = None,
1807
2033
  recoverable: bool = True,
1808
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
+ )
1809
2047
  return _failed(
1810
2048
  error_code,
1811
- error.message,
2049
+ public_message,
1812
2050
  recoverable=recoverable,
1813
2051
  normalized_args=normalized_args,
1814
2052
  missing_fields=missing_fields,
1815
2053
  allowed_values=allowed_values,
1816
- details=details,
2054
+ details=merged_details,
1817
2055
  suggested_next_call=suggested_next_call,
1818
2056
  request_id=error.request_id,
1819
2057
  backend_code=error.backend_code,
1820
- http_status=error.http_status,
2058
+ http_status=public_http_status,
1821
2059
  )
1822
2060
 
1823
2061
 
@@ -1856,6 +2094,27 @@ def _coerce_api_error(error: Exception) -> QingflowApiError:
1856
2094
  return QingflowApiError(category="runtime", message=str(error))
1857
2095
 
1858
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
+
1859
2118
  def _coerce_positive_int(value: Any) -> int | None:
1860
2119
  if isinstance(value, bool) or value is None:
1861
2120
  return None
@@ -1883,6 +2142,25 @@ def _coerce_int_list(values: Any) -> list[int]:
1883
2142
  return result
1884
2143
 
1885
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
+
1886
2164
  def _empty_schema_result(title: str) -> dict[str, Any]:
1887
2165
  return {
1888
2166
  "formTitle": title,
@@ -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")