Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/plot-assistant-web-search.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@plotday/twister": minor
---

Added: `note.handler` Plot option for a single conversational mention handler (bypasses intent matching), `AIRequest.webSearch` for provider-native web search, `AIRequest.maxSteps` for agentic multi-step tool use, and `AICapabilities.webSearch`. `AITool.parameters` is now an optional deprecated alias for `inputSchema`. `AI.available()` return type widened to `AICapabilities | Promise<AICapabilities>` to reflect that it resolves asynchronously over RPC (await it).
56 changes: 46 additions & 10 deletions twister/src/tools/ai.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -69,8 +69,11 @@ export abstract class AI extends ITool {
* Returns which AI capabilities are currently available.
* Check this before calling prompt() or embed() to gracefully
* handle cases where AI is disabled by the user.
*
* Built-in tools are accessed as RPC stubs, so from a twist this call
* resolves asynchronously — always `await` it.
*/
abstract available(): AICapabilities;
abstract available(): AICapabilities | Promise<AICapabilities>;

/**
* Sends a request to an AI model and returns the response using the Vercel AI SDK.
Expand DownExpand Up@@ -134,7 +137,7 @@ export abstract class AI extends ITool {
* tools: {
* getWeather: {
* description: "Get weather for a city",
* parameters: Type.Object({
* inputSchema: Type.Object({
* city: Type.String()
* }),
* execute: async ({ city }) => {
Expand DownExpand Up@@ -165,6 +168,12 @@ export type AICapabilities = {
prompt: boolean;
/** Whether AI embedding generation is available. */
embed: boolean;
/**
* Whether provider-native web search is available. True for Plot AI and
* Anthropic/Google BYOK providers; false for OpenAI/custom BYOK providers
* that don't expose a server-side web search tool through this runtime.
*/
webSearch: boolean;
};

/**
Expand DownExpand Up@@ -345,6 +354,31 @@ export interface AIRequest<
* Controls diversity by limiting to top probability tokens.
*/
topP?: number;

/**
* Enable provider-native web search so the model can retrieve
* up-to-date information from the web. The search is executed
* server-side by the provider and any pages used are returned in
* {@link AIResponse.sources}.
*
* Only available on web-search-capable providers (Anthropic and
* Google). Check {@link AICapabilities.webSearch} via `available()`
* before relying on it; on unsupported providers the flag is ignored.
*
* Pass `true` for defaults, or an object to cap the number of searches.
*/
webSearch?: boolean | { maxUses?: number };

/**
* Maximum number of sequential generation steps for agentic tool use.
* When the model calls a tool, its result is fed back and the model is
* called again, up to `maxSteps` times, until it produces a final answer.
*
* Defaults to 1 (single step — tool calls are returned but not looped),
* preserving prior behavior. Set higher (e.g. 6) to let the model chain
* tool calls into a final answer.
*/
maxSteps?: number;
}

