trytilde 3.0.1__py3-none-any.whl

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 (135) hide show
  1. tilde/__init__.py +114 -0
  2. tilde/__main__.py +9 -0
  3. tilde/_cancel.py +50 -0
  4. tilde/_inference.py +106 -0
  5. tilde/_invocation.py +26 -0
  6. tilde/_otlp.py +35 -0
  7. tilde/_tools.py +182 -0
  8. tilde/_transport.py +28 -0
  9. tilde/agent_event_ingress/__init__.py +0 -0
  10. tilde/agent_event_ingress/v1/__init__.py +0 -0
  11. tilde/agent_event_ingress/v1/sidecars_connect.py +748 -0
  12. tilde/agent_event_ingress/v1/sidecars_pb2.py +128 -0
  13. tilde/agent_event_ingress/v1/sidecars_pb2.pyi +471 -0
  14. tilde/agent_host/__init__.py +0 -0
  15. tilde/agent_host/v1/__init__.py +0 -0
  16. tilde/agent_host/v1/agent_pb2.py +43 -0
  17. tilde/agent_host/v1/agent_pb2.pyi +56 -0
  18. tilde/channels.py +212 -0
  19. tilde/chat_proxy.py +295 -0
  20. tilde/clients.py +127 -0
  21. tilde/context.py +1006 -0
  22. tilde/deploy.py +420 -0
  23. tilde/discovery.py +127 -0
  24. tilde/fastapi.py +69 -0
  25. tilde/host.py +481 -0
  26. tilde/logging.py +213 -0
  27. tilde/management/__init__.py +0 -0
  28. tilde/management/v1/__init__.py +0 -0
  29. tilde/management/v1/access_connect.py +465 -0
  30. tilde/management/v1/access_pb2.py +57 -0
  31. tilde/management/v1/access_pb2.pyi +102 -0
  32. tilde/management/v1/agents_connect.py +668 -0
  33. tilde/management/v1/agents_pb2.py +73 -0
  34. tilde/management/v1/agents_pb2.pyi +132 -0
  35. tilde/management/v1/connections_connect.py +806 -0
  36. tilde/management/v1/connections_pb2.py +93 -0
  37. tilde/management/v1/connections_pb2.pyi +186 -0
  38. tilde/management/v1/deployments_connect.py +919 -0
  39. tilde/management/v1/deployments_pb2.py +109 -0
  40. tilde/management/v1/deployments_pb2.pyi +306 -0
  41. tilde/management/v1/identities_connect.py +660 -0
  42. tilde/management/v1/identities_pb2.py +74 -0
  43. tilde/management/v1/identities_pb2.pyi +167 -0
  44. tilde/management/v1/inference_connect.py +424 -0
  45. tilde/management/v1/inference_pb2.py +69 -0
  46. tilde/management/v1/inference_pb2.pyi +158 -0
  47. tilde/management/v1/logs_connect.py +278 -0
  48. tilde/management/v1/logs_pb2.py +56 -0
  49. tilde/management/v1/logs_pb2.pyi +123 -0
  50. tilde/management/v1/prompts_connect.py +290 -0
  51. tilde/management/v1/prompts_pb2.py +49 -0
  52. tilde/management/v1/prompts_pb2.pyi +36 -0
  53. tilde/management/v1/skills_connect.py +1815 -0
  54. tilde/management/v1/skills_pb2.py +147 -0
  55. tilde/management/v1/skills_pb2.pyi +324 -0
  56. tilde/management/v1/tilde_chat_connect.py +290 -0
  57. tilde/management/v1/tilde_chat_pb2.py +48 -0
  58. tilde/management/v1/tilde_chat_pb2.pyi +29 -0
  59. tilde/management/v1/tools_connect.py +1416 -0
  60. tilde/management/v1/tools_pb2.py +135 -0
  61. tilde/management/v1/tools_pb2.pyi +341 -0
  62. tilde/management/v1/tracing_connect.py +513 -0
  63. tilde/management/v1/tracing_pb2.py +78 -0
  64. tilde/management/v1/tracing_pb2.pyi +231 -0
  65. tilde/messages.py +129 -0
  66. tilde/prompts.py +130 -0
  67. tilde/provider/__init__.py +0 -0
  68. tilde/provider/tilde/__init__.py +0 -0
  69. tilde/provider/tilde/v1/__init__.py +0 -0
  70. tilde/provider/tilde/v1/chat_connect.py +2090 -0
  71. tilde/provider/tilde/v1/chat_pb2.py +165 -0
  72. tilde/provider/tilde/v1/chat_pb2.pyi +478 -0
  73. tilde/provider/v1/__init__.py +0 -0
  74. tilde/provider/v1/connections_connect.py +205 -0
  75. tilde/provider/v1/connections_pb2.py +49 -0
  76. tilde/provider/v1/connections_pb2.pyi +58 -0
  77. tilde/run/__init__.py +0 -0
  78. tilde/run/v1/__init__.py +0 -0
  79. tilde/run/v1/run_connect.py +367 -0
  80. tilde/run/v1/run_pb2.py +57 -0
  81. tilde/run/v1/run_pb2.pyi +80 -0
  82. tilde/runtime/__init__.py +0 -0
  83. tilde/runtime/v1/__init__.py +0 -0
  84. tilde/runtime/v1/agents_connect.py +473 -0
  85. tilde/runtime/v1/agents_pb2.py +61 -0
  86. tilde/runtime/v1/agents_pb2.pyi +86 -0
  87. tilde/runtime/v1/cache_pb2.py +38 -0
  88. tilde/runtime/v1/cache_pb2.pyi +13 -0
  89. tilde/runtime/v1/chat_connect.py +1460 -0
  90. tilde/runtime/v1/chat_pb2.py +130 -0
  91. tilde/runtime/v1/chat_pb2.pyi +275 -0
  92. tilde/runtime/v1/controls_connect.py +282 -0
  93. tilde/runtime/v1/controls_pb2.py +47 -0
  94. tilde/runtime/v1/controls_pb2.pyi +49 -0
  95. tilde/runtime/v1/prompts_connect.py +225 -0
  96. tilde/runtime/v1/prompts_pb2.py +43 -0
  97. tilde/runtime/v1/prompts_pb2.pyi +20 -0
  98. tilde/runtime/v1/skills_connect.py +655 -0
  99. tilde/runtime/v1/skills_pb2.py +77 -0
  100. tilde/runtime/v1/skills_pb2.pyi +154 -0
  101. tilde/setup/__init__.py +0 -0
  102. tilde/setup/v1/__init__.py +0 -0
  103. tilde/setup/v1/connections_connect.py +595 -0
  104. tilde/setup/v1/connections_pb2.py +101 -0
  105. tilde/setup/v1/connections_pb2.pyi +232 -0
  106. tilde/setup/v1/identity_verification_connect.py +282 -0
  107. tilde/setup/v1/identity_verification_pb2.py +49 -0
  108. tilde/setup/v1/identity_verification_pb2.pyi +35 -0
  109. tilde/skills.py +383 -0
  110. tilde/tool_host/v1/tool_host_connect.py +314 -0
  111. tilde/tool_host/v1/tool_host_pb2.py +66 -0
  112. tilde/tool_host/v1/tool_host_pb2.pyi +115 -0
  113. tilde/tool_hosts.py +515 -0
  114. tilde/tracing.py +147 -0
  115. tilde/types/__init__.py +0 -0
  116. tilde/types/v1/__init__.py +0 -0
  117. tilde/types/v1/access_pb2.py +47 -0
  118. tilde/types/v1/access_pb2.pyi +123 -0
  119. tilde/types/v1/agent_pb2.py +53 -0
  120. tilde/types/v1/agent_pb2.pyi +151 -0
  121. tilde/types/v1/chat_pb2.py +81 -0
  122. tilde/types/v1/chat_pb2.pyi +316 -0
  123. tilde/types/v1/connections_pb2.py +79 -0
  124. tilde/types/v1/connections_pb2.pyi +268 -0
  125. tilde/types/v1/deployment_pb2.py +51 -0
  126. tilde/types/v1/deployment_pb2.pyi +129 -0
  127. tilde/types/v1/prompt_pb2.py +45 -0
  128. tilde/types/v1/prompt_pb2.pyi +96 -0
  129. tilde/types/v1/runtime_event_pb2.py +62 -0
  130. tilde/types/v1/runtime_event_pb2.pyi +243 -0
  131. tilde/types/v1/skill_pb2.py +49 -0
  132. tilde/types/v1/skill_pb2.pyi +150 -0
  133. trytilde-3.0.1.dist-info/METADATA +325 -0
  134. trytilde-3.0.1.dist-info/RECORD +135 -0
  135. trytilde-3.0.1.dist-info/WHEEL +4 -0
