trytilde 3.0.1__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 (137) hide show
  1. trytilde-3.0.1/.gitignore +51 -0
  2. trytilde-3.0.1/PKG-INFO +325 -0
  3. trytilde-3.0.1/README.md +307 -0
  4. trytilde-3.0.1/hatch_build.py +70 -0
  5. trytilde-3.0.1/pyproject.toml +34 -0
  6. trytilde-3.0.1/src/tilde/__init__.py +114 -0
  7. trytilde-3.0.1/src/tilde/__main__.py +9 -0
  8. trytilde-3.0.1/src/tilde/_cancel.py +50 -0
  9. trytilde-3.0.1/src/tilde/_inference.py +106 -0
  10. trytilde-3.0.1/src/tilde/_invocation.py +26 -0
  11. trytilde-3.0.1/src/tilde/_otlp.py +35 -0
  12. trytilde-3.0.1/src/tilde/_tools.py +182 -0
  13. trytilde-3.0.1/src/tilde/_transport.py +28 -0
  14. trytilde-3.0.1/src/tilde/agent_event_ingress/__init__.py +0 -0
  15. trytilde-3.0.1/src/tilde/agent_event_ingress/v1/__init__.py +0 -0
  16. trytilde-3.0.1/src/tilde/agent_event_ingress/v1/sidecars_connect.py +748 -0
  17. trytilde-3.0.1/src/tilde/agent_event_ingress/v1/sidecars_pb2.py +128 -0
  18. trytilde-3.0.1/src/tilde/agent_event_ingress/v1/sidecars_pb2.pyi +471 -0
  19. trytilde-3.0.1/src/tilde/agent_host/__init__.py +0 -0
  20. trytilde-3.0.1/src/tilde/agent_host/v1/__init__.py +0 -0
  21. trytilde-3.0.1/src/tilde/agent_host/v1/agent_pb2.py +43 -0
  22. trytilde-3.0.1/src/tilde/agent_host/v1/agent_pb2.pyi +56 -0
  23. trytilde-3.0.1/src/tilde/channels.py +212 -0
  24. trytilde-3.0.1/src/tilde/chat_proxy.py +295 -0
  25. trytilde-3.0.1/src/tilde/clients.py +127 -0
  26. trytilde-3.0.1/src/tilde/context.py +1006 -0
  27. trytilde-3.0.1/src/tilde/deploy.py +420 -0
  28. trytilde-3.0.1/src/tilde/discovery.py +127 -0
  29. trytilde-3.0.1/src/tilde/fastapi.py +69 -0
  30. trytilde-3.0.1/src/tilde/host.py +481 -0
  31. trytilde-3.0.1/src/tilde/logging.py +213 -0
  32. trytilde-3.0.1/src/tilde/management/__init__.py +0 -0
  33. trytilde-3.0.1/src/tilde/management/v1/__init__.py +0 -0
  34. trytilde-3.0.1/src/tilde/management/v1/access_connect.py +465 -0
  35. trytilde-3.0.1/src/tilde/management/v1/access_pb2.py +57 -0
  36. trytilde-3.0.1/src/tilde/management/v1/access_pb2.pyi +102 -0
  37. trytilde-3.0.1/src/tilde/management/v1/agents_connect.py +668 -0
  38. trytilde-3.0.1/src/tilde/management/v1/agents_pb2.py +73 -0
  39. trytilde-3.0.1/src/tilde/management/v1/agents_pb2.pyi +132 -0
  40. trytilde-3.0.1/src/tilde/management/v1/connections_connect.py +806 -0
  41. trytilde-3.0.1/src/tilde/management/v1/connections_pb2.py +93 -0
  42. trytilde-3.0.1/src/tilde/management/v1/connections_pb2.pyi +186 -0
  43. trytilde-3.0.1/src/tilde/management/v1/deployments_connect.py +919 -0
  44. trytilde-3.0.1/src/tilde/management/v1/deployments_pb2.py +109 -0
  45. trytilde-3.0.1/src/tilde/management/v1/deployments_pb2.pyi +306 -0
  46. trytilde-3.0.1/src/tilde/management/v1/identities_connect.py +660 -0
  47. trytilde-3.0.1/src/tilde/management/v1/identities_pb2.py +74 -0
  48. trytilde-3.0.1/src/tilde/management/v1/identities_pb2.pyi +167 -0
  49. trytilde-3.0.1/src/tilde/management/v1/inference_connect.py +424 -0
  50. trytilde-3.0.1/src/tilde/management/v1/inference_pb2.py +69 -0
  51. trytilde-3.0.1/src/tilde/management/v1/inference_pb2.pyi +158 -0
  52. trytilde-3.0.1/src/tilde/management/v1/logs_connect.py +278 -0
  53. trytilde-3.0.1/src/tilde/management/v1/logs_pb2.py +56 -0
  54. trytilde-3.0.1/src/tilde/management/v1/logs_pb2.pyi +123 -0
  55. trytilde-3.0.1/src/tilde/management/v1/prompts_connect.py +290 -0
  56. trytilde-3.0.1/src/tilde/management/v1/prompts_pb2.py +49 -0
  57. trytilde-3.0.1/src/tilde/management/v1/prompts_pb2.pyi +36 -0
  58. trytilde-3.0.1/src/tilde/management/v1/skills_connect.py +1815 -0
  59. trytilde-3.0.1/src/tilde/management/v1/skills_pb2.py +147 -0
  60. trytilde-3.0.1/src/tilde/management/v1/skills_pb2.pyi +324 -0
  61. trytilde-3.0.1/src/tilde/management/v1/tilde_chat_connect.py +290 -0
  62. trytilde-3.0.1/src/tilde/management/v1/tilde_chat_pb2.py +48 -0
  63. trytilde-3.0.1/src/tilde/management/v1/tilde_chat_pb2.pyi +29 -0
  64. trytilde-3.0.1/src/tilde/management/v1/tools_connect.py +1416 -0
  65. trytilde-3.0.1/src/tilde/management/v1/tools_pb2.py +135 -0
  66. trytilde-3.0.1/src/tilde/management/v1/tools_pb2.pyi +341 -0
  67. trytilde-3.0.1/src/tilde/management/v1/tracing_connect.py +513 -0
  68. trytilde-3.0.1/src/tilde/management/v1/tracing_pb2.py +78 -0
  69. trytilde-3.0.1/src/tilde/management/v1/tracing_pb2.pyi +231 -0
  70. trytilde-3.0.1/src/tilde/messages.py +129 -0
  71. trytilde-3.0.1/src/tilde/prompts.py +130 -0
  72. trytilde-3.0.1/src/tilde/provider/__init__.py +0 -0
  73. trytilde-3.0.1/src/tilde/provider/tilde/__init__.py +0 -0
  74. trytilde-3.0.1/src/tilde/provider/tilde/v1/__init__.py +0 -0
  75. trytilde-3.0.1/src/tilde/provider/tilde/v1/chat_connect.py +2090 -0
  76. trytilde-3.0.1/src/tilde/provider/tilde/v1/chat_pb2.py +165 -0
  77. trytilde-3.0.1/src/tilde/provider/tilde/v1/chat_pb2.pyi +478 -0
  78. trytilde-3.0.1/src/tilde/provider/v1/__init__.py +0 -0
  79. trytilde-3.0.1/src/tilde/provider/v1/connections_connect.py +205 -0
  80. trytilde-3.0.1/src/tilde/provider/v1/connections_pb2.py +49 -0
  81. trytilde-3.0.1/src/tilde/provider/v1/connections_pb2.pyi +58 -0
  82. trytilde-3.0.1/src/tilde/run/__init__.py +0 -0
  83. trytilde-3.0.1/src/tilde/run/v1/__init__.py +0 -0
  84. trytilde-3.0.1/src/tilde/run/v1/run_connect.py +367 -0
  85. trytilde-3.0.1/src/tilde/run/v1/run_pb2.py +57 -0
  86. trytilde-3.0.1/src/tilde/run/v1/run_pb2.pyi +80 -0
  87. trytilde-3.0.1/src/tilde/runtime/__init__.py +0 -0
  88. trytilde-3.0.1/src/tilde/runtime/v1/__init__.py +0 -0
  89. trytilde-3.0.1/src/tilde/runtime/v1/agents_connect.py +473 -0
  90. trytilde-3.0.1/src/tilde/runtime/v1/agents_pb2.py +61 -0
  91. trytilde-3.0.1/src/tilde/runtime/v1/agents_pb2.pyi +86 -0
  92. trytilde-3.0.1/src/tilde/runtime/v1/cache_pb2.py +38 -0
  93. trytilde-3.0.1/src/tilde/runtime/v1/cache_pb2.pyi +13 -0
  94. trytilde-3.0.1/src/tilde/runtime/v1/chat_connect.py +1460 -0
  95. trytilde-3.0.1/src/tilde/runtime/v1/chat_pb2.py +130 -0
  96. trytilde-3.0.1/src/tilde/runtime/v1/chat_pb2.pyi +275 -0
  97. trytilde-3.0.1/src/tilde/runtime/v1/controls_connect.py +282 -0
  98. trytilde-3.0.1/src/tilde/runtime/v1/controls_pb2.py +47 -0
  99. trytilde-3.0.1/src/tilde/runtime/v1/controls_pb2.pyi +49 -0
  100. trytilde-3.0.1/src/tilde/runtime/v1/prompts_connect.py +225 -0
  101. trytilde-3.0.1/src/tilde/runtime/v1/prompts_pb2.py +43 -0
  102. trytilde-3.0.1/src/tilde/runtime/v1/prompts_pb2.pyi +20 -0
  103. trytilde-3.0.1/src/tilde/runtime/v1/skills_connect.py +655 -0
  104. trytilde-3.0.1/src/tilde/runtime/v1/skills_pb2.py +77 -0
  105. trytilde-3.0.1/src/tilde/runtime/v1/skills_pb2.pyi +154 -0
  106. trytilde-3.0.1/src/tilde/setup/__init__.py +0 -0
  107. trytilde-3.0.1/src/tilde/setup/v1/__init__.py +0 -0
  108. trytilde-3.0.1/src/tilde/setup/v1/connections_connect.py +595 -0
  109. trytilde-3.0.1/src/tilde/setup/v1/connections_pb2.py +101 -0
  110. trytilde-3.0.1/src/tilde/setup/v1/connections_pb2.pyi +232 -0
  111. trytilde-3.0.1/src/tilde/setup/v1/identity_verification_connect.py +282 -0
  112. trytilde-3.0.1/src/tilde/setup/v1/identity_verification_pb2.py +49 -0
  113. trytilde-3.0.1/src/tilde/setup/v1/identity_verification_pb2.pyi +35 -0
  114. trytilde-3.0.1/src/tilde/skills.py +383 -0
  115. trytilde-3.0.1/src/tilde/tool_host/v1/tool_host_connect.py +314 -0
  116. trytilde-3.0.1/src/tilde/tool_host/v1/tool_host_pb2.py +66 -0
  117. trytilde-3.0.1/src/tilde/tool_host/v1/tool_host_pb2.pyi +115 -0
  118. trytilde-3.0.1/src/tilde/tool_hosts.py +515 -0
  119. trytilde-3.0.1/src/tilde/tracing.py +147 -0
  120. trytilde-3.0.1/src/tilde/types/__init__.py +0 -0
  121. trytilde-3.0.1/src/tilde/types/v1/__init__.py +0 -0
  122. trytilde-3.0.1/src/tilde/types/v1/access_pb2.py +47 -0
  123. trytilde-3.0.1/src/tilde/types/v1/access_pb2.pyi +123 -0
  124. trytilde-3.0.1/src/tilde/types/v1/agent_pb2.py +53 -0
  125. trytilde-3.0.1/src/tilde/types/v1/agent_pb2.pyi +151 -0
  126. trytilde-3.0.1/src/tilde/types/v1/chat_pb2.py +81 -0
  127. trytilde-3.0.1/src/tilde/types/v1/chat_pb2.pyi +316 -0
  128. trytilde-3.0.1/src/tilde/types/v1/connections_pb2.py +79 -0
  129. trytilde-3.0.1/src/tilde/types/v1/connections_pb2.pyi +268 -0
  130. trytilde-3.0.1/src/tilde/types/v1/deployment_pb2.py +51 -0
  131. trytilde-3.0.1/src/tilde/types/v1/deployment_pb2.pyi +129 -0
  132. trytilde-3.0.1/src/tilde/types/v1/prompt_pb2.py +45 -0
  133. trytilde-3.0.1/src/tilde/types/v1/prompt_pb2.pyi +96 -0
  134. trytilde-3.0.1/src/tilde/types/v1/runtime_event_pb2.py +62 -0
  135. trytilde-3.0.1/src/tilde/types/v1/runtime_event_pb2.pyi +243 -0
  136. trytilde-3.0.1/src/tilde/types/v1/skill_pb2.py +49 -0
  137. trytilde-3.0.1/src/tilde/types/v1/skill_pb2.pyi +150 -0