/**
Expand DownExpand Up@@ -742,17 +776,19 @@ export interface ToolExecutionOptions {
*/
export type AITool<PARAMETERS extends ToolParameters = any, RESULT = any> = {
/**
* The schema of the input that the tool expects. The language model will use this to generate the input.
* It is also used to validate the output of the language model.
* Use descriptions to make the input understandable for the language model.
* The schema of the input that the tool expects, expressed as a Typebox
* schema. The language model uses this to generate (and the runtime to
* validate) the tool input. Use field descriptions to make the input
* understandable for the model.
*
* This is the canonical field read by the runtime. `parameters` is an
* accepted alias for backwards compatibility.
*/
parameters: PARAMETERS;
inputSchema: TSchema;
/**
* The schema of the input that the tool expects. The language model will use this to generate the input.
* It is also used to validate the output of the language model.
* Use descriptions to make the input understandable for the language model.
* @deprecated Alias for {@link inputSchema}. Prefer `inputSchema`.
*/
inputSchema: TSchema;
parameters?: PARAMETERS;
/**
* An optional description of what the tool does.
* Will be used by the language model to decide whether to use the tool.
Expand Down
21 changes: 21 additions & 0 deletions twister/src/tools/plot.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -247,6 +247,27 @@ export abstract class Plot extends ITool {
* ```
*/
intents?: NoteIntentHandler[];
/**
* Single conversational handler for mentions.
*
* When set, EVERY mention of this twist is routed directly to this
* handler — intent matching (and the built-in "What can you do?" /
* "Remove yourself" intents) is skipped. Use this to build a
* general-purpose conversational assistant that responds to any
* request, rather than classifying into a fixed set of `intents`.
*
* `handler` and `intents` are mutually exclusive; when both are
* present, `handler` takes precedence and `intents` is ignored.
*
* @example
* ```typescript
* note: {
* defaultMention: true,
* handler: this.respond, // (note: Note) => Promise<void>
* }
* ```
*/
handler?: (note: Note) => Promise<void>;
};
/** Enable link processing from connected source channels. */
link?: true | {
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/plot-assistant-web-search.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@plotday/twister": minor
---

Added: `note.handler` Plot option for a single conversational mention handler (bypasses intent matching), `AIRequest.webSearch` for provider-native web search, `AIRequest.maxSteps` for agentic multi-step tool use, and `AICapabilities.webSearch`. `AITool.parameters` is now an optional deprecated alias for `inputSchema`. `AI.available()` return type widened to `AICapabilities | Promise<AICapabilities>` to reflect that it resolves asynchronously over RPC (await it).
56 changes: 46 additions & 10 deletions twister/src/tools/ai.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -69,8 +69,11 @@ export abstract class AI extends ITool {
* Returns which AI capabilities are currently available.
* Check this before calling prompt() or embed() to gracefully
* handle cases where AI is disabled by the user.
*
* Built-in tools are accessed as RPC stubs, so from a twist this call
* resolves asynchronously — always `await` it.
*/
abstract available(): AICapabilities;
abstract available(): AICapabilities | Promise<AICapabilities>;

/**
* Sends a request to an AI model and returns the response using the Vercel AI SDK.
Expand DownExpand Up@@ -134,7 +137,7 @@ export abstract class AI extends ITool {
* tools: {
* getWeather: {
* description: "Get weather for a city",
* parameters: Type.Object({
* inputSchema: Type.Object({
* city: Type.String()
* }),
* execute: async ({ city }) => {
Expand DownExpand Up@@ -165,6 +168,12 @@ export type AICapabilities = {
prompt: boolean;
/** Whether AI embedding generation is available. */
embed: boolean;
/**
* Whether provider-native web search is available. True for Plot AI and
* Anthropic/Google BYOK providers; false for OpenAI/custom BYOK providers
* that don't expose a server-side web search tool through this runtime.
*/
webSearch: boolean;
};

/**
Expand DownExpand Up@@ -345,6 +354,31 @@ export interface AIRequest<
* Controls diversity by limiting to top probability tokens.
*/
topP?: number;

/**
* Enable provider-native web search so the model can retrieve
* up-to-date information from the web. The search is executed
* server-side by the provider and any pages used are returned in
* {@link AIResponse.sources}.
*
* Only available on web-search-capable providers (Anthropic and
* Google). Check {@link AICapabilities.webSearch} via `available()`
* before relying on it; on unsupported providers the flag is ignored.
*
* Pass `true` for defaults, or an object to cap the number of searches.
*/
webSearch?: boolean | { maxUses?: number };

/**
* Maximum number of sequential generation steps for agentic tool use.
* When the model calls a tool, its result is fed back and the model is
* called again, up to `maxSteps` times, until it produces a final answer.
*
* Defaults to 1 (single step — tool calls are returned but not looped),
* preserving prior behavior. Set higher (e.g. 6) to let the model chain
* tool calls into a final answer.
*/
maxSteps?: number;
}

