diff --git a/connectors/gemini/delegate.js b/connectors/gemini/delegate.js index 3665fce..8519079 100644 --- a/connectors/gemini/delegate.js +++ b/connectors/gemini/delegate.js @@ -24,10 +24,10 @@ import { geminiChat } from "./client.js"; import { githubRequest } from "../github/client.js"; import { readFileViaBlob } from "../github/helpers.js"; import { queryTelemetry } from "../cloudflare/observability.js"; -import { notionRequest, notionRichTextToString, notionPageTitle, notionDatabaseTitle } from "../notion/client.js"; +import { notionRequest, notionRichTextToString, notionPageTitle, notionDatabaseTitle, notionBlocksToText } from "../notion/client.js"; import { DEFAULT_OWNER } from "../../config.js"; -const HARD_MAX_STEPS = 10; +const HARD_MAX_STEPS = 20; // --------------------------------------------------------------------------- // Delegated function declarations -- Gemini's "tools" param (a subset of @@ -123,6 +123,29 @@ const FUNCTIONS = [ return JSON.stringify(data).slice(0, 30000); }, }, + { + name: "notion_get_page", + description: "Read a Notion page's title and text content by page ID (read-only). Use this after notion_search finds a candidate page, to actually see what's on it -- notion_search only returns titles/ids, not content.", + parameters: { + type: "object", + properties: { + page_id: { type: "string", description: "Notion page ID, e.g. from notion_search results" }, + }, + required: ["page_id"], + }, + execute: async ({ page_id }) => { + const [page, blocksData] = await Promise.all([ + notionRequest(`/pages/${page_id}`), + notionRequest(`/blocks/${page_id}/children?page_size=100`), + ]); + const title = notionPageTitle(page); + const blocks = blocksData.results || []; + const content = notionBlocksToText(blocks) || "(no content)"; + const hasMore = blocksData.has_more ? "\n[note: page has more than 100 blocks, only the first 100 are shown]" : ""; + const text = `# ${title}\n${content}${hasMore}`; + return text.length > 20000 ? text.slice(0, 20000) + "\n...[truncated]" : text; + }, + }, { name: "notion_search", description: "Search pages and databases in the Notion workspace (read-only).", @@ -226,7 +249,7 @@ export async function runInvestigation({ task, max_steps = 6 }) { transcript.push(`[step ${step}] ${name}(${JSON.stringify(args || {})}) -> ${resultText.length > 300 ? resultText.slice(0, 300) + "…" : resultText}`); responseParts.push({ functionResponse: { name, response: { result: resultText } } }); } - contents.push({ role: "function", parts: responseParts }); + contents.push({ role: "user", parts: responseParts }); } return { answer: `(Investigation stopped after reaching the step cap of ${cappedSteps} without a final answer -- the task may need to be narrowed, or more steps requested up to the hard cap of ${HARD_MAX_STEPS}.)`, steps: cappedSteps, transcript }; diff --git a/connectors/gemini/tools.js b/connectors/gemini/tools.js index 70280bd..e4d5cfa 100644 --- a/connectors/gemini/tools.js +++ b/connectors/gemini/tools.js @@ -98,7 +98,7 @@ export function register(server) { "Delegate an open-ended, multi-step READ-ONLY investigation to Gemini instead of doing it yourself one tool call at a time. Gemini runs its own loop server-side -- reading GitHub files/trees/commits, Cloudflare Workers logs, and Notion pages/databases across as many turns as it needs (bounded by max_steps) -- and returns one synthesized answer. Use this for things like \"why is CI failing on PR #42\" or \"summarize what changed in this repo over the last week\" where you'd otherwise need 5-10 separate manual tool calls. Not for anything requiring a write -- this tool is read-only by design.", { task: z.string().describe("The investigation task/question, described with enough context (repo names, time ranges, etc.) for Gemini to act without needing to ask you anything back -- it can't."), - max_steps: z.number().optional().describe("Max tool-use turns Gemini gets before being forced to answer (default 6, hard cap 10 regardless of this value)."), + max_steps: z.number().optional().describe("Max tool-use turns Gemini gets before being forced to answer (default 6, hard cap 20 regardless of this value)."), log_to_notion: z.boolean().optional().describe("Whether to log the task, step-by-step tool calls, and final answer as a page under the Gemini section of Notion (default: true). Write always targets the fixed Gemini root page."), }, async ({ task, max_steps = 6, log_to_notion = true }) => {