···69 "cache_control": {"type": "ephemeral"},
70 }
71 ],
72+ "messages": self._inject_cache_breakpoints(messages),
73 }
7475 if tools:
···97 content=content,
98 stop_reason=msg.stop_reason or "end_turn", # type: ignore TODO: fix this
99 )
100+101+ @staticmethod
102+ def _inject_cache_breakpoints(
103+ messages: list[dict[str, Any]],
104+ ) -> list[dict[str, Any]]:
105+ """
106+ a helper that adds cache_control breakpoints to the conversation so that
107+ the conversation prefix is cached across successive calls. we place a single
108+ breakpoint in th last message's content block, combined with the sys-prompt
109+ and tool defs breakpoints. ensures that we stay in the 4-breakpoint limit
110+ that ant requires
111+ """
112+ if not messages:
113+ return messages
114+115+ # shallow-copy the list so we don't mutate the caller's conversation
116+ messages = list(messages)
117+ last_msg = dict(messages[-1])
118+ content = last_msg["content"]
119+120+ if isinstance(content, str):
121+ last_msg["content"] = [
122+ {
123+ "type": "text",
124+ "text": content,
125+ "cache_control": {"type": "ephemeral"},
126+ }
127+ ]
128+ elif isinstance(content, list) and content:
129+ content = [dict(b) for b in content]
130+ content[-1] = dict(content[-1])
131+ content[-1]["cache_control"] = {"type": "ephemeral"}
132+ last_msg["content"] = content
133+134+ messages[-1] = last_msg
135+ return messages
136137138class OpenAICompatibleClient(AgentClient):
+21
src/agent/prompt.py
···208"""
2092100000000000000000000211def build_system_prompt():
212 """
213 Here we put together the base system prompt for the agent. The system prompt does _not_ change based on inputs, so that proper caching across sessions can take place.
···226# Osprey Documentation
227228{OSPREY_RULE_GUIDANCE}
00229 """
230231 return system_prompt
···208"""
209210211+CLICKHOUSE_SQL_TIPS = """
212+# ClickHouse SQL Tips
213+214+- **DateTime filtering**: The `__timestamp` column is `DateTime64(3)`. Do NOT use raw ISO strings. Use `parseDateTimeBestEffort()`:
215+ ```sql
216+ WHERE __timestamp >= parseDateTimeBestEffort('2026-02-06 04:30:00')
217+ ```
218+ To compute a relative time in TypeScript, format it as `YYYY-MM-DD HH:MM:SS`:
219+ ```typescript
220+ const ts = new Date(Date.now() - 30 * 60 * 1000).toISOString().slice(0, 19).replace('T', ' ');
221+ ```
222+- **Array slicing**: ClickHouse does NOT support `array[1:5]` syntax. Use `arraySlice(array, offset, length)`:
223+ ```sql
224+ arraySlice(groupArray(DISTINCT UserId), 1, 5) as sample_accounts
225+ ```
226+- **Error handling**: When running multiple independent queries, use `Promise.allSettled()` instead of `Promise.all()` so one failure doesn't crash the rest. Check each result's `.status` field.
227+"""
228+229+230def build_system_prompt():
231 """
232 Here we put together the base system prompt for the agent. The system prompt does _not_ change based on inputs, so that proper caching across sessions can take place.
···245# Osprey Documentation
246247{OSPREY_RULE_GUIDANCE}
248+249+{CLICKHOUSE_SQL_TIPS}
250 """
251252 return system_prompt
+20
src/tools/deno/tools.ts
···9 query: (sql: string): Promise<unknown> => callTool("clickhouse.query", { sql }),
10};
110000012export const domain = {
13 /** Lookup A, AAAA, NS, MX, TXT, CNAME, and SOA for a given input domain */
14 checkDomain: (domain: string): Promise<unknown> => callTool("domain.checkDomain", { domain }),
0000015};
1617export const osprey = {
···41 /** Remove a moderation label from a subject (account or record) */
42 removeLabel: (subject: string, label: string): Promise<unknown> => callTool("ozone.removeLabel", { subject, label }),
43};
0000000000
···9 query: (sql: string): Promise<unknown> => callTool("clickhouse.query", { sql }),
10};
1112+export const content = {
13+ /** Find similar posts in the network using ClickHouse's ngramDistance function. Useful for detecting coordinated spam, copypasta, or templated abuse content. Returns posts ordered by similarity score. */
14+ similarity: (text: string, threshold?: number, limit?: number): Promise<unknown> => callTool("content.similarity", { text, threshold, limit }),
15+};
16+17export const domain = {
18 /** Lookup A, AAAA, NS, MX, TXT, CNAME, and SOA for a given input domain */
19 checkDomain: (domain: string): Promise<unknown> => callTool("domain.checkDomain", { domain }),
20+};
21+22+export const ip = {
23+ /** GeoIP and ASN lookup for an IP address. Returns geographic location (country, region, city, coordinates, timezone), network information (ISP, org, ASN), and flags for mobile, proxy, and hosting IPs. */
24+ lookup: (ip: string): Promise<unknown> => callTool("ip.lookup", { ip }),
25};
2627export const osprey = {
···51 /** Remove a moderation label from a subject (account or record) */
52 removeLabel: (subject: string, label: string): Promise<unknown> => callTool("ozone.removeLabel", { subject, label }),
53};
54+55+export const url = {
56+ /** Follow a URL through its redirect chain (up to 10 hops), recording each hop's URL and HTTP status code. Flags known URL shorteners. Useful for investigating obfuscated or shortened links in spam/phishing content. */
57+ expand: (url: string): Promise<unknown> => callTool("url.expand", { url }),
58+};
59+60+export const whois = {
61+ /** Look up WHOIS registration data for a domain. Returns registrar, creation/expiration dates, name servers, registrant info, and domain age in days. Domain age is a key T&S signal — newly registered domains are heavily used for spam and phishing. */
62+ lookup: (domain: string): Promise<unknown> => callTool("whois.lookup", { domain }),
63+};
-16
src/tools/executor.py
···302{self._database_schema}
303304Use these exact column names when writing SQL queries. Do NOT guess column names.
305-306-## ClickHouse SQL Tips
307-308-- **DateTime filtering**: The `__timestamp` column is `DateTime64(3)`. Do NOT use raw ISO strings. Use `parseDateTimeBestEffort()`:
309- ```sql
310- WHERE __timestamp >= parseDateTimeBestEffort('2026-02-06 04:30:00')
311- ```
312- To compute a relative time in TypeScript, format it as `YYYY-MM-DD HH:MM:SS`:
313- ```typescript
314- const ts = new Date(Date.now() - 30 * 60 * 1000).toISOString().slice(0, 19).replace('T', ' ');
315- ```
316-- **Array slicing**: ClickHouse does NOT support `array[1:5]` syntax. Use `arraySlice(array, offset, length)`:
317- ```sql
318- arraySlice(groupArray(DISTINCT UserId), 1, 5) as sample_accounts
319- ```
320-- **Error handling**: When running multiple independent queries, use `Promise.allSettled()` instead of `Promise.all()` so one failure doesn't crash the rest. Check each result's `.status` field.
321"""
322323 osprey_section = ""
···302{self._database_schema}
303304Use these exact column names when writing SQL queries. Do NOT guess column names.
0000000000000000305"""
306307 osprey_section = ""