/**
Expand DownExpand Up@@ -742,17 +776,19 @@ export interface ToolExecutionOptions {
*/
export type AITool<PARAMETERS extends ToolParameters = any, RESULT = any> = {
/**
* The schema of the input that the tool expects. The language model will use this to generate the input.
* It is also used to validate the output of the language model.
* Use descriptions to make the input understandable for the language model.
* The schema of the input that the tool expects, expressed as a Typebox
* schema. The language model uses this to generate (and the runtime to
* validate) the tool input. Use field descriptions to make the input
* understandable for the model.
*
* This is the canonical field read by the runtime. `parameters` is an
* accepted alias for backwards compatibility.
*/
parameters: PARAMETERS;
inputSchema: TSchema;
/**
* The schema of the input that the tool expects. The language model will use this to generate the input.
* It is also used to validate the output of the language model.
* Use descriptions to make the input understandable for the language model.
* @deprecated Alias for {@link inputSchema}. Prefer `inputSchema`.
*/
inputSchema: TSchema;
parameters?: PARAMETERS;
/**
* An optional description of what the tool does.
* Will be used by the language model to decide whether to use the tool.
Expand Down
21 changes: 21 additions & 0 deletions twister/src/tools/plot.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -247,6 +247,27 @@ export abstract class Plot extends ITool {
* ```
*/
intents?: NoteIntentHandler[];
/**
* Single conversational handler for mentions.
*
* When set, EVERY mention of this twist is routed directly to this
* handler — intent matching (and the built-in "What can you do?" /
* "Remove yourself" intents) is skipped. Use this to build a
* general-purpose conversational assistant that responds to any
* request, rather than classifying into a fixed set of `intents`.
*
* `handler` and `intents` are mutually exclusive; when both are
* present, `handler` takes precedence and `intents` is ignored.
*
* @example
* ```typescript
* note: {
* defaultMention: true,
* handler: this.respond, // (note: Note) => Promise<void>
* }
* ```
*/
handler?: (note: Note) => Promise<void>;
};
/** Enable link processing from connected source channels. */
link?: true | {
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/plot-assistant-web-search.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@plotday/twister": minor
---

Added: `note.handler` Plot option for a single conversational mention handler (bypasses intent matching), `AIRequest.webSearch` for provider-native web search, `AIRequest.maxSteps` for agentic multi-step tool use, and `AICapabilities.webSearch`. `AITool.parameters` is now an optional deprecated alias for `inputSchema`. `AI.available()` return type widened to `AICapabilities | Promise<AICapabilities>` to reflect that it resolves asynchronously over RPC (await it).
56 changes: 46 additions & 10 deletions twister/src/tools/ai.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -69,8 +69,11 @@ export abstract class AI extends ITool {
* Returns which AI capabilities are currently available.
* Check this before calling prompt() or embed() to gracefully
* handle cases where AI is disabled by the user.
*
* Built-in tools are accessed as RPC stubs, so from a twist this call
* resolves asynchronously — always `await` it.
*/
abstract available(): AICapabilities;
abstract available(): AICapabilities | Promise<AICapabilities>;

/**
* Sends a request to an AI model and returns the response using the Vercel AI SDK.
Expand DownExpand Up@@ -134,7 +137,7 @@ export abstract class AI extends ITool {
* tools: {
* getWeather: {
* description: "Get weather for a city",
* parameters: Type.Object({
* inputSchema: Type.Object({
* city: Type.String()
* }),
* execute: async ({ city }) => {
Expand DownExpand Up@@ -165,6 +168,12 @@ export type AICapabilities = {
prompt: boolean;
/** Whether AI embedding generation is available. */
embed: boolean;
/**
* Whether provider-native web search is available. True for Plot AI and
* Anthropic/Google BYOK providers; false for OpenAI/custom BYOK providers
* that don't expose a server-side web search tool through this runtime.
*/
webSearch: boolean;
};

/**
Expand DownExpand Up@@ -345,6 +354,31 @@ export interface AIRequest<
* Controls diversity by limiting to top probability tokens.
*/
topP?: number;

/**
* Enable provider-native web search so the model can retrieve
* up-to-date information from the web. The search is executed
* server-side by the provider and any pages used are returned in
* {@link AIResponse.sources}.
*
* Only available on web-search-capable providers (Anthropic and
* Google). Check {@link AICapabilities.webSearch} via `available()`
* before relying on it; on unsupported providers the flag is ignored.
*
* Pass `true` for defaults, or an object to cap the number of searches.
*/
webSearch?: boolean | { maxUses?: number };

/**
* Maximum number of sequential generation steps for agentic tool use.
* When the model calls a tool, its result is fed back and the model is
* called again, up to `maxSteps` times, until it produces a final answer.
*
* Defaults to 1 (single step — tool calls are returned but not looped),
* preserving prior behavior. Set higher (e.g. 6) to let the model chain
* tool calls into a final answer.
*/
maxSteps?: number;
}