tilde/__init__.py ADDED
@@ -0,0 +1,114 @@
1
+ """Tilde Python SDK: agent hosts, invocation context, channel tools and management clients.
2
+
3
+ Generated contracts live beside these modules under the proto package names, for example
4
+ ``tilde.runtime.v1.chat_pb2`` and ``tilde.types.v1.chat_pb2``. Run ``sdk/py/scripts/generate.py``
5
+ after changing proto/.
6
+ """
7
+
8
+ from tilde._cancel import Cancellation, InvocationCancelled, StopLoop
9
+ from tilde._inference import Inference, inference
10
+ from tilde._invocation import current_invocation
11
+ from tilde._tools import (
12
+ BundledOptions,
13
+ BundledTools,
14
+ Tool,
15
+ ToolAnnotations,
16
+ ToolCatalog,
17
+ define_tools,
18
+ tool_id,
19
+ )
20
+ from tilde.channels import Channels, ChannelTool, ChannelTools
21
+ from tilde.clients import (
22
+ ManagementClient,
23
+ RuntimeClient,
24
+ create_management_client,
25
+ create_runtime_client,
26
+ create_tilde_chat_client,
27
+ )
28
+ from tilde.context import AgentContext, DownloadedAttachment, SteeringInput
29
+ from tilde.host import (
30
+ ConnectedAgent,
31
+ HostState,
32
+ InvocationOptions,
33
+ Wake,
34
+ connect_agent,
35
+ create_lambda_handler,
36
+ run_connected_agent,
37
+ run_invocation,
38
+ )
39
+ from tilde.logging import DeploymentLogging, agent_log_processor, configure_deployment_logging
40
+ from tilde.messages import (
41
+ ContextMessage,
42
+ ConversationMessage,
43
+ GoalMessage,
44
+ MessageClient,
45
+ MessageHistory,
46
+ ObjectiveMessage,
47
+ TaskMessage,
48
+ )
49
+ from tilde.prompts import PromptDefinition, define_prompt
50
+ from tilde.skills import (
51
+ SkillDefinition,
52
+ SkillFile,
53
+ SkillFileBody,
54
+ SkillsClient,
55
+ SkillsDefinition,
56
+ define_skill,
57
+ define_skills,
58
+ )
59
+ from tilde.tracing import agent_span_processor
60
+
61
+ __all__ = [
62
+ "Inference",
63
+ "AgentContext",
64
+ "BundledOptions",
65
+ "BundledTools",
66
+ "Cancellation",
67
+ "ChannelTool",
68
+ "ChannelTools",
69
+ "Channels",
70
+ "ConnectedAgent",
71
+ "ContextMessage",
72
+ "ConversationMessage",
73
+ "DeploymentLogging",
74
+ "DownloadedAttachment",
75
+ "GoalMessage",
76
+ "HostState",
77
+ "InvocationCancelled",
78
+ "InvocationOptions",
79
+ "ManagementClient",
80
+ "MessageClient",
81
+ "MessageHistory",
82
+ "ObjectiveMessage",
83
+ "PromptDefinition",
84
+ "RuntimeClient",
85
+ "SkillDefinition",
86
+ "SkillFile",
87
+ "SkillFileBody",
88
+ "SkillsClient",
89
+ "SkillsDefinition",
90
+ "SteeringInput",
91
+ "StopLoop",
92
+ "TaskMessage",
93
+ "Tool",
94
+ "ToolAnnotations",
95
+ "ToolCatalog",
96
+ "Wake",
97
+ "agent_log_processor",
98
+ "agent_span_processor",
99
+ "configure_deployment_logging",
100
+ "connect_agent",
101
+ "create_lambda_handler",
102
+ "create_management_client",
103
+ "create_runtime_client",
104
+ "create_tilde_chat_client",
105
+ "current_invocation",
106
+ "define_prompt",
107
+ "define_skill",
108
+ "define_skills",
109
+ "define_tools",
110
+ "inference",
111
+ "run_connected_agent",
112
+ "run_invocation",
113
+ "tool_id",
114
+ ]
tilde/__main__.py ADDED
@@ -0,0 +1,9 @@
1
+ """``python -m tilde deploy [ENTRY] ...``; see ``tilde.deploy``."""
2
+
3
+ import sys
4
+
5
+ from tilde.deploy import main
6
+
7
+ if len(sys.argv) < 2 or sys.argv[1] != "deploy":
8
+ sys.exit("usage: python -m tilde deploy [ENTRY] [options] (see python -m tilde deploy --help)")
9
+ sys.exit(main(sys.argv[2:]))
tilde/_cancel.py ADDED
@@ -0,0 +1,50 @@
1
+ """Cooperative cancellation shared by the host and the invocation context."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from collections.abc import Callable
7
+
8
+
9
+ class StopLoop(BaseException):
10
+ """Raised by ``ctx.stop()``; frameworks must let it propagate like a cancellation."""
11
+
12
+
13
+ class InvocationCancelled(BaseException):
14
+ """The invocation is no longer active (stop control, host shutdown, lost callbacks)."""
15
+
16
+
17
+ class Cancellation:
18
+ """AbortSignal equivalent: aborts once, remembers why and notifies registered callbacks."""
19
+
20
+ def __init__(self) -> None:
21
+ self._event = asyncio.Event()
22
+ self.reason: BaseException | None = None
23
+ self._callbacks: list[Callable[[], None]] = []
24
+
25
+ @property
26
+ def aborted(self) -> bool:
27
+ return self._event.is_set()
28
+
29
+ def abort(self, reason: BaseException | None = None) -> None:
30
+ if self._event.is_set():
31
+ return
32
+ self.reason = reason
33
+ self._event.set()
34
+ callbacks, self._callbacks = self._callbacks, []
35
+ for callback in callbacks:
36
+ callback()
37
+
38
+ def on_abort(self, callback: Callable[[], None]) -> None:
39
+ if self._event.is_set():
40
+ callback()
41
+ else:
42
+ self._callbacks.append(callback)
43
+
44
+ def check(self) -> None:
45
+ """Raise when aborted, like ``AbortSignal.throwIfAborted``."""
46
+ if self._event.is_set():
47
+ raise self.reason if self.reason is not None else InvocationCancelled()
48
+
49
+ async def wait(self) -> None:
50
+ await self._event.wait()
tilde/_inference.py ADDED
@@ -0,0 +1,106 @@
1
+ """Route model calls through Tilde's inference gateway with the invocation's own credentials.
2
+
3
+ ``ctx.inference("openai/prod")`` (or an alias the agent was given, such as ``"default"``)
4
+ returns what an OpenAI-compatible client needs: ``base_url``, a placeholder ``api_key`` and an
5
+ ``httpx`` client whose auth hook sends the current, renewed invocation token on every request.
6
+ The gateway swaps in the real provider credential and accounts the call, so agents never hold
7
+ provider keys.
8
+
9
+ ``tilde.inference(alias)`` is the module-level form for model clients built at import time:
10
+ every request resolves the invocation running it (token, callback URL, prompt stamps), and a
11
+ request made outside an invocation fails.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import re
17
+ from collections.abc import Callable, Generator
18
+ from typing import Any
19
+
20
+ import httpx
21
+
22
+ from tilde._invocation import require_invocation
23
+
24
+ _SLUG = re.compile(r"^([a-z0-9_]+/)?[A-Za-z0-9._@+:-]+$")
25
+ # Module-level clients are built before any invocation exists; each request is rebased onto
26
+ # the invocation's callback URL, so this host is never dialed.
27
+ _UNBOUND = "http://tilde.invalid"
28
+
29
+
30
+ class _InvocationAuth(httpx.Auth):
31
+ def __init__(self, prepare: Callable[[httpx.Request], httpx.Request]) -> None:
32
+ self._prepare = prepare
33
+
34
+ def auth_flow(self, request: httpx.Request) -> Generator[httpx.Request, httpx.Response, None]:
35
+ yield self._prepare(request)
36
+
37
+
38
+ class Inference:
39
+ """Connection details for one inference slug."""
40
+
41
+ api_key = "tilde"
42
+
43
+ def __init__(
44
+ self,
45
+ base_url: str,
46
+ authorization: Callable[[], str],
47
+ prompts: Callable[[], str] = lambda: "",
48
+ callback_url: Callable[[], str] | None = None,
49
+ ) -> None:
50
+ self.base_url = base_url
51
+ self._authorization = authorization
52
+ self._prompts = prompts
53
+ self._callback_url = callback_url
54
+ self.auth: httpx.Auth = _InvocationAuth(self.prepare)
55
+
56
+ def prepare(self, request: httpx.Request) -> httpx.Request:
57
+ """Stamp one outgoing request, for frameworks that hook requests instead of clients."""
58
+ if self._callback_url is not None:
59
+ base = httpx.URL(self._callback_url().rstrip("/"))
60
+ request.url = request.url.copy_with(
61
+ scheme=base.scheme,
62
+ host=base.host,
63
+ port=base.port,
64
+ raw_path=base.raw_path.rstrip(b"/") + request.url.raw_path,
65
+ )
66
+ request.headers["host"] = request.url.netloc.decode("ascii")
67
+ # Replaces whatever key the provider SDK set; the gateway authenticates this token.
68
+ request.headers["authorization"] = self._authorization()
69
+ stamps = self._prompts()
70
+ if stamps:
71
+ request.headers["x-tilde-prompt"] = stamps
72
+ else:
73
+ request.headers.pop("x-tilde-prompt", None)
74
+ return request
75
+
76
+ def async_client(self, **options: Any) -> httpx.AsyncClient:
77
+ """An ``httpx.AsyncClient`` for async provider SDKs (``AsyncOpenAI(http_client=...)``)."""
78
+ return httpx.AsyncClient(auth=self.auth, **{"timeout": 600.0, **options})
79
+
80
+ def client(self, **options: Any) -> httpx.Client:
81
+ """A synchronous ``httpx.Client`` for provider SDKs that also call synchronously."""
82
+ return httpx.Client(auth=self.auth, **{"timeout": 600.0, **options})
83
+
84
+
85
+ def _check(slug: str) -> None:
86
+ if not _SLUG.match(slug):
87
+ raise ValueError("Inference slug must be provider/account or an alias")
88
+
89
+
90
+ def bound_inference(
91
+ callback_url: str, slug: str, authorization: Callable[[], str], prompts: Callable[[], str]
92
+ ) -> Inference:
93
+ _check(slug)
94
+ return Inference(f"{callback_url.rstrip('/')}/inference/{slug}", authorization, prompts)
95
+
96
+
97
+ def inference(slug: str) -> Inference:
98
+ """Inference for the invocation running each request; safe to build at import time."""
99
+ _check(slug)
100
+ what = f"tilde.inference({slug!r})"
101
+ return Inference(
102
+ f"{_UNBOUND}/inference/{slug}",
103
+ lambda: require_invocation(what).trace_authorization(),
104
+ lambda: require_invocation(what).prompt_stamps(),
105
+ lambda: require_invocation(what).callback_url,
106
+ )
tilde/_invocation.py ADDED
@@ -0,0 +1,26 @@
1
+ """The invocation the current task is running, for module-level objects built at import time."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from contextvars import ContextVar
6
+ from typing import TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ from tilde.context import AgentContext
10
+
11
+ # Set by the host around ``run(ctx)``; asyncio tasks and ``asyncio.to_thread`` inherit it.
12
+ current: ContextVar[AgentContext | None] = ContextVar("tilde_invocation", default=None)
13
+
14
+
15
+ def current_invocation() -> AgentContext | None:
16
+ """The running invocation's context, or ``None`` outside ``run(ctx)``."""
17
+ return current.get()
18
+
19
+
20
+ def require_invocation(what: str) -> AgentContext:
21
+ ctx = current.get()
22
+ if ctx is None:
23
+ raise RuntimeError(
24
+ f"{what} was used outside a Tilde invocation; call it from within run(ctx)"
25
+ )
26
+ return ctx
tilde/_otlp.py ADDED
@@ -0,0 +1,35 @@
1
+ """Minimal OTLP/HTTP protobuf uploads to the invocation callback with the current connect token."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import urllib.error
7
+ import urllib.request
8
+ from collections.abc import Callable
9
+ from urllib.parse import urlsplit, urlunsplit
10
+
11
+ log = logging.getLogger("tilde")
12
+
13
+
14
+ def otlp_url(base_url: str, signal: str) -> str:
15
+ """Match the runtime RPC base path when a reverse proxy separates audiences by prefix."""
16
+ parts = urlsplit(base_url)
17
+ path = f"{parts.path.rstrip('/')}/v1/{signal}"
18
+ return urlunsplit((parts.scheme, parts.netloc, path, "", ""))
19
+
20
+
21
+ def post_protobuf(
22
+ url: str, payload: bytes, authorization: Callable[[], str], timeout: float = 15.0
23
+ ) -> bool:
24
+ request = urllib.request.Request(
25
+ url,
26
+ data=payload,
27
+ method="POST",
28
+ headers={"Content-Type": "application/x-protobuf", "Authorization": authorization()},
29
+ )
30
+ try:
31
+ with urllib.request.urlopen(request, timeout=timeout) as response: # noqa: S310 - caller-supplied callback URL
32
+ return 200 <= response.status < 300
33
+ except (urllib.error.URLError, OSError, ValueError) as error:
34
+ log.debug("OTLP upload to %s failed: %s", url, error)
35
+ return False
tilde/_tools.py ADDED
@@ -0,0 +1,182 @@
1
+ """Tools in ``ctx.tools``: provider tools published for the invocation (descriptions and schemas
2
+ come from Tilde) and SDK lifecycle helpers. The agent's bundled tools are its own framework tools,
3
+ published by each adapter's ``with_tilde_tools``."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import hashlib
8
+ import re
9
+ import uuid
10
+ from collections.abc import AsyncIterable, Awaitable, Callable, Mapping, Sequence
11
+ from dataclasses import dataclass, field
12
+ from typing import Any, Literal
13
+
14
+ ExecuteFn = Callable[[Any, str], Awaitable[Any]]
15
+ #: How calls of one of the agent's own tools show in end-user chats; traces keep full detail.
16
+ ToolDisplay = Literal["full", "summary", "hidden"]
17
+ StreamFn = Callable[[Any, AsyncIterable[Any], str], Awaitable[Any]]
18
+
19
+
20
+ @dataclass(slots=True, frozen=True)
21
+ class ToolAnnotations:
22
+ """Hints for ordering, confirming or parallelising calls; never authorization."""
23
+
24
+ read_only: bool = False
25
+ destructive: bool = False
26
+ idempotent: bool = False
27
+ open_world: bool = False
28
+
29
+
30
+ @dataclass(slots=True)
31
+ class Tool:
32
+ """One tool in ``ctx.tools``, or the published definition of a bundled tool."""
33
+
34
+ description: str
35
+ input_schema: dict[str, Any]
36
+ _execute: ExecuteFn = field(repr=False)
37
+ provider_id: str | None = None
38
+ chunk_schema: dict[str, Any] | None = None
39
+ _stream: StreamFn | None = field(default=None, repr=False)
40
+ output_schema: dict[str, Any] | None = None
41
+ #: Short label for transcripts.
42
+ summary: str | None = None
43
+ annotations: ToolAnnotations | None = None
44
+ #: Returns a ticket at once; the outcome arrives later as new input and through tools.result.
45
+ background: bool = False
46
+ display: ToolDisplay = "full"
47
+
48
+ async def execute(self, input: Any, *, tool_call_id: str | None = None) -> Any:
49
+ """Send completed input; ``tool_call_id`` is the framework's call ID for auditing."""
50
+ return await self._execute(input, tool_call_id or str(uuid.uuid4()))
51
+
52
+ @property
53
+ def can_stream(self) -> bool:
54
+ return self._stream is not None
55
+
56
+ async def stream(self, input: Any, chunks: AsyncIterable[Any], *, tool_call_id: str) -> Any:
57
+ """Send incremental input in the provider's ``chunk_schema`` format.
58
+
59
+ A framework must feed chunks explicitly; registering a tool never streams model tokens.
60
+ """
61
+ if self._stream is None:
62
+ raise ValueError("This tool does not accept incremental input")
63
+ return await self._stream(input, chunks, tool_call_id)
64
+
65
+
66
+ ToolCatalog = dict[str, Tool]
67
+
68
+
69
+ @dataclass(slots=True, frozen=True)
70
+ class BundledOptions:
71
+ """Tilde metadata for one of the agent's own framework tools, overriding what the framework
72
+ declares. ``display`` is how its calls show in end-user chats: with input and output, only
73
+ the summary, or hidden; traces keep full detail."""
74
+
75
+ summary: str | None = None
76
+ display: ToolDisplay = "full"
77
+ annotations: ToolAnnotations | None = None
78
+
79
+
80
+ @dataclass(slots=True, frozen=True)
81
+ class BundledTools:
82
+ """An agent's own framework tools with their Tilde options, from ``define_tools``."""
83
+
84
+ tools: list[Any]
85
+ options: dict[str, BundledOptions]
86
+
87
+
88
+ def define_tools(
89
+ tools: Sequence[Any], *, options: Mapping[str, BundledOptions] | None = None
90
+ ) -> BundledTools:
91
+ """Declare the agent's bundled tools: its framework's own tools (functions, ``@tool``s,
92
+ ``FunctionTool``s, ...) with Tilde options by tool name. Pass the result to the adapter's
93
+ per-invocation helper as ``bundled`` (or its parts to ``with_tilde_tools``); at module level,
94
+ ``tilde deploy`` declares the tools with the deployment."""
95
+ return BundledTools(list(tools), dict(options or {}))
96
+
97
+
98
+ @dataclass(slots=True, frozen=True)
99
+ class ToolSpec:
100
+ """One of the agent's framework tools as Tilde publishes it per invocation and ``tilde deploy``
101
+ declares it, with its Tilde options applied. Each adapter's ``describe_tool`` builds it."""
102
+
103
+ name: str
104
+ description: str
105
+ input_schema: dict[str, Any]
106
+ output_schema: dict[str, Any] | None = None
107
+ summary: str | None = None
108
+ annotations: ToolAnnotations | None = None
109
+ display: ToolDisplay = "full"
110
+
111
+
112
+ def tool_spec(
113
+ name: str,
114
+ description: str,
115
+ input_schema: dict[str, Any],
116
+ *,
117
+ output_schema: dict[str, Any] | None = None,
118
+ annotations: ToolAnnotations | None = None,
119
+ options: BundledOptions | None = None,
120
+ ) -> ToolSpec:
121
+ """What the framework declares, overridden by ``options``."""
122
+ options = options or BundledOptions()
123
+ return ToolSpec(
124
+ name=name,
125
+ description=description,
126
+ input_schema=input_schema,
127
+ output_schema=output_schema,
128
+ summary=options.summary,
129
+ annotations=options.annotations or annotations,
130
+ display=options.display,
131
+ )
132
+
133
+
134
+ def bundled_tool(spec: ToolSpec, execute: ExecuteFn) -> Tool:
135
+ """Internal: the definition adapters publish for a framework tool. ``execute`` runs it for a
136
+ ``tools.execute`` routed here and must audit the call itself, exactly as a direct call is."""
137
+ return Tool(
138
+ description=spec.description,
139
+ input_schema=spec.input_schema,
140
+ _execute=execute,
141
+ output_schema=spec.output_schema,
142
+ summary=spec.summary,
143
+ annotations=spec.annotations,
144
+ display=spec.display,
145
+ )
146
+
147
+
148
+ _MODEL_INVALID = re.compile(r"[^a-zA-Z0-9_-]")
149
+
150
+
151
+ def model_tool_name(name: str, invalid: re.Pattern[str] = _MODEL_INVALID) -> str:
152
+ """The name a framework shows the model for a Tilde tool: ``name`` with characters matching
153
+ ``invalid`` (the framework's disallowed characters) replaced by ``_``. Catalog names
154
+ (``{source}.{tool}``) can exceed the 64 characters models accept, so a longer result is
155
+ truncated and suffixed with a hash of ``name``, keeping it stable and distinct from other
156
+ long names sharing its start. Adapters bind each alias to its catalog tool when converting.
157
+ Matches ``modelToolName`` in the TypeScript SDK."""
158
+ key = invalid.sub("_", name)
159
+ if len(key) <= 64:
160
+ return key
161
+ return f"{key[:55]}_{hashlib.sha256(name.encode()).hexdigest()[:8]}"
162
+
163
+
164
+ def tool_id(invocation: str, call: str, name: str) -> str:
165
+ """Stable UUID derived from the invocation and the framework's tool-call ID."""
166
+ if not call:
167
+ raise ValueError("Tool call ID is required")
168
+ h = hashlib.sha256(f"{invocation}:{call}:{name}".encode()).hexdigest()
169
+ return f"{h[:8]}-{h[8:12]}-4{h[13:16]}-a{h[17:20]}-{h[20:32]}"
170
+
171
+
172
+ def as_object(input: Any) -> dict[str, Any]:
173
+ if not isinstance(input, dict):
174
+ raise ValueError("Tool input must be an object")
175
+ return input
176
+
177
+
178
+ def as_string(input: dict[str, Any], key: str) -> str:
179
+ value = input.get(key)
180
+ if not isinstance(value, str):
181
+ raise ValueError(f"{key} must be a string")
182
+ return value
tilde/_transport.py ADDED
@@ -0,0 +1,28 @@
1
+ """One HTTP/2 transport per process for callbacks, matching the Node SDK's HTTP/2 clients."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import pyqwest
6
+
7
+ _transport: pyqwest.HTTPTransport | None = None
8
+
9
+
10
+ def http_client() -> pyqwest.Client:
11
+ global _transport
12
+ if _transport is None:
13
+ # Plaintext callback URLs use HTTP/2 prior knowledge; TLS negotiates HTTP/2 through ALPN.
14
+ # Redirects are refused so bearer credentials never follow an upstream redirect.
15
+ _transport = pyqwest.HTTPTransport(
16
+ http_version=pyqwest.HTTPVersion.HTTP2,
17
+ follow_redirects=False,
18
+ tls_include_system_certs=True,
19
+ )
20
+ return pyqwest.Client(_transport)
21
+
22
+
23
+ async def close_http_client() -> None:
24
+ """Close pooled callback connections, for example before a test server shuts down."""
25
+ global _transport
26
+ transport, _transport = _transport, None
27
+ if transport is not None:
28
+ await transport.aclose()
File without changes
File without changes