claritty-sdk 2.0.4__tar.gz → 2.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/PKG-INFO +1 -1
  2. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/__init__.py +9 -36
  3. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/cli_verify.py +48 -1
  4. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/context.py +3 -14
  5. claritty_sdk-2.2.0/claritty_sdk/decorators.py +116 -0
  6. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/graph.py +54 -105
  7. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/llm.py +35 -4
  8. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/manifest.py +415 -6
  9. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/runtime/expressions.py +29 -8
  10. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/runtime/platform_client.py +9 -7
  11. claritty_sdk-2.2.0/claritty_sdk/runtime/team_runner.py +285 -0
  12. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/runtime/tool_loop.py +27 -3
  13. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/runtime/workflow_engine.py +336 -25
  14. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk.egg-info/PKG-INFO +1 -1
  15. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk.egg-info/SOURCES.txt +8 -5
  16. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/pyproject.toml +1 -1
  17. claritty_sdk-2.2.0/tests/test_agent_reasoning.py +66 -0
  18. claritty_sdk-2.2.0/tests/test_execution_end_to_end.py +134 -0
  19. claritty_sdk-2.2.0/tests/test_manifest_doc_parity.py +44 -0
  20. claritty_sdk-2.2.0/tests/test_parallel_fanin.py +78 -0
  21. claritty_sdk-2.2.0/tests/test_team_mode.py +168 -0
  22. claritty_sdk-2.2.0/tests/test_workflow_engine_failfast.py +201 -0
  23. claritty_sdk-2.0.4/claritty_sdk/decorators.py +0 -283
  24. claritty_sdk-2.0.4/claritty_sdk/enums.py +0 -20
  25. claritty_sdk-2.0.4/claritty_sdk/executor.py +0 -154
  26. claritty_sdk-2.0.4/claritty_sdk/registry.py +0 -151
  27. claritty_sdk-2.0.4/claritty_sdk/trigger_manager.py +0 -164
  28. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/README.md +0 -0
  29. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/cli_eval.py +0 -0
  30. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/eval.py +0 -0
  31. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/__init__.py +0 -0
  32. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/__init__.pyi +0 -0
  33. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/airtable.pyi +0 -0
  34. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/anthropic.pyi +0 -0
  35. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/asana.pyi +0 -0
  36. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/calendly.pyi +0 -0
  37. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/catalog.py +0 -0
  38. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/clickup.pyi +0 -0
  39. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/client.py +0 -0
  40. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/discord.pyi +0 -0
  41. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/dropbox.pyi +0 -0
  42. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/executor_client.py +0 -0
  43. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/github.pyi +0 -0
  44. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/gitlab.pyi +0 -0
  45. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/gmail.pyi +0 -0
  46. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/google_calendar.pyi +0 -0
  47. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/google_drive.pyi +0 -0
  48. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/hubspot.pyi +0 -0
  49. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/jira.pyi +0 -0
  50. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/linear.pyi +0 -0
  51. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/linkedin.pyi +0 -0
  52. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/mailchimp.pyi +0 -0
  53. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/mcp_client.py +0 -0
  54. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/microsoft_teams.pyi +0 -0
  55. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/notion.pyi +0 -0
  56. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/notion_mcp.pyi +0 -0
  57. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/openai.pyi +0 -0
  58. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/outlook.pyi +0 -0
  59. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/outlook_calendar.pyi +0 -0
  60. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/pipedrive.pyi +0 -0
  61. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/platform_creds.py +0 -0
  62. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/postgres.pyi +0 -0
  63. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/posthog.pyi +0 -0
  64. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/s3.pyi +0 -0
  65. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/salesforce.pyi +0 -0
  66. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/slack.pyi +0 -0
  67. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/stripe.pyi +0 -0
  68. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/trello.pyi +0 -0
  69. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/twilio.pyi +0 -0
  70. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/x_twitter.pyi +0 -0
  71. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/runtime/__init__.py +0 -0
  72. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/runtime/bootstrap.py +0 -0
  73. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/runtime/schema_validate.py +0 -0
  74. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/runtime/tracing.py +0 -0
  75. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/validators.py +0 -0
  76. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk.egg-info/dependency_links.txt +0 -0
  77. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk.egg-info/entry_points.txt +0 -0
  78. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk.egg-info/requires.txt +0 -0
  79. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk.egg-info/top_level.txt +0 -0
  80. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/setup.cfg +0 -0
  81. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/tests/test_graph_emitter.py +0 -0
  82. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/tests/test_integration_broker_dispatch.py +0 -0
  83. {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/tests/test_manifest_triggers.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: claritty-sdk
3
- Version: 2.0.4
3
+ Version: 2.2.0
4
4
  Summary: Official SDK for building apps on the Claritty Platform
5
5
  Author: Claritty Platform
6
6
  License: MIT
@@ -32,10 +32,11 @@ LLM client (existing)::
32
32
 
33
33
  from claritty_sdk.llm import get_llm_client
34
34
 
35
- The legacy decorators ``@workflow``, ``@uses_agent``, and
36
- ``@trigger_template`` are deprecated. Workflows and triggers live in
37
- ``app.yaml`` now; their decorators emit DeprecationWarning and will be
38
- removed in v3.
35
+ Workflows and triggers are declarative — they live in ``app.yaml``
36
+ (``intelligence.yaml``) and run on ``claritty_sdk.runtime``. The v1
37
+ decorator/registry runtime (``@workflow``/``@uses_agent``/``@trigger_template``,
38
+ ``AgentRegistry``/``WorkflowRegistry``/``TriggerTemplateRegistry``,
39
+ ``WorkflowExecutor``) has been removed — there is one canonical (v2) runtime.
39
40
  """
40
41
 
41
42
  from . import llm
@@ -45,24 +46,9 @@ from .decorators import (
45
46
  get_registered_tool,
46
47
  list_registered_tool_ids,
47
48
  tool,
48
- trigger_template,
49
- uses_agent,
50
- workflow,
51
- )
52
- from .enums import ExecutionMode, TriggerTemplateType
53
- from .executor import WorkflowExecutor # deprecated; legacy apps still import
54
- from .graph import build_graph # deprecated; legacy apps still import
55
- from .registry import (
56
- AgentMeta,
57
- AgentRegistry,
58
- TriggerMeta,
59
- TriggerTemplateRegistry,
60
- WorkflowMeta,
61
- WorkflowRegistry,
62
- WorkflowStep,
63
49
  )
50
+ from .graph import build_graph
64
51
  from .runtime import get_user_context, use_user_context
65
- from .trigger_manager import DynamicTriggerManager
66
52
 
67
53
  __all__ = [
68
54
  # v2 decorators
@@ -79,24 +65,11 @@ __all__ = [
79
65
  "list_registered_tool_ids",
80
66
  # llm
81
67
  "llm",
82
- # legacy (deprecated; kept for one release)
83
- "workflow",
84
- "uses_agent",
85
- "trigger_template",
86
- "ExecutionMode",
87
- "TriggerTemplateType",
88
- "AgentRegistry",
89
- "WorkflowRegistry",
90
- "TriggerTemplateRegistry",
91
- "AgentMeta",
92
- "WorkflowMeta",
93
- "WorkflowStep",
94
- "TriggerMeta",
95
- "WorkflowExecutor",
96
- "DynamicTriggerManager",
68
+ # canvas graph (manifest-driven)
97
69
  "build_graph",
70
+ # per-request user context
98
71
  "get_user_context",
99
72
  "use_user_context",
100
73
  ]
101
74
 
102
- __version__ = "2.0.0"
75
+ __version__ = "2.2.0"
@@ -4,6 +4,13 @@ Phase 5.8 — the single CLI that runs in both pre-commit hooks and CI.
4
4
  Hard-fails on any of:
5
5
 
6
6
  * Invalid ``app.yaml`` (shape or cross-reference)
7
+ * Manifest SEMANTIC errors that pydantic shape-validation can't see —
8
+ the same rules the platform applies at deploy
9
+ (``manifest-doc-validate.ts``): ``${steps.*}``/``${input.*}`` piping,
10
+ dotted-tool↔integration coupling, team-roster existence, and the
11
+ RUNNABLE + FIRED workflow invariant. So an app that passes
12
+ ``seed verify`` passes platform validation — no cryptic
13
+ post-upload "validation failed".
7
14
  * Invalid ``backend/custom/tools/*/impl.py`` (signature, decorator,
8
15
  banned imports, byte ceiling — same YAML rules the platform's
9
16
  ``CustomToolService`` uses)
@@ -14,7 +21,7 @@ Exit codes mirror ``cli_eval.py``::
14
21
 
15
22
  0 everything passes
16
23
  1 invalid manifest shape
17
- 2 manifest reference error (catalog id doesn't resolve)
24
+ 2 manifest reference / semantic error
18
25
  3 setup error (no app.yaml, no catalog)
19
26
  4 secret-shaped string found
20
27
 
@@ -23,6 +30,7 @@ Usage::
23
30
  claritty seed verify # current dir
24
31
  claritty seed verify /path/to/seed # explicit root
25
32
  claritty seed verify --catalog /path # override catalog root
33
+ claritty seed verify --lenient # skip the RUNNABLE+FIRED gate (mid-build)
26
34
  claritty seed verify --skip-secrets # debug only — never use in CI
27
35
  """
28
36
 
@@ -259,6 +267,15 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
259
267
  action="store_true",
260
268
  help="Skip the secret-shape scanner. NEVER use in CI.",
261
269
  )