/**
Expand DownExpand Up@@ -742,17 +776,19 @@ export interface ToolExecutionOptions {
*/
export type AITool<PARAMETERS extends ToolParameters = any, RESULT = any> = {
/**
* The schema of the input that the tool expects. The language model will use this to generate the input.
* It is also used to validate the output of the language model.
* Use descriptions to make the input understandable for the language model.
* The schema of the input that the tool expects, expressed as a Typebox
* schema. The language model uses this to generate (and the runtime to
* validate) the tool input. Use field descriptions to make the input
* understandable for the model.
*
* This is the canonical field read by the runtime. `parameters` is an
* accepted alias for backwards compatibility.
*/
parameters: PARAMETERS;
inputSchema: TSchema;
/**
* The schema of the input that the tool expects. The language model will use this to generate the input.
* It is also used to validate the output of the language model.
* Use descriptions to make the input understandable for the language model.
* @deprecated Alias for {@link inputSchema}. Prefer `inputSchema`.
*/
inputSchema: TSchema;
parameters?: PARAMETERS;
/**
* An optional description of what the tool does.
* Will be used by the language model to decide whether to use the tool.
Expand Down
21 changes: 21 additions & 0 deletions twister/src/tools/plot.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -247,6 +247,27 @@ export abstract class Plot extends ITool {
* ```
*/
intents?: NoteIntentHandler[];
/**
* Single conversational handler for mentions.
*
* When set, EVERY mention of this twist is routed directly to this
* handler — intent matching (and the built-in "What can you do?" /
* "Remove yourself" intents) is skipped. Use this to build a
* general-purpose conversational assistant that responds to any
* request, rather than classifying into a fixed set of `intents`.
*
* `handler` and `intents` are mutually exclusive; when both are
* present, `handler` takes precedence and `intents` is ignored.
*
* @example
* ```typescript
* note: {
* defaultMention: true,
* handler: this.respond, // (note: Note) => Promise<void>
* }
* ```
*/
handler?: (note: Note) => Promise<void>;
};
/** Enable link processing from connected source channels. */
link?: true | {
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/plot-assistant-web-search.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@plotday/twister": minor
---

Added: `note.handler` Plot option for a single conversational mention handler (bypasses intent matching), `AIRequest.webSearch` for provider-native web search, `AIRequest.maxSteps` for agentic multi-step tool use, and `AICapabilities.webSearch`. `AITool.parameters` is now an optional deprecated alias for `inputSchema`. `AI.available()` return type widened to `AICapabilities | Promise<AICapabilities>` to reflect that it resolves asynchronously over RPC (await it).
56 changes: 46 additions & 10 deletions twister/src/tools/ai.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -69,8 +69,11 @@ export abstract class AI extends ITool {
* Returns which AI capabilities are currently available.
* Check this before calling prompt() or embed() to gracefully
* handle cases where AI is disabled by the user.
*
* Built-in tools are accessed as RPC stubs, so from a twist this call
* resolves asynchronously — always `await` it.
*/
abstract available(): AICapabilities;
abstract available(): AICapabilities | Promise<AICapabilities>;

/**
* Sends a request to an AI model and returns the response using the Vercel AI SDK.
Expand DownExpand Up@@ -134,7 +137,7 @@ export abstract class AI extends ITool {
* tools: {
* getWeather: {
* description: "Get weather for a city",
* parameters: Type.Object({
* inputSchema: Type.Object({
* city: Type.String()
* }),
* execute: async ({ city }) => {
Expand DownExpand Up@@ -165,6 +168,12 @@ export type AICapabilities = {
prompt: boolean;
/** Whether AI embedding generation is available. */
embed: boolean;
/**
* Whether provider-native web search is available. True for Plot AI and
* Anthropic/Google BYOK providers; false for OpenAI/custom BYOK providers
* that don't expose a server-side web search tool through this runtime.
*/
webSearch: boolean;
};

/**
Expand DownExpand Up@@ -345,6 +354,31 @@ export interface AIRequest<
* Controls diversity by limiting to top probability tokens.
*/
topP?: number;

/**
* Enable provider-native web search so the model can retrieve
* up-to-date information from the web. The search is executed
* server-side by the provider and any pages used are returned in
* {@link AIResponse.sources}.
*
* Only available on web-search-capable providers (Anthropic and
* Google). Check {@link AICapabilities.webSearch} via `available()`
* before relying on it; on unsupported providers the flag is ignored.
*
* Pass `true` for defaults, or an object to cap the number of searches.
*/
webSearch?: boolean | { maxUses?: number };

/**
* Maximum number of sequential generation steps for agentic tool use.
* When the model calls a tool, its result is fed back and the model is
* called again, up to `maxSteps` times, until it produces a final answer.
*
* Defaults to 1 (single step — tool calls are returned but not looped),
* preserving prior behavior. Set higher (e.g. 6) to let the model chain
* tool calls into a final answer.
*/
maxSteps?: number;
}