@@ -0,0 +1,51 @@
1
+ /target/
2
+ /dist/
3
+ node_modules/
4
+ # Build output of an older example-agent layout; the examples live under sdk/ts/examples.
5
+ /dev/example-agent-1/
6
+ /web/dist/
7
+ /.tools/
8
+ /.env.*
9
+ !/.env.example
10
+ !/.env.test
11
+ /.run/
12
+ .run/
13
+
14
+ /sdk/ts/**/dist/
15
+ # Generated TypeScript contracts; @trytilde/contracts builds them from proto/.
16
+ /sdk/ts/packages/contracts/gen/
17
+
18
+ /web/provider-dist/
19
+
20
+ __pycache__/
21
+
22
+ # Local runtime credentials must not enter the initial repository commit.
23
+ /.env
24
+ log
25
+
26
+ /.local/
27
+
28
+ # Bifrost benchmark container state
29
+ /dev/inference-bench/bifrost/*
30
+ !/dev/inference-bench/bifrost/config.json
31
+
32
+ # Local Claude Code worktrees
33
+ /.claude/worktrees/
34
+
35
+ # Python SDK: uv environments and generated contracts (sdk/py/scripts/generate.py).
36
+ /sdk/py/.venv/
37
+ /sdk/py/.venv-crewai/
38
+ /sdk/py/**/.venv/
39
+ /sdk/py/packages/tilde/src/tilde/agent_event_ingress/
40
+ /sdk/py/packages/tilde/src/tilde/agent_host/
41
+ /sdk/py/packages/tilde/src/tilde/ingress/
42
+ /sdk/py/packages/tilde/src/tilde/management/
43
+ /sdk/py/packages/tilde/src/tilde/provider/
44
+ /sdk/py/packages/tilde/src/tilde/run/
45
+ /sdk/py/packages/tilde/src/tilde/runtime/
46
+ /sdk/py/packages/tilde/src/tilde/setup/
47
+ /sdk/py/packages/tilde/src/tilde/tool_host/
48
+ /sdk/py/packages/tilde/src/tilde/types/
49
+ /sdk/py/**/*.egg-info/
50
+ .pytest_cache/
51
+ .ruff_cache/
@@ -0,0 +1,325 @@
1
+ Metadata-Version: 2.5
2
+ Name: trytilde
3
+ Version: 3.0.1
4
+ Summary: Tilde agent SDK for Python: dial-in agent hosts, invocation context, channel tools, FastAPI integration and the chat proxy
5
+ License-Expression: Apache-2.0
6
+ Requires-Python: >=3.11
7
+ Requires-Dist: connectrpc<1,>=0.12
8
+ Requires-Dist: httpx>=0.27
9
+ Requires-Dist: opentelemetry-api>=1.30
10
+ Requires-Dist: opentelemetry-exporter-otlp-proto-common>=1.30
11
+ Requires-Dist: opentelemetry-sdk>=1.30
12
+ Requires-Dist: protobuf>=5.29
13
+ Requires-Dist: pydantic>=2.7
14
+ Requires-Dist: pyqwest>=0.5
15
+ Provides-Extra: fastapi
16
+ Requires-Dist: fastapi>=0.115; extra == 'fastapi'
17
+ Description-Content-Type: text/markdown
18
+
19
+ # Tilde Python SDK
20
+
21
+ `trytilde` (import `tilde`) is the Python counterpart of `@trytilde/sdk`: the same dial-in
22
+ agent host, invocation-bound context, channel tools, message history, run reports, tracing and
23
+ logging, plus FastAPI integration and the server-side chat proxy. Framework adapters live in
24
+ sibling packages: `trytilde-langchain`, `trytilde-pydantic-ai`, `trytilde-openai-agents` and
25
+ `trytilde-agno`.
26
+
27
+ ```sh
28
+ cd sdk/py
29
+ uv sync --all-packages # workspace environment with every package and example
30
+ uv run python scripts/generate.py # Protobuf messages and Connect stubs from proto/
31
+ uv run pytest # adapter unit tests and the host tests against a fake gateway
32
+ ```
33
+
34
+ Generated contracts sit inside the package under the proto package names
35
+ (`tilde.runtime.v1.chat_pb2`, `tilde.types.v1.chat_pb2`, `tilde.run.v1.run_connect`, ...);
36
+ they are not committed. Every RPC uses `connectrpc` with Google protobuf messages over
37
+ `pyqwest` HTTP/2.
38
+
39
+ ## Agent host
40
+
41
+ ```python
42
+ from tilde import AgentContext, run_connected_agent
43
+
44
+
45
+ async def run(ctx: AgentContext) -> None:
46
+ await ctx.reason("Inspecting the request.") # execution activity, never a message
47
+ goal = await ctx.goals.create(objective=ctx.objective)
48
+ task = await ctx.tasks.create(title="Reply", goal_id=goal.id)
49
+
50
+ async def words():
51
+ yield "Hello "
52
+ yield "there."
53
+
54
+ await ctx.send_native_message(words())
55
+ await ctx.tasks.update(id=task.id, status="completed")
56
+ await ctx.goals.update(id=goal.id, status="completed")
57
+ await ctx.set_run_status("completed")
58
+ ctx.stop()
59
+
60
+
61
+ run_connected_agent(run=run) # TILDE_GATEWAY_URL and TILDE_DEPLOYMENT_TOKEN from the environment
62
+ ```
63
+
64
+ Tilde never calls an agent over HTTP. A host dials out: `connect_agent` opens
65
+ `tilde.run.v1.RunService.Watch` with the deployment token (from `RegisterDeployment` or
66
+ `IssueDeploymentToken`) and receives each wake as a stream frame, so the process needs no
67
+ inbound port, public URL or shared secret. Wakes run concurrently; the host heartbeats every
68
+ 3 seconds and reconnects after 1 second whenever the stream ends, until `close()`.
69
+ `run_connected_agent(**options)` is the blocking form for standalone scripts: it connects,
70
+ waits for SIGINT/SIGTERM and closes. Inside an existing event loop use `connect_agent`:
71
+
72
+ ```python
73
+ from tilde import connect_agent
74
+
75
+ host = connect_agent(gateway_url=RUNTIME_URL, deployment_token=TOKEN, run=run, ready=database_is_up)
76
+ await host.wait() # until host.close()
77
+ ```
78
+
79
+ `gateway_url` and `deployment_token` default to `TILDE_GATEWAY_URL` and
80
+ `TILDE_DEPLOYMENT_TOKEN`. `ready` is an optional sync or async check whose result is sent with
81
+ every heartbeat (not ready when it raises); Tilde only routes wakes to ready instances.
82
+
83
+ For each wake the host opens `InvocationControlService.WatchCommands` before user code starts,
84
+ fetches the invocation-scoped tool catalog, and reports `accepted`, every `reason()` delta and
85
+ exactly one `stopped` through `RunService.Report` with the invocation token.
86
+
87
+ `ctx.tools` holds SDK-local `stop`, `goals.*` and `tasks.*` helpers plus provider tools with
88
+ server-authored descriptions, JSON schemas and optional chunk schemas. `ctx.channel.current`
89
+ exposes only the inbound connection's tools; `ctx.channel.slack`, `github`, `agentmail`, `linq`,
90
+ `whatsapp`, `telnyx_whatsapp` and `native` resolve providers, `ctx.channel.for_connection(id)`
91
+ and `ctx.channel.provider(id)` select explicitly, and `call_channel_tool(name, json)` reaches
92
+ custom providers. Tools are awaited with the framework's tool-call ID:
93
+ `await ctx.channel.slack.send_message({"channelId": "C1", "text": "Hi"}, tool_call_id=call_id)`.
94
+ Streaming tools accept an async iterable of chunks in the provider's chunk format.
95
+
96
+ Provider tools carry `summary`, `output_schema`, `annotations` and `background`;
97
+ `ctx.tool_source(slug)` returns one tool source's tools keyed by tool name. `ctx.agent_tools`
98
+ collects everything Tilde gives the agent besides messaging (tool sources, Tilde's built-in
99
+ tools, personal tools), beside `ctx.channel.current`.
100
+
101
+ The agent's bundled tools are its own framework tools, written the framework's way and run in
102
+ its process. Each adapter's `await with_tilde_tools(ctx, native_tools, options=...)` returns the
103
+ framework's tool collection with the current channel's tools, `ctx.agent_tools` and the native
104
+ tools, publishes the native tools to Tilde (so `tools.search` and `tools.schemas` describe them,
105
+ a `tools.execute` naming one runs it here, and the agent's Tools tab lists them read-only), and
106
+ audits every call once with its summary. `BundledOptions(summary=..., display="summary",
107
+ annotations=ToolAnnotations(read_only=True))` sets the Tilde metadata per tool name; `display`
108
+ (`"full"`, `"summary"` or `"hidden"`) is how its calls show in end-user chats, and traces keep
109
+ full detail. Call it once per invocation; the set is replaced each time.
110
+
111
+ ```python
112
+ from langchain_core.tools import tool
113
+ from tilde import BundledOptions
114
+ from tilde_langchain import with_tilde_tools
115
+
116
+
117
+ @tool
118
+ def read_file(path: str) -> str:
119
+ """Read a file from the workspace."""
120
+ return Path(path).read_text()
121
+
122
+
123
+ tools = await with_tilde_tools(
124
+ ctx, [read_file], options={"read_file": BundledOptions(summary="Read a file")}
125
+ )
126
+ ```
127
+
128
+ `ctx.message.history(limit=..., before_message_id=..., include_objective=True, include_work=False)`
129
+ returns typed `ConversationMessage`, `ObjectiveMessage`, `GoalMessage` and `TaskMessage` items;
130
+ the adapters convert them to framework messages and hydrate attachments through the scoped
131
+ `ctx.attachments.download`. `ctx.agents.*` and `ctx.invoke_agent(...)` call the registry with the
132
+ current token; the server checks every grant.
133
+
134
+ Cancellation is cooperative asyncio cancellation: a stop control or a lost callback connection
135
+ cancels the task running `run`, and `ctx.stop()` raises `StopLoop` (a `BaseException`, so it
136
+ passes through framework `except Exception` handlers). Call `ctx.check()` at framework
137
+ checkpoints. Returning from `run` never publishes a message; unfinished runs become waiting
138
+ unless `set_run_status` says otherwise. Connect tokens are renewed at four minutes; a denied
139
+ renewal aborts execution. `checkpoint=` receives the context on suspension and must quiesce
140
+ the framework before returning.
141
+
142
+ ## Inference, prompts and skills
143
+
144
+ ```python
145
+ import tilde
146
+
147
+ MODEL = tilde.inference("default") # module scope: resolves the running invocation per request
148
+ SYSTEM = tilde.define_prompt(
149
+ "system",
150
+ template="You are {{name}}.\n{{> rules}}",
151
+ sections={"rules": "Be brief."},
152
+ config={"model": "gpt-4o-mini"},
153
+ )
154
+ SKILLS = tilde.define_skills("skills") # folders holding a SKILL.md, relative to this file
155
+
156
+
157
+ async def run(ctx: tilde.AgentContext) -> None:
158
+ client = AsyncOpenAI(
159
+ base_url=MODEL.base_url, api_key=MODEL.api_key, http_client=MODEL.async_client()
160
+ )
161
+ instructions = SYSTEM.render(name="Support") # marks the prompt active for this invocation
162
+ ```
163
+
164
+ `tilde.inference(alias)` returns the same `Inference` as `ctx.inference(alias)`, but every
165
+ request resolves the invocation running it (token, callback URL, active prompt stamps), so model
166
+ clients can be built at import time; a request outside `run(ctx)` raises. Both send
167
+ `x-tilde-prompt: name@hash[, ...]` for prompts made active with `ctx.prompt(definition)` or by
168
+ rendering one inside the invocation. `define_skill(name, description, instructions, files=...)`
169
+ declares a skill in code with a generated `SKILL.md`.
170
+
171
+ At runtime `ctx.skills` reads every skill the invocation may use: `list()` (each
172
+ `SkillSummary` says whether it is `deployed`, shipped beside the code, or assigned through the
173
+ registry), `read(name, path="SKILL.md")` (text inline, other files as a short-lived
174
+ `download_url`) and `summary()` (a system-prompt block). `directory()` keeps the registry skills
175
+ in `$TILDE_SKILLS_DIR` (the OS temp directory by default) `/tilde-skills/<agent id>/<name>/…`
176
+ for frameworks that load skill folders from disk: the engine pushes the invocation's skills and
177
+ versions with each wake, and only new and newer versions are downloaded and removed skills
178
+ deleted, so a skill assigned in Tilde reaches
179
+ the next invocation without a redeploy. Frameworks without native skills convert
180
+ `ctx.skills.tools()` (`list_skills`, `read_skill`) like channel tools and add `summary()` to
181
+ their instructions. Framework adapters stamp dynamic prompts with
182
+ `ctx.activate_prompt(name, hash)`.
183
+
184
+ Prompts and skills are registered with a deployment:
185
+
186
+ ```sh
187
+ python -m tilde deploy [ENTRY] [--agent-id ID] [--url URL] [--api-key KEY] \
188
+ [--target gateway|sidecar|lambda] [--function-arn ARN] [--external-id ID] [--label L] \
189
+ [--dry-run] [--json]
190
+ ```
191
+
192
+ `ENTRY` is a file or dotted module (default `main.py`), imported with `TILDE_DISCOVERY=1`
193
+ (`connect_agent`/`run_connected_agent` then do nothing) and not as `__main__`. The globals of
194
+ the entry and of every module loaded from the working directory are scanned for `define_*`
195
+ objects and offered to framework discoverers (entry point group `tilde.discover`, for example
196
+ `trytilde-crewai`). The inventory goes to stderr; `--dry-run` prints the
197
+ `DeploymentDeclarations` JSON; otherwise binary or large skill files are uploaded, the
198
+ deployment is registered (`$TILDE_AGENT_ID`, `$TILDE_URL` = management API, and on Tilde Cloud
199
+ `$TILDE_API_KEY`; open-source Tilde needs no key) and stdout is the deployment token (`--json`: `{"deploymentId", "token", "created"}`).
200
+
201
+ ## FastAPI
202
+
203
+ ```python
204
+ from fastapi import FastAPI
205
+ from tilde.fastapi import agent_lifespan, mount_chat_proxy
206
+
207
+ app = FastAPI(lifespan=agent_lifespan(run=run))
208
+ mount_chat_proxy(
209
+ app,
210
+ "/api/chat",
211
+ agent_id=os.environ["TILDE_AGENT_ID"],
212
+ api_key=os.environ["TILDE_CHAT_API_KEY"],
213
+ resolve_identity=lambda request, agent_id: request.session.get("user_id"),
214
+ )
215
+ ```
216
+
217
+ `agent_lifespan(**connect_agent_options)` calls `connect_agent` on startup and `close()` on
218
+ shutdown, so the application hosts an agent without exposing any agent route. Any ASGI server
219
+ works.
220
+
221
+ ## Chat proxy
222
+
223
+ `tilde.chat_proxy.create_chat_proxy(...)` is the Python `@trytilde/chat-proxy`: a pure ASGI
224
+ reverse proxy for the `tilde.provider.tilde.v1.ChatService` RPCs used by the embeddable chat UI.
225
+ It only forwards declared methods, replaces browser credentials with the server-held API key,
226
+ sets the base64url `x-tilde-identity` header from `resolve_identity(request, agent_id)`, keeps
227
+ cookies at the host, rejects cross-origin browser requests and upstream redirects, streams
228
+ request and response bodies without buffering, and never exposes upstream error details.
229
+ `GET /api/chat/agents` lists the agents the caller may use. The same-origin check honors
230
+ `X-Forwarded-Proto`/`X-Forwarded-Host` from a TLS-terminating reverse proxy; pass
231
+ `trust_forwarded_headers=False` when the ASGI server is exposed directly. Multi-agent proxies take
232
+ `agents=[ChatAgentConfig(agent_id, api_key, base_url=...), ...]` and route
233
+ `/api/chat/{agentId}/tilde.provider.tilde.v1.ChatService/{Method}`.
234
+
235
+ ## Cloud invoke
236
+
237
+ `create_lambda_handler(run=run)` returns an AWS Lambda handler for the JSON-encoded
238
+ `InvokeRequest` delivered by the cloud invoke API. It runs the invocation through the same
239
+ execution path as a Watch wake and reports through `RunService`.
240
+
241
+ ## Tool hosts
242
+
243
+ `tilde.tool_hosts` serves your own tools to agents. Input and output schemas come from the
244
+ pydantic annotations; an `Auth` publishes a provider whose connections (instances) carry
245
+ credentials, parsed into the method's model as `ctx.auth`.
246
+
247
+ ```python
248
+ from pydantic import BaseModel, SecretStr
249
+ from tilde.tool_hosts import Auth, Method, ToolContext, create_tool_host
250
+
251
+
252
+ class ApiKey(BaseModel):
253
+ api_key: SecretStr # credential fields are strings; SecretStr renders as a secret field
254
+
255
+
256
+ async def verify(auth: ApiKey, instance) -> str | None:
257
+ return "acme" # account label; raise to refuse (the message is shown on the setup form)
258
+
259
+
260
+ auth = Auth(
261
+ id="acme-crm",
262
+ name="Acme CRM",
263
+ methods={"api_key": Method(name="API key", schema=ApiKey)},
264
+ verify=verify,
265
+ )
266
+
267
+
268
+ class SearchInput(BaseModel):
269
+ q: str
270
+
271
+
272
+ class SearchOutput(BaseModel):
273
+ results: list[str]
274
+
275
+
276
+ @auth.tool(description="Search the CRM.")
277
+ async def search(input: SearchInput, ctx: ToolContext[ApiKey]) -> SearchOutput:
278
+ return SearchOutput(results=[])
279
+
280
+
281
+ await create_tool_host(auth=auth, tools=[search]).run() # TILDE_GATEWAY_URL, TILDE_TOOL_HOST_TOKEN
282
+ ```
283
+
284
+ `@tool(...)` declares a tool without credentials. `create_tool_host` dials
285
+ `ToolHostService.Watch`, runs calls concurrently and reconnects with backoff until `close()`;
286
+ `create_tool_lambda_handler(auth=..., tools=[...])` answers the Protobuf-JSON `LambdaRequest`
287
+ events of a Lambda tool host. A raised exception's message becomes the tool's error, as does an
288
+ output that fails the output model.
289
+
290
+ ## Tracing and logs
291
+
292
+ Hosts install an OpenTelemetry tracer and logger provider by default. The W3C context of the
293
+ wake becomes an `agent.invoke` server span; spans and log records emitted inside the
294
+ invocation's asyncio context are batched per invocation and uploaded as OTLP/HTTP protobuf to
295
+ the callback base plus `/v1/traces` and `/v1/logs` with the current connect token. Records from
296
+ the standard `logging` module are routed through a root handler; nothing outside an invocation
297
+ or configured deployment scope is exported. Pass `tracing="existing"` / `logging="existing"`
298
+ and add `agent_span_processor` / `agent_log_processor` to your own providers to keep them.
299
+ `connect_agent` exports logs outside an invocation with the deployment credentials (one
300
+ deployment per process); call `configure_deployment_logging(gateway_url, deployment_token)`
301
+ earlier to capture logs emitted before it.
302
+
303
+ ## Management, chat and runtime clients
304
+
305
+ ```python
306
+ from tilde import create_management_client, create_tilde_chat_client
307
+ from tilde.management.v1.tilde_chat_pb2 import GetCredentialsRequest
308
+ from tilde.provider.tilde.v1 import chat_pb2
309
+
310
+ tilde = create_management_client("http://127.0.0.1:8080")
311
+ # Management provisions credentials; chat operations use the Tilde chat provider.
312
+ credentials = await tilde.tilde_chat.get_credentials(GetCredentialsRequest(agent_id=agent_id))
313
+ chat = create_tilde_chat_client(
314
+ "http://127.0.0.1:8080", agent_id, api_key=credentials.api_key, identity="alice"
315
+ )
316
+ user = (await chat.get_identity(chat_pb2.GetIdentityRequest())).user
317
+ ```
318
+
319
+ `ManagementClient` groups `access`, `agents`, `connections`, `deployments`,
320
+ `identities`, `inference`, `logs`, `tilde_chat` and `traces`; `RuntimeClient` groups `agents`
321
+ and `chat`. `create_tilde_chat_client` is the server-side client for the built-in chat provider:
322
+ resolve `identity` from your authenticated application user, never from browser input. All
323
+ clients use the generated request messages directly. Open-source Tilde's management API is
324
+ unauthenticated (secure it with your own proxy); pass `access_token=` for Tilde Cloud, which
325
+ requires a management credential.
@@ -0,0 +1,307 @@
1
+ # Tilde Python SDK
2
+
3
+ `trytilde` (import `tilde`) is the Python counterpart of `@trytilde/sdk`: the same dial-in
4
+ agent host, invocation-bound context, channel tools, message history, run reports, tracing and
5
+ logging, plus FastAPI integration and the server-side chat proxy. Framework adapters live in
6
+ sibling packages: `trytilde-langchain`, `trytilde-pydantic-ai`, `trytilde-openai-agents` and
7
+ `trytilde-agno`.
8
+
9
+ ```sh
10
+ cd sdk/py
11
+ uv sync --all-packages # workspace environment with every package and example
12
+ uv run python scripts/generate.py # Protobuf messages and Connect stubs from proto/
13
+ uv run pytest # adapter unit tests and the host tests against a fake gateway
14
+ ```
15
+
16
+ Generated contracts sit inside the package under the proto package names
17
+ (`tilde.runtime.v1.chat_pb2`, `tilde.types.v1.chat_pb2`, `tilde.run.v1.run_connect`, ...);
18
+ they are not committed. Every RPC uses `connectrpc` with Google protobuf messages over
19
+ `pyqwest` HTTP/2.
20
+
21
+ ## Agent host
22
+
23
+ ```python
24
+ from tilde import AgentContext, run_connected_agent
25
+
26
+
27
+ async def run(ctx: AgentContext) -> None:
28
+ await ctx.reason("Inspecting the request.") # execution activity, never a message
29
+ goal = await ctx.goals.create(objective=ctx.objective)
30
+ task = await ctx.tasks.create(title="Reply", goal_id=goal.id)
31
+
32
+ async def words():
33
+ yield "Hello "
34
+ yield "there."
35
+
36
+ await ctx.send_native_message(words())
37
+ await ctx.tasks.update(id=task.id, status="completed")
38
+ await ctx.goals.update(id=goal.id, status="completed")
39
+ await ctx.set_run_status("completed")
40
+ ctx.stop()
41
+
42
+
43
+ run_connected_agent(run=run) # TILDE_GATEWAY_URL and TILDE_DEPLOYMENT_TOKEN from the environment
44
+ ```
45
+
46
+ Tilde never calls an agent over HTTP. A host dials out: `connect_agent` opens
47
+ `tilde.run.v1.RunService.Watch` with the deployment token (from `RegisterDeployment` or
48
+ `IssueDeploymentToken`) and receives each wake as a stream frame, so the process needs no
49
+ inbound port, public URL or shared secret. Wakes run concurrently; the host heartbeats every
50
+ 3 seconds and reconnects after 1 second whenever the stream ends, until `close()`.
51
+ `run_connected_agent(**options)` is the blocking form for standalone scripts: it connects,
52
+ waits for SIGINT/SIGTERM and closes. Inside an existing event loop use `connect_agent`:
53
+
54
+ ```python
55
+ from tilde import connect_agent
56
+
57
+ host = connect_agent(gateway_url=RUNTIME_URL, deployment_token=TOKEN, run=run, ready=database_is_up)
58
+ await host.wait() # until host.close()
59
+ ```
60
+
61
+ `gateway_url` and `deployment_token` default to `TILDE_GATEWAY_URL` and
62
+ `TILDE_DEPLOYMENT_TOKEN`. `ready` is an optional sync or async check whose result is sent with
63
+ every heartbeat (not ready when it raises); Tilde only routes wakes to ready instances.
64
+
65
+ For each wake the host opens `InvocationControlService.WatchCommands` before user code starts,
66
+ fetches the invocation-scoped tool catalog, and reports `accepted`, every `reason()` delta and
67
+ exactly one `stopped` through `RunService.Report` with the invocation token.
68
+
69
+ `ctx.tools` holds SDK-local `stop`, `goals.*` and `tasks.*` helpers plus provider tools with
70
+ server-authored descriptions, JSON schemas and optional chunk schemas. `ctx.channel.current`
71
+ exposes only the inbound connection's tools; `ctx.channel.slack`, `github`, `agentmail`, `linq`,
72
+ `whatsapp`, `telnyx_whatsapp` and `native` resolve providers, `ctx.channel.for_connection(id)`
73
+ and `ctx.channel.provider(id)` select explicitly, and `call_channel_tool(name, json)` reaches
74
+ custom providers. Tools are awaited with the framework's tool-call ID:
75
+ `await ctx.channel.slack.send_message({"channelId": "C1", "text": "Hi"}, tool_call_id=call_id)`.
76
+ Streaming tools accept an async iterable of chunks in the provider's chunk format.
77
+
78
+ Provider tools carry `summary`, `output_schema`, `annotations` and `background`;
79
+ `ctx.tool_source(slug)` returns one tool source's tools keyed by tool name. `ctx.agent_tools`
80
+ collects everything Tilde gives the agent besides messaging (tool sources, Tilde's built-in
81
+ tools, personal tools), beside `ctx.channel.current`.
82
+
83
+ The agent's bundled tools are its own framework tools, written the framework's way and run in
84
+ its process. Each adapter's `await with_tilde_tools(ctx, native_tools, options=...)` returns the
85
+ framework's tool collection with the current channel's tools, `ctx.agent_tools` and the native
86
+ tools, publishes the native tools to Tilde (so `tools.search` and `tools.schemas` describe them,
87
+ a `tools.execute` naming one runs it here, and the agent's Tools tab lists them read-only), and
88
+ audits every call once with its summary. `BundledOptions(summary=..., display="summary",
89
+ annotations=ToolAnnotations(read_only=True))` sets the Tilde metadata per tool name; `display`
90
+ (`"full"`, `"summary"` or `"hidden"`) is how its calls show in end-user chats, and traces keep
91
+ full detail. Call it once per invocation; the set is replaced each time.
92
+
93
+ ```python
94
+ from langchain_core.tools import tool
95
+ from tilde import BundledOptions
96
+ from tilde_langchain import with_tilde_tools
97
+
98
+
99
+ @tool
100
+ def read_file(path: str) -> str:
101
+ """Read a file from the workspace."""
102
+ return Path(path).read_text()
103
+
104
+
105
+ tools = await with_tilde_tools(
106
+ ctx, [read_file], options={"read_file": BundledOptions(summary="Read a file")}
107
+ )
108
+ ```
109
+
110
+ `ctx.message.history(limit=..., before_message_id=..., include_objective=True, include_work=False)`
111
+ returns typed `ConversationMessage`, `ObjectiveMessage`, `GoalMessage` and `TaskMessage` items;
112
+ the adapters convert them to framework messages and hydrate attachments through the scoped
113
+ `ctx.attachments.download`. `ctx.agents.*` and `ctx.invoke_agent(...)` call the registry with the
114
+ current token; the server checks every grant.
115
+
116
+ Cancellation is cooperative asyncio cancellation: a stop control or a lost callback connection
117
+ cancels the task running `run`, and `ctx.stop()` raises `StopLoop` (a `BaseException`, so it
118
+ passes through framework `except Exception` handlers). Call `ctx.check()` at framework
119
+ checkpoints. Returning from `run` never publishes a message; unfinished runs become waiting
120
+ unless `set_run_status` says otherwise. Connect tokens are renewed at four minutes; a denied
121
+ renewal aborts execution. `checkpoint=` receives the context on suspension and must quiesce
122
+ the framework before returning.
123
+
124
+ ## Inference, prompts and skills
125
+
126
+ ```python
127
+ import tilde
128
+
129
+ MODEL = tilde.inference("default") # module scope: resolves the running invocation per request
130
+ SYSTEM = tilde.define_prompt(
131
+ "system",
132
+ template="You are {{name}}.\n{{> rules}}",
133
+ sections={"rules": "Be brief."},
134
+ config={"model": "gpt-4o-mini"},
135
+ )
136
+ SKILLS = tilde.define_skills("skills") # folders holding a SKILL.md, relative to this file
137
+
138
+
139
+ async def run(ctx: tilde.AgentContext) -> None:
140
+ client = AsyncOpenAI(
141
+ base_url=MODEL.base_url, api_key=MODEL.api_key, http_client=MODEL.async_client()
142
+ )
143
+ instructions = SYSTEM.render(name="Support") # marks the prompt active for this invocation
144
+ ```
145
+
146
+ `tilde.inference(alias)` returns the same `Inference` as `ctx.inference(alias)`, but every
147
+ request resolves the invocation running it (token, callback URL, active prompt stamps), so model
148
+ clients can be built at import time; a request outside `run(ctx)` raises. Both send
149
+ `x-tilde-prompt: name@hash[, ...]` for prompts made active with `ctx.prompt(definition)` or by
150
+ rendering one inside the invocation. `define_skill(name, description, instructions, files=...)`
151
+ declares a skill in code with a generated `SKILL.md`.
152
+
153
+ At runtime `ctx.skills` reads every skill the invocation may use: `list()` (each
154
+ `SkillSummary` says whether it is `deployed`, shipped beside the code, or assigned through the
155
+ registry), `read(name, path="SKILL.md")` (text inline, other files as a short-lived
156
+ `download_url`) and `summary()` (a system-prompt block). `directory()` keeps the registry skills
157
+ in `$TILDE_SKILLS_DIR` (the OS temp directory by default) `/tilde-skills/<agent id>/<name>/…`
158
+ for frameworks that load skill folders from disk: the engine pushes the invocation's skills and
159
+ versions with each wake, and only new and newer versions are downloaded and removed skills
160
+ deleted, so a skill assigned in Tilde reaches
161
+ the next invocation without a redeploy. Frameworks without native skills convert
162
+ `ctx.skills.tools()` (`list_skills`, `read_skill`) like channel tools and add `summary()` to
163
+ their instructions. Framework adapters stamp dynamic prompts with
164
+ `ctx.activate_prompt(name, hash)`.
165
+
166
+ Prompts and skills are registered with a deployment:
167
+
168
+ ```sh
169
+ python -m tilde deploy [ENTRY] [--agent-id ID] [--url URL] [--api-key KEY] \
170
+ [--target gateway|sidecar|lambda] [--function-arn ARN] [--external-id ID] [--label L] \
171
+ [--dry-run] [--json]
172
+ ```
173
+
174
+ `ENTRY` is a file or dotted module (default `main.py`), imported with `TILDE_DISCOVERY=1`
175
+ (`connect_agent`/`run_connected_agent` then do nothing) and not as `__main__`. The globals of
176
+ the entry and of every module loaded from the working directory are scanned for `define_*`
177
+ objects and offered to framework discoverers (entry point group `tilde.discover`, for example
178
+ `trytilde-crewai`). The inventory goes to stderr; `--dry-run` prints the
179
+ `DeploymentDeclarations` JSON; otherwise binary or large skill files are uploaded, the
180
+ deployment is registered (`$TILDE_AGENT_ID`, `$TILDE_URL` = management API, and on Tilde Cloud
181
+ `$TILDE_API_KEY`; open-source Tilde needs no key) and stdout is the deployment token (`--json`: `{"deploymentId", "token", "created"}`).
182
+
183
+ ## FastAPI
184
+
185
+ ```python
186
+ from fastapi import FastAPI
187
+ from tilde.fastapi import agent_lifespan, mount_chat_proxy
188
+
189
+ app = FastAPI(lifespan=agent_lifespan(run=run))
190
+ mount_chat_proxy(
191
+ app,
192
+ "/api/chat",
193
+ agent_id=os.environ["TILDE_AGENT_ID"],
194
+ api_key=os.environ["TILDE_CHAT_API_KEY"],
195
+ resolve_identity=lambda request, agent_id: request.session.get("user_id"),
196
+ )
197
+ ```
198
+
199
+ `agent_lifespan(**connect_agent_options)` calls `connect_agent` on startup and `close()` on
200
+ shutdown, so the application hosts an agent without exposing any agent route. Any ASGI server
201
+ works.
202
+
203
+ ## Chat proxy
204
+
205
+ `tilde.chat_proxy.create_chat_proxy(...)` is the Python `@trytilde/chat-proxy`: a pure ASGI
206
+ reverse proxy for the `tilde.provider.tilde.v1.ChatService` RPCs used by the embeddable chat UI.
207
+ It only forwards declared methods, replaces browser credentials with the server-held API key,
208
+ sets the base64url `x-tilde-identity` header from `resolve_identity(request, agent_id)`, keeps
209
+ cookies at the host, rejects cross-origin browser requests and upstream redirects, streams
210
+ request and response bodies without buffering, and never exposes upstream error details.
211
+ `GET /api/chat/agents` lists the agents the caller may use. The same-origin check honors
212
+ `X-Forwarded-Proto`/`X-Forwarded-Host` from a TLS-terminating reverse proxy; pass
213
+ `trust_forwarded_headers=False` when the ASGI server is exposed directly. Multi-agent proxies take
214
+ `agents=[ChatAgentConfig(agent_id, api_key, base_url=...), ...]` and route
215
+ `/api/chat/{agentId}/tilde.provider.tilde.v1.ChatService/{Method}`.
216
+
217
+ ## Cloud invoke
218
+
219
+ `create_lambda_handler(run=run)` returns an AWS Lambda handler for the JSON-encoded
220
+ `InvokeRequest` delivered by the cloud invoke API. It runs the invocation through the same
221
+ execution path as a Watch wake and reports through `RunService`.
222
+
223
+ ## Tool hosts
224
+
225
+ `tilde.tool_hosts` serves your own tools to agents. Input and output schemas come from the
226
+ pydantic annotations; an `Auth` publishes a provider whose connections (instances) carry
227
+ credentials, parsed into the method's model as `ctx.auth`.
228
+
229
+ ```python
230
+ from pydantic import BaseModel, SecretStr
231
+ from tilde.tool_hosts import Auth, Method, ToolContext, create_tool_host
232
+
233
+
234
+ class ApiKey(BaseModel):
235
+ api_key: SecretStr # credential fields are strings; SecretStr renders as a secret field
236
+
237
+
238
+ async def verify(auth: ApiKey, instance) -> str | None:
239
+ return "acme" # account label; raise to refuse (the message is shown on the setup form)
240
+
241
+
242
+ auth = Auth(
243
+ id="acme-crm",
244
+ name="Acme CRM",
245
+ methods={"api_key": Method(name="API key", schema=ApiKey)},
246
+ verify=verify,
247
+ )
248
+
249
+
250
+ class SearchInput(BaseModel):
251
+ q: str
252
+
253
+
254
+ class SearchOutput(BaseModel):
255
+ results: list[str]
256
+
257
+
258
+ @auth.tool(description="Search the CRM.")
259
+ async def search(input: SearchInput, ctx: ToolContext[ApiKey]) -> SearchOutput:
260
+ return SearchOutput(results=[])
261
+
262
+
263
+ await create_tool_host(auth=auth, tools=[search]).run() # TILDE_GATEWAY_URL, TILDE_TOOL_HOST_TOKEN
264
+ ```
265
+
266
+ `@tool(...)` declares a tool without credentials. `create_tool_host` dials
267
+ `ToolHostService.Watch`, runs calls concurrently and reconnects with backoff until `close()`;
268
+ `create_tool_lambda_handler(auth=..., tools=[...])` answers the Protobuf-JSON `LambdaRequest`
269
+ events of a Lambda tool host. A raised exception's message becomes the tool's error, as does an
270
+ output that fails the output model.
271
+
272
+ ## Tracing and logs
273
+
274
+ Hosts install an OpenTelemetry tracer and logger provider by default. The W3C context of the
275
+ wake becomes an `agent.invoke` server span; spans and log records emitted inside the
276
+ invocation's asyncio context are batched per invocation and uploaded as OTLP/HTTP protobuf to
277
+ the callback base plus `/v1/traces` and `/v1/logs` with the current connect token. Records from
278
+ the standard `logging` module are routed through a root handler; nothing outside an invocation
279
+ or configured deployment scope is exported. Pass `tracing="existing"` / `logging="existing"`
280
+ and add `agent_span_processor` / `agent_log_processor` to your own providers to keep them.
281
+ `connect_agent` exports logs outside an invocation with the deployment credentials (one
282
+ deployment per process); call `configure_deployment_logging(gateway_url, deployment_token)`
283
+ earlier to capture logs emitted before it.
284
+
285
+ ## Management, chat and runtime clients
286
+
287
+ ```python
288
+ from tilde import create_management_client, create_tilde_chat_client
289
+ from tilde.management.v1.tilde_chat_pb2 import GetCredentialsRequest
290
+ from tilde.provider.tilde.v1 import chat_pb2
291
+
292
+ tilde = create_management_client("http://127.0.0.1:8080")
293
+ # Management provisions credentials; chat operations use the Tilde chat provider.
294
+ credentials = await tilde.tilde_chat.get_credentials(GetCredentialsRequest(agent_id=agent_id))
295
+ chat = create_tilde_chat_client(
296
+ "http://127.0.0.1:8080", agent_id, api_key=credentials.api_key, identity="alice"
297
+ )
298
+ user = (await chat.get_identity(chat_pb2.GetIdentityRequest())).user
299
+ ```
300
+
301
+ `ManagementClient` groups `access`, `agents`, `connections`, `deployments`,
302
+ `identities`, `inference`, `logs`, `tilde_chat` and `traces`; `RuntimeClient` groups `agents`
303
+ and `chat`. `create_tilde_chat_client` is the server-side client for the built-in chat provider:
304
+ resolve `identity` from your authenticated application user, never from browser input. All
305
+ clients use the generated request messages directly. Open-source Tilde's management API is
306
+ unauthenticated (secure it with your own proxy); pass `access_token=` for Tilde Cloud, which
307
+ requires a management credential.