270
+ parser.add_argument(
271
+ "--lenient",
272
+ action="store_true",
273
+ help=(
274
+ "Skip the strict RUNNABLE+FIRED workflow gate (an app must ship a "
275
+ "workflow that actually runs and is fired by a trigger). Use mid-build; "
276
+ "the deploy path runs strict."
277
+ ),
278
+ )
262
279
  args = parser.parse_args(argv)
263
280
 
264
281
  seed_root = Path(args.seed_root).resolve()
@@ -305,6 +322,36 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
305
322
 
306
323
  return 1 if isinstance(e, _Pyd) else 2
307
324
 
325
+ # 1b) manifest SEMANTIC rules — the platform's deploy-time gate, run
326
+ # LOCALLY so the developer sees every problem before upload (not a cryptic
327
+ # "validation failed" afterwards). Strict by default (matches the
328
+ # developer-submission gate); --lenient drops only the RUNNABLE+FIRED check.
329
+ import yaml as _yaml
330
+
331
+ from .manifest import validate_manifest_doc
332
+
333
+ try:
334
+ raw_doc = _yaml.safe_load(app_yaml.read_text("utf-8")) or {}
335
+ except _yaml.YAMLError as e:
336
+ _fail(f"{app_yaml.name} is not valid YAML: {e}")
337
+ return 1
338
+ sem = validate_manifest_doc(
339
+ raw_doc, require_runnable_workflow=not args.lenient
340
+ )
341
+ if not sem.ok:
342
+ for err in sem.errors:
343
+ _fail(err)
344
+ _info(
345
+ "These are the SAME checks the platform runs at deploy. Fix them here "
346
+ "and the deploy will pass. (Run with --lenient mid-build to drop only "
347
+ "the runnable+fired-workflow gate.)"
348
+ )
349
+ return 2
350
+ _ok(
351
+ f"manifest semantics validated"
352
+ + ("" if args.lenient else " (runnable + fired workflow)")
353
+ )
354
+
308
355
  # 2) custom-tool rules