/**
Expand DownExpand Up@@ -742,17 +776,19 @@ export interface ToolExecutionOptions {
*/
export type AITool<PARAMETERS extends ToolParameters = any, RESULT = any> = {
/**
* The schema of the input that the tool expects. The language model will use this to generate the input.
* It is also used to validate the output of the language model.
* Use descriptions to make the input understandable for the language model.
* The schema of the input that the tool expects, expressed as a Typebox
* schema. The language model uses this to generate (and the runtime to
* validate) the tool input. Use field descriptions to make the input
* understandable for the model.
*
* This is the canonical field read by the runtime. `parameters` is an
* accepted alias for backwards compatibility.
*/
parameters: PARAMETERS;
inputSchema: TSchema;
/**
* The schema of the input that the tool expects. The language model will use this to generate the input.
* It is also used to validate the output of the language model.
* Use descriptions to make the input understandable for the language model.
* @deprecated Alias for {@link inputSchema}. Prefer `inputSchema`.
*/
inputSchema: TSchema;
parameters?: PARAMETERS;
/**
* An optional description of what the tool does.
* Will be used by the language model to decide whether to use the tool.
Expand Down
21 changes: 21 additions & 0 deletions twister/src/tools/plot.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -247,6 +247,27 @@ export abstract class Plot extends ITool {
* ```
*/
intents?: NoteIntentHandler[];
/**
* Single conversational handler for mentions.
*
* When set, EVERY mention of this twist is routed directly to this
* handler — intent matching (and the built-in "What can you do?" /
* "Remove yourself" intents) is skipped. Use this to build a
* general-purpose conversational assistant that responds to any
* request, rather than classifying into a fixed set of `intents`.
*
* `handler` and `intents` are mutually exclusive; when both are
* present, `handler` takes precedence and `intents` is ignored.
*
* @example
* ```typescript
* note: {
* defaultMention: true,
* handler: this.respond, // (note: Note) => Promise<void>
* }
* ```
*/
handler?: (note: Note) => Promise<void>;
};
/** Enable link processing from connected source channels. */
link?: true | {
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/plot-assistant-web-search.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@plotday/twister": minor
---

Added: `note.handler` Plot option for a single conversational mention handler (bypasses intent matching), `AIRequest.webSearch` for provider-native web search, `AIRequest.maxSteps` for agentic multi-step tool use, and `AICapabilities.webSearch`. `AITool.parameters` is now an optional deprecated alias for `inputSchema`. `AI.available()` return type widened to `AICapabilities | Promise<AICapabilities>` to reflect that it resolves asynchronously over RPC (await it).
56 changes: 46 additions & 10 deletions twister/src/tools/ai.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -69,8 +69,11 @@ export abstract class AI extends ITool {
* Returns which AI capabilities are currently available.
* Check this before calling prompt() or embed() to gracefully
* handle cases where AI is disabled by the user.
*
* Built-in tools are accessed as RPC stubs, so from a twist this call
* resolves asynchronously — always `await` it.
*/
abstract available(): AICapabilities;
abstract available(): AICapabilities | Promise<AICapabilities>;

/**
* Sends a request to an AI model and returns the response using the Vercel AI SDK.
Expand DownExpand Up@@ -134,7 +137,7 @@ export abstract class AI extends ITool {
* tools: {
* getWeather: {
* description: "Get weather for a city",
* parameters: Type.Object({
* inputSchema: Type.Object({
* city: Type.String()
* }),
* execute: async ({ city }) => {
Expand DownExpand Up@@ -165,6 +168,12 @@ export type AICapabilities = {
prompt: boolean;
/** Whether AI embedding generation is available. */
embed: boolean;
/**
* Whether provider-native web search is available. True for Plot AI and
* Anthropic/Google BYOK providers; false for OpenAI/custom BYOK providers
* that don't expose a server-side web search tool through this runtime.
*/
webSearch: boolean;
};

/**
Expand DownExpand Up@@ -345,6 +354,31 @@ export interface AIRequest<
* Controls diversity by limiting to top probability tokens.
*/
topP?: number;

/**
* Enable provider-native web search so the model can retrieve
* up-to-date information from the web. The search is executed
* server-side by the provider and any pages used are returned in
* {@link AIResponse.sources}.
*
* Only available on web-search-capable providers (Anthropic and
* Google). Check {@link AICapabilities.webSearch} via `available()`
* before relying on it; on unsupported providers the flag is ignored.
*
* Pass `true` for defaults, or an object to cap the number of searches.
*/
webSearch?: boolean | { maxUses?: number };

/**
* Maximum number of sequential generation steps for agentic tool use.
* When the model calls a tool, its result is fed back and the model is
* called again, up to `maxSteps` times, until it produces a final answer.
*
* Defaults to 1 (single step — tool calls are returned but not looped),
* preserving prior behavior. Set higher (e.g. 6) to let the model chain
* tool calls into a final answer.
*/
maxSteps?: number;
}

/**
Expand DownExpand Up@@ -742,17 +776,19 @@ export interface ToolExecutionOptions {
*/
export type AITool<PARAMETERS extends ToolParameters = any, RESULT = any> = {
/**
* The schema of the input that the tool expects. The language model will use this to generate the input.
* It is also used to validate the output of the language model.
* Use descriptions to make the input understandable for the language model.
* The schema of the input that the tool expects, expressed as a Typebox
* schema. The language model uses this to generate (and the runtime to
* validate) the tool input. Use field descriptions to make the input
* understandable for the model.
*
* This is the canonical field read by the runtime. `parameters` is an
* accepted alias for backwards compatibility.
*/
parameters: PARAMETERS;
inputSchema: TSchema;
/**
* The schema of the input that the tool expects. The language model will use this to generate the input.
* It is also used to validate the output of the language model.
* Use descriptions to make the input understandable for the language model.
* @deprecated Alias for {@link inputSchema}. Prefer `inputSchema`.
*/
inputSchema: TSchema;
parameters?: PARAMETERS;
/**
* An optional description of what the tool does.
* Will be used by the language model to decide whether to use the tool.
Expand Down
21 changes: 21 additions & 0 deletions twister/src/tools/plot.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -247,6 +247,27 @@ export abstract class Plot extends ITool {
* ```
*/
intents?: NoteIntentHandler[];
/**
* Single conversational handler for mentions.
*
* When set, EVERY mention of this twist is routed directly to this
* handler — intent matching (and the built-in "What can you do?" /
* "Remove yourself" intents) is skipped. Use this to build a
* general-purpose conversational assistant that responds to any
* request, rather than classifying into a fixed set of `intents`.
*
* `handler` and `intents` are mutually exclusive; when both are
* present, `handler` takes precedence and `intents` is ignored.
*
* @example
* ```typescript
* note: {
* defaultMention: true,
* handler: this.respond, // (note: Note) => Promise<void>
* }
* ```
*/
handler?: (note: Note) => Promise<void>;
};
/** Enable link processing from connected source channels. */
link?: true | {
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/plot-assistant-web-search.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@plotday/twister": minor
---

Added: `note.handler` Plot option for a single conversational mention handler (bypasses intent matching), `AIRequest.webSearch` for provider-native web search, `AIRequest.maxSteps` for agentic multi-step tool use, and `AICapabilities.webSearch`. `AITool.parameters` is now an optional deprecated alias for `inputSchema`. `AI.available()` return type widened to `AICapabilities | Promise<AICapabilities>` to reflect that it resolves asynchronously over RPC (await it).
56 changes: 46 additions & 10 deletions twister/src/tools/ai.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -69,8 +69,11 @@ export abstract class AI extends ITool {
* Returns which AI capabilities are currently available.
* Check this before calling prompt() or embed() to gracefully
* handle cases where AI is disabled by the user.
*
* Built-in tools are accessed as RPC stubs, so from a twist this call
* resolves asynchronously — always `await` it.
*/
abstract available(): AICapabilities;
abstract available(): AICapabilities | Promise<AICapabilities>;

/**
* Sends a request to an AI model and returns the response using the Vercel AI SDK.
Expand DownExpand Up@@ -134,7 +137,7 @@ export abstract class AI extends ITool {
* tools: {
* getWeather: {
* description: "Get weather for a city",
* parameters: Type.Object({
* inputSchema: Type.Object({
* city: Type.String()
* }),
* execute: async ({ city }) => {
Expand DownExpand Up@@ -165,6 +168,12 @@ export type AICapabilities = {
prompt: boolean;
/** Whether AI embedding generation is available. */
embed: boolean;
/**
* Whether provider-native web search is available. True for Plot AI and
* Anthropic/Google BYOK providers; false for OpenAI/custom BYOK providers
* that don't expose a server-side web search tool through this runtime.
*/
webSearch: boolean;
};

/**
Expand DownExpand Up@@ -345,6 +354,31 @@ export interface AIRequest<
* Controls diversity by limiting to top probability tokens.
*/
topP?: number;

/**
* Enable provider-native web search so the model can retrieve
* up-to-date information from the web. The search is executed
* server-side by the provider and any pages used are returned in
* {@link AIResponse.sources}.
*
* Only available on web-search-capable providers (Anthropic and
* Google). Check {@link AICapabilities.webSearch} via `available()`
* before relying on it; on unsupported providers the flag is ignored.
*
* Pass `true` for defaults, or an object to cap the number of searches.
*/
webSearch?: boolean | { maxUses?: number };

/**
* Maximum number of sequential generation steps for agentic tool use.
* When the model calls a tool, its result is fed back and the model is
* called again, up to `maxSteps` times, until it produces a final answer.
*
* Defaults to 1 (single step — tool calls are returned but not looped),
* preserving prior behavior. Set higher (e.g. 6) to let the model chain
* tool calls into a final answer.
*/
maxSteps?: number;
}