309
356
  try:
310
357
  rules = load_rules(catalog_root)
@@ -82,22 +82,11 @@ class BaseAgent:
82
82
  ``claritty_sdk.runtime.tool_loop.run_agent`` drives the LLM tool-use loop
83
83
  for you and validates the output against ``app.yaml#agents[].output``.
84
84
 
85
- Do NOT implement ``execute()`` — that was the deprecated v1 contract; the
86
- v2 runtime never calls it, so an agent that only defines ``execute()``
87
- silently does nothing. ``run_agent`` raises if an agent has no
88
- ``system_prompt``/``prompt_file``.
85
+ Do NOT implement ``execute()`` — that was the v1 contract and has been
86
+ removed. Define a ``system_prompt`` (or ``prompt_file``); the v2 runtime's
87
+ ``run_agent`` raises if an agent has neither.
89
88
  """
90
89
 
91
- # Deprecated v1 entrypoint, kept only so the inherited symbol exists. The v2
92
- # tool-use loop never calls this; defining your own execute() is rejected at
93
- # build (materializer) and at boot (bootstrap).
94
- async def execute(self, context: "AgentContext") -> "AgentResult": # pragma: no cover - deprecated
95
- raise NotImplementedError(
96
- "execute() is the deprecated v1 agent contract and is never called "
97
- "by the v2 runtime. Define a `system_prompt` (or prompt_file) "
98
- "instead; claritty_sdk.runtime.tool_loop.run_agent drives the loop."
99
- )
100
-
101
90
 
102
91
  class GenericAgent(BaseAgent):
103
92
  """Pure-YAML agent. The runtime binds this when an `app.yaml` agent declares