/**
Expand DownExpand Up@@ -742,17 +776,19 @@ export interface ToolExecutionOptions {
*/
export type AITool<PARAMETERS extends ToolParameters = any, RESULT = any> = {
/**
* The schema of the input that the tool expects. The language model will use this to generate the input.
* It is also used to validate the output of the language model.
* Use descriptions to make the input understandable for the language model.
* The schema of the input that the tool expects, expressed as a Typebox
* schema. The language model uses this to generate (and the runtime to
* validate) the tool input. Use field descriptions to make the input
* understandable for the model.
*
* This is the canonical field read by the runtime. `parameters` is an
* accepted alias for backwards compatibility.
*/
parameters: PARAMETERS;
inputSchema: TSchema;
/**
* The schema of the input that the tool expects. The language model will use this to generate the input.
* It is also used to validate the output of the language model.
* Use descriptions to make the input understandable for the language model.
* @deprecated Alias for {@link inputSchema}. Prefer `inputSchema`.
*/
inputSchema: TSchema;
parameters?: PARAMETERS;
/**
* An optional description of what the tool does.
* Will be used by the language model to decide whether to use the tool.
Expand Down
21 changes: 21 additions & 0 deletions twister/src/tools/plot.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -247,6 +247,27 @@ export abstract class Plot extends ITool {
* ```
*/
intents?: NoteIntentHandler[];
/**
* Single conversational handler for mentions.
*
* When set, EVERY mention of this twist is routed directly to this
* handler — intent matching (and the built-in "What can you do?" /
* "Remove yourself" intents) is skipped. Use this to build a
* general-purpose conversational assistant that responds to any
* request, rather than classifying into a fixed set of `intents`.
*
* `handler` and `intents` are mutually exclusive; when both are
* present, `handler` takes precedence and `intents` is ignored.
*
* @example
* ```typescript
* note: {
* defaultMention: true,
* handler: this.respond, // (note: Note) => Promise<void>
* }
* ```
*/
handler?: (note: Note) => Promise<void>;
};
/** Enable link processing from connected source channels. */
link?: true | {
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/plot-assistant-web-search.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@plotday/twister": minor
---

Added: `note.handler` Plot option for a single conversational mention handler (bypasses intent matching), `AIRequest.webSearch` for provider-native web search, `AIRequest.maxSteps` for agentic multi-step tool use, and `AICapabilities.webSearch`. `AITool.parameters` is now an optional deprecated alias for `inputSchema`. `AI.available()` return type widened to `AICapabilities | Promise<AICapabilities>` to reflect that it resolves asynchronously over RPC (await it).
56 changes: 46 additions & 10 deletions twister/src/tools/ai.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -69,8 +69,11 @@ export abstract class AI extends ITool {
* Returns which AI capabilities are currently available.
* Check this before calling prompt() or embed() to gracefully
* handle cases where AI is disabled by the user.
*
* Built-in tools are accessed as RPC stubs, so from a twist this call
* resolves asynchronously — always `await` it.
*/
abstract available(): AICapabilities;
abstract available(): AICapabilities | Promise<AICapabilities>;

/**
* Sends a request to an AI model and returns the response using the Vercel AI SDK.
Expand DownExpand Up@@ -134,7 +137,7 @@ export abstract class AI extends ITool {
* tools: {
* getWeather: {
* description: "Get weather for a city",
* parameters: Type.Object({
* inputSchema: Type.Object({
* city: Type.String()
* }),
* execute: async ({ city }) => {
Expand DownExpand Up@@ -165,6 +168,12 @@ export type AICapabilities = {
prompt: boolean;
/** Whether AI embedding generation is available. */
embed: boolean;
/**
* Whether provider-native web search is available. True for Plot AI and
* Anthropic/Google BYOK providers; false for OpenAI/custom BYOK providers
* that don't expose a server-side web search tool through this runtime.
*/
webSearch: boolean;
};

/**
Expand DownExpand Up@@ -345,6 +354,31 @@ export interface AIRequest<
* Controls diversity by limiting to top probability tokens.
*/
topP?: number;

/**
* Enable provider-native web search so the model can retrieve
* up-to-date information from the web. The search is executed
* server-side by the provider and any pages used are returned in
* {@link AIResponse.sources}.
*
* Only available on web-search-capable providers (Anthropic and
* Google). Check {@link AICapabilities.webSearch} via `available()`
* before relying on it; on unsupported providers the flag is ignored.
*
* Pass `true` for defaults, or an object to cap the number of searches.
*/
webSearch?: boolean | { maxUses?: number };

/**
* Maximum number of sequential generation steps for agentic tool use.
* When the model calls a tool, its result is fed back and the model is
* called again, up to `maxSteps` times, until it produces a final answer.
*
* Defaults to 1 (single step — tool calls are returned but not looped),
* preserving prior behavior. Set higher (e.g. 6) to let the model chain
* tool calls into a final answer.
*/
maxSteps?: number;
}

/**
Expand DownExpand Up@@ -742,17 +776,19 @@ export interface ToolExecutionOptions {
*/
export type AITool<PARAMETERS extends ToolParameters = any, RESULT = any> = {
/**
* The schema of the input that the tool expects. The language model will use this to generate the input.
* It is also used to validate the output of the language model.
* Use descriptions to make the input understandable for the language model.
* The schema of the input that the tool expects, expressed as a Typebox
* schema. The language model uses this to generate (and the runtime to
* validate) the tool input. Use field descriptions to make the input
* understandable for the model.
*
* This is the canonical field read by the runtime. `parameters` is an
* accepted alias for backwards compatibility.
*/
parameters: PARAMETERS;
inputSchema: TSchema;
/**
* The schema of the input that the tool expects. The language model will use this to generate the input.
* It is also used to validate the output of the language model.
* Use descriptions to make the input understandable for the language model.
* @deprecated Alias for {@link inputSchema}. Prefer `inputSchema`.
*/
inputSchema: TSchema;
parameters?: PARAMETERS;
/**
* An optional description of what the tool does.
* Will be used by the language model to decide whether to use the tool.
Expand Down
21 changes: 21 additions & 0 deletions twister/src/tools/plot.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -247,6 +247,27 @@ export abstract class Plot extends ITool {
* ```
*/
intents?: NoteIntentHandler[];
/**
* Single conversational handler for mentions.
*
* When set, EVERY mention of this twist is routed directly to this
* handler — intent matching (and the built-in "What can you do?" /
* "Remove yourself" intents) is skipped. Use this to build a
* general-purpose conversational assistant that responds to any
* request, rather than classifying into a fixed set of `intents`.
*
* `handler` and `intents` are mutually exclusive; when both are
* present, `handler` takes precedence and `intents` is ignored.
*
* @example
* ```typescript
* note: {
* defaultMention: true,
* handler: this.respond, // (note: Note) => Promise<void>
* }
* ```
*/
handler?: (note: Note) => Promise<void>;
};
/** Enable link processing from connected source channels. */
link?: true | {
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/plot-assistant-web-search.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@plotday/twister": minor
---

Added: `note.handler` Plot option for a single conversational mention handler (bypasses intent matching), `AIRequest.webSearch` for provider-native web search, `AIRequest.maxSteps` for agentic multi-step tool use, and `AICapabilities.webSearch`. `AITool.parameters` is now an optional deprecated alias for `inputSchema`. `AI.available()` return type widened to `AICapabilities | Promise<AICapabilities>` to reflect that it resolves asynchronously over RPC (await it).
56 changes: 46 additions & 10 deletions twister/src/tools/ai.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -69,8 +69,11 @@ export abstract class AI extends ITool {
* Returns which AI capabilities are currently available.
* Check this before calling prompt() or embed() to gracefully
* handle cases where AI is disabled by the user.
*
* Built-in tools are accessed as RPC stubs, so from a twist this call
* resolves asynchronously — always `await` it.
*/
abstract available(): AICapabilities;
abstract available(): AICapabilities | Promise<AICapabilities>;

/**
* Sends a request to an AI model and returns the response using the Vercel AI SDK.
Expand DownExpand Up@@ -134,7 +137,7 @@ export abstract class AI extends ITool {
* tools: {
* getWeather: {
* description: "Get weather for a city",
* parameters: Type.Object({
* inputSchema: Type.Object({
* city: Type.String()
* }),
* execute: async ({ city }) => {
Expand DownExpand Up@@ -165,6 +168,12 @@ export type AICapabilities = {
prompt: boolean;
/** Whether AI embedding generation is available. */
embed: boolean;
/**
* Whether provider-native web search is available. True for Plot AI and
* Anthropic/Google BYOK providers; false for OpenAI/custom BYOK providers
* that don't expose a server-side web search tool through this runtime.
*/
webSearch: boolean;
};

/**
Expand DownExpand Up@@ -345,6 +354,31 @@ export interface AIRequest<
* Controls diversity by limiting to top probability tokens.
*/
topP?: number;

/**
* Enable provider-native web search so the model can retrieve
* up-to-date information from the web. The search is executed
* server-side by the provider and any pages used are returned in
* {@link AIResponse.sources}.
*
* Only available on web-search-capable providers (Anthropic and
* Google). Check {@link AICapabilities.webSearch} via `available()`
* before relying on it; on unsupported providers the flag is ignored.
*
* Pass `true` for defaults, or an object to cap the number of searches.
*/
webSearch?: boolean | { maxUses?: number };

/**
* Maximum number of sequential generation steps for agentic tool use.
* When the model calls a tool, its result is fed back and the model is
* called again, up to `maxSteps` times, until it produces a final answer.
*
* Defaults to 1 (single step — tool calls are returned but not looped),
* preserving prior behavior. Set higher (e.g. 6) to let the model chain
* tool calls into a final answer.
*/
maxSteps?: number;
}

/**
Expand DownExpand Up@@ -742,17 +776,19 @@ export interface ToolExecutionOptions {
*/
export type AITool<PARAMETERS extends ToolParameters = any, RESULT = any> = {
/**
* The schema of the input that the tool expects. The language model will use this to generate the input.
* It is also used to validate the output of the language model.
* Use descriptions to make the input understandable for the language model.
* The schema of the input that the tool expects, expressed as a Typebox
* schema. The language model uses this to generate (and the runtime to
* validate) the tool input. Use field descriptions to make the input
* understandable for the model.
*
* This is the canonical field read by the runtime. `parameters` is an
* accepted alias for backwards compatibility.
*/
parameters: PARAMETERS;
inputSchema: TSchema;
/**
* The schema of the input that the tool expects. The language model will use this to generate the input.
* It is also used to validate the output of the language model.
* Use descriptions to make the input understandable for the language model.
* @deprecated Alias for {@link inputSchema}. Prefer `inputSchema`.
*/
inputSchema: TSchema;
parameters?: PARAMETERS;
/**
* An optional description of what the tool does.
* Will be used by the language model to decide whether to use the tool.
Expand Down
21 changes: 21 additions & 0 deletions twister/src/tools/plot.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -247,6 +247,27 @@ export abstract class Plot extends ITool {
* ```
*/
intents?: NoteIntentHandler[];
/**
* Single conversational handler for mentions.
*
* When set, EVERY mention of this twist is routed directly to this
* handler — intent matching (and the built-in "What can you do?" /
* "Remove yourself" intents) is skipped. Use this to build a
* general-purpose conversational assistant that responds to any
* request, rather than classifying into a fixed set of `intents`.
*
* `handler` and `intents` are mutually exclusive; when both are
* present, `handler` takes precedence and `intents` is ignored.
*
* @example
* ```typescript
* note: {
* defaultMention: true,
* handler: this.respond, // (note: Note) => Promise<void>
* }
* ```
*/
handler?: (note: Note) => Promise<void>;
};
/** Enable link processing from connected source channels. */
link?: true | {
Expand Down
Loading