@@ -0,0 +1,116 @@
1
+ """SDK decorators.
2
+
3
+ In the v2 (manifest-first) SDK, decorators are **binders**: they attach
4
+ a Python symbol to an ``app.yaml`` entry by id. They no longer carry
5
+ schema, integrations, model, timeout, or any other data the manifest
6
+ declares — that data lives in ``app.yaml`` and is loaded by
7
+ ``claritty_sdk.runtime.bootstrap``.
8
+
9
+ Two decorators:
10
+
11
+ - ``@agent(id="...")`` on a ``BaseAgent`` subclass.
12
+ - ``@tool(id="...")`` on a plain function ``(input: dict, ctx: ToolCtx) -> dict``.
13
+
14
+ Workflows and triggers are declarative — they live in ``app.yaml#workflows``
15
+ and ``app.yaml#triggers`` and run on ``claritty_sdk.runtime``. The v1
16
+ ``@workflow``/``@uses_agent``/``@trigger_template`` decorators (and the
17
+ ``AgentRegistry``/``WorkflowRegistry``/``TriggerTemplateRegistry`` they fed)
18
+ have been removed.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import warnings
24
+ from typing import Any, Callable, Dict, List, Optional, Type
25
+
26
+
27
+ # ---------------------------------------------------------------------------
28
+ # @agent and @tool — manifest-first binders
29
+ # ---------------------------------------------------------------------------
30
+
31
+
32
+ def agent(
33
+ *,
34
+ id: str,
35
+ # Legacy kwargs are still accepted (and ignored) so an app that hasn't
36
+ # stripped them from `@agent(...)` keeps importing — the data lives in
37
+ # app.yaml#agents. A manifest-first agent should pass only `id=`.
38
+ name: Optional[str] = None,
39
+ description: str = "",
40
+ category: Optional[str] = None,
41
+ inputs: Optional[Dict[str, Any]] = None,
42
+ outputs: Optional[Dict[str, Any]] = None,
43
+ timeout: Optional[int] = None,
44
+ integrations: Optional[List[Any]] = None,
45
+ **_extra: Any,
46
+ ) -> Callable[[Type], Type]:
47
+ """Bind a Python class to ``app.yaml#agents[id=...]``.
48
+
49
+ The class should subclass ``BaseAgent`` and expose ``system_prompt: str``
50
+ (or ``prompt_file``) and optionally ``before(ctx)`` / ``after(ctx, output)``
51
+ / ``fallback(ctx)`` hooks. The Anthropic tool-use loop is run by
52
+ ``claritty_sdk.runtime.tool_loop``; the v2 runtime loads the handler class
53
+ from the manifest's ``handler:`` field, so no registry is involved.
54
+ """
55
+
56
+ def decorate(cls: Type) -> Type:
57
+ # Lightweight marker for introspection/debugging only — the v2 runtime
58
+ # resolves handlers from the manifest, not from this attribute.
59
+ cls.__clarity_agent__ = {"id": id} # type: ignore[attr-defined]
60
+ return cls
61
+
62
+ return decorate
63
+
64
+
65
+ # Tool binding: minimalist registry kept here so the tool-use loop and the
66
+ # workflow engine can dispatch ``tool_use`` blocks to the bound function.
67
+ _TOOLS: Dict[str, Callable[..., Any]] = {}
68
+
69
+
70
+ def tool(*, id: str, **_extra: Any) -> Callable[[Callable], Callable]:
71
+ """Bind a Python function to ``app.yaml#tools[id=...]``.
72
+
73
+ The function must take ``(input: dict, ctx: ToolCtx) -> dict`` (or
74
+ ``-> Awaitable[dict]`` — the runtime awaits if needed). The manifest
75
+ declares the input/output schema and the SDK validates both at
76
+ invocation time.
77
+
78
+ Example::
79
+
80
+ from claritty_sdk import tool, ToolCtx
81
+
82
+ @tool(id="db.save_task")
83
+ def save_task(input: dict, ctx: ToolCtx) -> dict:
84
+ task = ctx.db.tasks.insert(user_id=ctx.user_id, **input)
85
+ return {"task_id": task.id}
86
+ """
87
+
88
+ def decorate(fn: Callable) -> Callable:
89
+ if id in _TOOLS and _TOOLS[id] is not fn:
90
+ # Re-registration during dev (hot reload) is fine; warn so
91
+ # collisions across modules are visible.
92
+ warnings.warn(
93
+ f"@tool(id={id!r}) is being re-registered — earlier "
94
+ "binder is overwritten.",
95
+ stacklevel=2,
96
+ )
97
+ fn.__clarity_tool_id__ = id # type: ignore[attr-defined]
98
+ _TOOLS[id] = fn
99
+ return fn
100
+
101
+ return decorate
102
+
103
+
104
+ def get_registered_tool(tool_id: str) -> Optional[Callable[..., Any]]:
105
+ """Look up a tool handler by id. Used by the workflow engine and the
106
+ tool-use loop to dispatch ``tool_use`` blocks."""
107
+ return _TOOLS.get(tool_id)
108
+
109
+
110
+ def list_registered_tool_ids() -> List[str]:
111
+ return sorted(_TOOLS.keys())
112
+
113
+
114
+ def _clear_tool_registry() -> None:
115
+ """Test hook — not part of the public surface."""
116
+ _TOOLS.clear()
@@ -11,8 +11,6 @@ from __future__ import annotations
11
11
  import re
12
12
  from typing import Any, Dict, List, Optional, Set
13
13
 
14
- from .enums import ExecutionMode
15
- from .registry import AgentRegistry, TriggerTemplateRegistry, WorkflowRegistry
16
14
 
17
15
  GRAPH_VERSION = 1
18
16
 
@@ -105,6 +103,50 @@ def _resource_nodes_and_edges(
105
103
  "data": {"kind": "tool"},
106
104
  }
107
105
  )
106
+
107
+ # Tools that broker an integration server-side (e.g. a `publish` tool that
108
+ # posts through `linkedin`). Surface the integration wired to the tool node so
109
+ # tool-only automations show what they connect to — mirrors the agent loop
110
+ # above. The tool node is emitted here if no agent already declared it.
111
+ for t in manifest.tools or []:
112
+ tool_ints = list(getattr(t, "integrations", None) or [])
113
+ if not tool_ints:
114
+ continue
115
+ t_node = _tool_node_id(t.id)
116
+ if t.id not in seen_tool:
117
+ seen_tool.add(t.id)
118
+ nodes.append(
119
+ {
120
+ "id": t_node,
121
+ "type": "tool",
122
+ "name": t.id,
123
+ "data": {
124
+ "toolId": t.id,
125
+ "source": getattr(t, "source", None),
126
+ "handler": getattr(t, "handler", None),
127
+ "integrations": tool_ints,
128
+ },
129
+ }
130
+ )
131
+ for iid in tool_ints:
132
+ if iid not in seen_int:
133
+ seen_int.add(iid)
134
+ nodes.append(
135
+ {
136
+ "id": _integration_node_id(iid),
137
+ "type": "integration",
138
+ "name": iid,
139
+ "data": {"integrationId": iid},
140
+ }
141
+ )
142
+ edges.append(
143
+ {
144
+ "id": f"{t_node}->{_integration_node_id(iid)}",
145
+ "source": t_node,
146
+ "target": _integration_node_id(iid),
147
+ "data": {"kind": "integration"},
148
+ }
149
+ )
108
150
  return nodes, edges
109
151
 
110
152
 
@@ -280,9 +322,12 @@ def build_graph_from_manifest(manifest: Any) -> Dict[str, Any]:
280
322
 
281
323
 
282
324
  def build_graph() -> Dict[str, Any]:
283
- """Build the graph contract. Prefers the loaded v2 manifest (which carries
284
- YAML workflows/triggers + each agent's tools/integrations); falls back to
285
- the v1 decorator registries for legacy apps."""
325
+ """Build the canvas graph contract from the loaded v2 manifest (which
326
+ carries YAML workflows/triggers + each agent's tools/integrations).
327
+
328
+ Manifest-first is the only contract: if no app has been booted (no manifest
329
+ loaded), there is nothing to graph, so return an empty graph rather than
330
+ crashing the discovery endpoint."""
286
331
 
287
332
  # Lazy import to avoid a circular import at module load (bootstrap imports
288
333
  # the manifest + engine, not graph).
@@ -295,106 +340,10 @@ def build_graph() -> Dict[str, Any]:
295
340
  if boot is not None and getattr(boot, "manifest", None) is not None:
296
341
  return build_graph_from_manifest(boot.manifest)
297
342
 
298
- nodes: List[Dict[str, Any]] = []
299
- edges: List[Dict[str, Any]] = []
300
-
301
- for a in AgentRegistry.list_agents():
302
- # Normalize integrations (AgentMeta stores them as objects with an id).
303
- raw_ints = getattr(a, "integrations", None) or []
304
- ints = [
305
- (i if isinstance(i, str) else getattr(i, "id", None) or getattr(i, "service", None))
306
- for i in raw_ints
307
- ]
308
- nodes.append(
309
- {
310
- "id": _agent_node_id(a.id),
311
- "type": "agent",
312
- "name": a.name,
313
- "data": {
314
- "agentId": a.id,
315
- "category": a.category,
316
- "description": a.description,
317
- "inputs": a.inputs,
318
- "outputs": a.outputs,
319
- "tools": list(getattr(a, "tools", None) or []),
320
- "integrations": [i for i in ints if i],
321
- },
322
- }
323
- )
324
-
325
- for t in TriggerTemplateRegistry.list_templates():
326
- nodes.append(
327
- {
328
- "id": _trigger_node_id(t.id),
329
- "type": "trigger",
330
- "name": t.name,
331
- "data": {
332
- "triggerId": t.id,
333
- "templateType": getattr(t.template_type, "value", t.template_type),
334
- "workflowId": t.workflow_id,
335
- "category": t.category,
336
- },
337
- }
338
- )
339
-
340
- workflows_out: List[Dict[str, Any]] = []
341
- for wf in WorkflowRegistry.list_workflows():
342
- steps = wf.steps or []
343
- workflows_out.append(
344
- {
345
- "id": wf.id,
346
- "name": wf.name,
347
- "executionMode": getattr(wf.execution_mode, "value", wf.execution_mode),
348
- "steps": [
349
- {"agentId": s.agent_id, "outputKey": s.output_key, "inputFrom": s.input_from}
350
- for s in steps
351
- ],
352
- }
353
- )
354
-
355
- # Agent→agent edges. For SEQUENTIAL workflows, chain consecutive steps
356
- # (honoring explicit input_from); PARALLEL workflows have no inter-agent
357
- # edges (only the trigger fans into each agent).
358
- sequential = wf.execution_mode == ExecutionMode.SEQUENTIAL
359
- prev_node = None
360
- for step in steps:
361
- cur = _agent_node_id(step.agent_id)
362
- src = None
363
- if step.input_from:
364
- match = next((x for x in steps if x.output_key == step.input_from), None)
365
- if match:
366
- src = _agent_node_id(match.agent_id)
367
- elif sequential and prev_node:
368
- src = prev_node
369
- if src and src != cur:
370
- edges.append(
371
- {
372
- "id": f"{wf.id}:{src}->{cur}",
373
- "source": src,
374
- "target": cur,
375
- "data": {"workflowId": wf.id},
376
- }
377
- )
378
- prev_node = cur
379
-
380
- # Trigger→entry-agent edges.
381
- if steps:
382
- entry = _agent_node_id(steps[0].agent_id)
383
- for t in TriggerTemplateRegistry.list_templates():
384
- if t.workflow_id == wf.id:
385
- edges.append(
386
- {
387
- "id": f"trigger:{t.id}->{entry}",
388
- "source": _trigger_node_id(t.id),
389
- "target": entry,
390
- "data": {"workflowId": wf.id, "trigger": True},
391
- }
392
- )
393
-
394
343
  return {
395
344
  "version": GRAPH_VERSION,
396
- "nodes": nodes,
397
- "edges": edges,
398
- "workflows": workflows_out,
399
- "source": "runtime",
345
+ "nodes": [],
346
+ "edges": [],
347
+ "workflows": [],
348
+ "source": "manifest",
400
349
  }
@@ -168,6 +168,7 @@ class LlmClient:
168
168
  max_tokens: Optional[int] = None,
169
169
  system: Optional[str] = None,
170
170
  extra_headers: Optional[dict] = None,
171
+ thinking: Optional[dict] = None,
171
172
  ) -> "ChatToolsResult":
172
173
  """OpenAI-Chat-Completions tool calling.
173
174
 
@@ -181,7 +182,9 @@ class LlmClient:
181
182
  the proxy uses to enforce a per-run $ ceiling and surface a 402
182
183
  ``WORKFLOW_BUDGET_EXCEEDED`` envelope when exceeded.
183
184
  """
184
- body = self._build_body(messages, temperature, max_tokens, system, stream=False)
185
+ body = self._build_body(
186
+ messages, temperature, max_tokens, system, stream=False, thinking=thinking
187
+ )
185
188
  body["tools"] = tools
186
189
  if tool_choice is not None:
187
190
  body["tool_choice"] = tool_choice
@@ -272,6 +275,7 @@ class LlmClient:
272
275
  system: Optional[str],
273
276
  *,
274
277
  stream: bool,
278
+ thinking: Optional[dict] = None,
275
279
  ) -> dict:
276
280
  # Merge the active agent's user context (the platform's per-agent
277
281
  # "how to do your job" instructions) into the system prompt so it
@@ -304,7 +308,12 @@ class LlmClient:
304
308
  out["name"] = m["name"]
305
309
  msgs.append(out)
306
310
  body: dict = {"model": self.model, "messages": msgs}
307
- if temperature is not None:
311
+ if thinking is not None:
312
+ # Anthropic extended thinking. The metered proxy forwards this to the
313
+ # Anthropic `thinking` param. Anthropic requires temperature unset
314
+ # (=1) while thinking is on, so we DON'T send temperature here.
315
+ body["thinking"] = thinking
316
+ elif temperature is not None:
308
317
  body["temperature"] = temperature
309
318
  if max_tokens is not None:
310
319
  body["max_tokens"] = max_tokens
@@ -365,7 +374,29 @@ def _raise_for_status(res: httpx.Response) -> None:
365
374
  if res.is_success:
366
375
  return
367
376
  try:
368
- payload = res.json()
377
+ payload: Any = res.json()
369
378
  except Exception:
370
- payload = res.text
379
+ text = res.text or ""
380
+ # A non-JSON body means the response did NOT come from the clarity-api
381
+ # proxy (which always returns a JSON error envelope). The overwhelmingly
382
+ # common cause is a proxy URL pointing at the wrong host — e.g. the
383
+ # frontend (app.claritty.ai / :3000) instead of the API
384
+ # (api.claritty.ai / :4000) — so `/api/v1/chat/completions` 404/500s as
385
+ # an HTML page. Turn that opaque HTML dump into an actionable error that
386
+ # names the exact URL and the likely fix.
387
+ looks_like_html = "<html" in text[:512].lower() or "<!doctype" in text[:512].lower()
388
+ if looks_like_html:
389
+ payload = {
390
+ "message": (
391
+ f"the LLM proxy at {res.request.url} returned an HTML "
392
+ f"{res.status_code} page, not a JSON error — this host is "
393
+ "almost certainly NOT the clarity-api proxy. Check "
394
+ "CLARITTY_LLM_PROXY_URL / CLARITTY_PLATFORM_URL: it must be "
395
+ "the API host (e.g. https://api.claritty.ai), not the "
396
+ "frontend (app.claritty.ai) or a local web port (:3000)."
397
+ ),
398
+ "non_json_body": text[:500],
399
+ }
400
+ else:
401
+ payload = text
371
402
  raise LlmProxyError(res.status_code, payload)