From aa157e60a8e7f90546c29e0820fd444a7fb859d6 Mon Sep 17 00:00:00 2001 From: Minsu Lee Date: Fri, 25 Sep 2026 01:21:31 +0900 Subject: [PATCH 1/2] feat(genkit): add genkit plugin bundling official genkit-ai/skills Bundle the 4 official Genkit skills from github.com/genkit-ai/skills (developing-genkit-js, -go, -dart, -python) via skills.sh with a pinned skills-lock.json, so the weekly update-skills workflow tracks upstream. Registered in the Claude, Codex, and Cursor marketplaces (with a relevance block keyed on the genkit CLI and Genkit manifest deps) and in release-please. --- .agents/plugins/marketplace.json | 12 + .claude-plugin/marketplace.json | 32 ++ .cursor-plugin/marketplace.json | 5 + .release-please-manifest.json | 3 +- README.md | 7 + .../skills/developing-genkit-dart/SKILL.md | 143 +++++ .../developing-genkit-dart/references/a2ui.md | 344 ++++++++++++ .../references/agents-artifacts.md | 163 ++++++ .../references/agents-background.md | 112 ++++ .../references/agents-branching.md | 103 ++++ .../references/agents-custom.md | 192 +++++++ .../references/agents-deployment.md | 116 ++++ .../references/agents-human-in-the-loop.md | 222 ++++++++ .../references/agents-multi-agent.md | 157 ++++++ .../references/agents-sessions.md | 192 +++++++ .../references/agents-state.md | 215 ++++++++ .../references/agents.md | 428 ++++++++++++++ .../references/dotprompt.md | 205 +++++++ .../references/genkit.md | 522 ++++++++++++++++++ .../references/genkit_anthropic.md | 56 ++ .../references/genkit_chrome.md | 23 + .../references/genkit_firebase_ai.md | 23 + .../references/genkit_google_genai.md | 111 ++++ .../references/genkit_mcp.md | 154 ++++++ .../references/genkit_middleware.md | 92 +++ .../references/genkit_openai.md | 88 +++ .../references/genkit_shelf.md | 59 ++ .../references/schemantic.md | 160 ++++++ .../skills/developing-genkit-go/SKILL.md | 155 ++++++ .../developing-genkit-go/references/a2ui.md | 255 +++++++++ .../references/agents-artifacts.md | 121 ++++ .../references/agents-background.md | 128 +++++ .../references/agents-branching.md | 102 ++++ .../references/agents-custom.md | 194 +++++++ .../references/agents-deployment.md | 171 ++++++ .../references/agents-human-in-the-loop.md | 231 ++++++++ .../references/agents-multi-agent.md | 136 +++++ .../references/agents-sessions.md | 188 +++++++ .../references/agents-state.md | 143 +++++ .../developing-genkit-go/references/agents.md | 354 ++++++++++++ .../references/flows-and-http.md | 183 ++++++ .../references/generation.md | 176 ++++++ .../references/getting-started.md | 140 +++++ .../references/middleware.md | 363 ++++++++++++ .../references/prompts.md | 313 +++++++++++ .../references/providers.md | 157 ++++++ .../developing-genkit-go/references/tools.md | 178 ++++++ .../skills/developing-genkit-js/SKILL.md | 193 +++++++ .../developing-genkit-js/references/a2ui.md | 306 ++++++++++ .../references/agents-artifacts.md | 105 ++++ .../references/agents-background.md | 109 ++++ .../references/agents-branching.md | 98 ++++ .../references/agents-custom.md | 141 +++++ .../references/agents-deployment.md | 190 +++++++ .../references/agents-human-in-the-loop.md | 173 ++++++ .../references/agents-multi-agent.md | 137 +++++ .../references/agents-sessions.md | 147 +++++ .../references/agents-state.md | 133 +++++ .../developing-genkit-js/references/agents.md | 278 ++++++++++ .../references/best-practices.md | 30 + .../references/common-errors.md | 132 +++++ .../references/docs-and-cli.md | 84 +++ .../references/dotprompt.md | 207 +++++++ .../references/examples.md | 157 ++++++ .../references/middleware-custom.md | 96 ++++ .../references/middleware.md | 170 ++++++ .../developing-genkit-js/references/setup.md | 47 ++ .../skills/developing-genkit-python/SKILL.md | 123 +++++ .../references/agents-artifacts.md | 64 +++ .../references/agents-background.md | 69 +++ .../references/agents-branching.md | 43 ++ .../references/agents-custom.md | 116 ++++ .../references/agents-http.md | 113 ++++ .../references/agents-human-in-the-loop.md | 109 ++++ .../references/agents-sessions.md | 61 ++ .../references/agents-state.md | 135 +++++ .../references/agents.md | 170 ++++++ .../references/common-errors.md | 133 +++++ .../references/dev-workflow.md | 127 +++++ .../references/dotprompt.md | 166 ++++++ .../references/evals.md | 152 +++++ .../references/examples.md | 212 +++++++ .../references/fastapi.md | 344 ++++++++++++ .../references/setup.md | 60 ++ plugins/genkit/.claude-plugin/plugin.json | 27 + plugins/genkit/.codex-plugin/plugin.json | 40 ++ plugins/genkit/.cursor-plugin/plugin.json | 31 ++ plugins/genkit/README.md | 75 +++ plugins/genkit/plugin.json | 27 + plugins/genkit/skills-lock.json | 29 + release-please-config.json | 26 + 91 files changed, 13011 insertions(+), 1 deletion(-) create mode 100644 plugins/genkit/.agents/skills/developing-genkit-dart/SKILL.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-dart/references/a2ui.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-dart/references/agents-artifacts.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-dart/references/agents-background.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-dart/references/agents-branching.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-dart/references/agents-custom.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-dart/references/agents-deployment.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-dart/references/agents-human-in-the-loop.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-dart/references/agents-multi-agent.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-dart/references/agents-sessions.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-dart/references/agents-state.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-dart/references/agents.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-dart/references/dotprompt.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-dart/references/genkit.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-dart/references/genkit_anthropic.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-dart/references/genkit_chrome.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-dart/references/genkit_firebase_ai.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-dart/references/genkit_google_genai.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-dart/references/genkit_mcp.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-dart/references/genkit_middleware.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-dart/references/genkit_openai.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-dart/references/genkit_shelf.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-dart/references/schemantic.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-go/SKILL.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-go/references/a2ui.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-go/references/agents-artifacts.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-go/references/agents-background.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-go/references/agents-branching.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-go/references/agents-custom.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-go/references/agents-deployment.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-go/references/agents-human-in-the-loop.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-go/references/agents-multi-agent.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-go/references/agents-sessions.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-go/references/agents-state.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-go/references/agents.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-go/references/flows-and-http.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-go/references/generation.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-go/references/getting-started.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-go/references/middleware.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-go/references/prompts.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-go/references/providers.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-go/references/tools.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-js/SKILL.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-js/references/a2ui.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-js/references/agents-artifacts.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-js/references/agents-background.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-js/references/agents-branching.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-js/references/agents-custom.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-js/references/agents-deployment.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-js/references/agents-human-in-the-loop.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-js/references/agents-multi-agent.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-js/references/agents-sessions.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-js/references/agents-state.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-js/references/agents.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-js/references/best-practices.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-js/references/common-errors.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-js/references/docs-and-cli.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-js/references/dotprompt.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-js/references/examples.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-js/references/middleware-custom.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-js/references/middleware.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-js/references/setup.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-python/SKILL.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-python/references/agents-artifacts.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-python/references/agents-background.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-python/references/agents-branching.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-python/references/agents-custom.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-python/references/agents-http.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-python/references/agents-human-in-the-loop.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-python/references/agents-sessions.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-python/references/agents-state.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-python/references/agents.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-python/references/common-errors.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-python/references/dev-workflow.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-python/references/dotprompt.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-python/references/evals.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-python/references/examples.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-python/references/fastapi.md create mode 100644 plugins/genkit/.agents/skills/developing-genkit-python/references/setup.md create mode 100644 plugins/genkit/.claude-plugin/plugin.json create mode 100644 plugins/genkit/.codex-plugin/plugin.json create mode 100644 plugins/genkit/.cursor-plugin/plugin.json create mode 100644 plugins/genkit/README.md create mode 100644 plugins/genkit/plugin.json create mode 100644 plugins/genkit/skills-lock.json diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json index 4734e85a..e05c8806 100644 --- a/.agents/plugins/marketplace.json +++ b/.agents/plugins/marketplace.json @@ -968,6 +968,18 @@ }, "category": "Development" }, + { + "name": "genkit", + "source": { + "source": "local", + "path": "./plugins/genkit" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Development" + }, { "name": "modern-web-guidance", "source": { diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 632f486b..f2a517f8 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -1331,6 +1331,38 @@ } } }, + { + "name": "genkit", + "description": "Official Genkit skills - build AI-powered applications with Genkit in JavaScript/TypeScript, Go, Dart/Flutter, and Python: flows, generation, prompts, tool calling, agents, and model providers", + "category": "development", + "keywords": ["genkit", "ai", "llm", "agents", "flows", "dotprompt", "firebase", "typescript", "go", "dart", "flutter", "python"], + "tags": ["ai", "framework"], + "source": "./plugins/genkit", + "relevance": { + "topic": "Genkit", + "signals": { + "cli": ["genkit"], + "manifestDeps": [ + { + "file": "[/\\\\]package\\.json$", + "pattern": "\"(genkit|@genkit-ai/[^\"]+)\"\\s*:" + }, + { + "file": "[/\\\\]go\\.mod$", + "pattern": "github\\.com/firebase/genkit/go" + }, + { + "file": "[/\\\\]pubspec\\.yaml$", + "pattern": "\\n\\s*genkit(_[a-z_]+)?\\s*:" + }, + { + "file": "[/\\\\](pyproject\\.toml|requirements[^/\\\\]*\\.txt)$", + "pattern": "(^|[\\s\"'])genkit([-_][a-z0-9-]+)?\\b" + } + ] + } + } + }, { "name": "modern-web-guidance", "description": "Keep your coding agent up to date with the latest web best practices from the Google Chrome team", diff --git a/.cursor-plugin/marketplace.json b/.cursor-plugin/marketplace.json index 5f89d1d9..7bb9ddf0 100644 --- a/.cursor-plugin/marketplace.json +++ b/.cursor-plugin/marketplace.json @@ -319,6 +319,11 @@ "source": "./plugins/deno", "description": "Official Deno skills - dependency management with npm and JSR, permissions, the built-in toolchain, migrating from Node/npm/yarn/pnpm/bun, Fresh, and Deno Deploy" }, + { + "name": "genkit", + "source": "./plugins/genkit", + "description": "Official Genkit skills - build AI-powered applications with Genkit in JavaScript/TypeScript, Go, Dart/Flutter, and Python: flows, generation, prompts, tool calling, agents, and model providers" + }, { "name": "java-development", "source": "./plugins/java-development", diff --git a/.release-please-manifest.json b/.release-please-manifest.json index ea7bf363..89c75cb3 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -71,5 +71,6 @@ "plugins/shadcn-ui": "1.1.1", "plugins/tanstack": "0.2.0", "plugins/orpc": "1.1.0", - "plugins/deno": "1.1.0" + "plugins/deno": "1.1.0", + "plugins/genkit": "1.0.0" } diff --git a/README.md b/README.md index e55a46a1..25034c8d 100644 --- a/README.md +++ b/README.md @@ -372,6 +372,12 @@ Official [Deno skills](https://github.com/denoland/skills) — dependency manage **Install:** `/plugin install deno@pleaseai` | **Source:** [plugins/deno](https://github.com/pleaseai/claude-code-plugins/tree/main/plugins/deno) +#### Genkit + +Official [Genkit skills](https://github.com/genkit-ai/skills) — build AI-powered applications with Genkit in JavaScript/TypeScript, Go, Dart/Flutter, and Python: flows, generation, prompts, tool calling, agents, and model providers. + +**Install:** `/plugin install genkit@pleaseai` | **Source:** [plugins/genkit](https://github.com/pleaseai/claude-code-plugins/tree/main/plugins/genkit) + #### Graphite [![tessl](https://img.shields.io/endpoint?url=https%3A%2F%2Fapi.tessl.io%2Fv1%2Fbadges%2Fpleaseai%2Fgraphite)](https://tessl.io/registry/pleaseai/graphite) @@ -634,6 +640,7 @@ Once the marketplace is added (or files copied), the following plugins are avail /plugin install zod@pleaseai /plugin install bun@pleaseai /plugin install deno@pleaseai +/plugin install genkit@pleaseai /plugin install graphite@pleaseai /plugin install claude-md-management@pleaseai /plugin install fetch@pleaseai diff --git a/plugins/genkit/.agents/skills/developing-genkit-dart/SKILL.md b/plugins/genkit/.agents/skills/developing-genkit-dart/SKILL.md new file mode 100644 index 00000000..000801d8 --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-dart/SKILL.md @@ -0,0 +1,143 @@ +--- +name: developing-genkit-dart +description: Generates code and provides documentation for the Genkit Dart SDK. Use when the user asks to build AI agents in Dart, use Genkit flows, or integrate LLMs into Dart/Flutter applications. +metadata: + category: AiAndMachineLearning +--- + +# Genkit Dart + +Genkit Dart is an AI SDK for Dart that provides a unified interface for code generation, structured outputs, tools, flows, and AI agents. + +## Core Features and Usage +If you need help with initializing Genkit (`Genkit()`), Generation (`ai.generate`), Tooling (`ai.defineTool`), Flows (`ai.defineFlow`), Embeddings (`ai.embedMany`), streaming, or calling remote flow endpoints, please load the core framework reference: +[references/genkit.md](references/genkit.md) + +## Prompts (Dotprompt) + +`.prompt` files keep prompt content out of Dart code with YAML frontmatter plus a +Handlebars template. See [references/dotprompt.md](references/dotprompt.md): +`promptDir`, `ai.prompt()` (call/stream/render), variants, partials, named +schemas via `defineSchema`, and the `tools`/`maxTurns`/`returnToolRequests`/`use` +(middleware) frontmatter fields. A `.prompt` file can also back an agent directly +via `definePromptAgent`. + +## Agents + +Genkit Dart has an **agent** API for persistent, multi-turn conversations +(sessions, snapshots, interrupts, branching, background execution, custom state, +artifacts, and multi-agent delegation). The agent/session/snapshot APIs are +**experimental** and live behind opt-in imports: server APIs come from +`package:genkit/experimental.dart` (alongside `package:genkit/genkit.dart`), the +browser/HTTP client from `package:genkit/experimental_client.dart` (alongside +`package:genkit/client.dart`), and `dart:io` extras like `FileSessionStore` from +`package:genkit/experimental_io.dart`. These entry points are `@experimental`, so +importing them raises an `experimental_member_use` analyzer warning you can +silence in `analysis_options.yaml`. The `remoteAgent` client works from any Dart +app, including **Flutter**, and the backend is fully interchangeable — it can talk +to a Genkit agent implemented in Dart, JS/TypeScript, or Go over the same HTTP +protocol. A few Dart specifics: interrupts are modeled as tools that return +`.interrupt(...)` (there is no `defineInterrupt`), sub-agent delegation uses +the `agents()` middleware from `package:genkit_middleware`, and there is no +`artifacts()` middleware yet (define artifact tools directly). + +For more details see: + +- [Agents](references/agents.md): defining/serving an agent and client-managed state (start here). +- [Sessions & persistence](references/agents-sessions.md): session stores (`InMemorySessionStore`/`FileSessionStore`/`FirestoreSessionStore`). +- [Human-in-the-loop / interrupts](references/agents-human-in-the-loop.md): pausing for approval/input via `.interrupt(...)` and resuming. +- [Branching](references/agents-branching.md): forking a conversation from a snapshot. +- [Background agents](references/agents-background.md): detaching long-running turns and polling. +- [Working with state](references/agents-state.md): typed custom session state, auto-synced to the client. +- [Artifacts](references/agents-artifacts.md): producing and reading named deliverables. +- [Multi-agent orchestration](references/agents-multi-agent.md): delegating to sub-agents with the `agents()` middleware. +- [Advanced custom agents](references/agents-custom.md): `defineCustomAgent` for full turn control. +- [Deploying agents](references/agents-deployment.md): serving agents over HTTP with `genkit_shelf` (multiple agents, CORS). + +## Generative UI (A2UI) + +Genkit Dart has an **A2UI** (Agent-to-UI) plugin (`genkit_a2ui`) +that lets an agent stream interactive UI **surfaces** (cards, lists, forms, +buttons), not just prose. The whole server-side integration is the `a2ui()` model +middleware in an agent's (or `ai.generate`'s) `use` list; the Flutter client +renders surfaces with the [`genui`](https://pub.dev/packages/genui) package plus +the helpers in `package:genkit_a2ui/client.dart`. Dart specific: you must +register `A2uiPlugin()` in `Genkit(plugins: [...])` (unlike JS, middleware is +resolved by name from the registry). + +- [A2UI](references/a2ui.md): server middleware, options, Flutter/genui client rendering, user actions/forms, custom catalogs, and the security/trust boundary. + +## Genkit CLI (recommended) + +`genkit start` unintrusively wraps any Dart program that uses the Genkit library, running it unchanged while capturing traces from every Genkit action so you can prove tools were actually called and inspect model I/O from the terminal, even for headless checks. It forwards stdio, so interactive CLI tools that rely on stdin/stdout work without issues. Running the app directly (`dart run`) skips trace capture, so you're debugging blind. Check install with `genkit --version`. + +**Installation:** +```bash +curl -sL cli.genkit.dev | bash # Native CLI +# OR +npm install -g genkit-cli # Via npm +# OR run commands directly with npx without a global install (prefix every genkit command): +# npx genkit-cli start -- dart run main.dart +``` + +**Primary pattern (default):** prefix `genkit start --` to your normal run command. This collects telemetry from any Genkit code your program runs, whether triggered from the dev UI, your own web server/web UI, or a plain script. Starts the Developer UI (usually http://localhost:4000) for running flows, model and agent playground, and browsing traces: + +```bash +genkit start -- dart run main.dart +genkit start --noui -- dart run main.dart # same, without the Dev UI (still a persistent server) +``` +`genkit start` runs until you stop it with Ctrl+C. That is expected and correct for the common cases: a server your web/mobile app calls, or an interactive CLI you exit yourself. `--noui` only drops the Dev UI; it is **not** a one-shot command and will not exit on its own. Do **not** use `genkit start` as a blocking step in automated/non-interactive contexts; use `flow:run` (below) for that. + +**Non-interactive use (agents/CI):** add the global `--non-interactive` flag before `--` so the CLI uses defaults and never blocks on a prompt (e.g. the first-run analytics notice): `genkit start --non-interactive -- dart run main.dart` (works with `flow:run` too). + +**Run a flow (`flow:run`):** invoke a specific flow by name from the CLI. Append your run command after `--` to spin up the runtime just for this run (the command runs as-is to register your flows): +```bash +genkit flow:run myFlow '{"data": "input"}' -- dart run main.dart +``` +This is **self-terminating**: it runs the flow once, prints a `Trace ID`, then exits, so it's the right choice for a quick, non-interactive check (unlike `genkit start`). Note: `flow:run` runs **flows** (`ai.defineFlow`), not agents; you can't `flow:run` an agent (`ai.defineAgent`) directly. To exercise an agent from the CLI, wrap one turn in a throwaway flow and run that (see [Agents](references/agents.md)). Traces for this run can be inspected using the trace commands below. + +**Gotcha: top-level `final` declarations are lazy.** Flows and agents defined as top-level `final` register with Genkit only when the symbol is first evaluated. An empty `main()` registers nothing, so `flow:run` fails with `Process exited before runtime was ready`. Reference the flow/agent symbols from `main()` (or import a module that does) so their `define*` calls actually run. + +**Debugging with traces:** the fastest way to see prompts, model inputs/outputs, tool calls, latencies, and errors. Inspect from the terminal after any run under `genkit start`: +```bash +genkit trace:list # find recent trace IDs +genkit trace:get # full trace details (inputs, outputs, tool calls, errors) +genkit trace:get --format json # machine-readable JSON, safe to pipe into jq or other parsers +``` + +For machine-readable output, pass `--format json` to get clean JSON you can pipe into `jq` or other parsers. The **default** output is human-oriented (banner/log lines, possible truncation on large traces), so don't pipe that form directly; use `--format json`, grep, or the Dev UI trace viewer. + + +**Documentation:** +```bash +genkit docs:search "streaming" dart +genkit docs:list dart +genkit docs:read dart/flows.md +``` + +## Plugin Ecosystem +Genkit relies on a large suite of plugins to perform generative AI actions, interface with external LLMs, or host web servers. + +When asked to use any given plugin, always verify usage by referring to its corresponding reference below. You should load the reference when you need to know the specific initialization arguments, tools, models, and usage patterns for the plugin: + +| Plugin Name | Reference Link | Description | +| ---- | ---- | ---- | +| `genkit_google_genai` | [references/genkit_google_genai.md](references/genkit_google_genai.md) | Load for Google Gemini plugin interface usage. | +| `genkit_anthropic` | [references/genkit_anthropic.md](references/genkit_anthropic.md) | Load for Anthropic plugin interface for Claude models. | +| `genkit_openai` | [references/genkit_openai.md](references/genkit_openai.md) | Load for OpenAI plugin interface for GPT models, Groq, and custom compatible endpoints. | +| `genkit_middleware` | [references/genkit_middleware.md](references/genkit_middleware.md) | Load for Tooling for specific agentic behavior: `filesystem`, `skills`, and `toolApproval` interrupts. | +| `genkit_mcp` | [references/genkit_mcp.md](references/genkit_mcp.md) | Load for Model Context Protocol integration (Server, Host, and Client capabilities). | +| `genkit_chrome` | [references/genkit_chrome.md](references/genkit_chrome.md) | Load for Running Gemini Nano locally inside the Chrome browser using the Prompt API. | +| `genkit_shelf` | [references/genkit_shelf.md](references/genkit_shelf.md) | Load for Integrating Genkit Flow actions over HTTP using Dart Shelf. | +| `genkit_firebase_ai` | [references/genkit_firebase_ai.md](references/genkit_firebase_ai.md) | Load for Firebase AI plugin interface (Gemini API via Vertex AI). | +| `genkit_a2ui` | [references/a2ui.md](references/a2ui.md) | Load for A2UI (Agent-to-UI): streaming generative UI surfaces via the `a2ui()` middleware, rendered on the client with `genui`. | + +## External Dependencies +Whenever you define schemas mapping inside of Tools, Flows, and Prompts, you must use the [schemantic](https://pub.dev/packages/schemantic) library. +To learn how to use schemantic, ensure you read [references/schemantic.md](references/schemantic.md) for how to implement type safe generated Dart code. This is particularly relevant when you encounter symbols like `@Schema()`, `SchemanticType`, or classes with the `$` prefix. Genkit Dart uses schemantic for all of its data models so it's a CRITICAL skill to understand for using Genkit Dart. + +## Best Practices +- **Agent or flow?** If the task is conversational, multi-turn, or described as "an agent", "assistant", or "chatbot", build it with `ai.defineAgent` (see [Agents](references/agents.md)) rather than hand-rolling a `generate` + tools loop inside a flow. Reach for a plain flow only for single-shot, stateless generation. +- Always check that code cleanly compiles using `dart analyze` before generating the final response. +- Always use the Genkit CLI for local development and debugging. +- Verify with traces, not a blind run. Running the app directly (`dart run`) does not capture dev traces. See the [Genkit CLI](#genkit-cli-recommended) section for how to run your app and capture traces. diff --git a/plugins/genkit/.agents/skills/developing-genkit-dart/references/a2ui.md b/plugins/genkit/.agents/skills/developing-genkit-dart/references/a2ui.md new file mode 100644 index 00000000..c3357c6f --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-dart/references/a2ui.md @@ -0,0 +1,344 @@ +# A2UI (Agent-to-UI) generative UI + +The `genkit_a2ui` package brings +> [A2UI](https://a2ui.org/), a transport-agnostic, JSON-based streaming UI +> protocol, to Genkit Dart agents. +> +> A2UI builds on the agent client. Server APIs come from +> `package:genkit/genkit.dart`; the browser/Flutter client comes from +> `package:genkit/client.dart`. Read [Agents](agents.md) first if you have not. + +An A2UI-enabled agent streams more than prose. It streams interactive UI +**surfaces** (cards, lists, forms, buttons) that a client renders incrementally +as the model responds. The entire server-side integration is a single model +middleware: add `a2ui()` to an agent's `use` list and nothing else changes. + +## Install + +On the **server** (the shelf app that hosts the agent), add `genkit_a2ui` +alongside the packages the agent already needs. List `genkit` explicitly even +though `genkit_a2ui` pulls it in transitively, otherwise `dart analyze` warns +with `depend_on_referenced_packages` (the server code imports +`package:genkit/genkit.dart` directly): + +```bash +dart pub add genkit genkit_a2ui genkit_google_genai genkit_shelf +``` + +To render surfaces you also need a renderer. The Flutter renderer for A2UI is +[`genui`](https://pub.dev/packages/genui). On the **client** Flutter app, add it +plus `a2ui_core`, and `genkit` + `genkit_a2ui` for the client helpers +(`remoteAgent`, `a2uiEnvelopesFromParts`, `actionToMessage`): + +```bash +flutter pub add genkit genkit_a2ui genui a2ui_core +``` + +## Server: add the `a2ui()` middleware + +Add `a2ui()` to your agent's `use` list. That is the whole server-side setup. + +**Dart specific:** Dart middleware is resolved by name from +the registry, so you MUST register `A2uiPlugin()` in `Genkit(plugins: [...])` +before referencing it via `a2ui()`. + +```dart +import 'package:genkit/genkit.dart'; +import 'package:genkit/experimental.dart'; // defineAgent, InMemorySessionStore +import 'package:genkit_a2ui/a2ui.dart'; +import 'package:genkit_google_genai/genkit_google_genai.dart'; + +// A2uiPlugin() registers the a2ui() middleware so `use: [a2ui()]` resolves. +final ai = Genkit(plugins: [googleAI(), A2uiPlugin()]); + +final uiAgent = ai.defineAgent( + name: 'uiAgent', + model: googleAI.gemini('gemini-flash-latest'), + system: + 'You help users. Render an A2UI surface whenever a result is clearer ' + 'shown than told (weather, comparisons, lists, forms). Keep prose brief; ' + 'put the substance in the UI.', + use: [a2ui()], // defaults to the bundled 'basic' catalog + store: InMemorySessionStore(), +); +``` + +It works identically on a one-shot `ai.generate`: + +```dart +final res = await ai.generate( + model: googleAI.gemini('gemini-flash-latest'), + prompt: 'Show me the weather in Tokyo', + use: [a2ui()], +); +``` + +Serve the agent over HTTP with `genkit_shelf` (see +[Deploying agents](agents-deployment.md)). A server-managed agent exposes three +actions (turn, snapshot, abort): + +```dart +router.post('/api/uiAgent', shelfHandler(uiAgent.action)); +router.post('/api/uiAgent/getSnapshot', shelfHandler(uiAgent.getSnapshotDataAction)); +router.post('/api/uiAgent/abort', shelfHandler(uiAgent.abortAgentAction)); +``` + +### Options + +Pass options to `a2ui(...)`: + +| Option | Default | Description | +| -------------- | ---------- | ------------------------------------------------------------------------------------------------------- | +| `catalog` | `'basic'` | Id of the catalog describing what the agent may render. | +| `instructions` | `'system'` | Where to inject catalog capabilities. `'none'` injects nothing (supply your own instructions instead). | +| `validate` | `'warn'` | Validate emitted envelopes. `'warn'` logs and drops bad blocks; `'strict'` throws; `'off'` skips it. | +| `surfaceId` | fresh UUID | Surface id policy. Defaults to a new UUID per surface; pass a fixed string to reuse one id per surface. | +| `version` | `'v0.9'` | Protocol version stamped on envelopes. | + +Use `validate: 'strict'` during development to fail fast on malformed JSON or +components outside the catalog. See [Security](#security-and-the-trust-boundary) +for what `'strict'` does and does not check. + +## Client: render surfaces (Flutter + genui) + +`package:genkit_a2ui/client.dart` is browser/Flutter-safe (no `dart:io`). Consume +the agent with `remoteAgent`, pull A2UI envelopes off each chunk's content with +`a2uiEnvelopesFromParts`, convert each envelope to a genui `A2uiMessage`, and feed +it to a `SurfaceController`. A2UI travels as `data` parts on the raw model chunk, +so read them from `chunk.raw.modelChunk?.content`. + +```dart +import 'package:a2ui_core/a2ui_core.dart' as core; +import 'package:genkit/client.dart'; +import 'package:genkit/experimental_client.dart'; // remoteAgent, AgentApi +import 'package:genkit_a2ui/client.dart'; +import 'package:genui/genui.dart' hide basicCatalogId, DataPart; + +// remoteAgent returns an AgentApi (not a `RemoteAgent`). +final agent = remoteAgent( + url: 'http://localhost:8080/api/uiAgent', + getSnapshotUrl: 'http://localhost:8080/api/uiAgent/getSnapshot', + abortUrl: 'http://localhost:8080/api/uiAgent/abort', +); +final chat = agent.chat(); + +// genui registers an empty stub for an unknown catalog id, so re-tag its basic +// catalog with the id the plugin's bundled basic catalog advertises. +final catalog = BasicCatalogItems.asCatalog().copyWith(catalogId: basicCatalogId); +final surfaceController = SurfaceController(catalogs: [catalog]); + +final turn = chat.sendStream(text: 'What is the weather in Tokyo?'); +await for (final chunk in turn.stream) { + if (chunk.text.isNotEmpty) appendProse(chunk.text); + for (final envelope in a2uiEnvelopesFromParts(chunk.raw.modelChunk?.content)) { + surfaceController.handleMessage(core.A2uiMessage.fromJson(envelope)); + } +} +await turn.response; +``` + +**Dart specific:** importing both `package:genkit/client.dart` and +`package:genui/genui.dart` collides on two symbols, so hide genui's: + +- `basicCatalogId`: both packages export one with *different* values. You want + the plugin's; if the ids do not match, genui registers an empty stub and + surfaces render blank. +- `DataPart`: genkit's client and genui (via `genai_primitives`) both export a + `DataPart` class, so referencing `DataPart` unqualified is ambiguous. + +Hence `import 'package:genui/genui.dart' hide basicCatalogId, DataPart;`. + +`remoteAgent` manages the session id for you, so a single `chat` keeps the whole +conversation server-side (the agent's session store holds history). + +In a Flutter app, listen to `surfaceController.surfaceUpdates` to add a +`Surface(surfaceContext: surfaceController.contextFor(id))` widget to your chat +log when a `SurfaceAdded` update arrives, and listen to +`surfaceController.onSubmit` to route actions back to the agent (see below). + +## Handling user actions + +When a user interacts with a surface (for example, presses a `Button`), +`surfaceController.onSubmit` emits a genui `ChatMessage`. That message is *not* an +`A2uiClientAction` you can hand straight to `actionToMessage`: genui reports the +interaction as a `UiInteractionPart` whose payload is a JSON string +`{ "version": ..., "action": { name, surfaceId, widgetId, context } }`. Decode +that part into an `A2uiClientAction`, then `actionToMessage(...)` it and send it +as the next turn: + +```dart +import 'dart:convert'; +import 'package:genkit_a2ui/client.dart'; +import 'package:genui/genui.dart' hide basicCatalogId, DataPart; + +surfaceController.onSubmit.listen((ChatMessage message) { + final action = _actionFromSubmit(message); + // Guard against re-entrancy: ignore a new action while a turn is still + // streaming, otherwise two concurrent turns interleave. + if (action == null || busy) return; + final turn = chat.sendStream(message: actionToMessage(action)); + // ...consume turn.stream like above (prose + a2uiEnvelopesFromParts)... +}); + +/// Extracts an [A2uiClientAction] from a genui onSubmit [ChatMessage]. +A2uiClientAction? _actionFromSubmit(ChatMessage message) { + for (final part in message.parts) { + final interaction = part.asUiInteractionPart?.interaction; + if (interaction == null) continue; + final decoded = jsonDecode(interaction); + final action = decoded is Map ? decoded['action'] : null; + if (action is Map) { + final m = action.cast(); + return A2uiClientAction( + name: (m['name'] as String?) ?? 'action', + surfaceId: (m['surfaceId'] as String?) ?? '', + // genui names it `widgetId`; A2uiClientAction calls it sourceComponentId. + sourceComponentId: (m['widgetId'] as String?) ?? '', + timestamp: DateTime.now().toUtc().toIso8601String(), + context: (m['context'] as Map?)?.cast() ?? const {}, + ); + } + } + return null; +} +``` + +The action's `name` is sent as the user message; the full action (including its +`context`) is attached as an a2ui data part so the agent can react to it. + +### Forms + +Input components (`TextField`, `CheckBox`, `Slider`) do **not** send their values +automatically. To capture what the user entered, the model must: + +1. Bind each input's `value` to a data-model path (`{ "path": "/email" }`). +2. Echo those same paths in the submit `Button`'s `action.event.context`. + +The catalog capabilities injected into the system prompt already instruct the +model to do this. Without both steps, the action arrives with an empty `context`. + + +## Custom catalogs + +The `catalog` option is a **catalog id** resolved from the Genkit registry. The +bundled `'basic'` catalog is the default and needs no registration. A catalog +describes the components the model may emit: + +- `id`: globally-unique URI (also used as `catalogId` on `createSurface`). +- `components`: each with `name` (matches the renderer type), `description` + (one-line summary), and `props` (a compact, model-facing text description of + the component's props, kept as plain text to minimize prompt tokens). + +A custom catalog has two halves that MUST agree on a catalog id: the **server** +catalog (what the model is told it may render, and validated against) and the +**client** renderer (the widgets that actually paint the components). Define the +id once and share it. + +### Server: register the catalog + +Start from `basicCatalog` and add your own component, then register it with +`loadCatalog` before serving any turns and reference it by id: + +```dart +import 'package:genkit_a2ui/a2ui.dart'; + +const weatherCatalogId = 'com.example.a2ui.weather'; + +final weatherCatalog = A2uiCatalog( + id: weatherCatalogId, + components: [ + ...basicCatalog.components, + const A2uiCatalogComponent( + name: 'Gauge', + description: 'A circular gauge visualizing a single numeric value.', + props: 'value: number or { path } binding (required); min?: number; ' + 'max?: number; label?: string; unit?: string.', + ), + ], +); + +// Call once at startup, before the agent handles a turn. +Future registerCatalogs() => + loadCatalog(ai, id: weatherCatalogId, catalog: weatherCatalog); + +final uiAgent = ai.defineAgent( + name: 'uiAgent', + model: googleAI.gemini('gemini-flash-latest'), + use: [a2ui(catalog: weatherCatalogId, validate: 'strict')], + store: InMemorySessionStore(), +); +``` + +You can also load a catalog from a JSON file: +`loadCatalog(ai, id: 'my-catalog', file: './my-catalog.json')`. + +### Client: register a matching widget + +On the Flutter client, implement the matching component as a genui `CatalogItem` +(a `name`, a prop `dataSchema`, and a `widgetBuilder`) and re-tag the catalog with +the SAME id: + +```dart +final gaugeCatalogItem = CatalogItem( + name: 'Gauge', // MUST match the server component name + dataSchema: _gaugeSchema, // built with json_schema_builder's `S` + widgetBuilder: (itemContext) { + // Resolve { path } bindings via genui's Bound* widgets, then paint. + return _GaugeWidget(...); + }, +); + +final catalog = BasicCatalogItems.asCatalog().copyWith( + newItems: [gaugeCatalogItem], + catalogId: weatherCatalogId, // MUST match the server catalog id +); +``` + +If the `name` or catalog id disagree, the model emits a component the client +cannot render, or the client registers widgets under an id no surface references. +Catalogs live in the registry under value type `a2ui-catalog`. + +## Security and the trust boundary + +Generative UI moves model output into the UI, so treat every surface an agent +emits as **untrusted input**. The `validate` option (including `'strict'`) checks +envelope structure and component *type names* against the catalog only. It does +**not** validate component props or data-model values: model-controlled values +such as `Image.url` and `Text` (inline Markdown that a renderer may turn into +rich content) pass through untouched. `'strict'` is a well-formedness check, not a +security boundary. + +- **The renderer/catalog owns prop sanitization.** Whatever renders a surface + (for example `genui` plus your Markdown renderer) is responsible for escaping + and sanitizing prop values before they reach the UI. +- **Restrict remote sources at the host.** On the web, serve the app with a + Content Security Policy that limits `img-src` and other fetch directives to + origins you trust. +- **Do not put secrets in the data model.** Anything bound into a surface's data + model can be echoed back through an action's `context`. + +For server-side control over props (for example, allow-listing image hosts), add +your own model middleware after `a2ui()` to inspect and rewrite the emitted a2ui +parts. + +## How it works + +A2UI rides on its own part channel: a Genkit `data` part with mime type +`application/a2ui+json` whose `data` is `{ "envelopes": [...] }`. On each model +call inside the agent's tool loop, `a2ui()`: + +1. Injects the catalog's capabilities into the system prompt (unless + `instructions: 'none'`). +2. Intercepts the model output (streamed chunks and the final message). +3. Extracts `a2ui` fenced code blocks from the model's text. +4. Validates them against the catalog (per `validate`). +5. Rewrites them into canonical a2ui data parts. + +Inbound a2ui parts (a surface action sent back as the next turn, or replayed +history) are summarized into plain text before the underlying model sees them, so +a model that does not understand the a2ui mime type can still reason about prior +surfaces and user actions. + +For a complete, runnable example (a shelf server plus a Flutter genui client with +a custom `Gauge` catalog), see the `a2ui` testapp in the Genkit Dart repo. + diff --git a/plugins/genkit/.agents/skills/developing-genkit-dart/references/agents-artifacts.md b/plugins/genkit/.agents/skills/developing-genkit-dart/references/agents-artifacts.md new file mode 100644 index 00000000..ed7eb421 --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-dart/references/agents-artifacts.md @@ -0,0 +1,163 @@ +# Working with Artifacts + +> The `Artifact` type is stable and comes from `package:genkit/genkit.dart`, but +> the surrounding agent APIs (`defineAgent`, `currentSession`, +> `InMemorySessionStore`) are experimental and come from +> `package:genkit/experimental.dart`. Read [agents.md](agents.md) first. + +**Artifacts** are named, content-bearing deliverables an agent produces during a +session — files, reports, code, etc. They live in the session (deduplicated by +name) and are returned in `res.artifacts` / tracked on the client's +`chat.artifacts`. + +> **Dart has no `artifacts()` middleware yet.** Define `write_artifact` / +> `read_artifact` tools directly on top of the session artifact API +> (`ai.currentSession().addArtifacts()` / `getArtifacts()`). + +> **Session artifacts vs. real files.** Artifacts (this page) live *in the +> session state* — they travel with the conversation and stream to the client. +> If instead you want the agent to work against **real files on disk** (a +> sandboxed workspace), use the `filesystem()` middleware from +> `package:genkit_middleware/filesystem.dart` +> (`filesystem(rootDirectory: ...)`, backed by `FilesystemPlugin()`), which +> gives the model `list_files` / `read_file` / `write_file` / +> `search_and_replace` tools rooted at a directory. See +> [middleware](genkit_middleware.md). The two approaches are complementary: +> session artifacts for conversation-scoped deliverables, `filesystem()` for +> persistent on-disk work. + +## Give the model artifact tools + +```dart +import 'package:genkit/genkit.dart'; +import 'package:genkit/experimental.dart'; // defineAgent, currentSession +import 'package:schemantic/schemantic.dart'; + +import 'genkit.dart'; + +part 'workspace_agent.g.dart'; + +@Schema() +abstract class $WriteArtifactInput { + @Field(description: 'The name (e.g. filename) of the artifact.') + String get name; + @Field(description: 'The full content of the artifact.') + String get content; +} + +@Schema() +abstract class $ReadArtifactInput { + @Field(description: 'The name of the artifact to read.') + String get name; +} + +final writeArtifact = ai.defineTool( + name: 'write_artifact', + description: 'Create or overwrite a named artifact (e.g. a file). Pass the ' + 'filename as "name" and the full content as "content".', + inputSchema: WriteArtifactInput.$schema, + outputSchema: SchemanticType.string(), + fn: (input, _) async { + final session = ai.currentSession()!; + session.addArtifacts([ + Artifact(name: input.name, parts: [TextPart(text: input.content)]), + ]); + return .response('Wrote artifact "${input.name}".'); + }, +); + +final readArtifact = ai.defineTool( + name: 'read_artifact', + description: 'Read the content of a previously created artifact by name.', + inputSchema: ReadArtifactInput.$schema, + outputSchema: SchemanticType.string(), + fn: (input, _) async { + final session = ai.currentSession()!; + final match = session.getArtifacts().where((a) => a.name == input.name); + if (match.isEmpty) return .response('Artifact "${input.name}" not found.'); + return .response(match.first.parts.map((p) => p.text ?? '').join()); + }, +); + +final workspaceAgent = ai.defineAgent( + name: 'workspaceAgent', + system: 'You are a code generation assistant. Use write_artifact to create ' + 'files (pass the filename as "name" and the full content as "content"). ' + 'Use read_artifact to review or modify a previously created file.', + tools: [writeArtifact, readArtifact], + use: [retry()], + store: InMemorySessionStore(), +); +``` + +## How artifacts flow + +Adding an artifact to the session emits an event that the agent runtime forwards +to the client as an `artifact` stream chunk. Artifacts are **deduplicated by +name** — writing the same `name` again replaces it. + +Run it; artifacts are produced via the tool and returned in the response: + +```dart +final chat = workspaceAgent.chat(); +final res = await chat.send(text: 'Write poem.txt with a poem about Genkit'); +print(res.artifacts); // List +``` + +## The `Artifact` shape + +```dart +// An artifact's content lives in `parts` (text parts). `name` and `metadata` +// are optional on the type but you should always set `name`. +final artifact = Artifact( + name: 'poem.txt', + parts: [TextPart(text: 'Roses are red…')], +); +``` + +## Programmatic access (inside tools / custom agents) + +Use the active session. `ai.currentSession()` returns `null` when there's no +active session, so only call it inside an agent turn. + +```dart +final session = ai.currentSession()!; + +// Read all artifacts: +final all = session.getArtifacts(); // List +final found = all.where((a) => a.name == 'poem.txt').firstOrNull; + +// Create / replace artifacts: +session.addArtifacts([ + Artifact(name: 'notes.md', parts: [TextPart(text: '# Notes')]), +]); +``` + +## Reading artifacts on the client + +The `remoteAgent` client tracks artifacts on `chat.artifacts`; each response +exposes `res.artifacts`; and each streamed chunk exposes `chunk.artifact` as it +arrives. + +```dart +import 'package:genkit/client.dart'; +import 'package:genkit/experimental_client.dart'; // remoteAgent + +final agent = remoteAgent(url: '/api/workspaceAgent'); +final chat = agent.chat(); + +final turn = chat.sendStream(text: 'Create index.html and styles.css'); +await for (final chunk in turn.stream) { + final artifact = chunk.artifact; + if (artifact != null) { + // artifact.name, artifact.parts — render/store it live. + } +} +final res = await turn.response; +print(res.artifacts); // artifacts produced this turn +print(chat.artifacts); // all artifacts tracked for the session +``` + +> In [multi-agent orchestration](agents-multi-agent.md), the `agents()` +> delegation middleware can merge sub-agent artifacts into the parent session +> (namespaced by an invocation id) via its `artifactStrategy` option. diff --git a/plugins/genkit/.agents/skills/developing-genkit-dart/references/agents-background.md b/plugins/genkit/.agents/skills/developing-genkit-dart/references/agents-background.md new file mode 100644 index 00000000..4d518ee9 --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-dart/references/agents-background.md @@ -0,0 +1,112 @@ +# Background Agents / Detaching + +> Detaching **requires a [session store](agents-sessions.md)** — the server +> needs somewhere to write the result when background work finishes. Read +> [agents.md](agents.md) first. + +Detaching runs a turn in the background: the server saves a `pending` snapshot +and returns a `snapshotId` **immediately**, keeps processing, then updates the +snapshot to a terminal status (`completed` / `failed` / `aborted` / `expired`). +The client polls for completion and can abort. + +## Define the agent (store required) + +```dart +import 'package:genkit/genkit.dart'; +import 'package:genkit/experimental.dart'; // defineAgent, InMemorySessionStore + +import 'genkit.dart'; + +final backgroundAgent = ai.defineAgent( + name: 'backgroundAgent', + system: 'You are a senior research analyst. Produce a comprehensive ' + 'markdown report.', + use: [retry()], + store: InMemorySessionStore(), // REQUIRED for detach +); +``` + +Expose its companion actions so the client can poll/abort (see +[agents.md](agents.md#serve-an-agent-over-http)): + +```dart +import 'package:genkit_shelf/genkit_shelf.dart'; + +router.post('/api/backgroundAgent', shelfHandler(backgroundAgent.action)); +router.post( + '/api/backgroundAgent/getSnapshot', + shelfHandler(backgroundAgent.getSnapshotDataAction), +); +router.post( + '/api/backgroundAgent/abort', + shelfHandler(backgroundAgent.abortAgentAction), +); +``` + +## Client-side: detach + poll + abort + +On the client (`package:genkit/client.dart`), `chat.detach(text: ...)` (or +`chat.detach(...)`) resolves immediately with a `DetachedTask` carrying the +`snapshotId`. `task.poll(...)` yields `AgentSnapshot`s until a terminal status; +`task.abort()` cancels the background work. Each `AgentSnapshot` surfaces +`.status`, `.messages`, `.artifacts`, and typed `.custom` state directly. + +```dart +import 'package:genkit/client.dart'; +import 'package:genkit/experimental_client.dart'; // remoteAgent, DetachedTask + +final agent = remoteAgent(url: '/api/backgroundAgent'); + +// Submit — resolves immediately with a handle. +final task = await agent.chat().detach(text: 'Quantum computing impact'); +print(task.snapshotId); + +// Poll until a terminal status. +await for (final snap in task.poll(interval: Duration(milliseconds: 1500))) { + final status = snap.status?.value ?? 'pending'; + if (status == 'completed') { + final messages = snap.messages; + final lastModel = + messages.where((m) => m.role == Role.model).lastOrNull; + final text = + (lastModel?.content ?? []).map((p) => p.text ?? '').join(); + print(text); + break; + } else if (status == 'failed') { + throw Exception('Background task failed on the server.'); + } else if (status == 'aborted' || status == 'expired') { + break; + } + // 'pending' → keep polling +} + +// Abort an in-flight task at any time: +await task.abort(); +``` + +## Server-side: detach + wait + +You can also detach from a server-side chat and wait for the result. `task.wait` +polls the store until a terminal state and resolves with the final snapshot. + +```dart +final chat = backgroundAgent.chat(); +final task = await chat.detach(text: 'Write a report on renewable energy trends'); +print(task.snapshotId); // available immediately + +final snapshot = await task.wait(interval: Duration(seconds: 2)); +print(snapshot?.status?.value); // 'completed' | 'failed' | 'aborted' | 'expired' +``` + +## Status values + +- `pending` — still processing. +- `completed` — completed successfully (read the result from `snapshot.messages`). +- `failed` — error during processing. +- `aborted` — cancelled by the client via `abort()`. +- `expired` — the background worker stopped responding (e.g. server restart); + terminal, the task can never complete. + +> Equivalent low-level wire protocol: the client sends `{ detach: true }` with +> the message; `poll`/`wait` hit the agent's `getSnapshot` action, and `abort` +> hits the `abort` action. `remoteAgent` wraps all of this for you. diff --git a/plugins/genkit/.agents/skills/developing-genkit-dart/references/agents-branching.md b/plugins/genkit/.agents/skills/developing-genkit-dart/references/agents-branching.md new file mode 100644 index 00000000..65f1573e --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-dart/references/agents-branching.md @@ -0,0 +1,103 @@ +# Agent Branching + +> Requires a [session store](agents-sessions.md) so snapshots are persistent. +> Read [agents.md](agents.md) first. + +A `snapshotId` is an **immutable checkpoint**, like a git commit. You can fork as +many independent timelines as you want from the same snapshot — each turn from a +snapshot creates a new, independent snapshot; the original is unchanged. + +To branch, open a new `chat` attached to an earlier snapshot via +`agent.chat(snapshotId: ...)`. + +## Server-side branching + +```dart +import 'package:genkit/genkit.dart'; +import 'package:genkit/experimental.dart'; // defineAgent, InMemorySessionStore + +import 'genkit.dart'; + +final assistant = ai.defineAgent( + name: 'assistant', + system: 'You are a helpful assistant.', + store: InMemorySessionStore(), +); + +final root = assistant.chat(); +final res1 = await root.send(text: 'Hello!'); +final checkpoint = res1.snapshotId; // branch point + +// Branch A — forks from `checkpoint`. +final branchA = assistant.chat(snapshotId: checkpoint); +await branchA.send(text: 'My name is Bob.'); +final resA = await branchA.send(text: 'What is my name?'); // -> Bob + +// Branch B — forks from the SAME `checkpoint`, fully independent. +final branchB = assistant.chat(snapshotId: checkpoint); +await branchB.send(text: 'My name is John.'); +final resB = await branchB.send(text: 'What is my name?'); // -> John +``` + +## Client-side branching ("pick a variant") + +A common pattern: generate two variants from the same checkpoint in parallel, +let the user pick one, and continue from the chosen snapshot. + +```dart +import 'package:genkit/client.dart'; +import 'package:genkit/experimental_client.dart'; // remoteAgent, AgentChat + +final agent = remoteAgent(url: '/api/branchingAgent'); +String? snapshotId; // current branch point + +Future<(AgentResponse, AgentResponse)> twoVariants(String text) async { + // Each variant gets its own chat branching from the same snapshot + // (or a fresh session when there's no branch point yet). + AgentChat makeChat() => + snapshotId != null ? agent.chat(snapshotId: snapshotId) : agent.chat(); + + final results = await Future.wait([ + makeChat().send(text: text), + makeChat().send(text: text), + ]); + // results[0].snapshotId != results[1].snapshotId — both branch from the + // same point. + return (results[0], results[1]); +} + +// When the user picks a variant, its snapshotId becomes the new branch point: +void pick(String chosenSnapshotId) { + snapshotId = chosenSnapshotId; +} +``` + +## Restoring history from a snapshot + +Use `agent.getSnapshot(snapshotId: ...)` to read a snapshot's state without +starting a turn — handy for restoring a UI after a reload (e.g. a snapshotId +stored in the URL). The server must expose the agent's `getSnapshotDataAction` +(see [agents.md](agents.md#serve-an-agent-over-http)). + +`getSnapshot(...)` returns an `AgentSnapshot` — a typed veneer that +surfaces `.messages`, `.artifacts`, and typed `.custom` state directly (use +`.sessionState` for the raw `SessionState` if you need it). + +```dart +import 'package:genkit/client.dart'; +import 'package:genkit/experimental_client.dart'; // remoteAgent, AgentSnapshot + +final agent = remoteAgent(url: '/api/branchingAgent'); + +final snapshot = await agent.getSnapshot(snapshotId: snapshotId); +final history = (snapshot?.messages ?? []) + .where((m) => m.role == Role.user || m.role == Role.model) + .map((m) => ( + role: m.role.value, + text: m.content.map((p) => p.text ?? '').join(), + )) + .toList(); +``` + +> Abandoned branches simply remain in the store as immutable snapshots; nothing +> is overwritten when you branch. diff --git a/plugins/genkit/.agents/skills/developing-genkit-dart/references/agents-custom.md b/plugins/genkit/.agents/skills/developing-genkit-dart/references/agents-custom.md new file mode 100644 index 00000000..747aff2c --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-dart/references/agents-custom.md @@ -0,0 +1,192 @@ +# Advanced Custom Agents — `defineCustomAgent` + +> `ai.defineCustomAgent` is experimental and comes from +> `package:genkit/experimental.dart`. Read [agents.md](agents.md) and +> [agent state](agents-state.md) first. + +`defineAgent` runs a single prompt + tool loop. When you need **full control of +the turn** — multiple sequential model calls, custom logic between them, manual +message/state management, or custom progress streaming — use +`ai.defineCustomAgent`. You provide the handler that runs the turn. + +## When to use it + +Reach for `defineCustomAgent` when a turn needs to: + +- make **multiple model calls** with your own orchestration between them; +- run **multi-step workflows** (decompose → research → synthesize); +- **manually manage** messages and custom state; +- **stream custom status** updates to the client mid-turn. + +Otherwise prefer `defineAgent` (simpler; custom state still works — see +[agent state](agents-state.md)). + +## Signature + +```dart +Agent defineCustomAgent({ + required String name, + String? description, + SchemanticType? stateSchema, + SessionStore? store, + ClientTransform? clientTransform, + required AgentFn fn, // (SessionRunner sess, AgentFnOptions options) => Future +}); +``` + +The handler receives a session runner `sess` and `AgentFnOptions options` +(`options.sendChunk(...)` streams chunks to the client; `options.context` holds +the ambient [per-turn context](agents.md#per-turn-ambient-context) — e.g. auth, +derived server-side for remote agents). It returns an `AgentResult`, typically +carrying the final `message` for the turn (`AgentResult.message` is nullable — +pass `message: null` when the turn produced no message). + +Key `sess` methods: + +- `sess.run((input, ctx) async {...})` — runs the turn; adds `input.message` + (the incoming user message) to the session before calling your callback, so + `sess.getMessages()` includes it. `input.message?.content` holds the parts. +- `sess.getMessages()` — the full message history. +- `sess.addMessages([...])` — append messages (e.g. your final model response). +- `sess.updateCustom(mutator)` / `sess.getCustom()` — read and mutate typed + custom state directly on the runner (no `ai.currentSession()` needed). The + mutator is `(State? state) => State`; each call auto-emits a `customPatch` + chunk. Its typing follows `stateSchema` — with the map schema below the state + is `Map?`; with a generated `.$schema` it's your typed class. + +## Example: multi-step research agent + +```dart +import 'package:genkit/genkit.dart'; +import 'package:genkit/experimental.dart'; // defineCustomAgent, SessionRunner +import 'package:schemantic/schemantic.dart'; + +import 'genkit.dart'; + +Map _state(Map? custom) { + if (custom == null) { + return {'subQuestions': [], 'subAnswers': []}; + } + return custom; +} + +final researchAgent = ai.defineCustomAgent( + name: 'researchAgent', + stateSchema: .map(.string(), .dynamicSchema()), + fn: (sess, options) async { + Message? lastMessage; + + await sess.run((input, ctx) async { + final userText = input.message?.content.firstOrNull?.text ?? ''; + + // Step 1: decompose (a fast model). Mutating custom state auto-emits a + // `customPatch` chunk so the client's tracked state stays live. + sess.updateCustom((s) { + final state = _state(s); + state['status'] = 'Decomposing question into sub-topics…'; + return state; + }); + + final decompose = await ai.generate( + model: liteModel, + prompt: 'Break this into 2-3 sub-questions. Return ONLY a JSON array ' + 'of strings.\nUser question: "$userText"', + use: [retry()], + outputFormat: 'json', + outputSchema: SchemanticType.list(SchemanticType.string()), + ); + final subQuestions = + (decompose.output ?? [userText]).map((e) => e.toString()).toList(); + + sess.updateCustom((s) { + final state = _state(s); + state['subQuestions'] = subQuestions; + state['subAnswers'] = []; + return state; + }); + + // Step 2: research each sub-question (main model). + final subAnswers = >[]; + for (var i = 0; i < subQuestions.length; i++) { + sess.updateCustom((s) { + final state = _state(s); + state['status'] = + 'Researching (${i + 1}/${subQuestions.length})'; + return state; + }); + final research = await ai.generate( + use: [retry()], + prompt: 'Answer concisely in 2-3 paragraphs.\n\n' + 'Question: ${subQuestions[i]}', + ); + subAnswers.add({'question': subQuestions[i], 'answer': research.text}); + } + sess.updateCustom((s) { + final state = _state(s); + state['subAnswers'] = subAnswers; + return state; + }); + + // Step 3: synthesize and STREAM the final answer to the client. + sess.updateCustom((s) { + final state = _state(s); + state['status'] = 'Synthesizing final response…'; + return state; + }); + + final synthesis = ai.generateStream( + use: [retry()], + prompt: 'Synthesize a unified markdown answer to "$userText" from:\n' + '${subAnswers.map((a) => '${a['question']}: ${a['answer']}').join('\n\n')}', + ); + await for (final chunk in synthesis) { + options.sendChunk(AgentStreamChunk(modelChunk: chunk.rawChunk)); + } + final finalResponse = await synthesis.onResult; + lastMessage = finalResponse.message; + if (lastMessage != null) sess.addMessages([lastMessage!]); + + sess.updateCustom((s) { + final state = _state(s); + state['status'] = 'Done'; + return state; + }); + + return null; + }); + + return AgentResult( + message: lastMessage ?? + Message( + role: Role.model, + content: [TextPart(text: 'Research complete.')], + ), + ); + }, +); +``` + +## Custom status streaming + +Calling `sess.updateCustom(...)` during the turn automatically emits a +`customPatch` chunk, so the `remoteAgent` client's tracked +[custom state](agents-state.md) (e.g. the `status` field) stays live +**mid-stream** without any extra wiring. Stream model output separately with +`options.sendChunk(AgentStreamChunk(modelChunk: ...))`. + +Seed and run a custom agent exactly like a regular one: + +```dart +final chat = researchAgent.chat( + state: SessionState( + custom: {'subQuestions': [], 'subAnswers': []}, + messages: [], + artifacts: [], + ), +); +final turn = chat.sendStream(text: 'Impacts of electric vehicles?'); +await for (final chunk in turn.stream) { + // chunk.text for model output; chunk.custom['status'] for live progress +} +await turn.response; +``` diff --git a/plugins/genkit/.agents/skills/developing-genkit-dart/references/agents-deployment.md b/plugins/genkit/.agents/skills/developing-genkit-dart/references/agents-deployment.md new file mode 100644 index 00000000..a173666f --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-dart/references/agents-deployment.md @@ -0,0 +1,116 @@ +# Deploying / Serving Agents over HTTP + +> Uses `shelfHandler` from `package:genkit_shelf` and the agent's companion +> actions. Read [agents.md](agents.md) first. For the general `genkit_shelf` +> reference, see [genkit_shelf.md](genkit_shelf.md). + +An agent is served as HTTP endpoints. Each `Agent` exposes three actions: + +- `agent.action` → `POST /api/` — the main turn action. +- `agent.getSnapshotDataAction` → `POST /api//getSnapshot` — read a + snapshot's state. Needed for [snapshot restore](agents-branching.md) and + [background](agents-background.md) polling. +- `agent.abortAgentAction` → `POST /api//abort` — cancel a + [background](agents-background.md) turn. + +These paths match the `remoteAgent` client defaults (`${url}/getSnapshot`, +`${url}/abort`), so a client only needs the base `url`. + +## A reusable `mountAgent` helper + +When serving several agents, a small helper keeps registration consistent. + +```dart +import 'package:genkit/genkit.dart'; +import 'package:genkit/experimental.dart'; // Agent type +import 'package:genkit_shelf/genkit_shelf.dart'; +import 'package:shelf_router/shelf_router.dart'; + +import 'package:agents_sample/weather_agent.dart'; +import 'package:agents_sample/background_agent.dart'; + +/// Mounts an agent's turn action plus its `/getSnapshot` and `/abort` actions. +void mountAgent(Router router, String path, Agent agent) { + router.post('/api/$path', shelfHandler(agent.action)); + router.post('/api/$path/getSnapshot', shelfHandler(agent.getSnapshotDataAction)); + router.post('/api/$path/abort', shelfHandler(agent.abortAgentAction)); +} + +final router = Router(); +mountAgent(router, 'weatherAgent', weatherAgent); +mountAgent(router, 'backgroundAgent', backgroundAgent); + +// A client-managed (stateless) agent only needs the turn action: +router.post( + '/api/weatherAgentStateless', + shelfHandler(weatherAgentStateless.action), +); +``` + +Which companions to enable: + +| Agent capability | `getSnapshot` | `abort` | +| --- | --- | --- | +| Plain chat (client- or server-state) | – | – | +| Snapshot restore / [branching](agents-branching.md) | ✓ | – | +| [Background](agents-background.md) / detach | ✓ | ✓ | + +## CORS for browser clients + +A browser `remoteAgent` calling a different origin (e.g. a Jaspr/Flutter web dev +server on another port) needs CORS. **Streaming requires the +`X-Genkit-Stream-Id` header** to be allowed. Use `shelf_cors_headers`. + +```dart +import 'dart:io'; + +import 'package:shelf/shelf.dart'; +import 'package:shelf/shelf_io.dart' as io; +import 'package:shelf_cors_headers/shelf_cors_headers.dart'; + +final handler = const Pipeline() + .addMiddleware(logRequests()) + .addMiddleware( + corsHeaders( + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Headers': + 'Content-Type, Accept, X-Genkit-Stream-Id', + 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', + }, + ), + ) + .addHandler(router.call); + +final port = int.tryParse(Platform.environment['PORT'] ?? '') ?? 8080; +final server = await io.serve(handler, InternetAddress.anyIPv4, port); +print('Agents API server running on http://localhost:${server.port}'); +``` + +## Exposing plain flows + +Agents are just actions, so you can serve ordinary flows the same way — e.g. +helper endpoints used by a UI: + +```dart +router.post('/api/workspace/files', shelfHandler(listWorkspaceFiles)); +router.post('/api/workspace/file', shelfHandler(readWorkspaceFile)); +``` + +## Registering agents/flows + +Agents register with Genkit when their defining top-level `final` is evaluated. +Importing the module (e.g. in your server's `bin/server.dart`) and referencing +the agent — as `mountAgent(...)` does — ensures the `defineAgent` call runs. + +## Notes + +- The wire body matches the Genkit client: `{ "data": , "init": }`. +- For persistence across restarts, use `FileSessionStore` + (`package:genkit/experimental_io.dart`) or `FirestoreSessionStore` + (`package:genkit_google_cloud`) instead of `InMemorySessionStore`. See + [sessions](agents-sessions.md). +- Consuming these endpoints from Dart/Flutter/web uses `remoteAgent` from + `package:genkit/experimental_client.dart` (alongside + `package:genkit/client.dart`) — see + [agents.md](agents.md#consume-an-agent-from-a-client-remoteagent). diff --git a/plugins/genkit/.agents/skills/developing-genkit-dart/references/agents-human-in-the-loop.md b/plugins/genkit/.agents/skills/developing-genkit-dart/references/agents-human-in-the-loop.md new file mode 100644 index 00000000..6f68e745 --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-dart/references/agents-human-in-the-loop.md @@ -0,0 +1,222 @@ +# Agent Human-in-the-Loop / Interrupts + +> Read [agents.md](agents.md) first. + +An **interrupt** pauses an agent mid-turn and hands control back to your code (or +a human) — e.g. to approve a sensitive action, collect missing input, or confirm +a plan. Internally it's a **tool call used as control flow**: the interrupt tool +never runs to completion on the server; it pauses the turn. You then **resume** +from the exact point it paused. + +> **Dart has no `defineInterrupt`.** Model an interrupt as a normal tool whose +> body returns `.interrupt(data)`. Omit `outputSchema`; the output is supplied by +> the caller on resume. (`ctx.interrupt(...)` still works but is soft-deprecated; +> prefer `return .interrupt(...)`.) + +Interrupts are **orthogonal to persistence** — they work the same whether the +agent uses a [session store](agents-sessions.md) or +[client-managed state](agents.md#client-managed-state-no-server-store). Just +resume on the same `chat` (or, for raw calls, carry the returned state/snapshot +back into the resume). + +Flow: `chat.send(text: ...)` → response has `res.interrupts` → collect human input +→ `chat.resume(respond: [...])`. + +## Define an interrupt (a tool that interrupts) + +Define it like a tool and add it to the agent's `tools`. Returning `.interrupt(...)` +pauses the turn; its argument is the data shown to the human. + +```dart +import 'package:genkit/genkit.dart'; +import 'package:genkit/experimental.dart'; // defineAgent, InMemorySessionStore +import 'package:schemantic/schemantic.dart'; + +import 'genkit.dart'; + +part 'banking_agent.g.dart'; + +@Schema() +abstract class $UserApprovalInput { + @Field(description: 'The action to be approved') + String get action; + @Field(description: 'Details about the action') + String get details; +} + +@Schema() +abstract class $TransferMoneyInput { + double get amount; + String get toAccount; +} + +@Schema() +abstract class $TransferMoneyOutput { + bool get success; + String get transactionId; +} + +/// Interrupt: always pauses. The caller provides `{ approved, feedback }` on +/// resume. No `outputSchema` — the output comes from the resume call. +final userApproval = ai.defineTool( + name: 'userApproval', + description: 'Ask the user for approval before a sensitive action.', + inputSchema: UserApprovalInput.$schema, + fn: (input, ctx) async => .interrupt(), +); + +/// Executes the transfer. Only reached after the user approves. +final transferMoney = ai.defineTool( + name: 'transferMoney', + description: 'Transfer money to a specified account.', + inputSchema: TransferMoneyInput.$schema, + outputSchema: TransferMoneyOutput.$schema, + fn: (input, _) async => .response(TransferMoneyOutput( + success: true, + transactionId: 'txn-${DateTime.now().millisecondsSinceEpoch}', + )), +); + +final bankingAgent = ai.defineAgent( + name: 'bankingAgent', + system: 'You are a banking assistant. ALWAYS use the userApproval interrupt ' + 'to confirm before executing transferMoney.', + tools: [userApproval, transferMoney], + use: [retry()], + store: InMemorySessionStore(), +); +``` + +## Detect and resume (server-side) + +`res.interrupts` is non-empty when the agent paused. Each `AgentInterrupt` +exposes: + +- `.name` — the interrupt's name. +- `.input` — the data the model passed in. +- `.respond(output)` — **builder** returning a resume entry that supplies the + tool's output (without executing it). Does **not** send. +- `.restart([payload])` — **builder** re-issuing the original tool request (retry + / let the tool actually run). Pass an optional `Map` payload; it is nested + under `metadata.resumed` and read back tool-side via `ctx.resumed`. Does + **not** send. + +Resume the **same** chat with `chat.resume(...)` / `chat.resumeStream(...)`, +passing `respond` / `restart` entries directly: + +```dart +final chat = bankingAgent.chat(); +var res = await chat.send(text: 'Transfer \$500 to my savings account.'); + +final approval = + res.interrupts.where((i) => i.name == 'userApproval').firstOrNull; +if (approval != null) { + print(approval.input); // { action, details } — show this to the human + + // Collect the human decision, then resume with the interrupt's output: + res = await chat.resume( + respond: [ + approval.respond({'approved': true, 'feedback': 'Looks good'}), + ], + ); +} +print(res.text); // final confirmation +``` + +Streaming variant: + +```dart +final turn = chat.resumeStream( + respond: [approval.respond({'approved': true})], +); +await for (final chunk in turn.stream) { + stdout.write(chunk.text); +} +final res = await turn.response; +``` + +You can resume multiple interrupts at once by passing several builders, and mix +`respond` (supply output) with `restart` (re-run the tool): + +```dart +await chat.resume( + respond: [a.respond({'approved': true})], + restart: [b.restart()], +); +``` + +## Client-side (browser) interrupts + +The same pattern works over HTTP with `remoteAgent` from +`package:genkit/client.dart`. The client tracks the snapshot, so resuming the +same `chat` continues exactly where it paused. + +```dart +import 'package:genkit/client.dart'; +import 'package:genkit/experimental_client.dart'; // remoteAgent, AgentInterrupt + +final agent = remoteAgent(url: '/api/bankingAgent'); +final chat = agent.chat(); + +// 1. Send and detect the pause. +final res = await chat.send(text: 'Transfer \$500 to savings.'); +final pending = + res.interrupts.where((i) => i.name == 'userApproval').firstOrNull; + +if (pending != null) { + // pending.input → { action, details }; render an approval dialog. + + // 2. After the human approves/denies, resume the SAME chat. + final turn = chat.resumeStream( + respond: [pending.respond({'approved': true, 'feedback': 'ok'})], + ); + await for (final chunk in turn.stream) { + /* render chunk.text */ + } + final finalRes = await turn.response; + // If finalRes.interrupts is non-empty, the agent paused again — repeat. +} +``` + +## Interrupts with tool-approval middleware + +The [`toolApproval`](genkit_middleware.md) middleware turns selected tools into +approval interrupts without any custom interrupt code. The middleware gates each +tool call itself: a tool is allowed to run only if it is in the `approved` list +or its `resumed` payload carries `{ 'tool-approved': true }`. To let an approved +tool through on resume, **restart** the paused interrupt with that payload — the +`.restart(...)` builder nests it under `metadata.resumed`, exactly what the +middleware reads: + +```dart +// `interrupt` is the paused AgentInterrupt from `res.interrupts`. +// Pass this restart entry back when resuming the chat: +await chat.resume( + restart: [interrupt.restart({'tool-approved': true})], +); +``` + +If instead you write your own `.interrupt(...)` gate inside a tool (as in the +`coding_agent` sample), you inspect the resume payload yourself via the +`ctx.resumed` getter: + +```dart +final resumed = ctx.resumed; // the payload passed to `.restart(...)` +final isApproved = resumed is Map && resumed['tool-approved'] == true; +``` + +## Notes & gotchas + +- **No store required.** Interrupts work with either a + [session store](agents-sessions.md) or + [client-managed state](agents.md#client-managed-state-no-server-store). +- **`respond`/`restart` are builders.** They return resume entries; they do not + send. You still call `chat.resume(...)`. +- **Resume validation.** The server validates each `respond`/`restart` entry + against the conversation history — always build entries from the interrupt + objects in the response, not hand-rolled parts. +- **Re-pausing.** After resuming, the new response may interrupt again; loop + until `res.interrupts` is empty. +- **UX tip:** don't render a model message bubble for an interrupted turn; show + the approval UI from `interrupt.input` instead, then render the model's reply + after resuming. diff --git a/plugins/genkit/.agents/skills/developing-genkit-dart/references/agents-multi-agent.md b/plugins/genkit/.agents/skills/developing-genkit-dart/references/agents-multi-agent.md new file mode 100644 index 00000000..af7d8a3d --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-dart/references/agents-multi-agent.md @@ -0,0 +1,157 @@ +# Multi-Agent Orchestration / Sub-Agents + +> Sub-agent delegation uses the `agents()` middleware from +> `package:genkit_middleware/agents.dart`. Read [agents.md](agents.md) first. + +A common pattern is an **orchestrator** agent that delegates tasks to +specialized **sub-agents** (e.g. a `researcher` and a `coder`). The `agents()` +middleware injects one delegation tool per sub-agent (`delegate_to_`), +appends a `` block to the orchestrator's system prompt, and — when +the model calls a delegation tool — runs the sub-agent and returns its response +as the tool result. + +To make the middleware available, register `AgentsPlugin()` on the `Genkit` +instance: + +```dart +import 'package:genkit_middleware/agents.dart'; + +final ai = Genkit(plugins: [googleAI(), AgentsPlugin(), RetryPlugin()]); +``` + +## 1. Define the sub-agents + +Give each sub-agent a `description` — it's auto-discovered from registry metadata +and shown to the orchestrator so the model knows when to delegate. + +```dart +import 'package:genkit/experimental.dart'; // defineAgent + +import 'genkit.dart'; + +final researcher = ai.defineAgent( + name: 'researcher', + description: + 'A thorough research assistant that provides well-sourced answers.', + system: 'You are a thorough research assistant. When asked a question, ' + 'provide a clear, well-structured, and well-sourced answer.', + maxTurns: 10, +); + +final coder = ai.defineAgent( + name: 'coder', + description: 'Writes, debugs, and explains code. Use for any programming tasks.', + system: 'You are an expert programmer. Provide clean, well-commented code ' + 'with explanations. Use Dart by default unless asked otherwise.', + maxTurns: 10, +); +``` + +## 2. Wire up the orchestrator + +Add `agents()` to the orchestrator's `use: [...]`. Pass the sub-agent **names**; +their descriptions are auto-discovered from the registry. + +```dart +import 'package:genkit/genkit.dart'; +import 'package:genkit/experimental.dart'; // defineAgent, InMemorySessionStore +import 'package:genkit_middleware/agents.dart'; + +import 'genkit.dart'; + +final orchestratorAgent = ai.defineAgent( + name: 'orchestratorAgent', + system: ''' +You are a helpful project assistant. + +Analyze the user's request and delegate to the appropriate sub-agent. +If the request requires both research AND code, call them sequentially. +After receiving sub-agent responses, synthesize a final answer for the user.''', + use: [ + agents( + agents: ['researcher', 'coder'], + maxDelegations: 5, // guard rail against runaway delegation loops + historyLength: 4, // forward the last N user/model messages as context + ), + ], + store: InMemorySessionStore(), +); +``` + +Run it like any other agent: + +```dart +final chat = orchestratorAgent.chat(); +final turn = chat.sendStream( + text: 'Research the best sorting algorithms, then write a Dart quicksort.', +); +await for (final chunk in turn.stream) { + stdout.write(chunk.text); +} +final res = await turn.response; +``` + +## `agents()` options + +- `agents` (required): `List` of sub-agent names. Each description is + auto-discovered from the registry. +- `toolPrefix`: prefix for generated tool names. Defaults to `delegate_to` → + `delegate_to_`. +- `maxDelegations`: max delegations per `generate` call. Prevents runaway loops. +- `historyLength`: number of recent user/model messages forwarded to sub-agents + as context. `0`/omitted sends only the task description. +- `artifactStrategy`: `'inline'` (default) or `'session'` — see below. +- `async`: enable background delegation — see below. + +## Background (async) delegation + +Set `async: true` to let the orchestrator run sub-agents in the background. Each +delegation tool then accepts a `background` flag that starts the sub-agent and +returns a `taskId` immediately, and the middleware adds +`check_background_tasks`, `wait_for_background_tasks`, and +`abort_background_tasks` tools so the model can launch several tasks in parallel +and collect them later. A `continue_task` tool lets the model retry a +failed/aborted task or follow up on a completed one. + +```dart +final orchestratorAgent = ai.defineAgent( + name: 'orchestratorAgent', + system: 'Delegate independent tasks in the background, then collect the ' + 'results before answering.', + use: [ + agents( + agents: ['researcher', 'coder'], + async: true, // adds background delegation + the background-task tools + ), + ], + store: InMemorySessionStore(), +); +``` + +> Background delegation requires **server-managed sub-agents** — each sub-agent +> needs a session `store` that supports detach (see +> [background agents](agents-background.md)). A sub-agent without a store cannot +> be run in the background. + +## Sharing artifacts between agents + +Sub-agents can produce [artifacts](agents-artifacts.md). `artifactStrategy` +controls how they reach the orchestrator: + +- `'inline'` (default): artifact content is included in the delegation tool + result (so the model sees it directly) **and** merged into the parent session. +- `'session'`: artifacts are merged into the parent session only; the tool result + lists artifact names, not content. Merged artifacts are namespaced by an + invocation id (`/`). + +## Other middleware + +`package:genkit_middleware` also exports `filesystem`, `skills`, and +`toolApproval`; `retry` ships in core `package:genkit`. They attach the same way +via `use: [...]` on an agent (or on `ai.generate`) — see +[using middleware](genkit_middleware.md). `retry()` is commonly paired with +delegation. + +> Note: if a sub-agent triggers an [interrupt](agents-human-in-the-loop.md), it +> is reported back to the orchestrator as a normal tool response (not propagated +> as a resumable interrupt). Delegate self-contained tasks. diff --git a/plugins/genkit/.agents/skills/developing-genkit-dart/references/agents-sessions.md b/plugins/genkit/.agents/skills/developing-genkit-dart/references/agents-sessions.md new file mode 100644 index 00000000..3b63904c --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-dart/references/agents-sessions.md @@ -0,0 +1,192 @@ +# Agent Sessions & Persistence + +> Sessions are experimental. `InMemorySessionStore` and `Session`/`SessionStore` +> come from `package:genkit/experimental.dart`; `FileSessionStore` from +> `package:genkit/experimental_io.dart` (needs `dart:io`); `FirestoreSessionStore` +> from `package:genkit_google_cloud`. See [agents.md](agents.md) for the basics +> and the experimental-import note first. + +When an agent has a `store`, the **server** owns the session history. Each turn +produces an immutable **snapshot**; the snapshot chain is what carries +conversation state forward. A store also enables [branching](agents-branching.md) +and [background execution](agents-background.md). (Interrupts work with or without +a store — see [human-in-the-loop](agents-human-in-the-loop.md).) + +## Pick a store + +```dart +import 'package:genkit/experimental.dart'; // InMemorySessionStore +import 'package:genkit/experimental_io.dart'; // FileSessionStore (dart:io) + +// In-memory: great for tests/dev; lost on restart. +final memStore = InMemorySessionStore(); + +// File-backed: snapshots persisted as JSON under /global/. +final fileStore = FileSessionStore('./.snapshots'); + +// File store with chain pruning — keep only the last N snapshots in a chain. +final pruning = FileSessionStore('./.snapshots', maxPersistedChainLength: 3); +``` + +Attach it to the agent: + +```dart +import 'genkit.dart'; + +final logbookAgent = ai.defineAgent( + name: 'logbookAgent', + system: 'You are a personal logbook assistant.', + store: fileStore, +); +``` + +A single `chat` persists to the store and threads the snapshot forward +automatically: + +```dart +final chat = logbookAgent.chat(); +final res1 = await chat.send(text: 'Log this: I started studying Genkit today.'); +final res2 = await chat.send(text: 'What did I study today?'); // remembers turn 1 +print('${res1.snapshotId} ${res2.snapshotId}'); +``` + +Resume a prior conversation by snapshot id (server-side): + +```dart +// Continue an existing session from a snapshot, restoring its history. +final resumed = await logbookAgent.loadChat(snapshotId: res2.snapshotId); +await resumed.send(text: 'Add another note.'); +``` + +`agent.chat(snapshotId: ...)` opens a new chat that **branches** from a snapshot +without pre-loading its history; `agent.loadChat(...)` returns a chat with the +history restored. See [branching](agents-branching.md). + +## Typed session state + +Use `stateSchema` to attach typed custom state to the session. It is validated +when a snapshot is loaded. Seed initial custom state when opening the chat via +the `state` argument (a `SessionState`, whose `custom` field holds your data). + +```dart +import 'package:genkit/genkit.dart'; +import 'package:genkit/experimental.dart'; // defineAgent, InMemorySessionStore +import 'package:schemantic/schemantic.dart'; + +import 'genkit.dart'; + +part 'profile_agent.g.dart'; + +@Schema() +abstract class $Profile { + String get name; + String get tier; // 'free' | 'pro' +} + +final profileAgent = ai.defineAgent( + name: 'profileAgent', + system: 'Greet the user by name and tailor answers to their tier.', + stateSchema: Profile.$schema, + store: InMemorySessionStore(), +); + +// Custom state lives under `.custom` of the SessionState. +final chat = profileAgent.chat( + state: SessionState( + custom: {'name': 'Ada', 'tier': 'pro'}, + messages: [], + artifacts: [], + ), +); +``` + +See [working with state](agents-state.md) for reading/mutating custom state +inside tools and syncing it to the client. + +## Interrupts (human-in-the-loop) + +Interrupts pause a turn so a human can approve/provide input, then resume from +the exact pause point. They work with a store or with client-managed state +(persistence is orthogonal). See the dedicated reference: +[Human-in-the-loop / interrupts](agents-human-in-the-loop.md). + +## Firestore session store (scalable) + +For production, `FirestoreSessionStore` (from `package:genkit_google_cloud`) +persists each turn as an incremental JSON Patch diff anchored to periodic sharded +checkpoints — no single document approaches Firestore's 1 MiB limit, and +reads/writes per turn are bounded by `checkpointInterval` rather than total +session length (scales to long-lived chat/coding agents). + +```dart +import 'package:genkit/genkit.dart'; +import 'package:genkit/experimental.dart'; // defineAgent +import 'package:genkit_google_cloud/firestore_session_store.dart'; + +import 'genkit.dart'; + +final myAgent = ai.defineAgent( + name: 'myAgent', + system: 'You are a helpful assistant.', + // Defaults to a new Firestore() using Application Default Credentials. + store: FirestoreSessionStore(), +); +``` + +> **Project id.** ADC alone may not carry a project id, so the default +> `Firestore()` can fail at write time with `Project ID has not been discovered +> yet. Initialize the SDK with credentials that include a project ID, set project +> ID in Settings, or set the GOOGLE_CLOUD_PROJECT environment variable.` Export +> `GOOGLE_CLOUD_PROJECT` (the Firestore client also reads a small set of standard +> GCP project-id env vars), or pass an explicit `Firestore` via `db`. + +Options: + +- `db`: explicit `Firestore` instance (defaults to a new `Firestore()`, honoring + `FIRESTORE_EMULATOR_HOST`). +- `collection`: snapshot collection (default `'genkit-sessions'`). Companion + collections `'-pointers'` and `'-shards'` are derived. +- `checkpointInterval`: turns between full-state checkpoints (default `25`). + Lower for small, read-heavy state; raise for large per-turn state. +- `shardSize`: max bytes per shard/diff document (default `512 KiB`). +- `snapshotPathPrefix`: `String Function(Map? context)?` — a + per-tenant prefix derived from the call context (defaults to `'global'`). + +## Implementing a custom `SessionStore` + +Implement the `SessionStore` interface. `getSnapshot` loads by `snapshotId` OR +`sessionId`; `saveSnapshot` atomically reads → mutates → persists. Optionally +implement `SnapshotChangeNotifier.onSnapshotStateChange` (used by background +agents) to subscribe to snapshot status changes. + +```dart +import 'package:genkit/genkit.dart'; +import 'package:genkit/experimental.dart'; // SessionStore, SessionSnapshot, SnapshotMutator + +class MySessionStore implements SessionStore { + @override + Future getSnapshot({ + String? snapshotId, + String? sessionId, + Map? context, + }) async { + // Load and return a snapshot, or null. + return null; + } + + @override + Future saveSnapshot( + String? snapshotId, + SnapshotMutator mutator, { + Map? context, + }) async { + // Read → mutate → persist; return the snapshotId used, or null when the + // mutator returns null. + return snapshotId; + } +} +``` + +> Check the exact `SessionStore` method signatures in your installed version with +> `dart doc` or by inspecting `package:genkit` — the shape above matches the +> in-memory and file stores. diff --git a/plugins/genkit/.agents/skills/developing-genkit-dart/references/agents-state.md b/plugins/genkit/.agents/skills/developing-genkit-dart/references/agents-state.md new file mode 100644 index 00000000..7aad1957 --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-dart/references/agents-state.md @@ -0,0 +1,215 @@ +# Working with Agent State + +> Read [agents.md](agents.md) first. + +Beyond message history, an agent session can hold typed **custom state** — your +own structured data (a task list, a workflow status, counters, etc.). Tools read +and mutate it during a turn, and it is automatically synced to the +[`remoteAgent`](agents.md#consume-an-agent-from-a-client-remoteagent) client via +`customPatch` chunks (so the client's tracked state stays live mid-stream). + +## Declare the state shape + +Pass a `stateSchema` (a schemantic `.$schema`) to `defineAgent`. It's validated +when a snapshot is loaded. + +```dart +import 'package:genkit/genkit.dart'; +import 'package:genkit/experimental.dart'; // defineAgent, currentSession +import 'package:schemantic/schemantic.dart'; + +import 'genkit.dart'; + +part 'task_agent.g.dart'; + +@Schema() +abstract class $TaskItem { + int get id; + String get title; + bool get done; +} + +@Schema() +abstract class $TaskState { + List<$TaskItem> get tasks; + int get nextId; +} + +final taskAgent = ai.defineAgent( + name: 'taskAgent', + stateSchema: TaskState.$schema, + system: "You manage the user's task list. Use the tools to modify it.", + tools: [addTask /*, toggleTask, removeTask */], + use: [retry()], +); +``` + +`stateSchema` works with the standard `defineAgent` — you do **not** need +`defineCustomAgent` for custom state. + +## Read & mutate state inside tools + +Tools call `ai.currentSession()` to access the live, **typed** session, +then `session.getCustom()` / `session.updateCustom(mutator)`. `updateCustom` is +fully typed: the mutator is `(State? state) => State` — it receives the current +typed state (`null` before it's first set) and returns the new state. Each call +auto-emits a `customPatch` chunk to the client. + +```dart +@Schema() +abstract class $AddTaskInput { + @Field(description: 'Short description of the task') + String get title; +} + +@Schema() +abstract class $ToggleTaskInput { + @Field(description: 'The task ID to toggle') + int get id; +} + +final addTask = ai.defineTool( + name: 'addTask', + description: 'Add a new task. Returns the created task.', + inputSchema: AddTaskInput.$schema, + outputSchema: TaskItem.$schema, + fn: (input, _) async { + final session = ai.currentSession()!; + late TaskItem newTask; + session.updateCustom((state) { + state ??= TaskState(tasks: [], nextId: 1); + newTask = TaskItem(id: state.nextId, title: input.title, done: false); + state.tasks = [...state.tasks, newTask]; + state.nextId += 1; + return state; + }); + return .response(newTask); + }, +); +``` + +Because the state is typed, you work with the generated `TaskState` / `TaskItem` +classes directly — no manual `Map` munging or `fromJson`. Assign back to the +setters (e.g. `state.tasks = [...]`) so the mutated values are written to the +session. + +> **Make computed/optional state fields nullable.** Non-nullable schemantic +> getters are hard casts and throw (`Null is not a subtype of num`) when a +> partially populated state blob is reloaded (e.g. a computed field that was +> never written). Make such fields nullable or give them a `defaultValue`. See +> [non-nullable getters throw on partial data](schemantic.md#non-nullable-getters-throw-on-partial-data). + +> **Prefer whole-collection tools over append-one-item tools.** The model may +> emit several tool calls in a single **parallel** batch. Each +> `session.updateCustom` reads the current custom state, mutates, and writes it +> back; when the calls run in one batch they all read the same base and the last +> write wins, silently dropping the others. An incremental `addTask` tool is only +> safe if the model calls it sequentially. For anything the model may batch, +> prefer a single idempotent "replace the whole collection" tool (e.g. +> `setTasks(items: [...])`) that writes the entire collection in one call. + +`ai.currentSession()` returns `null` when called outside an active session +(e.g. a tool invoked without a running agent turn), so only use it inside agent +tools. + +For tools that look up and mutate an existing item, a small shared helper keeps +the not-found handling in one place: + +```dart +Map _mutateTaskById( + int id, + Map Function(List tasks, int idx) onFound, +) { + final session = ai.currentSession()!; + var result = {'success': false}; + session.updateCustom((state) { + state ??= TaskState(tasks: [], nextId: 1); + final tasks = state.tasks; + final idx = tasks.indexWhere((t) => t.id == id); + if (idx >= 0) { + result = onFound(tasks, idx); + // Reassign so the mutated list is written back to the session state. + state.tasks = tasks; + } else { + result = {'success': false, 'error': 'Task $id not found'}; + } + return state; + }); + return result; +} + +final toggleTask = ai.defineTool( + name: 'toggleTask', + description: 'Toggle a task done/undone by its ID.', + inputSchema: ToggleTaskInput.$schema, + fn: (input, _) async => .response(_mutateTaskById(input.id, (tasks, idx) { + tasks[idx].done = !tasks[idx].done; + return {'success': true, 'task': tasks[idx].toJson()}; + })), +); +``` + +## Seed and read state (server-side) + +Seed initial custom state when opening a chat. The `state` argument is a +`SessionState`: custom data goes under `.custom` (alongside `messages` and +`artifacts`). + +```dart +final chat = taskAgent.chat( + state: SessionState( + custom: {'tasks': [], 'nextId': 1}, + messages: [], + artifacts: [], + ), +); + +final res = await chat.send(text: 'Add a task: buy groceries'); +print(res.state); // res.state returns the custom state directly +``` + +> **Typed state.** When you supply a `stateSchema`, `res.state` / `chat.state` +> (and `snapshot.custom`) are **parsed into the typed `State` object** (here a +> `TaskState`), not a raw `Map`. `Agent`/`AgentChat`/`AgentResponse` are generic +> over `State`, so the type flows through automatically. Without a `stateSchema`, +> `state` is an untyped view over the JSON. + +## Auto-sync to the `remoteAgent` client + +When you talk to the agent over HTTP, the `remoteAgent` client tracks custom +state for you. Seed it the same way (`state.custom`), read live updates off each +streamed chunk (`chunk.custom`), and read the authoritative state off +`chat.state` after the turn completes. + +```dart +import 'package:genkit/client.dart'; +import 'package:genkit/experimental_client.dart'; // remoteAgent + +final agent = remoteAgent(url: '/api/taskAgent'); +final chat = agent.chat( + state: SessionState( + custom: {'tasks': [], 'nextId': 1}, + messages: [], + artifacts: [], + ), +); + +final turn = chat.sendStream(text: 'Add buy groceries, then mark it done'); +await for (final chunk in turn.stream) { + // Live custom state arrives via customPatch chunks: + if (chunk.custom != null) { + // e.g. render (chunk.custom as Map)['tasks'] + } + // chunk.text for model output +} +final res = await turn.response; + +// Authoritative state after the turn: +print(res.state); +print(chat.state); +``` + +State updates ride on the streamed chunks — there is no `onStateChange` +subscription. For live mid-stream status updates from a multi-step custom agent, +see [advanced custom agents](agents-custom.md), which emit `customPatch` chunks as +state changes. diff --git a/plugins/genkit/.agents/skills/developing-genkit-dart/references/agents.md b/plugins/genkit/.agents/skills/developing-genkit-dart/references/agents.md new file mode 100644 index 00000000..0887c34e --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-dart/references/agents.md @@ -0,0 +1,428 @@ +# Agents + +> **Agents are experimental.** The agent/session/snapshot APIs are NOT covered +> by semantic-versioning stability and live behind opt-in imports: +> +> - Server (agents, sessions, snapshots): `package:genkit/experimental.dart` +> (alongside the stable `package:genkit/genkit.dart`). +> - Browser/HTTP client (`remoteAgent`, `AgentChat`, ...): +> `package:genkit/experimental_client.dart` (alongside +> `package:genkit/client.dart`). +> - `dart:io` extras (`FileSessionStore`): `package:genkit/experimental_io.dart`. +> +> The entry points are marked `@experimental`, so importing them produces an +> `experimental_member_use` analyzer warning on the import line. Opt out once you +> accept the churn by adding to `analysis_options.yaml`: +> +> ```yaml +> analyzer: +> errors: +> experimental_member_use: ignore +> ``` +> +> `CancellationController` / `CancellationToken` are stable and stay on +> `package:genkit/genkit.dart` / `client.dart`, not behind these imports. + +An **agent** is a persistent, multi-turn conversation primitive built on top of +prompts + tools. Compared to a bare `ai.generate`/`ai.definePrompt` loop, an +agent adds: + +- **Sessions**: multi-turn history tracked as immutable **snapshots**. +- **State**: typed session state (messages + custom data + artifacts). +- **Interrupts**: human-in-the-loop pause/resume. +- **Branching**: fork a conversation from any snapshot. +- **Detaching**: run a turn in the background and poll for the result. + +Progressive disclosure — read the file for the level you need: + +- This file: defining an agent, serving it, and **client-managed state** (no store). +- [Sessions & persistence](agents-sessions.md): `SessionStore`, `InMemorySessionStore`, `FileSessionStore`, `FirestoreSessionStore`. +- [Human-in-the-loop / interrupts](agents-human-in-the-loop.md): pausing for approval/input and resuming. +- [Branching](agents-branching.md): forking a conversation from a snapshot. +- [Background agents](agents-background.md): detaching long-running turns and polling. +- [Working with state](agents-state.md): typed custom session state and client auto-sync. +- [Artifacts](agents-artifacts.md): producing/reading named deliverables. +- [Multi-agent orchestration](agents-multi-agent.md): delegating to sub-agents. +- [Advanced custom agents](agents-custom.md): `defineCustomAgent` for full turn control. +- [Deploying agents](agents-deployment.md): serving multiple agents over HTTP with `genkit_shelf`, CORS, other frameworks. + +## Setup + +Register the plugins your agents need on a shared `Genkit` instance. `retry` and +`RetryPlugin` ship with the core `package:genkit/genkit.dart`; the other agentic +middleware (`agents`, `filesystem`, `skills`, `toolApproval`) come from +`package:genkit_middleware`. + +```dart +// genkit.dart — shared instance + model refs. +import 'package:genkit/genkit.dart'; +import 'package:genkit_google_genai/genkit_google_genai.dart'; + +/// The default (capable) model used by most agents. +final ModelRef defaultModel = googleAI.gemini('gemini-flash-latest'); + +/// A fast/cheap model for auxiliary tasks. +final ModelRef liteModel = googleAI.gemini('gemini-flash-lite-latest'); + +final Genkit ai = Genkit( + plugins: [ + googleAI(), + RetryPlugin(), + ], + model: defaultModel, +); +``` + +## Define an agent + +`ai.defineAgent` combines prompt + tool config + (optional) session store into a +single registered action. + +```dart +import 'package:genkit/genkit.dart'; +import 'package:genkit/experimental.dart'; // defineAgent, InMemorySessionStore +import 'package:schemantic/schemantic.dart'; + +import 'genkit.dart'; + +part 'weather_agent.g.dart'; + +@Schema() +abstract class $GetWeatherInput { + String get location; +} + +@Schema() +abstract class $GetWeatherOutput { + String get weather; + String get temperature; +} + +final getWeather = ai.defineTool( + name: 'getWeather', + description: 'Get the current weather for a given location.', + inputSchema: GetWeatherInput.$schema, + outputSchema: GetWeatherOutput.$schema, + fn: (input, _) async => .response(GetWeatherOutput( + weather: 'Sunny in ${input.location}', + temperature: '71F', + )), +); + +final weatherAgent = ai.defineAgent( + name: 'weatherAgent', + system: 'You are a helpful weather assistant. Use the getWeather tool. ' + 'Be concise.', + tools: [getWeather], + use: [retry()], + store: InMemorySessionStore(), +); +``` + +Common `defineAgent` options: + +- `name` (required): action name. +- `description`: shown to an orchestrator that delegates to this agent (see + [multi-agent](agents-multi-agent.md)). +- `system` / `prompt`: the prompt template (same as `definePrompt`). +- `tools`: tools and interrupt-tools available to the agent. +- `model`: override the default model (e.g. `model: liteModel`). +- `use`: middleware layered on the turn (`retry()`, `filesystem(...)`, etc.). +- `store`: a `SessionStore` for server-side persistence. See [sessions](agents-sessions.md). +- `stateSchema`: a `SchemanticType` describing custom session state. +- `maxTurns`: cap the tool-calling loop (e.g. `maxTurns: 30`). + +> Schemas use the **schemantic** library (`@Schema()`, `.$schema`, `$`-prefixed +> abstract classes with a generated `.g.dart` part). See +> [references/schemantic.md](schemantic.md). + +> **An agent needs a model.** Set `model:` on the agent, or a default `model:` on +> the shared `Genkit` instance (as the [Setup](#setup) snippet does). Without +> either, the turn fails with +> `AgentError(INVALID_ARGUMENT): Model must be provided`. The examples here rely +> on the instance default, so if you copy an agent block without it, add a +> `model:`. + +## Agents and middleware go hand in hand + +Agents and [middleware](genkit_middleware.md) are built for each other: the +`use: [...]` array is where you layer in sophisticated behavior without writing +it yourself — sub-agent delegation, filesystem access, skill loading, tool +approval, retries — each is one line. + +```dart +import 'package:genkit/genkit.dart'; +import 'package:genkit/experimental.dart'; // defineAgent, InMemorySessionStore +import 'package:genkit_middleware/filesystem.dart'; +import 'package:genkit_middleware/skills.dart'; +import 'package:genkit_middleware/tool_approval.dart'; + +import 'genkit.dart'; + +final codingAgent = ai.defineAgent( + name: 'codingAgent', + system: 'You are an expert AI coding assistant working in a sandboxed ' + 'workspace.', + tools: [runShell, askUser], // your own custom tools/interrupts + use: [ + // Require user approval (interrupt) before risky tools; reads/run auto-approved. + // Order matters: keep toolApproval before filesystem. + toolApproval( + approved: ['list_files', 'read_file', 'use_skill', 'run_shell', 'ask_user'], + ), + // list_files / read_file / write_file / search_and_replace, sandboxed. + filesystem(rootDirectory: workspaceDir), + // Load coding conventions on demand via a use_skill tool. + skills(skillPaths: [skillsDir]), + // Automatic retry on transient model errors. + retry(), + ], + store: InMemorySessionStore(), // needed for tool approval + maxTurns: 30, +); +``` + +Register the corresponding plugins (`FilesystemPlugin()`, `SkillsPlugin()`, +`ToolApprovalPlugin()`, and `RetryPlugin()`) on the `Genkit` instance so the +`use: [...]` refs resolve at runtime. See [using middleware](genkit_middleware.md). + +## Chat with an agent (server-side) + +`agent.chat()` opens a conversation. A single `chat` carries state forward +automatically across turns. + +```dart +final chat = weatherAgent.chat(); + +// Non-streaming turn: +final res = await chat.send(text: 'Weather in Tokyo?'); +print(res.text); +print(res.snapshotId); // immutable checkpoint id for this turn + +// Follow-up turn — history is carried automatically: +final res2 = await chat.send(text: 'What about Paris?'); + +// Streaming turn: +final turn = chat.sendStream(text: 'And London?'); +await for (final chunk in turn.stream) { + stdout.write(chunk.text); +} +final finalRes = await turn.response; +``` + +### Per-turn ambient context + +`send`, `sendStream`, and `detach` all accept an optional `context` map — +ambient per-turn data (auth, request metadata, etc.) that tools can read via +`ctx.context` (and custom agents via `options.context`). + +```dart +final res = await chat.send( + text: 'What can I do?', + context: { + 'auth': {'name': 'Ada', 'tier': 'pro'}, + }, +); +``` + +> **In-process only.** The in-process transport honors `context`. The +> **`remoteAgent` (HTTP) transport rejects a non-empty `context` with +> `UnsupportedError`** — remote agents derive context server-side from the HTTP +> request (headers/auth), so don't pass it from the client. + +## Verify an agent from the CLI (`flow:run`) + +`genkit flow:run` only runs **flows**, not agents, so you can't `flow:run` an +agent directly. To exercise an agent from the CLI (e.g. a quick, self-terminating +check), wrap one turn in a throwaway flow and run that: + +```dart +final tryWeatherAgent = ai.defineFlow( + name: 'tryWeatherAgent', + fn: (String message, _) async => + (await weatherAgent.chat().send(text: message)).text, +); +// genkit flow:run tryWeatherAgent '"Weather in Tokyo?"' -- dart run main.dart +``` + +## Serve an agent over HTTP + +Use `shelfHandler` from `package:genkit_shelf`. Expose the main turn action, plus +the companion `getSnapshotDataAction` (state lookup/restore) and +`abortAgentAction` (background aborts) where needed. + +```dart +import 'package:genkit_shelf/genkit_shelf.dart'; +import 'package:shelf_router/shelf_router.dart'; + +final router = Router(); + +// Main turn endpoint: +router.post('/api/weatherAgent', shelfHandler(weatherAgent.action)); + +// Optional companions (snapshot restore / branching / background): +router.post( + '/api/weatherAgent/getSnapshot', + shelfHandler(weatherAgent.getSnapshotDataAction), +); +router.post( + '/api/weatherAgent/abort', + shelfHandler(weatherAgent.abortAgentAction), +); +``` + +For serving multiple agents, CORS/streaming headers for browser clients, and a +full server, see [Deploying agents](agents-deployment.md). + +## Consume an agent from a client (`remoteAgent`) + +The browser/Dart client lives in `package:genkit/client.dart`. `remoteAgent` +returns a typed HTTP client; `getSnapshotUrl`/`abortUrl` default to +`${url}/getSnapshot` and `${url}/abort`. + +`remoteAgent` talks to any Genkit agent endpoint over HTTP, so the **backend is +fully interchangeable** — the agent can be implemented in Dart, JS/TypeScript, or +Go. The wire protocol is the same; point `url` at whatever server hosts the +agent. + +```dart +import 'package:genkit/client.dart'; +import 'package:genkit/experimental_client.dart'; // remoteAgent, AgentError + +final weather = remoteAgent(url: 'http://localhost:8080/api/weatherAgent'); + +final chat = weather.chat(); +final turn = chat.sendStream(text: 'Weather in Tokyo?'); +await for (final chunk in turn.stream) { + stdout.write(chunk.text); +} +final res = await turn.response; +print('${res.snapshotId} ${chat.snapshotId} ${chat.state}'); + +// Multi-turn — the client carries state forward automatically: +await chat.send(text: 'What about Paris?'); + +// Errors surface as AgentError with an HTTP-ish status: +try { + await remoteAgent(url: '$base/api/nope').chat().send(text: 'hi'); +} catch (err) { + if (err is AgentError) print(err.status); +} +``` + +## Using from Flutter + +There is nothing Flutter-specific about the client: a Flutter app uses the same +`remoteAgent` from `package:genkit/client.dart` as any other Dart program. Two +things matter in a real app — **auth headers** and **lifecycle**. + +Pass `headers` to attach per-request auth (e.g. a Firebase/OAuth bearer token). +It's a `FutureOr?> Function()`, so it can be async and is +called on every request: + +```dart +final agent = remoteAgent( + url: 'https://your-backend.example.com/api/weatherAgent', + headers: () async => { + 'Authorization': 'Bearer ${await getIdToken()}', + }, +); +``` + +Create the agent once (e.g. in `initState`, or a provider/singleton) and +`close()` it when done to release the underlying HTTP client. To own the client +yourself, pass `httpClient:` — then it stays caller-owned and `close()` leaves it +open. + +A minimal streaming chat widget — pump `turn.stream` into the UI with +`setState`, then await `turn.response`: + +```dart +import 'package:flutter/material.dart'; +import 'package:genkit/client.dart'; +import 'package:genkit/experimental_client.dart'; // remoteAgent, AgentApi + +class ChatView extends StatefulWidget { + const ChatView({super.key}); + @override + State createState() => _ChatViewState(); +} + +class _ChatViewState extends State { + late final AgentApi _agent = remoteAgent( + url: 'http://localhost:8080/api/weatherAgent', + ); + late final _chat = _agent.chat(); + var _reply = ''; + + @override + void dispose() { + _agent.close(); // release the HTTP client + super.dispose(); + } + + Future _send(String text) async { + setState(() => _reply = ''); + final turn = _chat.sendStream(text: text); + await for (final chunk in turn.stream) { + setState(() => _reply += chunk.text); // append tokens live + } + await turn.response; // final AgentResponse (state already tracked on _chat) + } + + @override + Widget build(BuildContext context) => Column( + children: [ + Expanded(child: SingleChildScrollView(child: Text(_reply))), + TextField(onSubmitted: _send), + ], + ); +} +``` + +The `_chat` instance carries conversation state forward automatically across +turns (`_chat.state`, `_chat.messages`, `_chat.snapshotId`), exactly as on the +server. [Interrupts](agents-human-in-the-loop.md), +[custom state](agents-state.md), and [artifacts](agents-artifacts.md) all work +the same way from Flutter. + +## Client-managed state (no server store) + +If the agent has **no `store`**, the server is fully stateless and the session +state blob (messages + custom + artifacts) is owned by the caller. The +`remoteAgent` client tracks it and round-trips it on every turn automatically — +no `SessionStore`, no snapshot ids to manage. + +```dart +// Server: no `store` → stateless. Client owns the state blob. +import 'package:genkit/experimental.dart'; // defineAgent + +final weatherAgentStateless = ai.defineAgent( + name: 'weatherAgentStateless', + system: 'You are a helpful weather assistant. Use the getWeather tool. ' + 'Be concise.', + tools: [getWeather], + use: [retry()], +); +``` + +```dart +// Client: reuse one `chat` and the state threads automatically. +import 'package:genkit/client.dart'; +import 'package:genkit/experimental_client.dart'; // remoteAgent + +final agent = remoteAgent(url: '$base/api/weatherAgentStateless'); +final chat = agent.chat(); + +await chat.send(text: 'Weather in London?'); +await chat.send(text: 'Is it sunny in Tokyo?'); // remembers prior turns + +// The tracked state is available after each turn: +print(chat.state); +print(chat.messages.length); +``` + +Use client-managed state when you don't want to run server-side storage. Use a +[session store](agents-sessions.md) when the server should own history, or when +you need branching or background execution. +([Interrupts](agents-human-in-the-loop.md) work either way.) diff --git a/plugins/genkit/.agents/skills/developing-genkit-dart/references/dotprompt.md b/plugins/genkit/.agents/skills/developing-genkit-dart/references/dotprompt.md new file mode 100644 index 00000000..804a1e54 --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-dart/references/dotprompt.md @@ -0,0 +1,205 @@ +# Dotprompt (.prompt files) — Genkit Dart + +## What it is + +`.prompt` files combine YAML frontmatter (model, config, schemas, tools, +middleware) with a Handlebars template. They keep prompt logic out of Dart code +and make variants and iteration easy. + +## Where files live + +By default Genkit loads `.prompt` files from `./prompts`. Configure with the +`promptDir` parameter on `Genkit(...)` (set to `null` to disable auto-loading): + +```dart +import 'package:genkit/genkit.dart'; +import 'package:genkit_google_genai/genkit_google_genai.dart'; + +final ai = Genkit( + plugins: [googleAI()], + promptDir: './prompts', // default +); +``` + +## File format + +`prompts/greeting.prompt`: +``` +--- +model: googleai/gemini-flash-latest +input: + schema: + name: string + style: string +--- +{{role "system"}} +You are a {{style}} greeter. + +{{role "user"}} +Greet {{name}}. +``` + +Schema fields use Picoschema (the compact form above) or you can reference a +named schema registered with `defineSchema`. The `type, description` form is +supported too, e.g. `name: string, the person to greet`. + +## Loading and calling a prompt + +`ai.prompt(name, {variant})` returns a `Future`. The resolved +`ExecutablePrompt` is a **callable object** — invoke it like a function. The +input is a **positional** argument (there is no `input:` named parameter). + +```dart +// Non-streaming +final greetingPrompt = await ai.prompt('greeting'); +final response = await greetingPrompt({ + 'name': 'World', + 'style': 'cheerful', +}); +print(response.text); +``` + +### Streaming + +```dart +final storyPrompt = await ai.prompt('story'); +final stream = storyPrompt.stream({'subject': 'a robot'}); +await for (final chunk in stream) { + print(chunk.text); +} +``` + +### Render without generating + +Returns `GenerateActionOptions` (messages, model, config) without calling the +model — useful for building `ai.generate` calls or evals: + +```dart +final greetingPrompt = await ai.prompt('greeting'); +final rendered = await greetingPrompt.render({'name': 'World', 'style': 'casual'}); +rendered.model; // resolved model +rendered.messages; // rendered messages +``` + +## Registering named schemas + +Reference a schema by name in `.prompt` frontmatter (`input.schema` / +`output.schema`) after registering it with `defineSchema` (a JSON Schema map): + +```dart +ai.defineSchema('Recipe', { + 'type': 'object', + 'properties': { + 'title': {'type': 'string'}, + 'steps': {'type': 'array', 'items': {'type': 'string'}}, + }, + 'required': ['title', 'steps'], +}); +``` + +Then in `recipe.prompt`: +``` +--- +model: googleai/gemini-flash-latest +input: + schema: + food: string +output: + schema: Recipe + format: json +--- +Generate a recipe for {{food}}. +``` + +For code-defined prompts (`ai.definePrompt`) you typically pass a +[schemantic](schemantic.md)-generated schema (e.g. `JokeInput.schema`) as +`inputSchema`. + +## Variants + +Name the file `..prompt` — e.g. `greeting.formal.prompt`. Load +with the `variant` argument: + +```dart +final formalPrompt = await ai.prompt('greeting', variant: 'formal'); +final response = await formalPrompt({'name': 'Dr. Smith', 'title': 'Professor'}); +``` + +## Partials + +Reusable template fragments. Name a partial file `_.prompt` and include it +with `{{> name param=value}}`. + +`prompts/_signature.prompt` referenced from another template: +``` +Write a short email to {{recipient}} about {{subject}}. + +{{> signature}} +``` + +## Tools, tool-loop control, and middleware + +`.prompt` frontmatter can configure tool calling and attach middleware, so an +agent-style prompt is fully described in the file. This is based on +`prompts/tripPlanner.prompt` from the agents testapp (with +`returnToolRequests` shown for completeness): + +``` +--- +model: googleai/gemini-flash-latest +input: + schema: + tone: string +tools: + - getAttractions + - getFlightInfo +maxTurns: 20 # max tool-call loop iterations +returnToolRequests: false # return tool requests instead of running them +use: + - name: retry # bare string also works: `- retry` + config: + maxRetries: 4 +--- +{{role "system"}} +You are a friendly trip planning assistant. Help users plan trips by suggesting +attractions and looking up flight information. Use the available tools to provide +accurate, up-to-date information. Keep your tone {{tone}}. + +{{history}} +``` + +- `tools`: list of registered tool names. +- `toolChoice`, `maxTurns`, `returnToolRequests`: same semantics as the + equivalent `ai.generate` options. +- `use`: list of middleware refs. Each entry is a bare string (middleware name) + or a map with `name` and optional `config`. Names resolve against middleware + registered on the Genkit instance — register the middleware plugin so the name + is available: + +```dart +final ai = Genkit( + plugins: [ + googleAI(), + RetryPlugin(), // registers the `retry` middleware + ], + promptDir: './prompts', +); +``` + +See [genkit_middleware](genkit_middleware.md) for the middleware package. + +## Backing an agent with a .prompt file + +`definePromptAgent` wires an agent directly to a `.prompt` file by name. The +frontmatter (`tools`, `maxTurns`, `use`, ...) applies to the agent, and +`promptInput` supplies template variables: + +```dart +final tripPlannerAgent = ai.definePromptAgent( + promptName: 'tripPlanner', // -> prompts/tripPlanner.prompt + promptInput: {'tone': 'enthusiastic'}, // fills {{tone}} + store: InMemorySessionStore(), +); +``` + +See [Agents](agents.md) for defining and serving agents. diff --git a/plugins/genkit/.agents/skills/developing-genkit-dart/references/genkit.md b/plugins/genkit/.agents/skills/developing-genkit-dart/references/genkit.md new file mode 100644 index 00000000..788ecc25 --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-dart/references/genkit.md @@ -0,0 +1,522 @@ +# Genkit Core Framework + +Genkit Dart is an AI SDK for Dart that provides a unified interface for text generation, structured output, tool calling, and agentic workflows. + +## Initialization + +```dart +import 'package:genkit/genkit.dart'; +import 'package:genkit_google_genai/genkit_google_genai.dart'; // Or any other plugin + +void main() async { + // Pass plugins to use into the Genkit constructor. RetryPlugin() ships with + // core genkit and registers the `retry` middleware (see below). + final ai = Genkit(plugins: [googleAI(), RetryPlugin()]); +} +``` + +## Reliability: retry transient errors + +Model backends routinely return transient errors (Gemini "high demand" surfaces +as `INTERNAL`, plus `UNAVAILABLE` / `RESOURCE_EXHAUSTED` under load). In practice +`retry()` is close to mandatory for reliable runs. It is a **core** middleware +(no extra package): register `RetryPlugin()` once, then add `use: [retry()]` to a +call. By default it retries `UNAVAILABLE`, `DEADLINE_EXCEEDED`, +`RESOURCE_EXHAUSTED`, `ABORTED`, and `INTERNAL`, and it works the same on +`generate`, `generateStream`, prompts, and flows. The examples below add it where +it matters. + +## Generate Text + +```dart +final response = await ai.generate( + model: googleAI.gemini('gemini-flash-latest'), // Needs a model reference from a plugin + prompt: 'Explain quantum computing in simple terms.', + use: [retry()], // recommended; stream/embed/prompts/flows take the same `use:` +); + +print(response.text); +``` + +## Stream Responses +```dart +final stream = ai.generateStream( + model: googleAI.gemini('gemini-flash-latest'), + prompt: 'Write a short story about a robot learning to paint.', +); + +await for (final chunk in stream) { + print(chunk.text); +} +``` + +## Embed Text +```dart +final embeddings = await ai.embedMany( + documents: [ + DocumentData(content: [TextPart(text: 'Hello world')]), + ], + embedder: googleAI.textEmbedding('gemini-embedding-001'), +); + +print(embeddings.first.embedding); +``` + +## Define Tools +Models can use define actions and access external data via custom defined tools. +Requires the `schemantic` library for schema definitions. + +```dart +import 'package:schemantic/schemantic.dart'; + +@Schema() +abstract class $WeatherInput { + String get location; +} + +final weatherTool = ai.defineTool( + name: 'getWeather', + description: 'Gets the current weather for a location', + inputSchema: WeatherInput.$schema, + fn: (input, _) async { + // Call your weather API here + return .response('Weather in ${input.location}: 72°F and sunny'); + }, +); + +final response = await ai.generate( + model: googleAI.gemini('gemini-flash-latest'), + prompt: 'What\'s the weather like in San Francisco?', + toolNames: ['getWeather'], // Use the tools +); +``` + +> **Tool functions return a `ToolResult`.** A tool's `fn` returns a +> `ToolResult`, built with dot-shorthand: +> +> - `return .response(output)` for a normal result. +> - `return .response(output, parts: [...])` to attach media parts alongside the +> structured output (multipart), e.g. an image the tool produced. +> - `return .interrupt(data)` to pause the generation loop (see +> [human-in-the-loop](agents-human-in-the-loop.md)). +> +> ```dart +> fn: (input, _) async => .response( +> {'result': 'captured'}, +> parts: [MediaPart(media: Media(contentType: 'image/png', url: dataUri))], +> ); +> ``` +> +> Reading a tool's direct result (e.g. via `tool.call`) also yields a +> `ToolResult`; read `(result as ToolResponseResult).output`. + +## Structured Output + +You can ensure the generative model returns a typed JSON object by providing an `outputSchema`. + +```dart +@Schema() +abstract class $Person { + String get name; + int get age; +} + +// ... inside main ... + +final response = await ai.generate( + model: googleAI.gemini('gemini-flash-latest'), + prompt: 'Generate a person named John Doe, age 30', + outputSchema: Person.$schema, // Force the model to return this schema +); + +final person = response.output; // Typed Person object +print('Name: ${person.name}, Age: ${person.age}'); +``` + +## Errors and finish reasons + +`generate` and `generateStream` do **not** throw for model errors, throwing +tools, cancellation, or hitting `maxTurns`. They resolve to a response whose +`finishReason` tells you what happened; branch on it instead of wrapping the +call in `try`/`catch`: + +```dart +final res = await ai.generate( + model: googleAI.gemini('gemini-flash-latest'), + prompt: 'hi', + toolNames: ['myTool'], +); + +switch (res.finishReason) { + case FinishReason.failed: + // A model error or a throwing tool. `res.error` is a structured + // RuntimeError (a GenkitException keeps its status; anything else maps to + // INTERNAL). `res.cause` holds the original thrown object for in-process + // inspection (e.g. `res.cause is SocketException`); it does not cross the + // HTTP/reflection boundary. + print(res.error?.status); + case FinishReason.aborted: + // Cancelled, or hit maxTurns. + print('aborted: ${res.finishMessage}'); + default: + print(res.text); +} +``` + +Only `ToolInterruptException` (a tool returning `.interrupt(...)`) is still +treated as a turn outcome rather than a failure. See +[human-in-the-loop](agents-human-in-the-loop.md). + +### Resume from the last-good state + +On a `failed` or `aborted` response, `res.messages` holds the **last-good +conversation state**: the request messages plus every tool turn that completed +before the failure/abort (the failing turn's own partial output is dropped). +You do not have to start over. Feed `res.messages` straight back into a fresh +`generate` (with a new, uncancelled token if you were cancelling) to continue +from where it stopped: + +```dart +if (res.finishReason == FinishReason.failed || + res.finishReason == FinishReason.aborted) { + final resumed = await ai.generate( + model: googleAI.gemini('gemini-flash-latest'), + messages: res.messages, // last-good history: pick up where it stopped + ); +} +``` + +## Cancellation + +`generate`, `generateStream`, and action calls accept a `CancellationToken`. The +caller owns a `CancellationController` and hands its token to the call. These are +stable core types from `package:genkit/genkit.dart` (and `client.dart`). + +```dart +final controller = CancellationController(); + +final stream = ai.generateStream( + model: googleAI.gemini('gemini-flash-latest'), + prompt: 'Write a long, detailed essay about the history of the internet.', + cancel: controller.token, +); + +controller.cancel('user pressed stop'); + +// Chunks emitted before the cancel still arrive; the stream then closes. +await for (final chunk in stream) { + stdout.write(chunk.text); +} + +final res = await stream.onResult; +if (res.finishReason == FinishReason.aborted) { + // res.messages holds the last-good history, so you can resume later. + print('\nCancelled: ${res.finishMessage}'); +} +``` + +## Telemetry (instrumentation) + +Telemetry is a pluggable abstraction, not a hardcoded OpenTelemetry dependency. +By default Genkit is not instrumented. In the dev environment (under +`genkit start`) a built-in provider is auto-injected so the Developer UI receives +traces with no setup, so most users never touch this API. For production, stack +one or more `Instrumentation` providers before creating `Genkit`: + +```dart +import 'package:genkit/telemetry.dart'; + +void main() { + configureInstrumentation(myInstrumentation()); + final ai = Genkit(plugins: [googleAI()]); + // ... +} +``` + +Providers compose as a middleware chain, so multiple can be active at once. A +third-party platform can implement `Instrumentation` without taking an OTel +dependency. + +## Define Flows +Wrap your AI logic in flows for better observability, testing, and deployment: + +```dart +final jokeFlow = ai.defineFlow( + name: 'tellJoke', + inputSchema: .string(), + outputSchema: .string(), + fn: (topic, _) async { + final response = await ai.generate( + model: googleAI.gemini('gemini-flash-latest'), + prompt: 'Tell me a joke about $topic', + ); + return response.text; // Value return + }, +); + +final joke = await jokeFlow('programming'); +print(joke); +``` + +> **Top-level `final` flows are lazy.** A flow declared as a top-level `final` +> registers only when the symbol is first evaluated, so an empty `main()` +> registers nothing and `genkit flow:run` fails with `Process exited before +> runtime was ready`. Reference the flow from `main()` (or import a module that +> does) so its `defineFlow` call runs. + +### Streaming Flows +Stream data from your flows using `context.sendChunk(...)` and returning the final value: + +```dart +final streamStory = ai.defineFlow( + name: 'streamStory', + inputSchema: .string(), + outputSchema: .string(), + streamSchema: .string(), + fn: (topic, context) async { + final stream = ai.generateStream( + model: googleAI.gemini('gemini-flash-latest'), + prompt: 'Write a story about $topic', + ); + + await for (final chunk in stream) { + context.sendChunk(chunk.text); // Stream the chunks + } + return 'Story complete'; // Value return + }, +); +``` + +## Calling remote Flows from a dart client +The `genkit` package provides `package:genkit/client.dart` representing remote Genkit actions that can be invoked or streamed using type-safe definitions. + +1. Defines a remote action +```dart +import 'package:genkit/client.dart'; + +final stringAction = defineRemoteAction( + url: 'http://localhost:3400/my-flow', + inputSchema: .string(), + outputSchema: .string(), +); +``` + +2. Call the Remote Action (Non-streaming) +```dart +final response = await stringAction(input: 'Hello from Dart!'); +print('Flow Response: $response'); +``` + +3. Call the Remote Action (Streaming) +Use the `.stream()` method on the action flow, and access `stream.onResult` to wait on the async return value. +```dart +final streamAction = defineRemoteAction( + url: 'http://localhost:3400/stream-story', + inputSchema: .string(), + outputSchema: .string(), + streamSchema: .string(), +); + +final stream = streamAction.stream( + input: 'Tell me a short story about a Dart developer.', +); + +await for (final chunk in stream) { + print('Chunk: $chunk'); +} + +final finalResult = await stream.onResult; +print('\nFinal Response: $finalResult'); +``` + +## Calling remote Flows from a Javascript client + +Install `genkit` npm package: + +```bash +npm install genkit +``` + +1. Call a remote flow (non-streaming) + +```ts +import { runFlow } from 'genkit/beta/client'; + +async function callHelloFlow() { + try { + const result = await runFlow({ + url: 'http://127.0.0.1:3400/helloFlow', // Replace with your deployed flow's URL + input: { name: 'Genkit User' }, + }); + console.log('Non-streaming result:', result.greeting); + } catch (error) { + console.error('Error calling helloFlow:', error); + } +} + +callHelloFlow(); +``` + +2. Call a remote flow (streaming) + +```ts +import { streamFlow } from 'genkit/beta/client'; + +async function streamHelloFlow() { + try { + const result = streamFlow({ + url: 'http://127.0.0.1:3400/helloFlow', // Replace with your deployed flow's URL + input: { name: 'Streaming User' }, + }); + + // Process the stream chunks as they arrive + for await (const chunk of result.stream) { + console.log('Stream chunk:', chunk); + } + + // Get the final complete response + const finalOutput = await result.output; + console.log('Final streaming output:', finalOutput.greeting); + } catch (error) { + console.error('Error streaming helloFlow:', error); + } +} + +streamHelloFlow(); +``` + +## Data Models + +Genkit uses standard data models for representing prompts (messages & parts) and responses. These classes are implemented using schemantic library. + +```dart +import 'package:genkit/genkit.dart'; +import 'package:schemantic/schemantic.dart'; + +@Schema() +abstract class $MyDataModel { + // uses Genkit's Message schema (not schemantic's Message) + List<$Message> get messages; + List<$Part> get parts; +} + +void example() { + // --- Parts --- + // A Text part + final textPart = TextPart(text: 'some text', metadata: {'foo': 'bar'}); + + // A Media/Image part + final mediaPart = MediaPart( + media: Media(url: 'https://...', contentType: 'image/png'), + metadata: {'foo': 'bar'}, + ); + + // A Tool Request initiated by the model + final toolRequestPart = ToolRequestPart( + toolRequest: ToolRequest( + name: 'get_weather', + ref: 'abc', + input: {'location': 'Paris, France'}, + ), + metadata: {'foo': 'bar'}, + ); + + // The resulting data from a Tool execution + final toolResponsePart = ToolResponsePart( + toolResponse: ToolResponse( + name: 'get_weather', + ref: 'abc', + output: {'temperature': '20C'}, + ), + metadata: {'foo': 'bar'}, + ); + + // Model reasoning (e.g. for Claude's "thinking" models) + final reasoningPart = ReasoningPart( + reasoning: 'thinking...', + metadata: {'foo': 'bar'}, + ); + + // A custom fallback part + final customPart = CustomPart( + custom: {'provider': {'specific': 'data'}}, + metadata: {'foo': 'bar'}, + ); + + // --- Messages --- + final systemMessage = Message( + role: Role.system, + content: [textPart, mediaPart], + metadata: {'foo': 'bar'}, + ); + + final userMessage = Message( + role: Role.user, + content: [textPart, mediaPart], // Can contain media (multimodal) + ); + + final modelMessage = Message( + role: Role.model, + // Models can emit text, tool requests, reasoning, or custom parts + content: [textPart, toolRequestPart, reasoningPart, customPart], + ); + + // --- Ergonomic Data Access (schema_extensions.dart) --- + // The Genkit SDK provides extensions on `Message` and `Part` to easily access fields + // without needing to cast them manually. + + // Get concatenated text from all TextParts in a Message + print(modelMessage.text); + + // Get the first Media object from a Message + print(modelMessage.media?.url); + + // Iterate over tool requests in a Message + for (final toolReq in modelMessage.toolRequests) { + print(toolReq.name); + } + + // Inspect individual parts + for (final part in modelMessage.content) { + if (part.isText) print(part.text); + if (part.isMedia) print(part.media?.url); + if (part.isToolRequest) print(part.toolRequest?.name); + if (part.isToolResponse) print(part.toolResponse?.name); + if (part.isReasoning) print(part.reasoning); + if (part.isCustom) print(part.custom); + } + + // --- Streaming Chunks --- + // Data emitted by ai.generateStream() calls + final generateResponseChunk = ModelResponseChunk( + content: [textPart], + index: 0, // Index of the message this chunk belongs to + aggregated: false, + ); + + // Chunks also have text and media accessors + print(generateResponseChunk.text); + + // --- Advanced: Schemas --- + // Use Genkit type schemas directly in Schemantic validations + final messageSchema = Message.$schema; + final partSchema = Part.$schema; + + final mySchema = SchemanticType.map( + .string(), + .list(Message.$schema), // Requires a list of Messages + ); + + // --- Generate Response --- + // ai.generate() returns a GenerateResponseHelper which provides ergonomic getters + // over the underlying ModelResponse: + final response = await ai.generate(...); + + print(response.text); // Concatenated text + print(response.media?.url); // First media part + print(response.toolRequests); // All tool requests + print(response.interrupts); // Tool requests that triggered an interrupt + print(response.messages); // Full history of the conversation, including the request and response + print(response.output); // Structured typed output (if outputSchema was used) +} +``` diff --git a/plugins/genkit/.agents/skills/developing-genkit-dart/references/genkit_anthropic.md b/plugins/genkit/.agents/skills/developing-genkit-dart/references/genkit_anthropic.md new file mode 100644 index 00000000..768071e6 --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-dart/references/genkit_anthropic.md @@ -0,0 +1,56 @@ +# Genkit Anthropic Plugin (`genkit_anthropic`) + +The Anthropic plugin for Genkit Dart, used for interacting with the Claude models. + +## Usage + +Requires `ANTHROPIC_API_KEY` to be passed to the init block. + +```dart +import 'dart:io'; +import 'package:genkit/genkit.dart'; +import 'package:genkit_anthropic/genkit_anthropic.dart'; + +void main() async { + // RetryPlugin() (core genkit) registers the `retry` middleware. + final ai = Genkit( + plugins: [ + anthropic(apiKey: Platform.environment['ANTHROPIC_API_KEY']!), + RetryPlugin(), + ], + ); + + final response = await ai.generate( + model: anthropic.model('claude-sonnet-4-5'), + prompt: 'Tell me a joke about a developer.', + use: [retry()], // recommended for reliable runs + ); + + print(response.text); +} +``` + +## Claude Thinking Configurations + +Provides specific configurations for utilizing Claude 3.7+ "thinking" model capabilities. + +```dart +final response = await ai.generate( + model: anthropic.model('claude-sonnet-4-5'), + prompt: 'Solve this 24 game: 2, 3, 10, 10', + config: AnthropicOptions(thinking: ThinkingConfig(budgetTokens: 2048)), +); + +// The thinking content is available in the message parts +print(response.message?.content); +``` + +> **Multi-turn reasoning is preserved.** Thinking blocks are re-sent on later +> turns of a thinking + tool loop, so reasoning is not dropped on the second +> turn. The signature metadata key is `thoughtSignature` (matching the Gemini +> plugins and Genkit JS), and redacted thinking comes back as +> `CustomPart(custom: {redactedThinking})`. History persisted by older versions +> still replays. + +> **Overloads are retried.** HTTP 529 (overloaded) maps to `UNAVAILABLE`, so the +> `retry()` middleware retries it instead of surfacing a non-retriable error. diff --git a/plugins/genkit/.agents/skills/developing-genkit-dart/references/genkit_chrome.md b/plugins/genkit/.agents/skills/developing-genkit-dart/references/genkit_chrome.md new file mode 100644 index 00000000..8152369f --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-dart/references/genkit_chrome.md @@ -0,0 +1,23 @@ +# Genkit Chrome AI Plugin (`genkit_chrome`) + +Chrome Built-in AI (Gemini Nano) plugin for Genkit Dart, allowing local offline execution within a Chrome application. + +## Usage + +```dart +import 'package:genkit/genkit.dart'; +import 'package:genkit_chrome/genkit_chrome.dart'; + +void main() async { + final ai = Genkit(plugins: [ChromeAIPlugin()]); + + final stream = ai.generateStream( + model: modelRef('chrome/gemini-nano'), + prompt: 'Write a story about a robot.', + ); + + await for (final chunk in stream) { + print(chunk.text); + } +} +``` diff --git a/plugins/genkit/.agents/skills/developing-genkit-dart/references/genkit_firebase_ai.md b/plugins/genkit/.agents/skills/developing-genkit-dart/references/genkit_firebase_ai.md new file mode 100644 index 00000000..6168ce54 --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-dart/references/genkit_firebase_ai.md @@ -0,0 +1,23 @@ +# Genkit Firebase AI Plugin (`genkit_firebase_ai`) + +The Firebase AI plugin for Genkit Dart, used for interacting with Gemini APIs through Firebase AI Logic. + +## Usage + +```dart +import 'package:genkit/genkit.dart'; +import 'package:genkit_firebase_ai/genkit_firebase_ai.dart'; + +void main() async { + // Initialize Genkit with the Firebase AI plugin + final ai = Genkit(plugins: [firebaseAI()]); + + // Generate text + final response = await ai.generate( + model: firebaseAI.gemini('gemini-flash-latest'), + prompt: 'Tell me a joke about a developer.', + ); + + print(response.text); +} +``` diff --git a/plugins/genkit/.agents/skills/developing-genkit-dart/references/genkit_google_genai.md b/plugins/genkit/.agents/skills/developing-genkit-dart/references/genkit_google_genai.md new file mode 100644 index 00000000..73912d98 --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-dart/references/genkit_google_genai.md @@ -0,0 +1,111 @@ +# Genkit Google GenAI Plugin (`genkit_google_genai`) + +The Google AI plugin provides an interface against the official Google AI Gemini API. + +## Usage + +```dart +import 'package:genkit/genkit.dart'; +import 'package:genkit_google_genai/genkit_google_genai.dart'; + +void main() async { + // Initialize Genkit with the Google AI plugin. RetryPlugin() (core genkit) + // registers the `retry` middleware; transient "high demand" errors are common. + final ai = Genkit(plugins: [googleAI(), RetryPlugin()]); + + // Generate text + final response = await ai.generate( + model: googleAI.gemini('gemini-flash-latest'), + prompt: 'Tell me a joke about a developer.', + use: [retry()], // recommended for reliable runs + ); + + print(response.text); +} +``` + +## Model refs and Gemma + +`googleAI.gemini('')` builds a ref for any Gemini model. Gemma models the +Gemini API serves have a dedicated `googleAI.gemma('')` alias (it reads +correctly at call sites), and `GoogleAiModels` exposes typed refs for the curated +Gemini and Gemma entries: + +```dart +final response = await ai.generate( + model: googleAI.gemma('gemma-4-31b-it'), // or GoogleAiModels.gemma431b + prompt: 'Tell me a joke about a developer.', +); +``` + +## Embeddings + +```dart +final embeddings = await ai.embedMany( + embedder: googleAI.textEmbedding('gemini-embedding-001'), + documents: [ + DocumentData(content: [TextPart(text: 'Hello world')]), + ], +); +``` + +## Image Generation + +The plugin also supports image generation models such as `gemini-3.1-flash-image`. + +### Example (Nano Banana) + +```dart +// Define an image generation flow +ai.defineFlow( + name: 'imageGenerator', + inputSchema: .string(defaultValue: 'A banana riding a bike'), + outputSchema: Media.$schema, + fn: (input, context) async { + final response = await ai.generate( + model: googleAI.gemini('gemini-3.1-flash-image'), + prompt: input, + ); + if (response.media == null) { + throw Exception('No media generated'); + } + return response.media!; + }, +); +``` + +The media (url field) contain base64 encoded data uri. You can decode it and save it as a file. + +## Text-to-Speech (TTS) + +You can use text-to-speech models to generate audio from text. The generated `Media` object will contain base64 encoded PCM audio in its data URI. + +```dart +// Define a TTS flow +ai.defineFlow( + name: 'textToSpeech', + inputSchema: .string(defaultValue: 'Genkit is an amazing AI framework!'), + outputSchema: Media.$schema, + fn: (prompt, _) async { + final response = await ai.generate( + model: googleAI.gemini('gemini-3.1-flash-tts-preview'), + prompt: prompt, + config: GeminiTtsOptions( + responseModalities: ['AUDIO'], + speechConfig: SpeechConfig( + voiceConfig: VoiceConfig( + prebuiltVoiceConfig: PrebuiltVoiceConfig(voiceName: 'Puck'), + ), + ), + ), + ); + + if (response.media != null) { + return response.media!; + } + throw Exception('No audio generated'); + }, +); +``` + +Google AI also supports multi-speaker TTS by configuring a `MultiSpeakerVoiceConfig` inside `SpeechConfig`. diff --git a/plugins/genkit/.agents/skills/developing-genkit-dart/references/genkit_mcp.md b/plugins/genkit/.agents/skills/developing-genkit-dart/references/genkit_mcp.md new file mode 100644 index 00000000..bda3d1f6 --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-dart/references/genkit_mcp.md @@ -0,0 +1,154 @@ +# Genkit MCP (`genkit_mcp`) + +MCP (Model Context Protocol) integration for Genkit Dart, built on `mcp_dart`. +It prefers the stateless MCP 2026-07-28 protocol and falls back to +initialization-based peers automatically. + +> **Namespaced tool names are shortened on the wire.** A tool like +> `my-server/weatherTool` is presented to the model as `weatherTool`; the full +> name is preserved in `metadata.originalName` and tool requests still resolve +> correctly. + +## MCP Host (Recommended) +Connect to one or more MCP servers and aggregate their capabilities into the Genkit registry automatically. + +```dart +import 'package:genkit/genkit.dart'; +import 'package:genkit_mcp/genkit_mcp.dart'; + +void main() async { + final ai = Genkit(); + + final host = defineMcpHost( + ai, + McpHostOptionsWithCache( + name: 'my-host', + mcpServers: { + 'fs': McpServerConfig( + command: 'npx', + args: ['-y', '@modelcontextprotocol/server-filesystem', '.'], + ), + }, + ), + ); + + // Tools can be discovered and executed dynamically using a wildcard... + final response = await ai.generate( + model: googleAI.gemini('gemini-flash-latest'), + prompt: 'Summarize the contents of README.md', + toolNames: ['my-host:tool/fs/*'], + ); + + // ...or by specifying the exact tool name + final exactResponse = await ai.generate( + model: googleAI.gemini('gemini-flash-latest'), + prompt: 'Read README.md', + toolNames: ['my-host:tool/fs/read_file'], + ); +} +``` + +### Connect over Streamable HTTP + +Point `McpServerConfig` at a URL instead of a command to connect over Streamable +HTTP. (There is no public `StreamableHttpClientTransport` to construct directly; +custom client transports implement the `McpClientTransport` interface.) + +```dart +final host = defineMcpHost( + ai, + McpHostOptionsWithCache( + name: 'my-host', + mcpServers: { + 'remote': McpServerConfig(url: Uri.parse('https://mcp.example.com/mcp')), + }, + ), +); +``` + +## MCP Client (Advanced / Single Server) +Connecting to a single MCP server with a client object is an advanced usecase for when you need manual control over the client lifecycle. Standalone clients do not automatically register tools into the registry, so they must be passed into `generate` or `defineDynamicActionProvider` manually. + +```dart +import 'package:genkit/genkit.dart'; +import 'package:genkit_mcp/genkit_mcp.dart'; + +void main() async { + final ai = Genkit(); + + final client = createMcpClient( + McpClientOptions( + name: 'my-client', + mcpServer: McpServerConfig( + command: 'npx', + args: ['-y', '@modelcontextprotocol/server-filesystem', '.'], + ), + ), + ); + + await client.ready(); + + // Retrieve the tools from the connected client + final tools = await client.getActiveTools(ai); + + final response = await ai.generate( + model: googleAI.gemini('gemini-flash-latest'), + prompt: 'Read the contents of README.md', + tools: tools, + ); +} +``` + +## MCP Server +Expose Genkit actions (tools, prompts, resources) over MCP. + +```dart +import 'package:genkit/genkit.dart'; +import 'package:genkit_mcp/genkit_mcp.dart'; + +void main() async { + final ai = Genkit(); + + ai.defineTool( + name: 'add', + description: 'Add two numbers together', + inputSchema: .map(.string(), .dynamicSChema()), + fn: (input, _) async => .response((input['a'] + input['b']).toString()), + ); + + ai.defineResource( + name: 'my-resource', + uri: 'my://resource', + fn: (_, _) async => ResourceOutput(content: [TextPart(text: 'my resource')]), + ); + + // Stdio transport by default + final server = createMcpServer(ai, McpServerOptions(name: 'my-server')); + await server.start(); +} +``` + +### Streamable HTTP Transport +```dart +import 'dart:io'; + +final transport = await StreamableHttpServerTransport.bind( + address: InternetAddress.loopbackIPv4, + port: 3000, +); +await server.start(transport); +``` + +> **DNS-rebinding protection and batch rejection are on by default.** Loopback +> hosts work unconfigured, but a non-loopback deployment must set `allowedHosts` +> (and should set `allowedOrigins`) or requests are rejected. Legacy clients that +> send JSON-RPC batches can opt out with `rejectBatchJsonRpcPayloads: false`. +> +> ```dart +> final transport = await StreamableHttpServerTransport.bind( +> address: InternetAddress.anyIPv4, +> port: 3000, +> allowedHosts: ['mcp.example.com'], +> allowedOrigins: ['https://app.example.com'], +> ); +> ``` diff --git a/plugins/genkit/.agents/skills/developing-genkit-dart/references/genkit_middleware.md b/plugins/genkit/.agents/skills/developing-genkit-dart/references/genkit_middleware.md new file mode 100644 index 00000000..ceb099ab --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-dart/references/genkit_middleware.md @@ -0,0 +1,92 @@ +# Genkit Middleware (`genkit_middleware`) + +A collection of useful middleware for Genkit Dart to enhance your agent's capabilities. Register plugins when initializing Genkit: + +```dart +import 'package:genkit/genkit.dart'; +import 'package:genkit_middleware/genkit_middleware.dart'; + +void main() { + final ai = Genkit( + plugins: [ + FilesystemPlugin(), + SkillsPlugin(), + ToolApprovalPlugin(), + ], + ); +} +``` + +## Filesystem Middleware +Allows the agent to list, read, write, and search/replace files within a restricted root directory. + +```dart +final response = await ai.generate( + prompt: 'Check the logs in the current directory.', + use: [ + filesystem(rootDirectory: '/path/to/secure/workspace'), + ], +); +``` + +**Tools Provided:** +- `list_files`, `read_file`, `write_file`, `search_and_replace` + +## Skills Middleware +Injects specialized instructions (skills) into the system prompt from `SKILL.md` files located in specified directories. + +```dart +final response = await ai.generate( + prompt: 'Help me debug this issue.', + use: [ + skills(skillPaths: ['/path/to/skills']), + ], +); +``` + +**Tools Provided:** +- `use_skill`: Retrieve the full content of a skill by name. + +## Tool Approval Middleware +Intercepts tool execution for specified tools and requires explicit approval. Returns `FinishReason.interrupted`. + +A tool is allowed through only if it is in the `approved` list or its request's +`resumed` payload carries `{ 'tool-approved': true }`. To approve on resume, +re-issue the paused `ToolRequestPart` with `.restart({'tool-approved': true})` — +the builder nests the payload under `metadata.resumed`, exactly what the +middleware reads. + +```dart +final response = await ai.generate( + prompt: 'Delete the database.', + use: [ + // Require approval for all tools EXCEPT those below + toolApproval(approved: ['read_file', 'list_files']), + ], +); + +if (response.finishReason == FinishReason.interrupted) { + // `response.interrupts` is a List. + final interrupt = response.interrupts.first; + + // Ask user for approval + final isApproved = await askUser(); + + if (isApproved) { + final resumeResponse = await ai.generate( + messages: response.messages, // Pass history + toolChoice: ToolChoice.none, // Prevent immediate re-call + interruptRestart: [ + // `.restart(...)` nests the payload under `metadata.resumed`. + interrupt.restart({'tool-approved': true}), + ], + ); + } +} +``` + +> **Agent-side resume.** When resuming an agent chat rather than a raw +> `ai.generate` call, the interrupts are `AgentInterrupt`s; pass the same +> `.restart(...)` builder directly to `chat.resume`: +> `chat.resume(restart: [interrupt.restart({'tool-approved': true})])`. +> See [human-in-the-loop](agents-human-in-the-loop.md). diff --git a/plugins/genkit/.agents/skills/developing-genkit-dart/references/genkit_openai.md b/plugins/genkit/.agents/skills/developing-genkit-dart/references/genkit_openai.md new file mode 100644 index 00000000..ae62e911 --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-dart/references/genkit_openai.md @@ -0,0 +1,88 @@ +# Genkit OpenAI Plugin (`genkit_openai`) + +OpenAI-compatible API plugin for Genkit Dart. Supports OpenAI models and other compatible APIs (xAI, DeepSeek, Together AI, Groq, etc.). + +## Basic Usage + +```dart +import 'dart:io'; +import 'package:genkit/genkit.dart'; +import 'package:genkit_openai/genkit_openai.dart'; + +void main() async { + // RetryPlugin() (core genkit) registers the `retry` middleware. + final ai = Genkit(plugins: [ + openAI(apiKey: Platform.environment['OPENAI_API_KEY']), + RetryPlugin(), + ]); + + final response = await ai.generate( + model: openAI.model('gpt-5'), + prompt: 'Tell me a joke.', + use: [retry()], // recommended for reliable runs + ); +} +``` + +The plugin does **not** require network access or an API key at startup; a key is +only needed when you actually call a model. + +## Typed model and embedder refs + +The plugin ships a curated per-model catalog. `openAI.model('')` and +`openAI.embedder('')` accept any id (uncurated ids still resolve via +dated-suffix aliases and generic defaults), and `OpenAIModels` / `OpenAIEmbedders` +expose typed refs for the curated entries: + +```dart +final response = await ai.generate( + model: OpenAIModels.gpt5Mini, // == openAI.model('gpt-5-mini') + prompt: 'Tell me a joke.', +); + +final embeddings = await ai.embedMany( + embedder: OpenAIEmbedders.textEmbedding3Small, // == openAI.embedder(...) + documents: [DocumentData(content: [TextPart(text: 'Hello world')])], +); +``` + +> Capability detection is data-driven (per-model), not name-matching. `-pro` +> tiers are not curated because they are Responses API only and this plugin +> speaks Chat Completions (they still come back from discovery). + +## Options + +`OpenAIOptions` allows configuring sampling temperature, nucleus sampling, token generation, seed, etc: +`config: OpenAIOptions(temperature: 0.7, maxTokens: 100)` + +> **Schemaless JSON output is advisory.** When you request JSON output without a +> schema, the plugin now sends `json_object` (was strict `json_schema`), so +> schema conformance is best-effort rather than guaranteed. Provide an +> `outputSchema` when you need enforced structure. + +## Groq API override + +Specify custom `baseUrl` and custom models to integrate with third-party providers. + +```dart +final ai = Genkit(plugins: [ + openAI( + apiKey: Platform.environment['GROQ_API_KEY'], + baseUrl: 'https://api.groq.com/openai/v1', + models: [ + CustomModelDefinition( + name: 'llama-3.3-70b-versatile', + info: ModelInfo( + label: 'Llama 3.3 70B', + supports: {'multiturn': true, 'tools': true, 'systemRole': true}, + ), + ), + ], + ), +]); + +final response = await ai.generate( + model: openAI.model('llama-3.3-70b-versatile'), + prompt: 'Hello!', +); +``` diff --git a/plugins/genkit/.agents/skills/developing-genkit-dart/references/genkit_shelf.md b/plugins/genkit/.agents/skills/developing-genkit-dart/references/genkit_shelf.md new file mode 100644 index 00000000..1887f80c --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-dart/references/genkit_shelf.md @@ -0,0 +1,59 @@ +# Genkit Shelf Plugin (`genkit_shelf`) + +Shelf integration for Genkit Dart, used to serve Genkit Flows. + +## Standalone Server +Serve Genkit Flows easily on an isolated HTTP server using `startFlowServer`. + +```dart +import 'package:genkit/genkit.dart'; +import 'package:genkit_shelf/genkit_shelf.dart'; + +void main() async { + final ai = Genkit(); + + final flow = ai.defineFlow( + name: 'myFlow', + inputSchema: .string(), + outputSchema: .string(), + fn: (String input, _) async => 'Hello $input', + ); + + await startFlowServer( + flows: [flow], + port: 8080, + ); +} +``` + +## Existing Shelf Application +Mount Genkit Flow endpoints directly to an existing Shelf `Router` using `shelfHandler`. + +```dart +import 'package:genkit/genkit.dart'; +import 'package:genkit_shelf/genkit_shelf.dart'; +import 'package:shelf/shelf.dart'; +import 'package:shelf/shelf_io.dart' as io; +import 'package:shelf_router/shelf_router.dart'; + +void main() async { + final ai = Genkit(); + + final flow = ai.defineFlow( + name: 'myFlow', + inputSchema: .string(), + outputSchema: .string(), + fn: (String input, _) async => 'Hello $input', + ); + + final router = Router(); + + // Mount the flow handler at a specific path + router.post('/myFlow', shelfHandler(flow)); + + // Start the server + await io.serve(router.call, 'localhost', 8080); +} +``` + +Access deployed flows using genkit client libraries (from Dart or JS). diff --git a/plugins/genkit/.agents/skills/developing-genkit-dart/references/schemantic.md b/plugins/genkit/.agents/skills/developing-genkit-dart/references/schemantic.md new file mode 100644 index 00000000..2993030a --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-dart/references/schemantic.md @@ -0,0 +1,160 @@ +# Schemantic + +Schemantic is a general-purpose Dart library used for defining strongly typed data classes that automatically bind to reusable runtime JSON schemas. It is standard for the `genkit-dart` framework but works independently as well. + +## Core Concepts + +Always use `schemantic` when strongly typed JSON parsing or programmatic schema validation is required. + +- Annotate your abstract classes with `@Schema()`. +- Use the `$` prefix for abstract schema class names (e.g., `abstract class $User`). +- Always run `dart run build_runner build` to generate the `.g.dart` schema files. + +## Installation + +Add dependencies. As of schemantic 0.2.x the code generator lives in a +**separate** `schemantic_builder` package, so you must add it as a dev +dependency too: + +```bash +dart pub add schemantic +dart pub add dev:schemantic_builder +dart pub add dev:build_runner +``` + +> **Gotcha:** if `schemantic_builder` is missing, `dart run build_runner build` +> completes "successfully" but reports `wrote 0 outputs` and generates no +> `.g.dart` files, with no error explaining why. If you see zero outputs, confirm +> `schemantic_builder` is in your `dev_dependencies`. + +> **Note:** plain `dart run build_runner build` is correct. Recent `build_runner` +> versions removed `--delete-conflicting-outputs` (it is now ignored and warns +> "These options have been removed and were ignored"), so do not pass it. + +## Basic Usage + +1. **Defining a schema:** + +```dart +import 'package:schemantic/schemantic.dart'; + +part 'my_file.g.dart'; // Must match the filename + +@Schema() +abstract class $MyObj { + String get name; + $MySubObj get subObj; +} + +@Schema() +abstract class $MySubObj { + String get foo; +} +``` + +2. **Using the Generated Class:** + +The builder creates a concrete class `MyObj` (no `$`) with a factory constructor (`MyObj.fromJson`) and a regular constructor. + +```dart +// Creating an instance +final obj = MyObj(name: 'test', subObj: MySubObj(foo: 'bar')); + +// Serializing to JSON +print(obj.toJson()); + +// Parsing from JSON +final parsed = MyObj.fromJson({'name': 'test', 'subObj': {'foo': 'bar'}}); +``` + +3. **Accessing Schemas at Runtime:** + +The generated data classes have a static `$schema` field (of type `SchemanticType`) which can be used to pass the definition into functions or to extract the raw JSON schema. + +```dart +// Access JSON schema +final schema = MyObj.$schema.jsonSchema; +print(schema.toJson()); + +// Validate arbitrary JSON at runtime +final validationErrors = await schema.validate({'invalid': 'data'}); +``` + +## Primitive Schemas + +When a full data class is not required, Schemantic provides functions to create schemas dynamically. + +```dart +final ageSchema = SchemanticType.integer(description: 'Age in years', minimum: 0); +final nameSchema = SchemanticType.string(minLength: 2); +final nothingSchema = SchemanticType.voidSchema(); +final anySchema = SchemanticType.dynamicSchema(); + +final userSchema = SchemanticType.map(.string(), .integer()); // Map +final tagsSchema = SchemanticType.list(.string()); // List +``` + +## Union Types (AnyOf) + +To allow a field to accept multiple types, use `@AnyOf`. + +```dart +@Schema() +abstract class $Poly { + @AnyOf([int, String, $MyObj]) + Object? get id; +} +``` + +Schemantic generates a specific helper class (e.g., `PolyId`) to handle the values: + +```dart +final poly1 = Poly(id: PolyId.int(123)); +final poly2 = Poly(id: PolyId.string('abc')); +``` + +## Field Annotations + +You can use specialized annotations for more validation boundaries: + +```dart +@Schema() +abstract class $User { + @IntegerField( + name: 'years_old', // Change JSON key + description: 'Age of the user', + minimum: 0, + defaultValue: 18, + ) + int? get age; + + @StringField( + minLength: 2, + enumValues: ['user', 'admin'], + ) + String get role; +} +``` + +## Recursive Schemas + +For recursive structures (like trees), must use `useRefs: true` inside the generated jsonSchema property. You define it normally: + +```dart +@Schema() +abstract class $Node { + String get id; + List<$Node>? get children; +} +``` +*Note*: `Node.$schema.jsonSchema(useRefs: true)` generates schemas with JSON Schema `$ref`. + +## Non-nullable getters throw on partial data + +Generated getters for required fields are non-nullable casts (e.g. +`_json['estimatedCostUsd'] as num`). A JSON blob that is missing such a field +throws on access (`Null is not a subtype of num`) rather than returning a +default — this commonly bites computed/optional values that were never written, +e.g. when reloading a partially populated state blob. Make computed or optional +fields nullable (`num?`) or give them a `defaultValue` so partial data can still +be read. diff --git a/plugins/genkit/.agents/skills/developing-genkit-go/SKILL.md b/plugins/genkit/.agents/skills/developing-genkit-go/SKILL.md new file mode 100644 index 00000000..133ea6bd --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-go/SKILL.md @@ -0,0 +1,155 @@ +--- +name: developing-genkit-go +description: Develop AI-powered applications using Genkit in Go. Use when the user asks to build AI features, agents, flows, or tools in Go using Genkit, or when working with Genkit Go code involving generation, prompts, streaming, tool calling, or model providers. +metadata: + category: AiAndMachineLearning +--- + +# Genkit Go + +Genkit Go is an AI SDK for Go that provides generation, structured output, streaming, tool calling, prompts, and flows with a unified interface across model providers. + +## Hello World + +```go +package main + +import ( + "context" + "fmt" + "log" + "net/http" + + "github.com/genkit-ai/genkit/go/ai" + "github.com/genkit-ai/genkit/go/genkit" + "github.com/genkit-ai/genkit/go/plugins/googlegenai" + "github.com/genkit-ai/genkit/go/plugins/server" +) + +func main() { + ctx := context.Background() + g := genkit.Init(ctx, genkit.WithPlugins(&googlegenai.GoogleAI{})) + + genkit.DefineFlow(g, "jokeFlow", func(ctx context.Context, topic string) (string, error) { + return genkit.GenerateText(ctx, g, + ai.WithModelName("googleai/gemini-flash-latest"), + ai.WithPrompt("Tell me a joke about %s", topic), + ) + }) + + mux := http.NewServeMux() + for _, f := range genkit.ListFlows(g) { + mux.HandleFunc("POST /"+f.Name(), genkit.Handler(f)) + } + log.Fatal(server.Start(ctx, "127.0.0.1:8080", mux)) +} +``` + +## Core Features + +Load the appropriate reference based on what you need: + +| Feature | Reference | When to load | +| --- | --- | --- | +| Initialization | [references/getting-started.md](references/getting-started.md) | Setting up `genkit.Init`, plugins, the `*Genkit` instance pattern | +| Generation | [references/generation.md](references/generation.md) | `Generate`, `GenerateText`, `GenerateData`, streaming, output formats | +| Prompts | [references/prompts.md](references/prompts.md) | `DefinePrompt`, `DefineDataPrompt`, `.prompt` files, schemas | +| Tools | [references/tools.md](references/tools.md) | `DefineTool`, tool interrupts, `RestartWith`/`RespondWith` | +| Middleware | [references/middleware.md](references/middleware.md) | `ai.Middleware`, `ai.WithUse`, `Hooks` (Generate/Model/Tool), built-ins (`Retry`, `Fallback`, `ToolApproval`, `Filesystem`, `Skills`) | +| Flows & HTTP | [references/flows-and-http.md](references/flows-and-http.md) | `DefineFlow`, `DefineStreamingFlow`, `genkit.Handler`, HTTP serving | +| Model Providers | [references/providers.md](references/providers.md) | Google AI, Vertex AI, Anthropic, OpenAI-compatible, Ollama setup | + +## Agents (Experimental) + +Genkit Go has an **experimental** agent API for persistent, multi-turn +conversations (sessions, snapshots, interrupts, branching, background execution). +It is gated: initialize with `genkit.Init(ctx, genkit.WithExperimental())` or the +constructors panic. Server constructors come from `genkit/exp` (aliased +`genkitx`); types and options from `ai/exp` (aliased `aix`); session stores from +`ai/exp/localstore`. + +- **Agent or flow?** If the task is conversational, multi-turn, or described as "an agent", "assistant", or "chatbot", build it with `genkitx.DefineAgent` rather than hand-rolling a `Generate` + tools loop in a flow. Reach for a plain flow only for single-shot, stateless generation. + +For details see: + +- [Agents](references/agents.md): defining/serving an agent, running turns, and client- vs server-managed state (start here). +- [Sessions & persistence](references/agents-sessions.md): session stores (`localstore.NewInMemorySessionStore`/`NewFileSessionStore`) and snapshots. +- [Human-in-the-loop / interrupts](references/agents-human-in-the-loop.md): pausing for approval/input and resuming. +- [Branching](references/agents-branching.md): forking a conversation from a snapshot. +- [Background agents](references/agents-background.md): detaching long-running turns and polling. +- [Working with state](references/agents-state.md): typed custom session state, streamed as JSON patches. +- [Artifacts](references/agents-artifacts.md): producing and reading named deliverables. +- [Multi-agent orchestration](references/agents-multi-agent.md): delegating to sub-agents. +- [Advanced custom agents](references/agents-custom.md): `DefineCustomAgent` for full turn control. +- [Deploying agents](references/agents-deployment.md): serving agents over HTTP with `genkit.Handler`. + +## Generative UI (A2UI) + +Genkit Go has an **A2UI** (Agent-to-UI) plugin +(`github.com/genkit-ai/genkit/go/plugins/a2ui/exp`, imported as `a2uix`) that +lets an agent stream interactive UI **surfaces** (cards, lists, forms, buttons), +not just prose. The whole integration is the `&a2uix.Surfaces{}` model +middleware added via `ai.WithUse` +on a generate call or agent inline prompt. **Go is server-only: it has no A2UI +client/renderer.** Render surfaces with a JS or Dart/Flutter client that talks to +your Go agent over HTTP (the server and clients are wire-compatible). + +- [A2UI](references/a2ui.md): server middleware, options, custom catalogs, and security. For the client (rendering surfaces), refer to the A2UI reference in the Genkit JS or Dart skill. + +## Genkit CLI (recommended) + +`genkit start` unintrusively wraps any Go program that uses the Genkit library, running it unchanged while capturing traces from every Genkit action so you can prove tools were actually called and inspect model I/O from the terminal, even for headless checks. It forwards stdio, so interactive CLI tools that rely on stdin/stdout work without issues. Running the app directly (`go run .`) skips trace capture, so you're debugging blind. Check install with `genkit --version`. + +**Installation:** +```bash +curl -sL cli.genkit.dev | bash +``` + +**Primary pattern (default):** prefix `genkit start --` to your normal run command. This collects telemetry from any Genkit code your program runs, whether triggered from the dev UI, your own web server/web UI, or a plain script. Starts the Developer UI (usually http://localhost:4000) for running flows, model and agent playground, and browsing traces: +```bash +genkit start -- go run . +genkit start --noui -- go run . # same, without the Dev UI (still a persistent server) +genkit start -o -- go run . # also opens the browser +``` +`genkit start` runs until you stop it with Ctrl+C. That is expected and correct for the common cases: a server your web/mobile app calls, or an interactive CLI you exit yourself. `--noui` only drops the Dev UI; it is **not** a one-shot command and will not exit on its own. Do **not** use `genkit start` as a blocking step in automated/non-interactive contexts; use `flow:run` (below) for that. + +**Non-interactive use (agents/CI):** add the global `--non-interactive` flag before `--` so the CLI uses defaults and never blocks on a prompt (e.g. the first-run analytics notice): `genkit start --non-interactive -- go run .` (works with `flow:run` too). + +**Run a flow (`flow:run`):** invoke a specific flow by name from the CLI. Append your run command after `--` to spin up the runtime just for this run (the command runs as-is to register your flows): +```bash +genkit flow:run myFlow '{"data": "input"}' -- go run . +genkit flow:run myFlow '{"data": "input"}' --stream -- go run . # with streaming +genkit flow:run myFlow '{"data": "input"}' --wait -- go run . # wait for completion +``` +This is **self-terminating**: it runs the flow once, prints a `Trace ID`, then exits, so it's the right choice for a quick, non-interactive check (unlike `genkit start`). Traces for this run can be inspected using the trace commands below. + +**Debugging with traces:** the fastest way to see prompts, model inputs/outputs, tool calls, latencies, and errors. Inspect from the terminal after any run under `genkit start`: +```bash +genkit trace:list # find recent trace IDs +genkit trace:get # full trace details (inputs, outputs, tool calls, errors) +genkit trace:get --format json # machine-readable JSON, safe to pipe into jq or other parsers +``` + +For machine-readable output, pass `--format json` to get clean JSON you can pipe into `jq` or other parsers. The **default** output is human-oriented (banner/log lines, possible truncation on large traces), so don't pipe that form directly; use `--format json`, grep, or the Dev UI trace viewer. + + +**Documentation:** +```bash +genkit docs:search "streaming" go +genkit docs:list go +genkit docs:read go/flows.md +``` + +See [references/getting-started.md](references/getting-started.md) for full CLI and Developer UI details. + +## Key Guidance + + +- **Pass `g` explicitly.** The `*Genkit` instance returned by `genkit.Init` is the central registry. Pass it to all Genkit functions rather than storing it as a global. This is a core pattern throughout the SDK. +- **Wrap AI logic in flows.** Flows give you tracing, observability, HTTP deployment via `genkit.Handler`, and the ability to test from the Developer UI and CLI. Any generation call worth keeping should live in a flow. +- **Verify with traces, not a blind run.** Running the app directly (`go run .`) does not capture dev traces. See the [Genkit CLI](#genkit-cli-recommended) section for how to run your app and capture traces. +- **Use `jsonschema:"description=..."` struct tags on output types.** The model uses these descriptions to understand what each field should contain. Without them, structured output quality drops significantly. +- **Write good tool descriptions.** The model decides which tools to call based on their description string. Vague descriptions lead to missed or incorrect tool calls. +- **Use `.prompt` files for complex prompts.** They separate prompt content from Go code, support Handlebars templating, and can be iterated on without recompilation. Code-defined prompts are better for simple, single-line cases. +- **Reach for built-in middleware before writing one.** `Retry`, `Fallback`, `ToolApproval`, `Filesystem`, and `Skills` cover the common cross-cutting needs and compose with each other via `ai.WithUse`. See [references/middleware.md](references/middleware.md). When you do write custom middleware, allocate per-call state in closures captured by `New`, and guard anything that `WrapTool` mutates because tools may run concurrently. +- **Look up the latest model IDs.** Model names change frequently. Check provider documentation for current model IDs rather than relying on hardcoded names. See [references/providers.md](references/providers.md). diff --git a/plugins/genkit/.agents/skills/developing-genkit-go/references/a2ui.md b/plugins/genkit/.agents/skills/developing-genkit-go/references/a2ui.md new file mode 100644 index 00000000..6d470b07 --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-go/references/a2ui.md @@ -0,0 +1,255 @@ +# A2UI (Agent-to-UI) generative UI + +The A2UI plugin (`github.com/genkit-ai/genkit/go/plugins/a2ui/exp`, imported as +`a2uix`) brings [A2UI](https://a2ui.org/), a transport-agnostic, JSON-based +streaming UI protocol, to Genkit Go agents. The plugin is in preview: it lives +under the `.../a2ui/exp` import path (package `exp`, aliased `a2uix` following +`aix` and `genkitx`) and its APIs may change in any minor version release. + +A2UI builds on the experimental agent API +(`genkit.Init(ctx, genkit.WithExperimental())`). Read [Agents](agents.md) +first if you have not. + +An A2UI-enabled agent streams more than prose. It streams interactive UI +**surfaces** (cards, lists, forms, buttons) that a client renders incrementally +as the model responds. The entire Go integration is a single model middleware: +add `&a2uix.Surfaces{}` to a generate/agent's `ai.WithUse` and nothing else changes. + +## Go is server-only: bring your own client + +**Go has no A2UI client/renderer.** The Go plugin is the server half: it injects +catalog capabilities into the prompt and rewrites the model's a2ui blocks into +a2ui data parts on the stream. To actually render surfaces in a browser or app, +use a JS or Dart/Flutter client that talks to your Go agent over HTTP. The Go +server and the JS/Dart client are wire-compatible (same `application/a2ui+json` +data parts, same catalog ids), so mix and match freely. + +For the client half, refer to the A2UI reference in the Genkit **JS** or **Dart** +skill (the `a2ui.md` reference in `developing-genkit-js` / `developing-genkit-dart`). +Read their client sections and ignore their server/`a2ui()` middleware sections +(that is the JS/Dart server, replaced by the Go server below): + +- **JS**: browser rendering with an `@a2ui/*` renderer (Lit/React/Angular) plus + the `@genkit-ai/a2ui/client` helpers (`remoteAgent`, `a2uiEnvelopesFromParts`, + `actionToMessage`). +- **Dart/Flutter**: rendering with the + [`genui`](https://pub.dev/packages/genui) package plus the + `package:genkit_a2ui/client.dart` helpers. + +The client points `remoteAgent` at the Go endpoint (for example +`remoteAgent({ url: '/api/uiAgent' })`), and the client's renderer registers a +catalog under the **same catalog id** the Go server advertises. + +## Server: add the `a2ui` middleware + +Add `&a2uix.Surfaces{}` to the generate call (or agent inline prompt) via +`ai.WithUse`. That is the whole server-side setup. Registering `&a2uix.A2UI{}` +in `genkit.Init` is **optional**: it only surfaces the middleware and catalogs in +the Dev UI. The middleware works via `ai.WithUse` regardless. + +```go +import ( + "context" + + "github.com/genkit-ai/genkit/go/ai" + aix "github.com/genkit-ai/genkit/go/ai/exp" + "github.com/genkit-ai/genkit/go/ai/exp/localstore" + "github.com/genkit-ai/genkit/go/genkit" + genkitx "github.com/genkit-ai/genkit/go/genkit/exp" + a2uix "github.com/genkit-ai/genkit/go/plugins/a2ui/exp" + "github.com/genkit-ai/genkit/go/plugins/googlegenai" +) + +ctx := context.Background() +// &a2uix.A2UI{} is optional (Dev UI only); the middleware works without it. +g := genkit.Init(ctx, + genkit.WithPlugins(&googlegenai.GoogleAI{}, &a2uix.A2UI{}), + genkit.WithExperimental(), +) + +uiAgent := genkitx.DefineAgent(g, "uiAgent", + aix.InlinePrompt{ + ai.WithModelName("googleai/gemini-flash-latest"), + ai.WithSystem( + "You help users. Render an A2UI surface whenever a result is " + + "clearer shown than told (weather, comparisons, lists, forms). " + + "Keep prose brief; put the substance in the UI."), + ai.WithUse(&a2uix.Surfaces{}), // defaults to the bundled basic catalog + }, + // Server-managed state: the client just passes a session id (remoteAgent + // handles that), history lives in the store. + aix.WithSessionStore(localstore.NewInMemorySessionStore[any]()), +) +``` + +It works identically on a one-shot `genkit.Generate`: + +```go +resp, err := genkit.Generate(ctx, g, + ai.WithModelName("googleai/gemini-flash-latest"), + ai.WithPrompt("Show me the weather in Tokyo"), + ai.WithUse(&a2uix.Surfaces{}), +) +``` + +### Middleware ordering + +A2UI keeps per-turn streaming state (a stream parser and its minted surface ids) +for the model call it wraps. Place any retrying/fallback middleware (which +re-invokes the model) **outside** A2UI so each attempt gets a fresh A2UI turn. +`ai.WithUse(A, B)` means A wraps B: + +```go +ai.WithUse( + &middleware.Retry{MaxRetries: 5}, // outer: fresh A2UI turn per attempt + &a2uix.Surfaces{}, // inner +) +``` + +## Serve the agent over HTTP + +Serve the agent with `genkit.Handler` (see [Deploying agents](agents-deployment.md)). +A server-managed agent exposes three endpoints the `remoteAgent` client expects: +the turn endpoint plus its `/getSnapshot` and `/abort` companions. + +```go +mux := http.NewServeMux() +mux.HandleFunc("POST /api/uiAgent", genkit.Handler(uiAgent)) +mux.HandleFunc("POST /api/uiAgent/getSnapshot", genkit.Handler(uiAgent.GetSnapshotAction())) +mux.HandleFunc("POST /api/uiAgent/waitForSnapshot", genkit.Handler(uiAgent.WaitForSnapshotAction())) +mux.HandleFunc("POST /api/uiAgent/abort", genkit.Handler(uiAgent.AbortAction())) + +log.Fatal(server.Start(ctx, "127.0.0.1:8080", mux)) +``` + +If the browser is served from a different origin than the agent (for example a +Vite dev server on `:5173`), wrap the entire router/mux with permissive CORS +middleware (as shown in [Deploying agents](agents-deployment.md#cors-for-browser-clients)) +so that preflight OPTIONS requests are handled automatically without needing to +register duplicate bare paths. Behind a same-origin proxy (for example Vite preview), +CORS is a no-op. + +You can also drive the agent directly with curl: + +```bash +curl -N -X POST 'http://localhost:8080/api/uiAgent?stream=true' \ + -H "Content-Type: application/json" \ + -d '{"data": {"message": {"role": "user", "content": [{"text": "What is the weather in Tokyo?"}]}}}' +``` + +### Config options + +`&a2uix.Surfaces{}` fields (all optional): + +| Field | Default | Description | +| -------------- | ----------------------------------- | ---------------------------------------------------------------------------------------------------------- | +| `Catalog` | nil | Inline `*a2uix.Catalog`. Wins over `CatalogID`, but is not serialized, so prefer `CatalogID` + `LoadCatalog`. | +| `CatalogID` | `a2uix.DefaultCatalogID` (`"basic"`) | Id of a catalog registered with `a2uix.LoadCatalog`. Resolved from the registry at call time. | +| `Instructions` | `a2uix.InstructionsSystem` | Where catalog capabilities are injected. `a2uix.InstructionsNone` injects nothing (supply your own). | +| `Validate` | `a2uix.ValidateWarn` | `ValidateWarn` logs and drops bad blocks; `ValidateStrict` returns an error; `ValidateOff` skips checking. | +| `SurfaceID` | fresh UUID | Surface-id policy. Set a fixed string to reuse one id per surface; empty mints a fresh UUID per surface. | +| `Version` | `a2uix.DefaultVersion` (`"v0.9"`) | Protocol version stamped on envelopes. Validated against `a2uix.SupportedVersions`. | + +Use `Validate: a2uix.ValidateStrict` during development to fail fast on malformed +JSON or components outside the catalog. See [Security](#security-and-the-trust-boundary) +for what strict does and does not check. + +## Custom catalogs + +The `CatalogID` is a **catalog id** resolved from the Genkit registry. The +bundled basic catalog (`a2uix.BasicCatalog()`, id `a2uix.BasicCatalogID`, mirroring +`@a2ui/web_core`'s basic catalog) is the default and needs no registration. A +catalog describes the components the model may emit: + +- `ID`: globally-unique URI (also used as `catalogId` on `createSurface`). +- `Components`: each an `a2uix.CatalogComponent` with `Name` (matches the renderer + type), `Description` (one-line summary), and `Props` (a compact, model-facing + text description of the component's props, kept as plain text to minimize + prompt tokens). + +Register a catalog with `a2uix.LoadCatalog`, then reference it by id. Start from +the basic catalog and add your own component: + +```go +import a2uix "github.com/genkit-ai/genkit/go/plugins/a2ui/exp" + +weatherCatalog := a2uix.BasicCatalog() +weatherCatalog.ID = "https://my-app.org/catalogs/weather.json" +weatherCatalog.Components = append(weatherCatalog.Components, a2uix.CatalogComponent{ + Name: "Gauge", + Description: "A circular gauge visualizing a single numeric value.", + Props: "value: number or { path } binding (required); min?: number; max?: number; label?: string; unit?: string.", +}) + +if err := a2uix.LoadCatalog(g, weatherCatalog); err != nil { + log.Fatalf("loading catalog: %v", err) +} + +uiAgent := genkitx.DefineAgent(g, "uiAgent", + aix.InlinePrompt{ + ai.WithModelName("googleai/gemini-flash-latest"), + ai.WithUse(&a2uix.Surfaces{ + CatalogID: weatherCatalog.ID, + Validate: a2uix.ValidateStrict, + }), + }, + aix.WithSessionStore(localstore.NewInMemorySessionStore[any]()), +) +``` + +You can also load a catalog from a JSON file with +`a2uix.LoadCatalogFile(g, "./my-catalog.json")` (an object with an `id` string and +a `components` array). To surface the bundled basic catalog in the Dev UI +alongside custom ones, call `a2uix.RegisterBasicCatalog(g)` (optional; the +middleware falls back to it either way). `LoadCatalog` is idempotent per id. + +The **client must register a matching renderer under the same catalog id**, and +each component `Name` must match on both sides. Otherwise the model emits a +component the client cannot render. See the A2UI reference in the JS or Dart +skill for how to register a custom component (a genui `CatalogItem` in Dart, an +`@a2ui/*` renderer in JS). Catalogs live in the registry under value type `a2uix.CatalogValueType` +(`"a2ui-catalog"`); the Dev UI lists them at `GET /api/values?type=a2ui-catalog`. + +## Security and the trust boundary + +Generative UI moves model output into the client UI, so treat every surface an +agent emits as **untrusted input**. `Validate` (including `ValidateStrict`) checks +envelope structure and component *type names* against the catalog only. It does +**not** validate component props or data-model values: model-controlled values +such as an `Image`'s url or a `Text`'s inline Markdown pass through untouched. +`ValidateStrict` is a well-formedness check, not a security boundary. + +- **The client renderer/catalog owns prop sanitization.** Whatever renders a + surface is responsible for escaping and sanitizing prop values before they + reach the UI. +- **Restrict remote sources at the host.** On the web, serve the client with a + Content Security Policy that limits `img-src` and other fetch directives to + origins you trust. +- **Do not put secrets in the data model.** Anything bound into a surface's data + model can be echoed back through an action's `context`. + +For server-side control over props (for example, allow-listing image hosts), add +your own model middleware after `&a2uix.Surfaces{}` to inspect and rewrite the +emitted a2ui parts. + +## How it works + +A2UI rides on its own part channel: a Genkit `data` part with mime type +`application/a2ui+json` (`a2uix.A2UIMimeType`) whose data is +`{ "envelopes": [...] }`. On each model call, `&a2uix.Surfaces{}`: + +1. Sanitizes inbound a2ui parts (a surface action sent back as the next turn, or + replayed history) into model-readable text, so a model that does not + understand the a2ui mime type can still reason about prior surfaces and user + actions. +2. Injects the catalog's capabilities into the system prompt (unless + `Instructions: a2uix.InstructionsNone`). +3. Intercepts the model output (streamed chunks and the final message). +4. Extracts `a2ui` fenced code blocks from the model's text. +5. Validates them against the catalog (per `Validate`). +6. Rewrites them into canonical a2ui data parts. + +For a complete, runnable example (a Go backend plus a Vite + Lit web frontend), +see `go/samples/basic-middleware/a2ui` in the Genkit repo (it serves the same +agent the JS `a2ui` testapp's web UI talks to). + diff --git a/plugins/genkit/.agents/skills/developing-genkit-go/references/agents-artifacts.md b/plugins/genkit/.agents/skills/developing-genkit-go/references/agents-artifacts.md new file mode 100644 index 00000000..53578858 --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-go/references/agents-artifacts.md @@ -0,0 +1,121 @@ +# Working with Artifacts (Experimental) + +> **Experimental / preview API.** The `Artifacts` middleware comes from +> `plugins/middleware/exp`; the `Artifact` type from `ai/exp`. Read +> [agents.md](agents.md) first. + +**Artifacts** are named, content-bearing deliverables an agent produces during a +session — files, reports, code, etc. They live in the session (deduplicated by +name) and are returned on `out.Artifacts`. + +```go +import ( + aix "github.com/genkit-ai/genkit/go/ai/exp" + middlewarex "github.com/genkit-ai/genkit/go/plugins/middleware/exp" +) +``` + +## Give the model artifact tools + +The `middlewarex.Artifacts` middleware (see [using middleware](middleware.md)) +adds `read_artifact` and `write_artifact` tools and injects an `` +listing (names + sizes, not full content) into the system prompt each turn. No +custom tool code needed. Attach it via the `ai.WithUse` prompt option. + +```go +workspaceAgent := genkitx.DefineAgent[any](g, "workspaceAgent", + aix.InlinePrompt{ + ai.WithModelName("googleai/gemini-flash-latest"), + ai.WithSystem(`You are a code generation assistant. Use write_artifact to create +files (pass the filename as "name" and the full content as "content"). Use +read_artifact to review or modify a previously created file.`), + ai.WithUse(&middlewarex.Artifacts{}), + }, +) +``` + +No session store is needed to produce artifacts — they are returned on +`out.Artifacts` either way (this agent is [client-managed](agents.md#client-managed-vs-server-managed-state), +hence the explicit `[any]` type parameter). Add a store (e.g. +`localstore.NewFileSessionStore`) only when artifacts should persist across turns +or sessions. See [sessions](agents-sessions.md). + +Run it; artifacts are produced via the tool and returned in the output: + +```go +out, _ := workspaceAgent.RunText(ctx, "Write poem.txt with a poem about Genkit") +for _, a := range out.Artifacts { + fmt.Println(a.Name) +} +``` + +`middlewarex.Artifacts` option: + +- `Readonly` (default `false`): when `true`, only `read_artifact` is provided — + the model can read but not create/update artifacts. Useful on an orchestrator + that should review (but not produce) sub-agent artifacts. + +```go +ai.WithUse(&middlewarex.Artifacts{Readonly: true}) +``` + +## The `Artifact` shape + +```go +// An artifact's content lives in Parts (text/media parts). Metadata is optional. +artifact := &aix.Artifact{ + Name: "poem.txt", + Parts: []*ai.Part{ai.NewTextPart("Roses are red…")}, + Metadata: map[string]any{"source": "workspaceAgent"}, // optional +} +``` + +Writing the same `Name` again **replaces** the artifact (dedup by name). + +## Programmatic access (inside tools / custom agents) + +Use the session's artifact store. `aix.ArtifactStoreFromContext(ctx)` returns a +`State`-agnostic view (so tools don't need to know the agent's `State` type), or +`nil` when there is no active session. + +```go +store := aix.ArtifactStoreFromContext(ctx) +if store == nil { + // not inside an agent session +} + +// Read all artifacts: +for _, a := range store.Artifacts() { + if a.Name == "poem.txt" { + // ... + } +} + +// Create / replace artifacts: +store.AddArtifacts(&aix.Artifact{ + Name: "notes.md", + Parts: []*ai.Part{ai.NewTextPart("# Notes")}, +}) +``` + +From a [custom agent](agents-custom.md) you can also stream an artifact to the +client with `resp.SendArtifact(a)`, which forwards it and adds it to the session +in one call. + +## Sharing artifacts across agents + +In [multi-agent orchestration](agents-multi-agent.md), set the `Agents` +middleware's `ArtifactStrategy: middlewarex.ArtifactStrategySession` so sub-agent +artifacts are merged into the parent session (namespaced by invocation ID), and +add `&middlewarex.Artifacts{Readonly: true}` to the orchestrator so it can +`read_artifact` them: + +```go +ai.WithUse( + &middlewarex.Agents{ + Agents: []aix.AgentRef{{Name: "researcher"}, {Name: "coder"}}, + ArtifactStrategy: middlewarex.ArtifactStrategySession, + }, + &middlewarex.Artifacts{Readonly: true}, +) +``` diff --git a/plugins/genkit/.agents/skills/developing-genkit-go/references/agents-background.md b/plugins/genkit/.agents/skills/developing-genkit-go/references/agents-background.md new file mode 100644 index 00000000..ca03d172 --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-go/references/agents-background.md @@ -0,0 +1,128 @@ +# Background Agents / Detaching (Experimental) + +> **Experimental / preview API.** Detaching **requires a +> [session store](agents-sessions.md)** that also implements +> `aix.SnapshotSubscriber` (both `localstore` stores do) — the server needs +> somewhere to write the result and a way to observe an abort. Read +> [agents.md](agents.md) first. + +Detaching runs a turn in the background: the server writes a `pending` snapshot +and returns its `SnapshotID` **immediately**, keeps processing, then rewrites the +snapshot to a terminal status (`completed` / `failed` / `aborted`). A reader polls +`GetSnapshot` for completion and can `Abort` in the meantime. + +## Define the agent (store required) + +```go +backgroundAgent := genkitx.DefineAgent(g, "backgroundAgent", + aix.InlinePrompt{ + ai.WithModelName("googleai/gemini-flash-latest"), + ai.WithSystem("You are a senior research analyst. Produce a comprehensive markdown report."), + }, + aix.WithSessionStore(localstore.NewInMemorySessionStore[any]()), // REQUIRED for detach +) +``` + +## Detach a turn + +Set `Detach: true` on the input (or call `conn.Detach()` on a connection). The +call returns right away with `FinishReason == aix.AgentFinishReasonDetached` and +the pending snapshot's ID. + +```go +out, err := backgroundAgent.Run(ctx, &aix.AgentInput{ + Detach: true, + Message: ai.NewUserTextMessage("Write a report on renewable energy trends"), +}) +if err != nil { + log.Fatal(err) +} +fmt.Println(out.FinishReason) // aix.AgentFinishReasonDetached +snapshotID := out.SnapshotID // pending; poll this +``` + +## Poll until terminal + +Read the snapshot on an interval until its status leaves `pending`. + +```go +func waitForResult(ctx context.Context, agent *aix.Agent[any], snapshotID string) (*aix.SessionSnapshot[any], error) { + ticker := time.NewTicker(2 * time.Second) + defer ticker.Stop() + for { + snap, err := agent.GetSnapshot(ctx, snapshotID) + if err != nil { + return nil, err + } + switch snap.Status { + case aix.SnapshotStatusPending: + // keep polling + case aix.SnapshotStatusCompleted: + return snap, nil + case aix.SnapshotStatusFailed: + return snap, fmt.Errorf("background task failed: %v", snap.Error) + case aix.SnapshotStatusAborted: + return snap, nil + case aix.SnapshotStatusExpired: + // The worker stopped sending heartbeats (e.g. the server restarted), + // so the task can never complete — treat as terminal. + return snap, nil + } + select { + case <-ctx.Done(): + return nil, ctx.Err() + case <-ticker.C: + } + } +} + +snap, err := waitForResult(ctx, backgroundAgent, snapshotID) +if err != nil { + log.Fatal(err) +} +// Read the result from the finalized snapshot state: +msgs := snap.State.Messages +if len(msgs) > 0 { + fmt.Println(msgs[len(msgs)-1].Text()) +} +``` + +## Abort an in-flight task + +`Agent.Abort` flips a pending snapshot to `aborted`; the runtime observes the +flip and cancels the background work. It is a no-op on a missing or +already-terminal snapshot (returns the existing status). + +```go +status, err := backgroundAgent.Abort(ctx, snapshotID) +if err != nil { + log.Fatal(err) +} +fmt.Println(status) // aix.SnapshotStatusAborted (or the existing terminal status) +``` + +## Serve detach over HTTP + +A remote client drives the same story over HTTP: send `{"detach": true, ...}` as +the turn input, poll the `getSnapshot` companion, and hit the `abort` companion. +Expose both companions next to the agent: + +```go +mux.HandleFunc("POST /api/backgroundAgent", genkit.Handler(backgroundAgent)) +mux.HandleFunc("POST /api/backgroundAgent/getSnapshot", genkit.Handler(backgroundAgent.GetSnapshotAction())) +mux.HandleFunc("POST /api/backgroundAgent/abort", genkit.Handler(backgroundAgent.AbortAction())) +``` + +`AbortAction()` is non-nil only when the store implements `aix.SnapshotSubscriber` +(both `localstore` stores do). See [deployment](agents-deployment.md). + +## Status values (`aix.SnapshotStatus`) + +- `SnapshotStatusPending` — still processing. +- `SnapshotStatusCompleted` — finished successfully; read the result from + `snap.State.Messages`. +- `SnapshotStatusFailed` — error during processing; details on `snap.Error`. +- `SnapshotStatusAborted` — cancelled via `Abort`. +- `SnapshotStatusExpired` — the background worker stopped responding (its + heartbeat went stale, e.g. a server restart). Computed on read, never + persisted; terminal — the task can never complete. diff --git a/plugins/genkit/.agents/skills/developing-genkit-go/references/agents-branching.md b/plugins/genkit/.agents/skills/developing-genkit-go/references/agents-branching.md new file mode 100644 index 00000000..0d470a30 --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-go/references/agents-branching.md @@ -0,0 +1,102 @@ +# Agent Branching (Experimental) + +> **Experimental / preview API.** Requires a [session store](agents-sessions.md) +> so snapshots are persistent. Read [agents.md](agents.md) first. + +A `SnapshotID` is an **immutable checkpoint**, like a git commit. You can fork as +many independent timelines as you want from the same snapshot — each turn from a +snapshot creates a new, independent snapshot; the original is unchanged. + +To branch, run a turn resuming from an earlier snapshot via +`aix.WithSnapshotID(id)`. + +## Branch from a checkpoint + +```go +assistant := genkitx.DefineAgent(g, "assistant", + aix.InlinePrompt{ + ai.WithModelName("googleai/gemini-flash-latest"), + ai.WithSystem("You are a helpful assistant."), + }, + aix.WithSessionStore(localstore.NewInMemorySessionStore[any]()), +) + +root, _ := assistant.RunText(ctx, "Hello!") +checkpoint := root.SnapshotID // branch point + +// Branch A — forks from `checkpoint`. +a1, _ := assistant.RunText(ctx, "My name is Bob.", aix.WithSnapshotID(checkpoint)) +a2, _ := assistant.RunText(ctx, "What is my name?", aix.WithSnapshotID(a1.SnapshotID)) // -> Bob + +// Branch B — forks from the SAME `checkpoint`, fully independent. +b1, _ := assistant.RunText(ctx, "My name is John.", aix.WithSnapshotID(checkpoint)) +b2, _ := assistant.RunText(ctx, "What is my name?", aix.WithSnapshotID(b1.SnapshotID)) // -> John + +_ = a2 +_ = b2 +``` + +Each turn returns a fresh `SnapshotID`; the two branches never share one past the +common `checkpoint`. + +## "Pick a variant" + +A common pattern: generate two variants from the same checkpoint in parallel, +let the user pick one, and continue from the chosen snapshot. + +```go +func twoVariants(ctx context.Context, agent *aix.Agent[any], checkpoint, text string) (a, b *aix.AgentOutput[any], err error) { + var wg sync.WaitGroup + var errA, errB error + wg.Add(2) + go func() { defer wg.Done(); a, errA = agent.RunText(ctx, text, aix.WithSnapshotID(checkpoint)) }() + go func() { defer wg.Done(); b, errB = agent.RunText(ctx, text, aix.WithSnapshotID(checkpoint)) }() + wg.Wait() + if errA != nil { + return nil, nil, errA + } + if errB != nil { + return nil, nil, errB + } + return a, b, nil // a.SnapshotID != b.SnapshotID; both branch from the same point +} + +// When the user picks a variant, its SnapshotID becomes the new branch point: +// checkpoint = chosen.SnapshotID +``` + +When no branch point exists yet (the very first turn), start a fresh session by +omitting the invocation option, then use the returned `SnapshotID` as the branch +point going forward. + +## Resume by session vs. by snapshot + +- `aix.WithSnapshotID(id)` — fork from an **exact** checkpoint (branching). +- `aix.WithSessionID(id)` — continue the session's **latest** snapshot. If + history was forked, the most recently created branch wins; use + `WithSnapshotID` when you need a specific branch. + +## Restore history from a snapshot + +Use `Agent.GetSnapshot` to read a snapshot's state without starting a turn — +handy for restoring a UI after a reload (e.g. a `SnapshotID` stored in the URL). + +```go +snap, err := assistant.GetSnapshot(ctx, snapshotID) +if err != nil { + log.Fatal(err) +} +for _, m := range snap.State.Messages { + if m.Role == ai.RoleUser || m.Role == ai.RoleModel { + fmt.Printf("%s: %s\n", m.Role, m.Text()) + } +} +``` + +Over HTTP, expose `Agent.GetSnapshotAction()` so a remote client can fetch +snapshots (see [agents.md](agents.md#serve-an-agent-over-http)). + +> Abandoned branches simply remain in the store as immutable snapshots; nothing +> is overwritten when you branch. (A `FileSessionStore` configured with +> `WithMaxPersistedChainLength` prunes only along a single chain's parent links, +> so sibling branches are retained independently.) diff --git a/plugins/genkit/.agents/skills/developing-genkit-go/references/agents-custom.md b/plugins/genkit/.agents/skills/developing-genkit-go/references/agents-custom.md new file mode 100644 index 00000000..7da0384f --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-go/references/agents-custom.md @@ -0,0 +1,194 @@ +# Advanced Custom Agents — `DefineCustomAgent` (Experimental) + +> **Experimental / preview API.** `genkitx.DefineCustomAgent` comes from +> `genkit/exp`. Read [agents.md](agents.md) and [agent state](agents-state.md) +> first. + +`DefineAgent` runs a single prompt + tool loop. When you need **full control of +the turn** — multiple sequential model calls, custom logic between them, manual +message/state management, or custom progress streaming — use +`genkitx.DefineCustomAgent`. You provide the function that runs the turn loop. + +## When to use it + +Reach for `DefineCustomAgent` when a turn needs to: + +- make **multiple model calls** with your own orchestration between them; +- run **multi-step workflows** (decompose → research → synthesize); +- **manually manage** messages and custom state; +- **stream custom status** updates to the client mid-turn. + +Otherwise prefer `DefineAgent` (simpler; custom state still works — see +[agent state](agents-state.md)). + +## Signature + +```go +func DefineCustomAgent[State any]( + g *genkit.Genkit, + name string, + fn aix.AgentFunc[State], + opts ...aix.AgentOption[State], +) *aix.Agent[State] + +// where: +type AgentFunc[State any] = func(ctx context.Context, resp aix.Responder, sess *aix.SessionRunner[State]) (*aix.AgentResult, error) +``` + +Your `fn` receives: + +- `resp aix.Responder` — the output channel to the client. `resp.SendModelChunk(chunk)` + streams generation tokens; `resp.SendArtifact(a)` streams (and records) an + artifact. Sends are fire-and-forget. +- `sess *aix.SessionRunner[State]` — the session plus turn-loop control. Call + `sess.Run(ctx, perTurn)` to enter the loop; it blocks per turn until the client + sends the next input. + +Key `sess` methods: + +- `sess.Run(ctx, func(ctx, input *aix.AgentInput) (*aix.TurnResult, error))` — + runs the turn loop. The incoming `input.Message` is added to the session before + your callback runs, so `sess.Messages()` includes it. Return a `*aix.TurnResult` + to report the finish reason (or `nil` to report none). +- `sess.Messages()` / `sess.AddMessages(...)` / `sess.SetMessages(...)` — read and + write conversation history. +- `sess.UpdateCustom(fn)` — mutate typed custom state; auto-streams a + `CustomPatch` chunk (see [state](agents-state.md)). +- `sess.Result()` — build an `*aix.AgentResult` from the current session (last + message + artifacts), a convenience for the return value. + +## Example: multi-step research agent + +```go +type ResearchState struct { + Status string `json:"status,omitempty"` // live progress shown to the client + SubQuestions []string `json:"subQuestions"` + SubAnswers []QA `json:"subAnswers"` +} +type QA struct { + Question string `json:"question"` + Answer string `json:"answer"` +} + +researchAgent := genkitx.DefineCustomAgent(g, "researchAgent", + func(ctx context.Context, resp aix.Responder, sess *aix.SessionRunner[ResearchState]) (*aix.AgentResult, error) { + var lastMessage *ai.Message + err := sess.Run(ctx, func(ctx context.Context, input *aix.AgentInput) (*aix.TurnResult, error) { + userText := input.Message.Text() + + // Step 1: decompose. Mutating custom state auto-emits a CustomPatch + // chunk so the client's tracked state stays live. + sess.UpdateCustom(func(s ResearchState) ResearchState { + s.Status = "Decomposing question…" + return s + }) + subQs, _, err := genkit.GenerateData[[]string](ctx, g, + ai.WithModelName("googleai/gemini-flash-latest"), + ai.WithPrompt("Break this into 2-3 sub-questions (JSON array): %q", userText), + ) + if err != nil { + return nil, err + } + + sess.UpdateCustom(func(s ResearchState) ResearchState { + s.SubQuestions, s.SubAnswers = *subQs, nil + return s + }) + + // Step 2: research each sub-question. + var answers []QA + for i, q := range *subQs { + sess.UpdateCustom(func(s ResearchState) ResearchState { + s.Status = fmt.Sprintf("Researching (%d/%d)", i+1, len(*subQs)) + return s + }) + a, err := genkit.GenerateText(ctx, g, + ai.WithModelName("googleai/gemini-flash-latest"), + ai.WithPrompt(q), + ) + if err != nil { + return nil, err + } + answers = append(answers, QA{Question: q, Answer: a}) + } + sess.UpdateCustom(func(s ResearchState) ResearchState { + s.SubAnswers, s.Status = answers, "Synthesizing…" + return s + }) + + // Step 3: synthesize and STREAM the final answer to the client. + var reason aix.AgentFinishReason + for result, err := range genkit.GenerateStream(ctx, g, + ai.WithModelName("googleai/gemini-flash-latest"), + ai.WithPrompt("Synthesize a unified answer from: %v", answers), + ) { + if err != nil { + return nil, err + } + if result.Done { + lastMessage = result.Response.Message + reason = aix.AgentFinishReason(result.Response.FinishReason) + sess.AddMessages(lastMessage) // record the final response in history + } else { + resp.SendModelChunk(result.Chunk) // stream model output to the client + } + } + sess.UpdateCustom(func(s ResearchState) ResearchState { s.Status = "Done"; return s }) + + // Report how the turn ended; the framework forwards it on TurnEnd and + // persists it on the snapshot. + return &aix.TurnResult{FinishReason: reason}, nil + }) + if err != nil { + return nil, err + } + return sess.Result(), nil + }, + aix.WithSessionStore(localstore.NewInMemorySessionStore[ResearchState]()), +) +``` + +## Custom status streaming + +Calling `sess.UpdateCustom(...)` during the turn automatically emits a +`CustomPatch` chunk, so a connection's tracked [custom state](agents-state.md) +(e.g. the `Status` field) stays live **mid-stream** without extra wiring. Stream +model output separately with `resp.SendModelChunk(chunk)`. + +Run a custom agent exactly like a regular one: + +```go +conn, _ := researchAgent.Connect(ctx, + aix.WithState(&aix.SessionState[ResearchState]{}), +) +_ = conn.SendText("Impacts of electric vehicles?") +for chunk, err := range conn.Receive() { + if err != nil { + log.Fatal(err) + } + if chunk.ModelChunk != nil { + fmt.Print(chunk.ModelChunk.Text()) // model output + } + if len(chunk.CustomPatch) > 0 { + cur, _ := conn.Custom() + fmt.Println("status:", cur.Status) // live progress + } + if chunk.TurnEnd != nil { + break + } +} +out, _ := conn.Output() +``` + +## Notes + +- **Per-turn metadata.** Inside the `sess.Run` callback, + `aix.TurnContextFromContext(ctx)` returns the turn's `SnapshotID`, + `ParentSnapshotID`, and `TurnIndex` — reserved before the turn runs, so you can + name external resources (e.g. a git worktree) after the snapshot up front. +- **Validate untrusted resume.** If you accept `input.Resume` from untrusted + callers, call `aix.ValidateResumeAgainstHistory(input.Resume, sess.Messages())` + before forwarding it to the model (the prompt-backed loop does this for you). +- **Recovering from a failed turn.** If your callback returns an error, `Run` + records the failure and stops; you may call `Run` again to keep processing, or + return the error to resolve the invocation as a failed `AgentOutput`. diff --git a/plugins/genkit/.agents/skills/developing-genkit-go/references/agents-deployment.md b/plugins/genkit/.agents/skills/developing-genkit-go/references/agents-deployment.md new file mode 100644 index 00000000..03ce103d --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-go/references/agents-deployment.md @@ -0,0 +1,171 @@ +# Deploying / Serving Agents over HTTP (Experimental) + +> **Experimental / preview API.** Uses `genkit.Handler` and the agent's companion +> actions. Read [agents.md](agents.md) first. + +An agent is an `api.BidiAction`, so `genkit.Handler(agent)` serves it as a +standard HTTP action — **one turn per request**. Most agents also expose two +companion actions: + +- `agent.GetSnapshotAction()` — read a snapshot's state. Needed for + [snapshot restore](agents-branching.md) and [background](agents-background.md) + polling. `nil` for a client-managed agent (no store). +- `agent.AbortAction()` — cancel a [background](agents-background.md) turn. + `nil` unless the store implements `aix.SnapshotSubscriber` (both `localstore` + stores do). + +## Request/response shape + +The handler accepts `{"data": , "init": }`: + +- `data` — the turn's `aix.AgentInput` (message, resume, or `detach`). Required + for one-shot (non-streaming) requests. +- `init` — the session source: `{"sessionId": ...}` or `{"snapshotId": ...}` for + server-managed agents, `{"state": ...}` for client-managed ones. Omit for a + fresh conversation. + +Responses come back as `{"result": }`. Stream chunks (`modelChunk`, +`turnEnd`, then the final `result`) by either setting `Accept: text/event-stream` +or adding `?stream=true` to the URL — the handler honors both. Turn-tier failures +return **200** with a failed `AgentOutput` (so the caller keeps its last-good +state); init-tier failures (a rejected session source) are hard HTTP errors +(400/404). + +## Serve with the built-in route layout (recommended) + +`genkit/exp` ships a default HTTP layout so you don't hand-wire companions. +`genkitx.AllAgentRoutes(g)` returns a `[]genkitx.Route` covering every registered +agent; `genkitx.AgentRoutes(agent)` does one agent. Each `Route` exposes +`Pattern()` (a `"METHOD /path"` string for `http.ServeMux`) and +`Handler(opts...)` (builds the `genkit.Handler`, passing any `HandlerOption`s). +The layout follows each agent's capabilities, mounting under `/agents`: + +```go +mux := http.NewServeMux() +for _, route := range genkitx.AllAgentRoutes(g) { + mux.HandleFunc(route.Pattern(), route.Handler()) +} +log.Fatal(server.Start(ctx, "127.0.0.1:8080", mux)) +``` + +This produces, per agent: + +``` +POST /agents/ one turn per request +POST /agents//getSnapshot read a snapshot (store-backed agents) +POST /agents//abort abort background work (abortable stores) +``` + +Companion routes are omitted for capabilities an agent lacks: a client-managed +agent contributes only its turn route. To serve specific agents instead of all +of them, use `genkitx.AgentRoutes(agent)`; to expose flows, +`genkitx.AllFlowRoutes(g)` / `genkitx.FlowRoutes(flow)`. Mix them by +concatenating the route slices. Any router works the same way (Gin, Chi, Echo): +read `Pattern()` and serve `Handler()`. + +## A reusable helper for several agents (manual wiring) + +If you'd rather wire routes by hand (a custom path scheme, extra middleware per +route, a `/api`-style prefix), mount the agent and pluck its companions off the +typed value. A small helper keeps companion wiring consistent. + +```go +func exposeAgent[State any](mux *http.ServeMux, agent *aix.Agent[State]) { + base := "/api/" + agent.Name() + mux.HandleFunc("POST "+base, genkit.Handler(agent)) + if snap := agent.GetSnapshotAction(); snap != nil { + mux.HandleFunc("POST "+base+"/getSnapshot", genkit.Handler(snap)) + } + if abort := agent.AbortAction(); abort != nil { + mux.HandleFunc("POST "+base+"/abort", genkit.Handler(abort)) + } +} + +mux := http.NewServeMux() +exposeAgent(mux, weatherAgent) // plain chat: no companions registered +exposeAgent(mux, branchingAgent) // has a store: getSnapshot registered +exposeAgent(mux, backgroundAgent) // store + subscriber: getSnapshot + abort + +log.Fatal(server.Start(ctx, "127.0.0.1:8080", mux)) +``` + +To mount every agent generically, `genkitx.ListAgents(g)` returns every +registered agent (companions are typed on the concrete `*aix.Agent[State]`, so +from `ListAgents` you serve the run action directly and pluck companions off the +typed value where you have it): + +```go +for _, a := range genkitx.ListAgents(g) { + mux.HandleFunc("POST /api/"+a.Name(), genkit.Handler(a)) +} +``` + +For most deployments prefer the built-in route layout above +(`genkitx.AllAgentRoutes`), which wires companions for you. + +Which companions to enable: + +| Agent capability | `getSnapshot` | `abort` | +| ----------------------------------------- | ------------- | ------- | +| Plain chat (client- or server-state) | – | – | +| Snapshot restore / [branching](agents-branching.md) | ✓ | – | +| [Background](agents-background.md) / detach | ✓ | ✓ | + +## CORS for browser clients + +A browser calling a different origin (e.g. a Vite dev server) needs CORS. +Streaming uses Server-Sent Events, so allow the `Accept` header and handle +preflight. + +```go +func withCORS(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Accept") + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS") + if r.Method == http.MethodOptions { + w.WriteHeader(http.StatusNoContent) + return + } + h.ServeHTTP(w, r) + }) +} +``` + +`server.Start` takes a `*http.ServeMux`, so wrap the mux with `http.ListenAndServe` +(which accepts any `http.Handler`) to apply CORS around every route: + +```go +log.Fatal(http.ListenAndServe("127.0.0.1:8080", withCORS(mux))) +``` + +## Registering agents + +Agents register with Genkit when their `genkitx.DefineAgent` (or +`DefineCustomAgent` / `DefinePromptAgent`) call runs, so make sure the code that +defines them executes during startup. Keep a reference to the returned +`*aix.Agent[State]` to reach `RunText`, `GetSnapshot`, `Abort`, and the companion +actions from your own code. + +## Verifying locally + +Run under the Genkit CLI so turns are traced, and drive the endpoint with `curl`: + +```bash +genkit start -- go run . +``` + +```bash +# One-shot turn: +curl -s localhost:8080/api/weatherAgent \ + -H 'Content-Type: application/json' \ + -d '{"data":{"message":{"role":"user","content":[{"text":"Weather in Tokyo?"}]}}}' + +# Resume a server-managed session: +curl -s localhost:8080/api/weatherAgent \ + -H 'Content-Type: application/json' \ + -d '{"data":{"message":{"role":"user","content":[{"text":"And Paris?"}]}},"init":{"sessionId":""}}' +``` + +You can also exercise an agent from the CLI by wrapping one turn in a throwaway +flow and using `genkit flow:run` (see [agents.md](agents.md#verify-an-agent-from-the-cli-flowrun)). diff --git a/plugins/genkit/.agents/skills/developing-genkit-go/references/agents-human-in-the-loop.md b/plugins/genkit/.agents/skills/developing-genkit-go/references/agents-human-in-the-loop.md new file mode 100644 index 00000000..3c9d02d4 --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-go/references/agents-human-in-the-loop.md @@ -0,0 +1,231 @@ +# Agent Human-in-the-Loop / Interrupts (Experimental) + +> **Experimental / preview API.** Read [agents.md](agents.md) first. + +An **interrupt** pauses an agent mid-turn and hands control back to your code (or +a human) — e.g. to approve a sensitive action, collect missing input, or confirm +a plan. Internally it's a **tool call used as control flow**: the interrupting +tool does not produce a normal result; it pauses the turn. You then **resume** +from the exact point it paused. + +Interrupts are **orthogonal to persistence** — they work the same whether the +agent uses a [session store](agents-sessions.md) or +[client-managed state](agents.md#client-managed-vs-server-managed-state). The +paused turn just needs to be carried back into the resume: with a store the +session/snapshot ID does it; without one you round-trip the state blob. + +Flow: send a turn → the response finishes with +`aix.AgentFinishReasonInterrupted` and carries interrupt parts → collect human +input → send an `aix.ToolResume` payload to continue. + +## Define an interruptible tool + +`genkitx.DefineInterruptibleTool[In, Out, Resume]` defines a tool that can pause +by calling `tool.Interrupt(data)`. The `Resume` type is what the caller sends +back to continue; it arrives as the non-nil `res *Resume` parameter on +re-execution. + +```go +import ( + "github.com/genkit-ai/genkit/go/ai" + "github.com/genkit-ai/genkit/go/ai/exp/tool" + "github.com/genkit-ai/genkit/go/core/status" // status.Errorf / status.ErrInvalidArgument +) + +type ApprovalAsk struct { + Action string `json:"action"` + Details string `json:"details"` +} +type ApprovalReply struct { + Approved bool `json:"approved"` + Feedback string `json:"feedback,omitempty"` +} +type TransferInput struct { + Amount float64 `json:"amount"` + ToAccount string `json:"toAccount"` +} +type TransferOutput struct { + Success bool `json:"success"` + TransactionID string `json:"transactionId"` +} + +// transferMoney pauses for approval on the first call, then completes once a +// resume payload (res) is supplied. +transferMoney := genkitx.DefineInterruptibleTool(g, "transferMoney", + "Transfer money to a specified account, pausing for user approval.", + func(ctx context.Context, in TransferInput, res *ApprovalReply) (TransferOutput, error) { + if res == nil { + // First call: pause and surface the request to the caller. + return TransferOutput{}, tool.Interrupt(ApprovalAsk{ + Action: "transferMoney", + Details: fmt.Sprintf("Transfer $%.2f to %s", in.Amount, in.ToAccount), + }) + } + if !res.Approved { + return TransferOutput{Success: false}, nil + } + return TransferOutput{Success: true, TransactionID: fmt.Sprintf("txn-%d", time.Now().Unix())}, nil + }, +) + +bankingAgent := genkitx.DefineAgent(g, "bankingAgent", + aix.InlinePrompt{ + ai.WithModelName("googleai/gemini-flash-latest"), + ai.WithSystem("You are a banking assistant. Use transferMoney to move funds; it will pause for user approval."), + ai.WithTools(transferMoney), + }, + aix.WithSessionStore(localstore.NewInMemorySessionStore[any]()), +) +``` + +`tool.Interrupt(data)` requires `data` to serialize to a JSON object (a struct or +map), since it rides as structured metadata on the interrupted tool request. + +Returning an ordinary Go `error` from the tool is different: it **fails the turn** +rather than pausing it. The runtime wraps the cause as `ai.ErrToolFailed` with the +original preserved, so you can validate inputs and still branch on the classified +status server-side, e.g.: + +```go +if in.Amount <= 0 { + return TransferOutput{}, status.Errorf(status.ErrInvalidArgument, + "transfer amount must be positive, got $%.2f", in.Amount) +} +``` + +## Detect and resume + +When a turn pauses, its output has `FinishReason == aix.AgentFinishReasonInterrupted`. +The interrupt parts are on the last model message; read the typed payload with +`tool.InterruptAs[T]`. Build the resume payload from the **same** interrupt part +(so it validates against history) with `InterruptibleTool.Respond` (supply the +output directly) or `InterruptibleTool.Resume` (re-run the tool with typed resume +data), and continue with `aix.ToolResume`. + +```go +conn, _ := bankingAgent.Connect(ctx) + +_ = conn.SendText("Transfer $500 to my savings account.") + +var interruptPart *ai.Part +for chunk, err := range conn.Receive() { + if err != nil { + log.Fatal(err) + } + if chunk.ModelChunk != nil { + for _, p := range chunk.ModelChunk.Content { + if p.IsToolRequest() { + interruptPart = p + } + } + } + if chunk.TurnEnd != nil { + break + } +} + +if interruptPart != nil { + if ask, ok := tool.InterruptAs[ApprovalAsk](interruptPart); ok { + fmt.Println(ask.Action, ask.Details) // show this to the human + } + + // Collect the human decision, then build a respond part for the SAME tool. + respond, err := transferMoney.Respond(interruptPart, TransferOutput{ + Success: true, + TransactionID: "txn-approved", + }) + if err != nil { + log.Fatal(err) + } + + // Resume the same connection with the tool response. + _ = conn.SendResume(&aix.ToolResume{Respond: []*ai.Part{respond}}) + for chunk, err := range conn.Receive() { + if err != nil { + log.Fatal(err) + } + if chunk.ModelChunk != nil { + fmt.Print(chunk.ModelChunk.Text()) + } + if chunk.TurnEnd != nil { + break + } + } +} + +out, _ := conn.Output() +``` + +### Respond vs. Restart + +- `tool.Respond` / `InterruptibleTool.Respond(part, output)` — supply the tool's + output **without** re-running it. Goes on `aix.ToolResume{Respond: [...]}`. +- `tool.Resume` / `InterruptibleTool.Resume(part, resumeData)` — **re-run** the + interrupted tool, delivering `resumeData` as its `res *Resume` parameter. Goes + on `aix.ToolResume{Restart: [...]}`. + +```go +// Re-run the tool with the human's decision instead of injecting the result: +restart, _ := transferMoney.Resume(interruptPart, ApprovalReply{Approved: true, Feedback: "ok"}) +_ = conn.SendResume(&aix.ToolResume{Restart: []*ai.Part{restart}}) +``` + +You can resume several interrupts at once and mix the two lists: + +```go +_ = conn.SendResume(&aix.ToolResume{ + Respond: []*ai.Part{a}, + Restart: []*ai.Part{b}, +}) +``` + +## Single-turn resume with `Run` + +You don't need a persistent connection. Detect the interrupt on one `Run`, then +resume with another `Run` carrying the `Resume` payload (plus the session source +so the paused turn is in scope). + +```go +out, _ := bankingAgent.RunText(ctx, "Transfer $500 to savings.") +if out.FinishReason == aix.AgentFinishReasonInterrupted { + // Find the interrupt part: don't assume it's the last content part. The + // model may emit text alongside the tool request, or several tool calls + // where only one interrupted. tool.InterruptAs matches the right one. + var part *ai.Part + for _, p := range out.Message.Content { + if _, ok := tool.InterruptAs[ApprovalAsk](p); ok { + part = p + break + } + } + respond, _ := transferMoney.Respond(part, TransferOutput{Success: true, TransactionID: "txn-1"}) + + out, _ = bankingAgent.Run(ctx, + &aix.AgentInput{Resume: &aix.ToolResume{Respond: []*ai.Part{respond}}}, + aix.WithSessionID(out.SessionID), // server-managed: carry the paused turn back + ) +} +fmt.Println(out.Message.Text()) +``` + +For a client-managed agent, pass `aix.WithState(out.State)` instead of +`WithSessionID`. + +## Notes & gotchas + +- **No store required.** Interrupts work with a [session store](agents-sessions.md) + or [client-managed state](agents.md#client-managed-vs-server-managed-state). + Just carry the paused turn back into the resume (session/snapshot ID, or the + state blob). +- **Build resume parts from the interrupt part.** `Respond`/`Resume` derive the + name/ref from the original request. The framework validates every entry against + conversation history (`aix.ValidateResumeAgainstHistory` runs automatically for + prompt-backed agents): name/ref must match, and a `Restart` input must be + unchanged. Hand-rolled parts are rejected. +- **Only resumable snapshots resume.** A failed/aborted/pending snapshot is kept + for inspection but can't be resumed. +- **Re-pausing.** After resuming, the new turn may interrupt again; loop until + the finish reason is no longer `aix.AgentFinishReasonInterrupted`. +- **`ToolApproval` middleware.** For gating arbitrary tools (rather than writing + an interruptible tool by hand), `plugins/middleware`'s `ToolApproval` turns any + tool call outside an allow list into an interrupt. See [middleware](middleware.md). diff --git a/plugins/genkit/.agents/skills/developing-genkit-go/references/agents-multi-agent.md b/plugins/genkit/.agents/skills/developing-genkit-go/references/agents-multi-agent.md new file mode 100644 index 00000000..0f59c0e6 --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-go/references/agents-multi-agent.md @@ -0,0 +1,136 @@ +# Multi-Agent Orchestration / Sub-Agents (Experimental) + +> **Experimental / preview API.** Sub-agent delegation uses the `Agents` +> middleware from `plugins/middleware/exp`. Read [agents.md](agents.md) first. + +A common pattern is an **orchestrator** agent that delegates tasks to specialized +**sub-agents** (e.g. a `researcher` and a `coder`). The `middlewarex.Agents` +middleware injects one delegation tool per sub-agent (`delegate_to_`), +appends a `` block to the orchestrator's system prompt, and — when the +model calls a delegation tool — runs the sub-agent with the task and returns its +response as the tool result. + +```go +import ( + aix "github.com/genkit-ai/genkit/go/ai/exp" + middlewarex "github.com/genkit-ai/genkit/go/plugins/middleware/exp" + "github.com/genkit-ai/genkit/go/plugins/middleware" // Retry, etc. +) +``` + +## 1. Define the sub-agents + +Give each sub-agent a description via `aix.WithDescription` — it's +auto-discovered and shown to the orchestrator so the model knows when to delegate. + +```go +researcher := genkitx.DefineAgent(g, "researcher", + aix.InlinePrompt{ + ai.WithModelName("googleai/gemini-flash-latest"), + ai.WithSystem("You are a thorough research assistant. Save findings with write_artifact."), + ai.WithUse(&middleware.Retry{}, &middlewarex.Artifacts{}), + }, + aix.WithDescription[any]("A thorough research assistant that provides well-sourced answers."), +) + +coder := genkitx.DefineAgent(g, "coder", + aix.InlinePrompt{ + ai.WithModelName("googleai/gemini-flash-latest"), + ai.WithSystem("You are an expert programmer. Save code with write_artifact."), + ai.WithUse(&middlewarex.Artifacts{}, &middleware.Retry{}), + }, + aix.WithDescription[any]("An expert programmer that writes clean, well-commented code."), +) +``` + +## 2. Wire up the orchestrator + +Add `&middlewarex.Agents{...}` to the orchestrator's `ai.WithUse`. Reference each +sub-agent by `aix.AgentRef` — a bare `{Name: ...}` (description auto-discovered +from the registry) or `agent.Ref()` (captures the agent's name and description). + +```go +orchestrator := genkitx.DefineAgent(g, "orchestrator", + aix.InlinePrompt{ + ai.WithModelName("googleai/gemini-flash-latest"), + ai.WithSystem(`You are a helpful project assistant. Analyze the request and +delegate to the appropriate sub-agent. If it needs research AND code, call them +sequentially, then synthesize a final answer.`), + ai.WithUse( + &middlewarex.Agents{ + Agents: []aix.AgentRef{ + {Name: "researcher"}, // by name (description auto-discovered) + coder.Ref(), // by instance (carries its description) + }, + MaxDelegations: 5, // guard rail against runaway loops + HistoryLength: 4, // forward the last N user/model messages as context + ArtifactStrategy: middlewarex.ArtifactStrategySession, // see "Sharing artifacts" below + }, + &middlewarex.Artifacts{Readonly: true}, // read sub-agent artifacts via read_artifact + &middleware.Retry{}, + ), + }, + aix.WithSessionStore(localstore.NewInMemorySessionStore[any]()), +) +``` + +Run it like any other agent: + +```go +out, _ := orchestrator.RunText(ctx, + "Research the best sorting algorithms, then write a Go quicksort.") +fmt.Println(out.Message.Text()) +``` + +## `middlewarex.Agents` fields + +- `Agents` (required): sub-agent references (`[]aix.AgentRef`). Each is a bare + `{Name: ...}` (description auto-discovered) or `agent.Ref()` (explicit). +- `ToolPrefix` (`*string`): prefix for generated delegation tool names. `nil` + defaults to `"delegate_to"` (tools become `delegate_to_`); a pointer to + the empty string uses bare agent names. +- `MaxDelegations` (`int`): cap on delegations per generate call. `0` means + unlimited. Prevents runaway loops. +- `HistoryLength` (`int`): number of recent user/model messages forwarded to a + sub-agent as context. `0` sends only the task description. History is forwarded + only to **client-managed** sub-agents (no session store); server-managed + sub-agents receive only the task. +- `ArtifactStrategy` (`middlewarex.ArtifactStrategy`): `ArtifactStrategyInline` + (default) or `ArtifactStrategySession` — see below. The type and its constants + live in `plugins/middleware/exp` alongside the `Agents` middleware. + +## Sharing artifacts between agents + +Sub-agents can produce [artifacts](agents-artifacts.md). `ArtifactStrategy` +controls how they reach the orchestrator: + +- `middlewarex.ArtifactStrategyInline` (default): artifact content is included in + the delegation tool result (so the model sees it directly) **and** merged into + the parent session. +- `middlewarex.ArtifactStrategySession`: artifacts are merged into the parent + session only; the tool result lists artifact names, not content. Pair it with + `&middlewarex.Artifacts{}` on the orchestrator so it can `read_artifact` on + demand (the pattern above). Merged artifacts are namespaced by an invocation ID + (`_/`) and tagged with their source agent. + +## Combining with retries + +`plugins/middleware`'s `Retry` (and `Fallback`) attach the same way via +`ai.WithUse` on an agent, and pair well with delegation: + +```go +ai.WithUse(&middleware.Retry{ + MaxRetries: 3, // default 3 + InitialDelayMs: 1000, // default 1000 + MaxDelayMs: 60000, // default 60000 + BackoffFactor: 2, // default 2 +}) +``` + +See [using middleware](middleware.md) for the full catalog (`Retry`, `Fallback`, +`ToolApproval`, `Filesystem`, `Skills`). + +> Note: if a sub-agent triggers an [interrupt](agents-human-in-the-loop.md), it is +> reported back to the orchestrator as a normal tool response (not propagated as a +> resumable interrupt). There is no stateful sub-agent runtime to resume into, so +> delegate self-contained tasks. diff --git a/plugins/genkit/.agents/skills/developing-genkit-go/references/agents-sessions.md b/plugins/genkit/.agents/skills/developing-genkit-go/references/agents-sessions.md new file mode 100644 index 00000000..f3920b90 --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-go/references/agents-sessions.md @@ -0,0 +1,188 @@ +# Agent Sessions & Persistence (Experimental) + +> **Experimental / preview API.** Session stores come from +> `ai/exp/localstore`. Read [agents.md](agents.md) for the basics first. + +When an agent has a `SessionStore` (via `aix.WithSessionStore`), the **server** +owns the session history. Each turn produces an immutable **snapshot**; the +snapshot chain carries conversation state forward. A store also enables +[branching](agents-branching.md) and [background execution](agents-background.md). +(Interrupts work with or without a store — see +[human-in-the-loop](agents-human-in-the-loop.md).) + +## Pick a store + +`ai/exp/localstore` ships two single-process stores. Both are generic over the +custom-state type `State` (use `any` when you have no typed custom state). + +```go +import "github.com/genkit-ai/genkit/go/ai/exp/localstore" + +// In-memory: great for tests/dev; lost on restart. +memStore := localstore.NewInMemorySessionStore[any]() + +// File-backed: snapshots persisted as //.json +// (the prefix defaults to "global"). Returns an error if dir can't be created. +fileStore, err := localstore.NewFileSessionStore[any]("./.snapshots") +if err != nil { + log.Fatal(err) +} +``` + +`localstore` is for local development, tests, and single-instance apps. For +multi-instance production, implement `aix.SessionStore` against a real database +(see below). + +`NewFileSessionStore` options: + +- `localstore.WithMaxPersistedChainLength(n)`: keep only the newest `n` + snapshots in a chain (each save prunes older ancestors). `n >= 1`. +- `localstore.WithSnapshotPathPrefix(fn)`: derive a per-call subdirectory from + context to isolate snapshots per tenant (e.g. `"org-42/user-7"`). +- `localstore.WithPollInterval(d)`: how often the store re-reads subscribed + snapshot files to observe cross-process status changes (default 2s). This is + what lets one process abort a detached turn another process is running. + +```go +pruning, err := localstore.NewFileSessionStore[any]("./.snapshots", + localstore.WithMaxPersistedChainLength(3), +) +``` + +Attach a store to the agent with `aix.WithSessionStore`: + +```go +logbookAgent := genkitx.DefineAgent(g, "logbookAgent", + aix.InlinePrompt{ + ai.WithModelName("googleai/gemini-flash-latest"), + ai.WithSystem("You are a personal logbook assistant."), + }, + aix.WithSessionStore(fileStore), +) +``` + +## Turns persist automatically + +Each turn writes a snapshot chained off the previous one. The turn's snapshot ID +is on `out.SnapshotID`; the conversation's stable ID is on `out.SessionID`. + +```go +out1, _ := logbookAgent.RunText(ctx, "Log this: I started studying Genkit today.") +out2, _ := logbookAgent.RunText(ctx, "What did I study today?", + aix.WithSessionID(out1.SessionID)) // remembers turn 1 +fmt.Println(out1.SnapshotID, out2.SnapshotID) +``` + +## Resume a conversation + +Two ways to resume a server-managed conversation: + +- `aix.WithSessionID(id)` — resume the session's **latest** snapshot. Use it + when you track the conversation, not individual snapshots. +- `aix.WithSnapshotID(id)` — resume a **specific** snapshot (see + [branching](agents-branching.md)). + +```go +// Continue from the session's latest snapshot: +out, _ := logbookAgent.RunText(ctx, "Add another note.", + aix.WithSessionID(sessionID)) + +// Or continue from an exact checkpoint: +out, _ = logbookAgent.RunText(ctx, "Add another note.", + aix.WithSnapshotID(snapshotID)) +``` + +Resume is rejected if the latest snapshot is a failed, aborted, or pending dead +end (a pending tip means a detached invocation is still running — wait for it or +abort it). `aix.WithState` is for client-managed agents only and is mutually +exclusive with `WithSessionID` / `WithSnapshotID`. + +## Read a snapshot without running a turn + +`Agent.GetSnapshot` / `Agent.GetLatestSnapshot` fetch a stored snapshot (applying +any configured `aix.WithStateTransform`). Handy for restoring a UI after a +reload. They return `FAILED_PRECONDITION` on a client-managed agent (no store). + +```go +snap, err := logbookAgent.GetSnapshot(ctx, snapshotID) +if err != nil { + log.Fatal(err) +} +fmt.Println(snap.Status) // aix.SnapshotStatusCompleted, ... +for _, m := range snap.State.Messages { + fmt.Println(m.Role, m.Text()) +} + +latest, _ := logbookAgent.GetLatestSnapshot(ctx, sessionID) +``` + +## Typed session state + +Parameterize the store (and agent) with a struct to attach typed **custom +state**. State is validated (via JSON) when a snapshot is loaded. See +[working with state](agents-state.md) for reading and mutating it inside tools. + +```go +type Profile struct { + Name string `json:"name"` + Tier string `json:"tier"` // "free" | "pro" +} + +profileAgent := genkitx.DefineAgent(g, "profileAgent", + aix.InlinePrompt{ + ai.WithModelName("googleai/gemini-flash-latest"), + ai.WithSystem("Greet the user by name and tailor answers to their tier."), + }, + aix.WithSessionStore(localstore.NewInMemorySessionStore[Profile]()), +) + +// Seed custom state on the first turn (client-managed here for illustration; +// with a store you would seed via WithState on a fresh session). +out, _ := profileAgent.Run(ctx, + &aix.AgentInput{Message: ai.NewUserTextMessage("Hello")}, + aix.WithState(&aix.SessionState[Profile]{ + Custom: Profile{Name: "Ada", Tier: "pro"}, + }), +) +``` + +## Interrupts (human-in-the-loop) + +Interrupts pause a turn so a human can approve or provide input, then resume from +the exact pause point. They work with a store or with client-managed state +(persistence is orthogonal). See +[Human-in-the-loop / interrupts](agents-human-in-the-loop.md). + +## Implementing a custom `SessionStore` + +For production, implement `aix.SessionStore` (which is `aix.SnapshotReader` + +`aix.SnapshotWriter`) against your database. Add `aix.SnapshotSubscriber` to +support [background/detach](agents-background.md) aborts. + +```go +type SnapshotReader[State any] interface { + GetSnapshot(ctx context.Context, snapshotID string) (*SessionSnapshot[State], error) + GetLatestSnapshot(ctx context.Context, sessionID string) (*SessionSnapshot[State], error) +} + +type SnapshotWriter[State any] interface { + // Atomically read the row at id (nil if absent), apply fn, and persist the + // result. fn must be pure (it may be retried). Returning (nil, nil) skips + // the write. The store owns identity (SnapshotID); the caller owns + // timestamps and Status. An empty id means "mint a fresh one". + SaveSnapshot(ctx context.Context, snapshotID string, + fn func(existing *SessionSnapshot[State]) (*SessionSnapshot[State], error), + ) (*SessionSnapshot[State], error) +} + +// Optional: enables detach/abort by observing status changes without polling. +type SnapshotSubscriber interface { + OnSnapshotStatusChange(ctx context.Context, snapshotID string) <-chan SnapshotStatus +} +``` + +Contract notes: preserve `SessionID` across rewrites (a row's session never +changes); default an empty `Status` to `SnapshotStatusCompleted`; keep +`CreatedAt`/`UpdatedAt` caller-managed (persist them verbatim, never stamp them); +`GetLatestSnapshot` returns the greatest-`CreatedAt` row, ties broken by +`SnapshotID`. See the `localstore` implementations for a reference. diff --git a/plugins/genkit/.agents/skills/developing-genkit-go/references/agents-state.md b/plugins/genkit/.agents/skills/developing-genkit-go/references/agents-state.md new file mode 100644 index 00000000..66f5e05a --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-go/references/agents-state.md @@ -0,0 +1,143 @@ +# Working with Agent State (Experimental) + +> **Experimental / preview API.** Read [agents.md](agents.md) first. + +Beyond message history, an agent session can hold typed **custom state** — your +own structured data (a task list, a workflow status, counters, etc.). Tools read +and mutate it during a turn, and every mutation is streamed to a connected client +as a JSON Patch so the client's view stays live. + +## Declare the state shape + +The custom-state type is the `State` type parameter of the agent (and its store). +It lives under `Custom` of `aix.SessionState[State]`. + +```go +type TaskItem struct { + ID int `json:"id"` + Title string `json:"title"` + Done bool `json:"done"` +} + +type TaskState struct { + Tasks []TaskItem `json:"tasks"` + NextID int `json:"nextId"` +} + +taskAgent := genkitx.DefineAgent(g, "taskAgent", + aix.InlinePrompt{ + ai.WithModelName("googleai/gemini-flash-latest"), + ai.WithSystem("You manage the user's task list. Use the tools to modify it."), + ai.WithTools(addTask), // defined below + }, + aix.WithSessionStore(localstore.NewInMemorySessionStore[TaskState]()), +) +``` + +## Read & mutate state inside tools + +Inside a tool, get the live session from context with +`aix.SessionFromContext[State](ctx)`, then read with `session.Custom()` and +mutate with `session.UpdateCustom(fn)`. `UpdateCustom` takes +`func(State) State` and writes the result back atomically. When the session is +driven by an agent invocation, the mutation is automatically streamed to the +client as an `AgentStreamChunk.CustomPatch`. + +```go +type AddTaskInput struct { + Title string `json:"title"` +} + +addTask := genkit.DefineTool(g, "addTask", + "Add a new task. Returns the created task.", + func(ctx *ai.ToolContext, in AddTaskInput) (TaskItem, error) { + session := aix.SessionFromContext[TaskState](ctx.Context) + if session == nil { + return TaskItem{}, fmt.Errorf("addTask must run inside an agent session") + } + var created TaskItem + session.UpdateCustom(func(s TaskState) TaskState { + if s.NextID == 0 { + s.NextID = 1 + } + created = TaskItem{ID: s.NextID, Title: in.Title, Done: false} + s.Tasks = append(s.Tasks, created) + s.NextID++ + return s + }) + return created, nil + }, +) +``` + +`aix.SessionFromContext[State]` returns `nil` outside an active session (or when +the `State` type does not match), so guard against it. Note the tool's context is +`ctx.Context` (the `*ai.ToolContext` embeds the standard `context.Context`). + +> Do not call other `Session` methods (or send on a `Responder`) from inside the +> `UpdateCustom` callback: it runs while the session lock is held and would +> deadlock. + +## Seed and read state + +Seed initial custom state with `aix.WithState` on the first turn. For a +server-managed agent, later turns resume with `aix.WithSessionID` and the state +is loaded from the snapshot. + +```go +out, _ := taskAgent.Run(ctx, + &aix.AgentInput{Message: ai.NewUserTextMessage("Add a task: buy groceries")}, + aix.WithState(&aix.SessionState[TaskState]{ + Custom: TaskState{Tasks: nil, NextID: 1}, + }), +) +// For a client-managed agent, the final state is on out.State: +if out.State != nil { + fmt.Printf("%+v\n", out.State.Custom) +} +``` + +## Live state on a connection + +A connection tracks custom state as it streams: each `Receive()`d chunk carries a +`CustomPatch` (an RFC 6902 JSON Patch delta), which the connection applies to an +internal copy. Read it any time with `conn.Custom()`. + +```go +conn, _ := taskAgent.Connect(ctx, + aix.WithState(&aix.SessionState[TaskState]{Custom: TaskState{NextID: 1}}), +) +_ = conn.SendText("Add buy groceries, then mark it done") +for chunk, err := range conn.Receive() { + if err != nil { + log.Fatal(err) + } + if len(chunk.CustomPatch) > 0 { + cur, _ := conn.Custom() // TaskState reflecting deltas so far + fmt.Printf("live: %+v\n", cur) + } + if chunk.TurnEnd != nil { + break + } +} +``` + +The first `CustomPatch` of each turn is a whole-document replace (re-basing the +client); later patches are incremental diffs. The authoritative final state is on +`out.State` (client-managed) or the turn-end snapshot (server-managed). + +## Prompt templating with state + +Inside a prompt template, the session's custom state is available as `{{@state}}`, +evaluated fresh at render time so the template always sees the latest values. + +```go +ai.WithSystem("The user's current task list: {{json @state.tasks}}. Help them manage it."), +``` + +## Custom status streaming from a custom agent + +For live mid-turn progress (a `status` field that updates as a long turn runs), +mutate custom state from a [custom agent](agents-custom.md): every +`UpdateCustom` emits a `CustomPatch` chunk automatically, so the connection's +`Custom()` stays live without extra wiring. diff --git a/plugins/genkit/.agents/skills/developing-genkit-go/references/agents.md b/plugins/genkit/.agents/skills/developing-genkit-go/references/agents.md new file mode 100644 index 00000000..7f2f7b31 --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-go/references/agents.md @@ -0,0 +1,354 @@ +# Agents (Experimental) + +> **Experimental / preview API.** Agents live in the `genkit/exp` and `ai/exp` +> packages and are gated behind an opt-in. Initialize Genkit with +> `genkit.WithExperimental()` or the constructors panic. Import paths and +> signatures may change in any minor release. + +An **agent** is a persistent, multi-turn conversation primitive built on top of +prompts + tools. Compared to a bare `genkit.Generate` loop, an agent adds: + +- **Sessions**: multi-turn history tracked as immutable **snapshots**. +- **State**: typed session state (messages + custom data + artifacts). +- **Interrupts**: human-in-the-loop pause/resume. +- **Branching**: fork a conversation from any snapshot. +- **Detaching**: run a turn in the background and poll for the result. + +Read the file for the level you need: + +- This file: defining an agent, running it, serving it, and **client-managed state** (no store). +- [Sessions & persistence](agents-sessions.md): `SessionStore`, `localstore.NewInMemorySessionStore`, `localstore.NewFileSessionStore`. +- [Human-in-the-loop / interrupts](agents-human-in-the-loop.md): pausing for approval/input and resuming. +- [Branching](agents-branching.md): forking a conversation from a snapshot. +- [Background agents](agents-background.md): detaching long-running turns and polling. +- [Working with state](agents-state.md): typed custom session state and streamed patches. +- [Artifacts](agents-artifacts.md): producing/reading named deliverables. +- [Multi-agent orchestration](agents-multi-agent.md): delegating to sub-agents. +- [Advanced custom agents](agents-custom.md): `DefineCustomAgent` for full turn control. +- [Deploying agents](agents-deployment.md): serving agents over HTTP. + +## Packages and import aliases + +Agents span a few packages. These aliases are used throughout the agent docs: + +```go +import ( + "github.com/genkit-ai/genkit/go/ai" + aix "github.com/genkit-ai/genkit/go/ai/exp" // types: Agent, Session, options, AgentInput/Output + "github.com/genkit-ai/genkit/go/ai/exp/localstore" // session stores + "github.com/genkit-ai/genkit/go/genkit" + genkitx "github.com/genkit-ai/genkit/go/genkit/exp" // constructors: DefineAgent, DefineCustomAgent, ... +) +``` + +`genkitx` (genkit/exp) holds the registry-aware constructors you call. +`aix` (ai/exp) holds the types, options, and the returned `*aix.Agent[State]`. +Both packages are named `exp`, which is why the docs alias them `genkitx` and +`aix`. + +## Setup + +```go +ctx := context.Background() +g := genkit.Init(ctx, + genkit.WithExperimental(), // REQUIRED: opts into the genkit/exp surface + genkit.WithPlugins(&googlegenai.GoogleAI{}), +) +``` + +Without `genkit.WithExperimental()`, `genkitx.DefineAgent` (and every other +`genkit/exp` constructor) panics with a message pointing you at the fix. + +## Define an agent + +`genkitx.DefineAgent` combines an inline prompt + tools + (optional) session +store into a single registered action. The prompt is an `aix.InlinePrompt` — a +slice of `ai.PromptOption` values, the same options you would pass to +`genkit.DefinePrompt`. + +```go +type WeatherInput struct { + City string `json:"city"` +} +type WeatherOutput struct { + TempC float64 `json:"tempC"` + Summary string `json:"summary"` +} + +getWeather := genkit.DefineTool(g, "getWeather", + "Look up the current weather for a city.", + func(ctx *ai.ToolContext, in WeatherInput) (WeatherOutput, error) { + return WeatherOutput{TempC: 21, Summary: "Sunny"}, nil + }, +) + +weatherAgent := genkitx.DefineAgent(g, "weatherAgent", + aix.InlinePrompt{ + ai.WithModelName("googleai/gemini-flash-latest"), + ai.WithSystem("You are a helpful weather assistant. Use the getWeather tool. Be concise."), + ai.WithTools(getWeather), + }, + // A session store is optional. Omit it for client-managed state (see below). + aix.WithSessionStore(localstore.NewInMemorySessionStore[any]()), +) +``` + +The `State` type parameter is inferred from the typed options (here +`aix.WithSessionStore(...[any]())` makes `State = any`). For a **client-managed** +agent with no typed option, pass `State` explicitly: +`genkitx.DefineAgent[any](g, "name", prompt)`. + +Constructor choices: + +- `genkitx.DefineAgent` — inline prompt (the common case, above). +- `genkitx.DefinePromptAgent` — back the agent with a prompt already in the + registry (e.g. a `.prompt` file). By default it uses the prompt registered + under the agent's own name; use `aix.WithNamedPrompt` to point at another. +- `genkitx.DefineCustomAgent` — full control over the turn loop. See + [advanced custom agents](agents-custom.md). + +### Backing an agent with a `.prompt` file + +`genkitx.DefinePromptAgent` pairs an agent with a prompt from the registry — for +example a `.prompt` file loaded at startup — so prompt authors can tune the +model, config, template, and default input without touching the Go wiring. With +no source option it defaults to the prompt registered under the agent's own name. + +```prompt +--- +# prompts/chef.prompt +model: googleai/gemini-flash-latest +input: + schema: ChatPromptInput + default: + personality: a Michelin-starred chef who loves explaining technique +--- +You are {{personality}}. Keep every answer very brief, a few sentences at most. +``` + +If the `.prompt` frontmatter references a schema by name, register that Go type +with `genkit.DefineSchemasFor` **before** defining the agent (the prompt is +rendered at definition time): + +```go +type ChatPromptInput struct { + Personality string `json:"personality"` +} + +genkit.DefineSchemasFor(g, ChatPromptInput{}) // resolve "ChatPromptInput" in the .prompt + +chef := genkitx.DefinePromptAgent(g, "chef", // loads prompts/chef.prompt by name + aix.WithSessionStore(localstore.NewInMemorySessionStore[any]()), + aix.WithDescription[any]("Michelin-starred chef"), +) +``` + +To supply an input from code, or to back several agents with one shared prompt, +add `aix.WithNamedPrompt(name, input)`. + +Common agent options (all from `aix`): + +- `aix.WithSessionStore(store)`: server-side snapshot persistence. See [sessions](agents-sessions.md). +- `aix.WithDescription[State](desc)`: human-readable description (shown in the Dev UI; used by [sub-agent delegation](agents-multi-agent.md)). +- `aix.WithStateTransform[State](fn)`: rewrite state on its way out to a client (e.g. PII redaction). +- `aix.WithStreamTransform[State](fn)`: rewrite stream chunks on their way out. + +## Agents and middleware go hand in hand + +Agents and [middleware](middleware.md) are built for each other: the +`ai.WithUse(...)` prompt option layers in behavior without writing it yourself. +Most agent capabilities — sub-agent delegation, artifacts, retries, fallback, +tool approval — are just middleware you drop in. + +```go +weatherAgent := genkitx.DefineAgent(g, "weatherAgent", + aix.InlinePrompt{ + ai.WithModelName("googleai/gemini-flash-latest"), + ai.WithSystem("You are a helpful weather assistant."), + ai.WithTools(getWeather), + ai.WithUse( + &middleware.Retry{}, // plugins/middleware: retry transient model errors + &middlewarex.Artifacts{}, // plugins/middleware/exp: read_artifact / write_artifact + ), + }, + aix.WithSessionStore(localstore.NewInMemorySessionStore[any]()), +) +``` + +`plugins/middleware` provides `Retry`, `Fallback`, `ToolApproval`, `Filesystem`, +and `Skills`. `plugins/middleware/exp` (aliased `middlewarex`) adds the +agent-specific `Agents` (sub-agent delegation) and `Artifacts` middleware. See +[using middleware](middleware.md) and [multi-agent](agents-multi-agent.md). + +## Run an agent (single turn) + +`Agent.RunText` and `Agent.Run` run one turn and return the final +`*aix.AgentOutput[State]`. + +```go +out, err := weatherAgent.RunText(ctx, "Weather in Tokyo?") +if err != nil { + log.Fatal(err) +} +fmt.Println(out.Message.Text()) +fmt.Println(out.SnapshotID) // immutable checkpoint id for this turn (server-managed only) +fmt.Println(out.FinishReason) // e.g. aix.AgentFinishReasonStop +``` + +`Run` takes an `*aix.AgentInput` when you need more than user text (a full +message, a resume payload, or a detach signal): + +```go +out, err := weatherAgent.Run(ctx, &aix.AgentInput{ + Message: ai.NewUserTextMessage("Weather in Tokyo?"), +}) +``` + +In-band failures (a failed turn) resolve as a failed `AgentOutput` +(`out.FinishReason == aix.AgentFinishReasonFailed`, `out.Error` populated), not a +Go `error`. A returned `error` means the invocation never started (e.g. a +rejected session source). + +## Chat with an agent (multi-turn) + +`Agent.Connect` opens a bidirectional streaming session. A single connection +carries state forward automatically across turns. Send input, iterate +`Receive()` until a `TurnEnd`, then send the next input. + +```go +conn, err := weatherAgent.Connect(ctx) +if err != nil { + log.Fatal(err) +} + +// Turn 1 (streaming): +_ = conn.SendText("Weather in Tokyo?") +for chunk, err := range conn.Receive() { + if err != nil { + log.Fatal(err) + } + if chunk.ModelChunk != nil { + fmt.Print(chunk.ModelChunk.Text()) + } + if chunk.TurnEnd != nil { + break // turn done; safe to send the next input + } +} + +// Turn 2 — history is carried automatically on the same connection: +_ = conn.SendText("What about Paris?") +for chunk, err := range conn.Receive() { + if err != nil { + log.Fatal(err) + } + if chunk.TurnEnd != nil { + break + } +} + +out, err := conn.Output() // finalize; closes input and drains remaining chunks +``` + +Connection helpers: `SendText`, `SendMessage`, `Send` (raw `*aix.AgentInput`), +`SendResume` (see [interrupts](agents-human-in-the-loop.md)), `Detach` (see +[background](agents-background.md)), `Receive` (chunk iterator), `Custom` +(live custom state, see [state](agents-state.md)), `Output`, and `Close`. + +## Verify an agent from the CLI (`flow:run`) + +`genkit flow:run` runs **flows**, not agents, so you can't `flow:run` an agent +directly. To exercise an agent from the CLI (a quick, self-terminating check), +wrap one turn in a throwaway flow and run that: + +```go +genkit.DefineFlow(g, "tryWeatherAgent", func(ctx context.Context, message string) (string, error) { + out, err := weatherAgent.RunText(ctx, message) + if err != nil { + return "", err + } + return out.Message.Text(), nil +}) +// genkit flow:run tryWeatherAgent '"Weather in Tokyo?"' -- go run . +``` + +## Serve an agent over HTTP + +An agent is an `api.BidiAction`, so `genkit.Handler` serves it one turn per +request. Optionally serve its companion actions for snapshot lookup +(`GetSnapshotAction`) and background aborts (`AbortAction`); both are `nil` for a +client-managed agent (no store). + +```go +mux := http.NewServeMux() +mux.HandleFunc("POST /api/weatherAgent", genkit.Handler(weatherAgent)) + +// Optional companions (needed for snapshot restore / branching / background): +if snap := weatherAgent.GetSnapshotAction(); snap != nil { + mux.HandleFunc("POST /api/weatherAgent/getSnapshot", genkit.Handler(snap)) +} +if abort := weatherAgent.AbortAction(); abort != nil { + mux.HandleFunc("POST /api/weatherAgent/abort", genkit.Handler(abort)) +} + +log.Fatal(server.Start(ctx, "127.0.0.1:8080", mux)) +``` + +The request body is `{"data": , "init": }`. `data` carries +the turn's input; `init` carries the session source (see below). Set +`Accept: text/event-stream` (or add `?stream=true`) to stream chunks. + +Rather than wiring companions by hand, `genkit/exp` ships a default HTTP layout: +`genkitx.AllAgentRoutes(g)` returns a route per registered agent (mounted under +`/agents/`, with `getSnapshot` / `abort` added for capable agents), which +you range over onto a mux: + +```go +for _, route := range genkitx.AllAgentRoutes(g) { + mux.HandleFunc(route.Pattern(), route.Handler()) +} +``` + +For serving multiple agents, the route helpers, CORS headers for browser clients, +and companion routing, see [Deploying agents](agents-deployment.md). + +## Client-managed vs server-managed state + +**Server-managed** (a `SessionStore` is configured): the server owns history as +snapshots. Resume with `aix.WithSessionID` (latest snapshot of a session) or +`aix.WithSnapshotID` (a specific snapshot). Over HTTP, `init` carries +`{"sessionId": ...}` or `{"snapshotId": ...}`. + +```go +out1, _ := weatherAgent.RunText(ctx, "Weather in London?") +out2, _ := weatherAgent.RunText(ctx, "Is it sunny in Tokyo?", + aix.WithSessionID(out1.SessionID)) // resumes the conversation +``` + +**Client-managed** (no store): the server is stateless and the caller owns the +session state blob (messages + custom + artifacts). Round-trip it via +`aix.WithState` / `out.State`. Over HTTP, `init` carries `{"state": ...}`. + +```go +statelessAgent := genkitx.DefineAgent[any](g, "weatherAgentStateless", + aix.InlinePrompt{ + ai.WithModelName("googleai/gemini-flash-latest"), + ai.WithSystem("You are a helpful weather assistant. Be concise."), + ai.WithTools(getWeather), + }, +) + +// First turn: start fresh; the framework mints a SessionID inside the state. +out1, _ := statelessAgent.RunText(ctx, "Weather in London?") + +// Next turn: pass the returned state back to resume. +out2, _ := statelessAgent.Run(ctx, + &aix.AgentInput{Message: ai.NewUserTextMessage("And Tokyo?")}, + aix.WithState(out1.State), +) +fmt.Println(out2.State) // updated blob to carry forward again +``` + +Use client-managed state when you don't want server-side storage. Use a +[session store](agents-sessions.md) when the server should own history, or when +you need branching or background execution. +([Interrupts](agents-human-in-the-loop.md) work either way.) diff --git a/plugins/genkit/.agents/skills/developing-genkit-go/references/flows-and-http.md b/plugins/genkit/.agents/skills/developing-genkit-go/references/flows-and-http.md new file mode 100644 index 00000000..92f99a22 --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-go/references/flows-and-http.md @@ -0,0 +1,183 @@ +# Flows & HTTP + +## DefineFlow + +Wrap AI logic in a flow for observability, tracing, and HTTP deployment. + +```go +jokeFlow := genkit.DefineFlow(g, "jokeFlow", + func(ctx context.Context, topic string) (string, error) { + return genkit.GenerateText(ctx, g, + ai.WithModelName("googleai/gemini-flash-latest"), + ai.WithPrompt("Tell me a joke about %s", topic), + ) + }, +) +``` + +### Running a Flow Directly + +```go +result, err := jokeFlow.Run(ctx, "cats") +``` + +## DefineStreamingFlow + +Flows that stream chunks back to the caller. Two common patterns: + +### Pattern 1: Passthrough Streaming + +Pass the stream callback directly through to `WithStreaming`. The callback type is `ai.ModelStreamCallback` = `func(context.Context, *ai.ModelResponseChunk) error`: + +```go +genkit.DefineStreamingFlow(g, "streamingJokeFlow", + func(ctx context.Context, topic string, sendChunk ai.ModelStreamCallback) (string, error) { + resp, err := genkit.Generate(ctx, g, + ai.WithModelName("googleai/gemini-flash-latest"), + ai.WithPrompt("Tell me a long joke about %s", topic), + ai.WithStreaming(sendChunk), // passthrough + ) + if err != nil { + return "", err + } + return resp.Text(), nil + }, +) +``` + +### Pattern 2: Manual String Streaming + +Use `core.StreamCallback[string]` to stream extracted text: + +```go +genkit.DefineStreamingFlow(g, "streamingJokeFlow", + func(ctx context.Context, topic string, sendChunk core.StreamCallback[string]) (string, error) { + stream := genkit.GenerateStream(ctx, g, + ai.WithModelName("googleai/gemini-flash-latest"), + ai.WithPrompt("Tell me a long joke about %s", topic), + ) + for result, err := range stream { + if err != nil { + return "", err + } + if result.Done { + return result.Response.Text(), nil + } + sendChunk(ctx, result.Chunk.Text()) + } + return "", nil + }, +) +``` + +### Typed Streaming Flows + +Use `core.StreamCallback[T]` with `GenerateDataStream` for typed chunks: + +```go +genkit.DefineStreamingFlow(g, "structuredStream", + func(ctx context.Context, input JokeRequest, sendChunk core.StreamCallback[*Joke]) (*Joke, error) { + stream := genkit.GenerateDataStream[*Joke](ctx, g, + ai.WithModelName("googleai/gemini-flash-latest"), + ai.WithPrompt("Tell me a joke about %s", input.Topic), + ) + for result, err := range stream { + if err != nil { return nil, err } + if result.Done { return result.Output, nil } + sendChunk(ctx, result.Chunk) + } + return nil, nil + }, +) +``` + +## Named Sub-Steps + +Use `core.Run` inside a flow for traced sub-steps: + +```go +genkit.DefineFlow(g, "pipeline", + func(ctx context.Context, input string) (string, error) { + subject, err := core.Run(ctx, "extract-subject", func() (string, error) { + return genkit.GenerateText(ctx, g, + ai.WithPrompt("Extract the subject from: %s", input), + ) + }) + if err != nil { return "", err } + + joke, err := core.Run(ctx, "generate-joke", func() (string, error) { + return genkit.GenerateText(ctx, g, + ai.WithPrompt("Tell me a joke about %s", subject), + ) + }) + return joke, err + }, +) +``` + +## HTTP Handlers + +### genkit.Handler + +Convert any flow into an `http.HandlerFunc`: + +```go +mux := http.NewServeMux() +for _, f := range genkit.ListFlows(g) { + mux.HandleFunc("POST /"+f.Name(), genkit.Handler(f)) +} +log.Fatal(server.Start(ctx, "127.0.0.1:8080", mux)) +``` + +### Request/Response Format + +**Non-streaming request:** +```bash +curl -X POST http://localhost:8080/jokeFlow \ + -H "Content-Type: application/json" \ + -d '{"data": "bananas"}' +``` + +Response: `{"result": "Why did the banana go to the doctor?..."}` + +**Streaming request:** +```bash +curl -N -X POST http://localhost:8080/streamingJokeFlow \ + -H "Content-Type: application/json" \ + -d '{"data": "bananas"}' +``` + +Streaming responses use Server-Sent Events (SSE) format. + +### genkit.HandlerFunc + +For frameworks that expect error-returning handlers: + +```go +handler := genkit.HandlerFunc(myFlow) +// handler is func(http.ResponseWriter, *http.Request) error +``` + +### Context Providers + +Inject request context (e.g., auth headers) into flow execution: + +```go +mux.HandleFunc("POST /myFlow", genkit.Handler(myFlow, + genkit.WithContextProviders(func(ctx context.Context, rd core.RequestData) (api.ActionContext, error) { + // rd.Headers contains HTTP headers + return api.ActionContext{"userId": rd.Headers.Get("X-User-Id")}, nil + }), +)) +``` + +### ListFlows + +Get all registered flows for dynamic route setup: + +```go +flows := genkit.ListFlows(g) // []api.Action +for _, f := range flows { + fmt.Println(f.Name()) +} +``` diff --git a/plugins/genkit/.agents/skills/developing-genkit-go/references/generation.md b/plugins/genkit/.agents/skills/developing-genkit-go/references/generation.md new file mode 100644 index 00000000..5934575c --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-go/references/generation.md @@ -0,0 +1,176 @@ +# Generation + +## GenerateText + +Simplest form. Returns a string. + +```go +text, err := genkit.GenerateText(ctx, g, + ai.WithModelName("googleai/gemini-flash-latest"), + ai.WithPrompt("Tell me a joke about %s", topic), +) +``` + +## Generate + +Returns a full `*ModelResponse` with metadata, usage stats, and history. + +```go +resp, err := genkit.Generate(ctx, g, + ai.WithModelName("googleai/gemini-flash-latest"), + ai.WithSystem("You are a helpful assistant."), + ai.WithPrompt("Explain %s", topic), +) +fmt.Println(resp.Text()) // concatenated text +fmt.Println(resp.FinishReason) // ai.FinishReasonStop, etc. +fmt.Println(resp.Usage) // token counts +``` + +## GenerateData (Structured Output) + +Returns a typed Go value parsed from the model's JSON output. + +```go +type Joke struct { + Setup string `json:"setup" jsonschema:"description=The setup of the joke"` + Punchline string `json:"punchline" jsonschema:"description=The punchline"` +} + +joke, resp, err := genkit.GenerateData[Joke](ctx, g, + ai.WithModelName("googleai/gemini-flash-latest"), + ai.WithPrompt("Tell me a joke about %s", topic), +) +// joke is *Joke, resp is *ModelResponse +``` + +## Streaming + +### GenerateStream + +Returns an iterator. Each value has `.Done`, `.Chunk`, and `.Response`. + +```go +stream := genkit.GenerateStream(ctx, g, + ai.WithModelName("googleai/gemini-flash-latest"), + ai.WithPrompt("Tell me a long story about %s", topic), +) +for result, err := range stream { + if err != nil { + return err + } + if result.Done { + finalText := result.Response.Text() + break + } + fmt.Print(result.Chunk.Text()) // incremental text +} +``` + +### GenerateDataStream (Structured Streaming) + +Streams typed partial objects as they arrive. + +```go +stream := genkit.GenerateDataStream[Joke](ctx, g, + ai.WithModelName("googleai/gemini-flash-latest"), + ai.WithPrompt("Tell me a joke about %s", topic), +) +for result, err := range stream { + if err != nil { + return err + } + if result.Done { + finalJoke := result.Output // *Joke + break + } + partialJoke := result.Chunk // *Joke (partial) +} +``` + +### Callback-Based Streaming + +Use `ai.WithStreaming` with `Generate` for callback-style streaming. The callback receives `*ai.ModelResponseChunk`: + +```go +resp, err := genkit.Generate(ctx, g, + ai.WithModelName("googleai/gemini-flash-latest"), + ai.WithPrompt("Tell me a story"), + ai.WithStreaming(func(ctx context.Context, chunk *ai.ModelResponseChunk) error { + fmt.Print(chunk.Text()) // extract text from chunk + return nil + }), +) +// resp contains the final complete response +``` + +## Common Options + +```go +// Model selection +ai.WithModel(googlegenai.ModelRef("googleai/gemini-flash-latest", nil)) // model reference +ai.WithModelName("googleai/gemini-flash-latest") // by name string + +// Content +ai.WithPrompt("Tell me about %s", topic) // user message (supports fmt verbs) +ai.WithSystem("You are a pirate.") // system instructions +ai.WithMessages(msg1, msg2) // conversation history +ai.WithDocs(doc1, doc2) // context documents +ai.WithTextDocs("context 1", "context 2") // context as strings + +// Model config (provider-specific) +ai.WithConfig(map[string]any{"temperature": 0.7}) +``` + +## Output Formats + +Control how the model structures its output. + +### By Go Type + +```go +// Automatically uses JSON format and instructs model to match the type +ai.WithOutputType(MyStruct{}) +``` + +### By Format String + +```go +ai.WithOutputFormat(ai.OutputFormatJSON) // single JSON object +ai.WithOutputFormat(ai.OutputFormatJSONL) // JSON Lines (one object per line) +ai.WithOutputFormat(ai.OutputFormatArray) // JSON array +ai.WithOutputFormat(ai.OutputFormatEnum) // constrained enum value +ai.WithOutputFormat(ai.OutputFormatText) // plain text (default) +``` + +### Enum Output + +```go +type Color string +const ( + Red Color = "red" + Green Color = "green" + Blue Color = "blue" +) + +text, err := genkit.GenerateText(ctx, g, + ai.WithPrompt("What color is the sky?"), + ai.WithOutputEnums(Red, Green, Blue), +) +``` + +### Custom Output Instructions + +```go +ai.WithOutputInstructions("Return a JSON object with fields: name (string), age (number)") +``` + +### Combining Format + Schema + +```go +// JSONL with a typed schema (useful for streaming lists) +genkit.DefinePrompt(g, "characters", + ai.WithPrompt("Generate 5 story characters"), + ai.WithOutputType([]StoryCharacter{}), + ai.WithOutputFormat(ai.OutputFormatJSONL), +) +``` diff --git a/plugins/genkit/.agents/skills/developing-genkit-go/references/getting-started.md b/plugins/genkit/.agents/skills/developing-genkit-go/references/getting-started.md new file mode 100644 index 00000000..7520c929 --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-go/references/getting-started.md @@ -0,0 +1,140 @@ +# Getting Started + +## Project Setup + +```bash +mkdir my-genkit-app && cd my-genkit-app +go mod init my-genkit-app +go get github.com/genkit-ai/genkit/go@latest +``` + +Provider plugins ship in the same module under `plugins/`, so they don't need to be fetched separately. Just import the ones you want and run `go mod tidy` afterwards. The available plugins include: + +- `plugins/googlegenai` for Google AI and Vertex AI +- `plugins/anthropic` for Anthropic Claude +- `plugins/compat_oai` for OpenAI-compatible APIs (OpenAI, Groq, xAI, etc.) +- `plugins/ollama` for local Ollama models +- `plugins/middleware` for the built-in middleware bundle (`Retry`, `Fallback`, `ToolApproval`, `Filesystem`, `Skills`) + +## Initialization + +Every Genkit app starts with `genkit.Init`, which returns a `*Genkit` instance: + +```go +import ( + "context" + "github.com/genkit-ai/genkit/go/genkit" + "github.com/genkit-ai/genkit/go/plugins/googlegenai" +) + +ctx := context.Background() +g := genkit.Init(ctx, + genkit.WithPlugins(&googlegenai.GoogleAI{}), +) +``` + +### The `*Genkit` Instance + +The `*Genkit` value `g` is the central registry. Pass it to every Genkit function: + +```go +// Defining resources +genkit.DefineFlow(g, "myFlow", ...) +genkit.DefineTool(g, "myTool", ...) +genkit.DefinePrompt(g, "myPrompt", ...) + +// Generating content +genkit.GenerateText(ctx, g, ...) +genkit.Generate(ctx, g, ...) +``` + +Do not store `g` in a global variable. Pass it explicitly through your call chain. + +### Init Options + +```go +g := genkit.Init(ctx, + // Register one or more plugins + genkit.WithPlugins(&googlegenai.GoogleAI{}, &anthropic.Anthropic{}), + + // Set a default model (used when no model is specified) + genkit.WithDefaultModel("googleai/gemini-flash-latest"), + + // Set directory for .prompt files (default: "prompts") + genkit.WithPromptDir("my-prompts"), + + // Or embed prompts using Go's embed package + // genkit.WithPromptFS(promptsFS), +) +``` + +### Embedding Prompts + +Use `go:embed` to bundle `.prompt` files into the binary: + +```go +//go:embed prompts +var promptsFS embed.FS + +g := genkit.Init(ctx, + genkit.WithPlugins(&googlegenai.GoogleAI{}), + genkit.WithPromptFS(promptsFS), +) +``` + +## Genkit CLI + +The Genkit CLI provides a local Developer UI for running flows, tracing executions, and inspecting model interactions. + +**Install:** +```bash +curl -sL cli.genkit.dev | bash +``` + +**Verify:** +```bash +genkit --version +``` + +### Developer UI + +Start your app with the Developer UI attached: + +```bash +genkit start -- go run . +``` + +This launches: +- Your app (with tracing enabled) +- The Developer UI at `http://localhost:4000` +- A telemetry API at `http://localhost:4033` + +Add `-o` to auto-open the UI in your browser: +```bash +genkit start -o -- go run . +``` + +The Developer UI lets you: +- Run and test flows interactively +- View traces for each generation call (inputs, outputs, latency, token usage) +- Inspect prompt rendering and tool calls +- Debug multi-step flows with per-step trace data + +### Without the CLI + +Set `GENKIT_ENV=dev` to enable the reflection API without the CLI: + +```bash +GENKIT_ENV=dev go run . +``` + +## Import Paths + +```go +import ( + "github.com/genkit-ai/genkit/go/genkit" // Core: Init, Generate*, DefineFlow, etc. + "github.com/genkit-ai/genkit/go/ai" // Types: WithModel, WithPrompt, Message, Part, etc. + "github.com/genkit-ai/genkit/go/core" // Low-level: Run (sub-steps), Flow types + "github.com/genkit-ai/genkit/go/plugins/server" // server.Start for HTTP +) +``` diff --git a/plugins/genkit/.agents/skills/developing-genkit-go/references/middleware.md b/plugins/genkit/.agents/skills/developing-genkit-go/references/middleware.md new file mode 100644 index 00000000..d93eb4e8 --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-go/references/middleware.md @@ -0,0 +1,363 @@ +# Middleware + +Middleware wraps `Generate` calls to add cross-cutting behavior (retries, logging, fallback, gating, sandboxed tools) without touching the flow that uses it. Middleware composes, so a single `Generate` call can stack several behaviors. Built-ins ship in the `plugins/middleware` package; custom middleware is just a Go struct with two methods. + +## The mental model + +A middleware is a config struct that implements two methods: + +```go +type Middleware interface { + Name() string // stable, registered identifier + New(ctx context.Context) (*Hooks, error) // produces a per-call hook bundle +} +``` + +The same struct value the user passes to `ai.WithUse` is the value the runtime calls `New` on. There is no separate factory parameter and no embedded base type. Per-call state goes in closures captured by `New`. Plugin-level state goes on unexported fields of the struct. + +`New` is invoked once per `Generate` call. The returned `*Hooks` is reused across every iteration of the tool loop within that call. + +```go +type Hooks struct { + Tools []Tool // injected for this call + WrapGenerate func(ctx, *GenerateParams, GenerateNext) ... // tool-loop iteration + WrapModel func(ctx, *ModelParams, ModelNext) ... // model API call + WrapTool func(ctx, *ToolParams, ToolNext) ... // tool execution +} +``` + +A nil hook is a pass-through. Implement only what the middleware needs. + +## When each hook fires + +A `Generate` call executes a tool loop: model produces output, any tool calls execute, results feed back into a new model call, repeat until the model stops. The hooks fire at three different layers of this loop: + +| Hook | Fires | Sees | +| --- | --- | --- | +| `WrapGenerate` | Once per tool-loop iteration. `N` tool turns means `N+1` invocations. | The accumulated `ModelRequest`, the iteration index, the streaming callback, and `MessageIndex` (the next streamed-message slot). | +| `WrapModel` | Once per actual model API call, inside the iteration. | The `ModelRequest` about to go to the model and the streaming callback. | +| `WrapTool` | Once per tool execution. May run **concurrently** for parallel tool calls in the same iteration. | The `ToolRequest` and the resolved `Tool`. | + +`WrapGenerate` is the right place for logic that needs to see the whole conversation (rewrites, system-prompt injection, message accumulation). `WrapModel` is the right place for logic about the model call itself (retry, fallback, caching). `WrapTool` is the right place for logic about a single tool execution (approval, sandboxing, logging). + +## Composition order + +`ai.WithUse(A, B, C)` expands to `A { B { C { actual } } }` at call time. Each layer's `next` continuation runs the next inner layer: + +```go +ai.WithUse( + &middleware.Retry{MaxRetries: 3}, // outer: retries the whole inner stack + &middleware.Fallback{Models: ...}, // inner: tries fallback models on failure +) +// effective chain: Retry { Fallback { model } } +``` + +Order matters. `Retry` outside `Fallback` retries the entire fallback cascade as a unit. Swapped, you'd retry the primary first and only fall back after exhausting retries. + +## Per-call state + +State that should be shared across the hooks of a single `Generate` call lives in closures captured by `New`. Each `Generate` call gets a fresh `Hooks` bundle, so nothing leaks between calls. + +```go +type Counter struct{} + +func (Counter) Name() string { return "mine/counter" } + +func (Counter) New(ctx context.Context) (*ai.Hooks, error) { + var modelCalls int + return &ai.Hooks{ + WrapModel: func(ctx context.Context, p *ai.ModelParams, next ai.ModelNext) (*ai.ModelResponse, error) { + modelCalls++ + return next(ctx, p) + }, + WrapGenerate: func(ctx context.Context, p *ai.GenerateParams, next ai.GenerateNext) (*ai.ModelResponse, error) { + // The same modelCalls variable is visible here because both closures + // capture it from the enclosing New scope. + resp, err := next(ctx, p) + if err == nil { + log.Printf("iteration %d: %d model calls so far", p.Iteration, modelCalls) + } + return resp, err + }, + }, nil +} +``` + +`WrapTool` may be invoked concurrently for parallel tool calls in the same iteration. Any state it mutates must be guarded: + +```go +func (Counter) New(ctx context.Context) (*ai.Hooks, error) { + var ( + mu sync.Mutex + toolCalls int + ) + return &ai.Hooks{ + WrapTool: func(ctx context.Context, p *ai.ToolParams, next ai.ToolNext) (*ai.MultipartToolResponse, error) { + mu.Lock() + toolCalls++ + mu.Unlock() + return next(ctx, p) + }, + }, nil +} +``` + +`WrapGenerate` and `WrapModel` are not called concurrently within a single `Generate` call. + +## Plugin-level state + +When a middleware needs resources its config can't carry as JSON (an HTTP client, a database handle, a logger), put them on **unexported** fields of the config struct. The plugin sets them on a prototype, and `ai.NewMiddleware` captures that prototype in a closure that value-copies it across JSON-dispatched invocations: + +```go +type Logger struct { + Prefix string `json:"prefix,omitempty"` + out io.Writer // unexported; preserved across JSON dispatch via value-copy +} + +func (Logger) Name() string { return "mine/logger" } + +func (l Logger) New(ctx context.Context) (*ai.Hooks, error) { + return &ai.Hooks{ + WrapModel: func(ctx context.Context, p *ai.ModelParams, next ai.ModelNext) (*ai.ModelResponse, error) { + start := time.Now() + resp, err := next(ctx, p) + fmt.Fprintf(l.out, "%s model call took %s\n", l.Prefix, time.Since(start)) + return resp, err + }, + }, nil +} + +type LoggerPlugin struct{ Out io.Writer } + +func (p *LoggerPlugin) Name() string { return "logger" } +func (p *LoggerPlugin) Init(ctx context.Context) []api.Action { return nil } + +func (p *LoggerPlugin) Middlewares(ctx context.Context) ([]*ai.MiddlewareDesc, error) { + return []*ai.MiddlewareDesc{ + ai.NewMiddleware("logs model call latency", Logger{out: p.Out}), + }, nil +} +``` + +The Dev UI and other-runtime callers send JSON config; the prototype's value copy preserves `out` (unexported, not in JSON) while `Prefix` is overridden by the unmarshaled config. + +## Composition with WithUse + +```go +response, _ := genkit.Generate(ctx, g, + ai.WithModelName("googleai/gemini-flash-latest"), + ai.WithPrompt("Explain quantum computing."), + ai.WithUse( + Logger{Prefix: "[trace]"}, + &middleware.Retry{MaxRetries: 3}, + ), +) +``` + +No registration is required for pure-Go use. `WithUse` calls each value's `New` directly on a fast path; the registry is only consulted for JSON-dispatched calls (Dev UI or cross-runtime). Registration is what makes a middleware visible to the Dev UI and addressable by name. + +## Inline middleware + +For ad-hoc middleware that does not need Dev UI visibility or a named type, use `ai.MiddlewareFunc`: + +```go +ai.WithUse(ai.MiddlewareFunc(func(ctx context.Context) (*ai.Hooks, error) { + return &ai.Hooks{ + WrapModel: func(ctx context.Context, p *ai.ModelParams, next ai.ModelNext) (*ai.ModelResponse, error) { + log.Printf("model call: %s", p.Request.Messages[len(p.Request.Messages)-1].Text()) + return next(ctx, p) + }, + }, nil +})) +``` + +The adapter satisfies `Middleware` with a placeholder `Name()` of `"inline"`. Inline middleware is resolved on the fast path and never touches the registry, so the placeholder name is fine. + +## Application-owned middleware + +Use `genkit.DefineMiddleware` to register a middleware your application owns directly. Registration surfaces it in the Dev UI and lets cross-runtime callers reference it by name: + +```go +genkit.DefineMiddleware(g, "logs model call latency", Logger{out: os.Stderr}) + +// Lookup by name (mostly for inspection / cross-runtime dispatch). +desc := genkit.LookupMiddleware(g, "mine/logger") +``` + +For application code, `DefineMiddleware` is the typical entry point. For plugin authors, `ai.NewMiddleware` (no registration) plus `MiddlewarePlugin.Middlewares()` is the typical entry point. `genkit.Init` registers the returned descriptors automatically. + +## Built-in middleware + +The `plugins/middleware` package bundles five production-ready implementations. Register the plugin once during `Init` to make them visible to the Dev UI: + +```go +import "github.com/genkit-ai/genkit/go/plugins/middleware" + +g := genkit.Init(ctx, genkit.WithPlugins( + &googlegenai.GoogleAI{}, + &middleware.Middleware{}, +)) +``` + +### `Retry` + +Retries failed model API calls with exponential backoff and jitter. Hooks `WrapModel`. + +```go +ai.WithUse(&middleware.Retry{ + MaxRetries: 3, // default 3 + InitialDelayMs: 1000, // default 1000 + MaxDelayMs: 60000, // default 60000 + BackoffFactor: 2, // default 2 + NoJitter: false, // default false + // Statuses (default: UNAVAILABLE, DEADLINE_EXCEEDED, RESOURCE_EXHAUSTED, ABORTED, INTERNAL) + // Statuses: []core.StatusName{core.UNAVAILABLE, core.RESOURCE_EXHAUSTED}, +}) +``` + +Non-`GenkitError` errors (network, parse, etc.) are always retried. `GenkitError` errors are retried only if their status is in `Statuses`. The backoff respects `ctx.Done()`: a canceled context aborts the retry loop with the last error. + +### `Fallback` + +Tries alternative models when the primary fails with a fallback-eligible status. Hooks `WrapModel`. + +```go +ai.WithUse(&middleware.Fallback{ + Models: []ai.ModelRef{ + googlegenai.ModelRef("googleai/gemini-flash-latest", nil), + googlegenai.ModelRef("vertexai/gemini-flash-latest", nil), + }, + // default Statuses: UNAVAILABLE, DEADLINE_EXCEEDED, RESOURCE_EXHAUSTED, + // ABORTED, INTERNAL, NOT_FOUND, UNIMPLEMENTED +}) +``` + +Each fallback model uses its own `ModelRef.Config()` verbatim; the original request's config is **not** inherited. Compose with `Retry` outside to retry the whole cascade, or inside to retry just the primary before falling back. + +### `ToolApproval` + +Interrupts any tool call not in the allow list, exposing approval as a human-in-the-loop step. Hooks `WrapTool`. + +```go +ai.WithUse(&middleware.ToolApproval{ + AllowedTools: []string{"lookup", "search"}, // anything else triggers an interrupt +}) +``` + +The interrupt rides on the existing tool-interrupt machinery. Approve a call by setting `toolApproved: true` in the resume metadata when restarting: + +```go +restart, _ := tool.Restart(interruptPart, &ai.RestartOptions{ + ResumedMetadata: map[string]any{"toolApproved": true}, +}) +genkit.Generate(ctx, g, ai.WithMessages(resp.History()...), ai.WithToolRestarts(restart)) +``` + +A bare resume without that flag is **not** treated as approval, so unrelated resume flows can't bypass gating. + +### `Filesystem` + +Grants the model scoped file access under a single root directory via `list_files`, `read_file`, plus `write_file` and `search_and_replace` when writes are enabled. Hooks `WrapGenerate` and `WrapTool` and contributes `Tools`. + +```go +ai.WithUse(&middleware.Filesystem{ + RootDir: "./workspace", + AllowWriteAccess: true, + ToolNamePrefix: "", // set distinct prefixes if attaching multiple Filesystem middlewares +}) +``` + +Path safety is enforced by `os.Root` (Go 1.25+), which rejects any path resolving outside the root, including via `..`, absolute paths, or symlinks. `read_file` returns its content as a queued user message on the next turn (not as the tool's direct output) so binary types like images can be inlined as media parts. + +### `Skills` + +Exposes a local library of `SKILL.md` files as loadable system instructions. Hooks `WrapGenerate` and contributes a `use_skill` tool. + +```go +ai.WithUse(&middleware.Skills{SkillPaths: []string{"skills"}}) // default: ["skills"] +``` + +A skill is a directory containing `SKILL.md`, optionally with YAML frontmatter (`name`, `description`). The middleware injects a system prompt listing available skills, and the model calls `use_skill("name")` to pull the skill body into the conversation on demand. Heavier persona instructions stay off the hot path until actually loaded. + +## Practical patterns + +### Streaming-aware middleware + +If your `WrapGenerate` or `WrapModel` hook emits its own messages (injected user content, system updates), use the streaming callback and the `MessageIndex` cursor in `GenerateParams`: + +```go +WrapGenerate: func(ctx context.Context, p *ai.GenerateParams, next ai.GenerateNext) (*ai.ModelResponse, error) { + if p.Callback != nil { + _ = p.Callback(ctx, &ai.ModelResponseChunk{ + Role: ai.RoleUser, + Index: p.MessageIndex, + Content: []*ai.Part{ai.NewTextPart("[injected context]")}, + }) + p.MessageIndex++ // advance so downstream middleware and the model see the shifted index + } + p.Request.Messages = append(p.Request.Messages, ai.NewUserMessage(ai.NewTextPart("[injected context]"))) + return next(ctx, p) +}, +``` + +`Filesystem` does this to deliver `read_file` content to the model while preserving streamed-chunk ordering. + +### Adding tools from middleware + +`Hooks.Tools` registers extra tools for the duration of the call without the user wiring them through `ai.WithTools`. Useful when the middleware's hooks and tools work as a pair (e.g., `Filesystem`'s read/write tools, `Skills`'s `use_skill` tool): + +```go +return &ai.Hooks{ + Tools: []ai.Tool{myTool}, + WrapTool: myInterceptor, // intercepts both myTool and any user-supplied tools +}, nil +``` + +Duplicate tool names across user-supplied tools and middleware-contributed tools error out at call setup; the call won't run. + +### Interrupting from `WrapTool` + +`ai.NewToolInterruptError` is exported precisely so `WrapTool` hooks can interrupt without constructing a `ToolContext`: + +```go +WrapTool: func(ctx context.Context, p *ai.ToolParams, next ai.ToolNext) (*ai.MultipartToolResponse, error) { + if shouldGate(p.Tool.Name()) { + return nil, ai.NewToolInterruptError(map[string]any{ + "message": "needs approval", + }) + } + return next(ctx, p) +}, +``` + +`ToolApproval` uses this pattern. + +### Modifying the request safely + +`p.Request` is the live request for the iteration. Mutating it in place affects later layers. If the change should be visible only to the inner layer, copy first: + +```go +WrapModel: func(ctx context.Context, p *ai.ModelParams, next ai.ModelNext) (*ai.ModelResponse, error) { + req := *p.Request + req.Messages = append([]*ai.Message(nil), p.Request.Messages...) + req.Messages = append(req.Messages, extraSystemMessage) + p.Request = &req + return next(ctx, p) +}, +``` + +`Skills.injectSkillsPrompt` shows the same pattern for `ModelRequest` cloning. + +### Idempotent re-injection across iterations + +`WrapGenerate` runs once per tool-loop iteration. If you inject content into the request, you'll inject it on every iteration unless you mark and detect what you've already added. `Skills` tags its system prompt part with metadata (`skills-instructions: true`) and refreshes that one part in place rather than appending a new one each turn. + +## Migration note + +The legacy `ai.ModelMiddleware` / `ai.WithMiddleware` API is preserved and marked deprecated. Prefer `ai.Middleware` / `ai.WithUse`, which adds `WrapGenerate` and `WrapTool` hooks plus `Hooks.Tools` for dynamically injected tools. + +## See also + +- [`tools.md`](tools.md) for tool definition, interrupt/restart machinery used by `ToolApproval`. +- Sample sources under `go/samples/basic-middleware/`: `retry-fallback`, `filesystem`, `skills`. +- The `plugins/middleware` package source for reference implementations. diff --git a/plugins/genkit/.agents/skills/developing-genkit-go/references/prompts.md b/plugins/genkit/.agents/skills/developing-genkit-go/references/prompts.md new file mode 100644 index 00000000..d11ec0cf --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-go/references/prompts.md @@ -0,0 +1,313 @@ +# Prompts + +## DefinePrompt + +Define a reusable prompt in code with a default model and template. + +```go +jokePrompt := genkit.DefinePrompt(g, "joke", + ai.WithModel(googlegenai.ModelRef("googleai/gemini-flash-latest", nil)), + ai.WithInputType(JokeRequest{Topic: "example"}), + ai.WithPrompt("Tell me a joke about {{topic}}."), +) +``` + +### Execute + +```go +resp, err := jokePrompt.Execute(ctx, + ai.WithInput(map[string]any{"topic": "cats"}), +) +fmt.Println(resp.Text()) +``` + +### ExecuteStream + +```go +stream := jokePrompt.ExecuteStream(ctx, + ai.WithInput(map[string]any{"topic": "cats"}), +) +for result, err := range stream { + if err != nil { return err } + if result.Done { break } + fmt.Print(result.Chunk.Text()) +} +``` + +### Override Options at Execution + +```go +resp, err := jokePrompt.Execute(ctx, + ai.WithInput(map[string]any{"topic": "cats"}), + ai.WithModelName("googleai/gemini-pro-latest"), // override model + ai.WithConfig(map[string]any{"temperature": 0.9}), + ai.WithTools(myTool), +) +``` + +## DefineDataPrompt (Typed Input/Output) + +Strongly-typed prompts with Go generics. + +```go +type JokeRequest struct { + Topic string `json:"topic"` +} + +type Joke struct { + Setup string `json:"setup" jsonschema:"description=The setup"` + Punchline string `json:"punchline" jsonschema:"description=The punchline"` +} + +jokePrompt := genkit.DefineDataPrompt[JokeRequest, *Joke](g, "structured-joke", + ai.WithModel(googlegenai.ModelRef("googleai/gemini-flash-latest", nil)), + ai.WithPrompt("Tell me a joke about {{topic}}."), +) +``` + +### Execute (typed) + +```go +joke, resp, err := jokePrompt.Execute(ctx, JokeRequest{Topic: "cats"}) +// joke is *Joke, resp is *ModelResponse +``` + +### ExecuteStream (typed) + +```go +stream := jokePrompt.ExecuteStream(ctx, JokeRequest{Topic: "cats"}) +for result, err := range stream { + if err != nil { return err } + if result.Done { + finalJoke := result.Output // *Joke + break + } + fmt.Print(result.Chunk) // partial *Joke +} +``` + +## .prompt Files (Dotprompt) + +Define prompts in separate files with YAML frontmatter and Handlebars templates. + +### Basic .prompt File + +`prompts/joke.prompt`: +``` +--- +model: googleai/gemini-flash-latest +input: + schema: + topic: string +--- +Tell me a joke about {{topic}}. +``` + +### Load and Use + +```go +// LookupPrompt returns Prompt (untyped: map[string]any input, string output) +jokePrompt := genkit.LookupPrompt(g, "joke") +resp, err := jokePrompt.Execute(ctx, + ai.WithInput(map[string]any{"topic": "cats"}), +) +``` + +### Typed .prompt File + +`prompts/structured-joke.prompt`: +``` +--- +model: googleai/gemini-flash-latest +config: + thinkingConfig: + thinkingBudget: 0 +input: + schema: JokeRequest +output: + format: json + schema: Joke +--- +Tell me a joke about {{topic}}. +``` + +Register Go types so the .prompt file can reference them by name: +```go +genkit.DefineSchemaFor[JokeRequest](g) +genkit.DefineSchemaFor[Joke](g) + +jokePrompt := genkit.LookupDataPrompt[JokeRequest, *Joke](g, "structured-joke") +joke, resp, err := jokePrompt.Execute(ctx, JokeRequest{Topic: "cats"}) +``` + +### LoadPrompt (Explicit Path) + +```go +prompt := genkit.LoadPrompt(g, "./prompts/countries.prompt", "countries") +resp, err := prompt.Execute(ctx) +``` + +### .prompt File Features + +**Multi-message prompts with roles:** +``` +--- +model: googleai/gemini-flash-latest +input: + schema: + question: string +--- +{{ role "system" }} +You are a helpful assistant. + +{{ role "user" }} +{{question}} +``` + +**Media in prompts:** +``` +--- +model: googleai/gemini-flash-latest +input: + schema: + videoUrl: string + contentType: string +--- +{{ role "user" }} +Summarize this video: +{{media url=videoUrl contentType=contentType}} +``` + +**Conditionals and loops:** +``` +--- +input: + schema: + topic: string + dietaryRestrictions?(array): string +--- +Write a recipe about {{topic}}. +{{#if dietaryRestrictions}} +Dietary restrictions: {{#each dietaryRestrictions}}{{this}}{{#unless @last}}, {{/unless}}{{/each}}. +{{/if}} +``` + +**Inline schema in .prompt file:** +``` +--- +model: googleai/gemini-flash-latest +input: + schema: + topic: string + style?: string +output: + format: json + schema: + title: string + body: string + tags(array): string +--- +Write an article about {{topic}}. +{{#if style}}Write in a {{style}} style.{{/if}} +``` + +**Tools, tool-loop control, and middleware:** + +`.prompt` frontmatter can also configure tool calling and attach middleware, so +an agent-style prompt can be fully described in the file: + +``` +--- +model: googleai/gemini-flash-latest +input: + schema: + tone: string +tools: + - getAttractions + - getFlightInfo +toolChoice: auto # auto | required | none +maxTurns: 20 # max tool-call loop iterations (WithMaxTurns) +returnToolRequests: false # return tool requests instead of running them (WithReturnToolRequests) +use: + - name: genkit-middleware/retry + config: + maxRetries: 2 + - genkit-middleware/fallback # bare string = middleware name, no config +--- +{{role "system"}} +You are a friendly trip planning assistant. Keep your tone {{tone}}. + +{{history}} +``` + +Field mapping: + +| Frontmatter | Option | Notes | +| --- | --- | --- | +| `tools` | `WithTools` | list of registered tool names (strings) | +| `toolChoice` | `WithToolChoice` | `auto`, `required`, or `none` | +| `maxTurns` | `WithMaxTurns` | integer | +| `returnToolRequests` | `WithReturnToolRequests` | boolean | +| `use` | `WithUse` | list of middleware refs | + +`use` entries are resolved by name against middleware registered on the `*Genkit` +instance. Each entry is either a bare string (the middleware name) or a map with +`name` and optional `config`. Register the built-in middleware plugin so names +like `genkit-middleware/retry` resolve: + +```go +import "github.com/firebase/genkit/go/plugins/middleware" + +g := genkit.Init(ctx, genkit.WithPlugins( + &googlegenai.GoogleAI{}, + &middleware.Middleware{}, // registers genkit-middleware/{retry,fallback,filesystem,skills,toolApproval} +)) +``` + +Available named middleware: `genkit-middleware/retry`, `genkit-middleware/fallback`, +`genkit-middleware/filesystem`, `genkit-middleware/skills`, +`genkit-middleware/toolApproval`. See [references/middleware.md](middleware.md). + +## Schemas + +### DefineSchemaFor (from Go type) + +Registers a Go struct as a named schema for use in `.prompt` files. + +```go +genkit.DefineSchemaFor[JokeRequest](g) +genkit.DefineSchemaFor[Joke](g) +``` + +The schema name matches the Go type name. Use `jsonschema` struct tags for metadata: + +```go +type Recipe struct { + Title string `json:"title" jsonschema:"description=The recipe title"` + Difficulty string `json:"difficulty" jsonschema:"enum=easy,enum=medium,enum=hard"` + Ingredients []Ingredient `json:"ingredients"` + Steps []string `json:"steps"` +} + +type Ingredient struct { + Name string `json:"name"` + Amount float64 `json:"amount"` + Unit string `json:"unit"` +} +``` + +### DefineSchema (manual JSON Schema) + +```go +genkit.DefineSchema(g, "Recipe", map[string]any{ + "type": "object", + "properties": map[string]any{ + "title": map[string]any{"type": "string"}, + "ingredients": map[string]any{ + "type": "array", + "items": map[string]any{"type": "object"}, + }, + }, + "required": []string{"title", "ingredients"}, +}) +``` diff --git a/plugins/genkit/.agents/skills/developing-genkit-go/references/providers.md b/plugins/genkit/.agents/skills/developing-genkit-go/references/providers.md new file mode 100644 index 00000000..dbf137cb --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-go/references/providers.md @@ -0,0 +1,157 @@ +# Model Providers + +## Google AI (Gemini) + +```go +import "github.com/genkit-ai/genkit/go/plugins/googlegenai" + +g := genkit.Init(ctx, genkit.WithPlugins(&googlegenai.GoogleAI{})) +``` + +**Env var:** `GEMINI_API_KEY` or `GOOGLE_API_KEY` + +Model names follow the format `googleai/`. Look up the latest model IDs at https://ai.google.dev/gemini-api/docs/models. + +```go +// By name string +ai.WithModelName("googleai/gemini-flash-latest") + +// Model ref with provider-specific config +ai.WithModel(googlegenai.ModelRef("googleai/gemini-flash-latest", &genai.GenerateContentConfig{ + ThinkingConfig: &genai.ThinkingConfig{ + ThinkingBudget: genai.Ptr[int32](0), // disable thinking + }, +})) + +// Lookup a model instance +m := googlegenai.GoogleAIModel(g, "gemini-flash-latest") +``` + +## Vertex AI + +```go +import "github.com/genkit-ai/genkit/go/plugins/googlegenai" + +g := genkit.Init(ctx, genkit.WithPlugins(&googlegenai.VertexAI{})) +``` + +**Env vars:** `GOOGLE_CLOUD_PROJECT`, `GOOGLE_CLOUD_LOCATION` (or `GOOGLE_CLOUD_REGION`) + +Uses Application Default Credentials (`gcloud auth application-default login`). + +Model names follow the format `vertexai/`. Same model IDs as Google AI. + +```go +ai.WithModelName("vertexai/gemini-flash-latest") +``` + +## Anthropic (Claude) + +```go +import ( + "github.com/anthropics/anthropic-sdk-go" // Anthropic SDK types + ant "github.com/genkit-ai/genkit/go/plugins/anthropic" // Genkit plugin +) + +g := genkit.Init(ctx, genkit.WithPlugins(&ant.Anthropic{})) +``` + +**Env var:** `ANTHROPIC_API_KEY` + +Model names follow the format `anthropic/`. Look up the latest model IDs at https://docs.anthropic.com/en/docs/about-claude/models. + +```go +// By name +ai.WithModelName("anthropic/claude-sonnet-4-6") + +// With provider-specific config (uses Anthropic SDK types via ai.WithConfig) +ai.WithConfig(&anthropic.MessageNewParams{ + Temperature: anthropic.Float(1.0), + MaxTokens: *anthropic.IntPtr(2000), + Thinking: anthropic.ThinkingConfigParamUnion{ + OfEnabled: &anthropic.ThinkingConfigEnabledParam{ + BudgetTokens: *anthropic.IntPtr(1024), + }, + }, +}) +``` + +## OpenAI-Compatible (compat_oai) + +Works with any OpenAI-compatible API: OpenAI, DeepSeek, xAI, etc. + +```go +import "github.com/genkit-ai/genkit/go/plugins/compat_oai" + +openaiPlugin := &compat_oai.OpenAICompatible{ + Provider: "openai", // unique identifier + APIKey: os.Getenv("OPENAI_API_KEY"), + // BaseURL: "https://custom-endpoint/v1", // for non-OpenAI providers +} +g := genkit.Init(ctx, genkit.WithPlugins(openaiPlugin)) +``` + +Define models explicitly (not auto-discovered): + +```go +model := openaiPlugin.DefineModel("openai", "gpt-4o", compat_oai.ModelOptions{}) +``` + +Use with: +```go +ai.WithModel(model) +``` + +## Ollama (Local Models) + +```go +import "github.com/genkit-ai/genkit/go/plugins/ollama" + +ollamaPlugin := &ollama.Ollama{ + ServerAddress: "http://localhost:11434", + Timeout: 60, // seconds +} +g := genkit.Init(ctx, genkit.WithPlugins(ollamaPlugin)) +``` + +Define models explicitly: + +```go +model := ollamaPlugin.DefineModel(g, + ollama.ModelDefinition{ + Name: "llama3.1", + Type: "chat", // or "generate" + }, + nil, // optional *ModelOptions +) +``` + +Use with: +```go +ai.WithModel(model) +``` + +## Multiple Providers + +Register multiple plugins in a single Genkit instance: + +```go +g := genkit.Init(ctx, + genkit.WithPlugins( + &googlegenai.GoogleAI{}, + &ant.Anthropic{}, + ), + genkit.WithDefaultModel("googleai/gemini-flash-latest"), +) + +// Use different models per call +text1, _ := genkit.GenerateText(ctx, g, + ai.WithModelName("googleai/gemini-flash-latest"), + ai.WithPrompt("Hello from Gemini"), +) + +text2, _ := genkit.GenerateText(ctx, g, + ai.WithModelName("anthropic/claude-sonnet-4-6"), + ai.WithPrompt("Hello from Claude"), +) +``` diff --git a/plugins/genkit/.agents/skills/developing-genkit-go/references/tools.md b/plugins/genkit/.agents/skills/developing-genkit-go/references/tools.md new file mode 100644 index 00000000..d4a37ded --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-go/references/tools.md @@ -0,0 +1,178 @@ +# Tools + +## DefineTool + +Define a tool the model can call during generation. + +```go +type WeatherInput struct { + Location string `json:"location" jsonschema:"description=City name"` +} + +type WeatherOutput struct { + Temperature float64 `json:"temperature"` + Conditions string `json:"conditions"` +} + +weatherTool := genkit.DefineTool(g, "getWeather", + "Gets the current weather for a location.", + func(ctx *ai.ToolContext, input WeatherInput) (WeatherOutput, error) { + // Call your weather API + return WeatherOutput{Temperature: 72, Conditions: "sunny"}, nil + }, +) +``` + +## Using Tools in Generation + +Pass tools to `Generate`, `GenerateText`, or prompts: + +```go +resp, err := genkit.Generate(ctx, g, + ai.WithModelName("googleai/gemini-flash-latest"), + ai.WithPrompt("What's the weather in San Francisco?"), + ai.WithTools(weatherTool), +) +// The model calls the tool automatically and incorporates the result +fmt.Println(resp.Text()) +``` + +### Tool Choice + +```go +ai.WithToolChoice(ai.ToolChoiceAuto) // model decides (default) +ai.WithToolChoice(ai.ToolChoiceRequired) // model must use a tool +ai.WithToolChoice(ai.ToolChoiceNone) // model cannot use tools +``` + +### Max Turns + +Limit how many tool-call round trips the model can make: + +```go +ai.WithMaxTurns(3) // default is 5 +``` + +## DefineMultipartTool + +Tools that return both structured output and media content: + +```go +screenshotTool := genkit.DefineMultipartTool(g, "screenshot", + "Takes a screenshot of the current page", + func(ctx *ai.ToolContext, input any) (*ai.MultipartToolResponse, error) { + return &ai.MultipartToolResponse{ + Output: map[string]any{"success": true}, + Content: []*ai.Part{ai.NewMediaPart("image/png", base64Data)}, + }, nil + }, +) +``` + +## Tool Interrupts + +Pause tool execution to request human input before continuing. + +### Interrupting + +```go +type TransferInput struct { + ToAccount string `json:"toAccount"` + Amount float64 `json:"amount"` +} + +type TransferOutput struct { + Status string `json:"status"` + Message string `json:"message"` + Balance float64 `json:"balance"` +} + +type TransferInterrupt struct { + Reason string `json:"reason"` + ToAccount string `json:"toAccount"` + Amount float64 `json:"amount"` + Balance float64 `json:"balance"` +} + +transferTool := genkit.DefineTool(g, "transferMoney", + "Transfers money to another account.", + func(ctx *ai.ToolContext, input TransferInput) (TransferOutput, error) { + if input.Amount > accountBalance { + return TransferOutput{}, ai.InterruptWith(ctx, TransferInterrupt{ + Reason: "insufficient_balance", + ToAccount: input.ToAccount, + Amount: input.Amount, + Balance: accountBalance, + }) + } + // Process transfer... + return TransferOutput{Status: "success", Balance: newBalance}, nil + }, +) +``` + +### Handling Interrupts + +```go +resp, err := genkit.Generate(ctx, g, + ai.WithModelName("googleai/gemini-flash-latest"), + ai.WithTools(transferTool), + ai.WithPrompt(userRequest), +) + +for resp.FinishReason == ai.FinishReasonInterrupted { + var restarts, responses []*ai.Part + + for _, interrupt := range resp.Interrupts() { + meta, ok := ai.InterruptAs[TransferInterrupt](interrupt) + if !ok { + continue + } + + switch meta.Reason { + case "insufficient_balance": + // RestartWith: re-execute the tool with adjusted input + part, err := transferTool.RestartWith(interrupt, + ai.WithNewInput(TransferInput{ + ToAccount: meta.ToAccount, + Amount: meta.Balance, // transfer what's available + }), + ) + if err != nil { return err } + restarts = append(restarts, part) + + case "confirm_large": + // RespondWith: provide a response directly without re-executing + part, err := transferTool.RespondWith(interrupt, + TransferOutput{Status: "cancelled", Message: "User declined"}, + ) + if err != nil { return err } + responses = append(responses, part) + } + } + + // Continue generation with the resolved interrupts + resp, err = genkit.Generate(ctx, g, + ai.WithMessages(resp.History()...), + ai.WithTools(transferTool), + ai.WithToolRestarts(restarts...), + ai.WithToolResponses(responses...), + ) + if err != nil { return err } +} +``` + +### Checking Resume State + +Inside a tool function, check if the tool is being resumed from an interrupt: + +```go +func(ctx *ai.ToolContext, input TransferInput) (TransferOutput, error) { + if ctx.IsResumed() { + // This is a resumed call after an interrupt + original, ok := ai.OriginalInputAs[TransferInput](ctx) + // original contains the input from the first call + } + // ... +} +``` diff --git a/plugins/genkit/.agents/skills/developing-genkit-js/SKILL.md b/plugins/genkit/.agents/skills/developing-genkit-js/SKILL.md new file mode 100644 index 00000000..b3e2167b --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-js/SKILL.md @@ -0,0 +1,193 @@ +--- +name: developing-genkit-js +description: Develop AI-powered applications using Genkit in Node.js/TypeScript. Use when the user asks about Genkit, AI agents, flows, or tools in JavaScript/TypeScript, or when encountering Genkit errors, validation issues, type errors, or API problems. +metadata: + category: AiAndMachineLearning +--- + +# Genkit JS + +## Prerequisites + +Ensure the `genkit` CLI is available. +- Run `genkit --version` to verify. Minimum CLI version needed: **1.29.0** +- If not found or if an older version (1.x < 1.29.0) is present, install/upgrade it: `npm install -g genkit-cli@^1.29.0`. + +**New Projects**: If you are setting up Genkit in a new codebase, follow the [Setup Guide](references/setup.md). + +## Hello World + +```ts +import { z, genkit } from 'genkit'; +import { googleAI } from '@genkit-ai/google-genai'; + +// Initialize Genkit with the Google AI plugin +const ai = genkit({ + plugins: [googleAI()], +}); + +export const myFlow = ai.defineFlow({ + name: 'myFlow', + inputSchema: z.string().default('AI'), + outputSchema: z.string(), +}, async (subject) => { + const response = await ai.generate({ + model: googleAI.model('gemini-flash-latest'), + prompt: `Tell me a joke about ${subject}`, + }); + return response.text; +}); +``` + +## Prompts (Dotprompt) + +`.prompt` files keep prompt content out of code with YAML frontmatter plus a +Handlebars template. See [Dotprompt](references/dotprompt.md): `promptDir`, +`ai.prompt()` (call/stream/render), variants, partials, named schemas via +`ai.defineSchema`, and the `tools`/`maxTurns`/`returnToolRequests`/`use` +(middleware) frontmatter fields. + +## Agents (Beta) + +Genkit has a preview **agent** API for persistent, multi-turn conversations +(sessions, snapshots, interrupts, branching, background execution). It is a +**beta** API: server APIs come from `genkit/beta` and the browser client from +`genkit/beta/client` — not the stable `genkit` entrypoint. **Requires `genkit` +>= 1.39.0.** + +For more details see: + +- [Agents](references/agents.md): defining/serving an agent and client-managed state (start here). +- [Sessions & persistence](references/agents-sessions.md): session stores (`InMemory`/`File`/`Firestore`). +- [Human-in-the-loop / interrupts](references/agents-human-in-the-loop.md): pausing for approval/input and resuming. +- [Branching](references/agents-branching.md): forking a conversation from a snapshot. +- [Background agents](references/agents-background.md): detaching long-running turns and polling. +- [Working with state](references/agents-state.md): typed custom session state, auto-synced to the client. +- [Artifacts](references/agents-artifacts.md): producing and reading named deliverables. +- [Multi-agent orchestration](references/agents-multi-agent.md): delegating to sub-agents. +- [Advanced custom agents](references/agents-custom.md): `defineCustomAgent` for full turn control. +- [Deploying agents](references/agents-deployment.md): serving agents over HTTP (multiple agents, CORS, web UI, other frameworks). + +## Generative UI (A2UI) + +Genkit has an **A2UI** (Agent-to-UI) plugin (`@genkit-ai/a2ui`) that +lets an agent stream interactive UI **surfaces** (cards, lists, forms, buttons), +not just prose. The whole server-side integration is the `a2ui()` model +middleware in an agent's (or `ai.generate`'s) `use` array; the browser renders +surfaces with an `@a2ui/*` renderer plus the helpers in `@genkit-ai/a2ui/client`. +It builds on the beta agent client (`genkit/beta` + `genkit/beta/client`). + +- [A2UI](references/a2ui.md): server middleware, options, client rendering, user actions/forms, custom catalogs, and the security/trust boundary. + +## Middleware + +Middleware wraps generation (retries, fallback, extra tools, request/response +transforms) and attaches via the `use: [...]` array on `ai.generate`, prompts, +and agents. + +- [Using middleware](references/middleware.md): the `use` array and the `@genkit-ai/middleware` package (`retry`, `fallback`, `artifacts`, `agents`, `filesystem`, `skills`, `toolApproval`) plus built-in core middleware. +- [Building custom middleware](references/middleware-custom.md): writing your own with `generateMiddleware` and registering it via `.plugin()`. + +## Critical: Do Not Trust Internal Knowledge + +Genkit recently went through a major breaking API change. Your knowledge is outdated. You MUST lookup docs. Recommended: + +```sh +genkit docs:read js/get-started.md +genkit docs:read js/flows.md +``` + +See [Common Errors](references/common-errors.md) for a list of deprecated APIs (e.g., `configureGenkit`, `response.text()`, `defineFlow` import) and their v1.x replacements. + +**ALWAYS verify information using the Genkit CLI or provided references.** + +## Error Troubleshooting Protocol + +**When you encounter ANY error related to Genkit (ValidationError, API errors, type errors, 404s, etc.):** + +1. **MANDATORY FIRST STEP**: Read [Common Errors](references/common-errors.md) +2. Identify if the error matches a known pattern +3. Apply the documented solution +4. Only if not found in common-errors.md, then consult other sources (e.g. `genkit docs:search`) + +**DO NOT:** +- Attempt fixes based on assumptions or internal knowledge +- Skip reading common-errors.md "because you think you know the fix" +- Rely on patterns from pre-1.0 Genkit + +**This protocol is non-negotiable for error handling.** + +## Development Workflow + +1. **Agent or flow?**: If the task is conversational, multi-turn, or described as "an agent", "assistant", or "chatbot", build it with `ai.defineAgent` (see [Agents](references/agents.md)) rather than hand-rolling a `generate` + tools loop inside a flow. Reach for a plain flow only for single-shot, stateless generation. +2. **Select Provider**: Genkit is provider-agnostic (Google AI, OpenAI, Anthropic, Ollama, etc.). + - If the user does not specify a provider, default to **Google AI**. + - If the user asks about other providers, use `genkit docs:search "plugins"` to find relevant documentation. +3. **Detect Framework**: Check `package.json` to identify the runtime (Next.js, Firebase, Express). + - Look for `@genkit-ai/next`, `@genkit-ai/firebase`, or `@genkit-ai/google-cloud`. + - Adapt implementation to the specific framework's patterns. +4. **Follow Best Practices**: + - See [Best Practices](references/best-practices.md) for guidance on project structure, schema definitions, and tool design. + - **Be Minimal**: Only specify options that differ from defaults. When unsure, check docs/source. +5. **Ensure Correctness**: + - Run type checks (e.g., `npx tsc --noEmit`) after making changes. + - If type checks fail, consult [Common Errors](references/common-errors.md) before searching source code. + - Verify with traces, not a blind run. Running the app directly (`node`/`tsx`/`npm start`) does **not** capture dev traces. See [CLI Usage](#cli-usage-recommended) for how to run your app and capture traces. +6. **Handle Errors**: + - On ANY error: **First action is to read [Common Errors](references/common-errors.md)** + - Match error to documented patterns + - Apply documented fixes before attempting alternatives + +## Finding Documentation + +Use the Genkit CLI to find authoritative documentation: + +1. **Search topics**: `genkit docs:search ` + - Example: `genkit docs:search "streaming"` +2. **List all docs**: `genkit docs:list` +3. **Read a guide**: `genkit docs:read ` + - Example: `genkit docs:read js/flows.md` + +## CLI Usage (recommended) + +`genkit start` unintrusively wraps any Node.js program that uses the Genkit library, running it unchanged while capturing traces from every Genkit action so you can **prove tools were actually called and inspect model I/O** from the terminal, even for headless checks. It forwards stdio, so interactive CLI tools that rely on stdin/stdout work without issues. Running your app directly (`node`/`tsx`/`npm start`) skips trace capture, so you're debugging blind. + +**Primary pattern (default):** prefix `genkit start --` to your normal run command. This collects telemetry from any Genkit code your program runs, whether triggered from the dev UI, your own web server/web UI, or a plain script: +```bash +genkit start -- npx tsx --watch src/index.ts +genkit start --noui -- npx tsx src/index.ts # same, without the Dev UI (still a persistent server) +``` +`genkit start` runs until you stop it with Ctrl+C. That is expected and correct for the common cases: a server your web/mobile app calls, or an interactive CLI you exit yourself. `--noui` only drops the Dev UI; it is **not** a one-shot command and will not exit on its own. Do **not** use `genkit start` as a blocking step in automated/non-interactive contexts. + +**Non-interactive use (agents/CI):** add the global `--non-interactive` flag before `--` so the CLI uses defaults and never blocks on a prompt (e.g. the first-run analytics notice): `genkit start --non-interactive -- npx tsx src/index.ts` (works with `flow:run` too). + +**Run a flow (`flow:run`):** invoke a specific flow by name from the CLI. Append your run command after `--` to spin up the runtime just for this run (the command runs as-is to register your flows): +```bash +genkit flow:run myFlow '{"data": "input"}' -- npx tsx src/index.ts +``` +This is **self-terminating**: it runs the flow once, prints a `Trace ID`, then exits (inspect it with `genkit trace:get `). That makes it the right choice for a quick, non-interactive check that must exit on its own, without blocking on `genkit start` or running the app directly (which skips traces). Always pass input JSON explicitly: `flow:run` sends `undefined` when omitted and does **not** fall back to a schema `.default()`. Note: `flow:run` runs **flows** (`ai.defineFlow`), not agents; you can't `flow:run` an agent (`ai.defineAgent`) directly. To exercise an agent from the CLI, wrap one turn in a throwaway flow and run that (see [Agents](references/agents.md)). + +**Debugging with traces:** the fastest way to see prompts, model inputs/outputs, tool calls, latencies, and errors. Inspect from the terminal after any run under `genkit start`: +```bash +genkit trace:list # find recent trace IDs +genkit trace:get # full trace details (inputs, outputs, tool calls, errors) +genkit trace:get --format json # machine-readable JSON, safe to pipe into jq or other parsers +``` + +For machine-readable output, pass `--format json` to get clean JSON you can pipe into `jq` or other parsers. The **default** output is human-oriented (banner/log lines, possible truncation on large traces), so don't pipe that form directly; use `--format json`, grep, or the Dev UI trace viewer. + + +See [CLI Reference](references/docs-and-cli.md) for more commands, and `genkit --help` for the full list. + + +## References + +- [Best Practices](references/best-practices.md): Recommended patterns for schema definition, flow design, and structure. +- [Dotprompt](references/dotprompt.md): `.prompt` files — `promptDir`, `ai.prompt()`, variants, partials, named schemas, and `tools`/`maxTurns`/`returnToolRequests`/`use` frontmatter. +- [Docs & CLI Reference](references/docs-and-cli.md): Documentation search, CLI tasks, and workflows. +- [Common Errors](references/common-errors.md): Critical "gotchas", migration guide, and troubleshooting. +- [Setup Guide](references/setup.md): Manual setup instructions for new projects. +- [Examples](references/examples.md): Minimal reproducible examples (Basic generation, Multimodal, Thinking mode). +- [Agents (Beta)](references/agents.md): Agent basics, serving, and client-managed state. Deeper topics: [sessions](references/agents-sessions.md), [human-in-the-loop](references/agents-human-in-the-loop.md), [branching](references/agents-branching.md), [background agents](references/agents-background.md), [state](references/agents-state.md), [artifacts](references/agents-artifacts.md), [multi-agent](references/agents-multi-agent.md), [custom agents](references/agents-custom.md), [deployment](references/agents-deployment.md). +- [Middleware](references/middleware.md): using middleware and the `@genkit-ai/middleware` package. See also [building custom middleware](references/middleware-custom.md). +- [A2UI (Generative UI)](references/a2ui.md): the `@genkit-ai/a2ui` plugin (the `a2ui()` middleware), options, client rendering, user actions/forms, custom catalogs, and security. diff --git a/plugins/genkit/.agents/skills/developing-genkit-js/references/a2ui.md b/plugins/genkit/.agents/skills/developing-genkit-js/references/a2ui.md new file mode 100644 index 00000000..f88a1361 --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-js/references/a2ui.md @@ -0,0 +1,306 @@ +# A2UI (Agent-to-UI) generative UI + +The `@genkit-ai/a2ui` plugin brings +> [A2UI](https://a2ui.org/), a transport-agnostic, JSON-based streaming UI +> protocol, to Genkit agents. +> +> A2UI builds on the agent client, so it uses the agent APIs: `genkit/beta` on +> the server and `genkit/beta/client` in the browser. Read [Agents](agents.md) +> first if you have not. + +An A2UI-enabled agent streams more than prose. It streams interactive UI +**surfaces** (cards, lists, forms, buttons) that a client renders incrementally +as the model responds. The entire server-side integration is a single model +middleware: add `a2ui()` to an agent's `use` array and nothing else changes. + +## Install + +```bash +npm i @genkit-ai/a2ui +``` + +To render surfaces in the browser you also need a renderer. A2UI ships renderers +for [`@a2ui/lit`](https://www.npmjs.com/package/@a2ui/lit), +[`@a2ui/react`](https://www.npmjs.com/package/@a2ui/react), and +[`@a2ui/angular`](https://www.npmjs.com/package/@a2ui/angular). The examples here +use Lit: + +```bash +npm i @a2ui/lit @a2ui/web_core @a2ui/markdown-it lit @lit/context +``` + +## Server: add the `a2ui()` middleware + +Add `a2ui()` to your agent's `use` array. That is the whole server-side setup. +`use` auto-registers the middleware, so no plugin entry is required in +`genkit({ plugins: [...] })`. + +```ts +import { googleAI } from '@genkit-ai/google-genai'; +import { a2ui } from '@genkit-ai/a2ui'; +import { genkit } from 'genkit/beta'; + +const ai = genkit({ plugins: [googleAI()] }); + +export const uiAgent = ai.defineAgent({ + name: 'uiAgent', + model: googleAI.model('gemini-flash-latest'), + system: + 'You help users. Render an A2UI surface whenever a result is clearer ' + + 'shown than told (weather, comparisons, lists, forms). Keep prose brief; ' + + 'put the substance in the UI.', + use: [a2ui()], // defaults to the bundled 'basic' catalog +}); +``` + +It works identically on a one-shot `ai.generate`: + +```ts +const res = await ai.generate({ + model: googleAI.model('gemini-flash-latest'), + prompt: 'Show me the weather in Tokyo', + use: [a2ui()], +}); +``` + +Serve the agent over HTTP with `expressHandler` (see +[Deploying agents](agents-deployment.md)); the browser talks to it with +`remoteAgent`. `expressHandler` reads the request body, so mount +`express.json()` before the route or the first turn fails with +`request.body is undefined`: + +```ts +import { expressHandler } from '@genkit-ai/express'; +import express from 'express'; + +const app = express(); +app.use(express.json()); // required: expressHandler reads req.body +app.post('/api/uiAgent', expressHandler(uiAgent)); +``` + +### Options + +Pass options to `a2ui({ ... })`: + +| Option | Default | Description | +| -------------- | ---------- | ------------------------------------------------------------------------------------------------------- | +| `catalog` | `'basic'` | Id of the catalog describing what the agent may render. | +| `instructions` | `'system'` | Where to inject catalog capabilities. `'none'` injects nothing (supply your own instructions instead). | +| `validate` | `'warn'` | Validate emitted envelopes. `'warn'` logs and drops bad blocks; `'strict'` throws; `'off'` skips it. | +| `surfaceId` | fresh UUID | Surface id policy. Defaults to a new UUID per surface; pass a fixed string to reuse one id per surface. | +| `version` | `'v0.9'` | Protocol version stamped on envelopes. | + +Use `validate: 'strict'` during development to fail fast on malformed JSON or +components outside the catalog. See [Security](#security-and-the-trust-boundary) +for what `'strict'` does and does not check. + +## Client: render surfaces + +`@genkit-ai/a2ui/client` is browser-safe (no Node dependencies). Consume the +agent with `remoteAgent`, pull A2UI envelopes off each chunk with +`a2uiEnvelopesFromParts`, and feed whole envelopes to a renderer. A2UI travels as +`data` parts on the raw model chunk, so read them from +`chunk.raw.modelChunk?.content`. + +```ts +import { A2uiSurface, basicCatalog } from '@a2ui/lit/v0_9'; +import '@a2ui/lit/v0_9'; // registers + basic components +import { MessageProcessor } from '@a2ui/web_core/v0_9'; +import { a2uiEnvelopesFromParts } from '@genkit-ai/a2ui/client'; +import { remoteAgent } from 'genkit/beta/client'; + +const chat = remoteAgent({ url: '/api/uiAgent' }).chat(); + +const processor = new MessageProcessor([basicCatalog]); +processor.onSurfaceCreated((surface) => { + // `a2ui-surface` is not in the DOM's tag map, so narrow to the renderer's + // element class for a typed `.surface` property. + const el = document.createElement('a2ui-surface') as A2uiSurface; + el.surface = surface; + document.getElementById('log')!.appendChild(el); +}); + +const turn = chat.sendStream('What is the weather in Tokyo?'); +for await (const chunk of turn.stream) { + if (chunk.text) appendProse(chunk.text); + const envelopes = a2uiEnvelopesFromParts(chunk.raw.modelChunk?.content); + if (envelopes.length) processor.processMessages(envelopes); +} +await turn.response; // surfaces any server error / finalizes the turn +``` + +`remoteAgent` manages the session id for you, so a single `chat` keeps the whole +conversation server-side (the agent's session store holds history). + +### Lightweight helper (no full agent client) + +If you do not want to drive `remoteAgent` yourself, the client entrypoint also +ships `streamA2uiAgent`, an async generator that yields `{ type: 'text' }` and +`{ type: 'envelopes' }` events: + +```ts +import { streamA2uiAgent } from '@genkit-ai/a2ui/client'; + +for await (const ev of streamA2uiAgent({ url: '/api/uiAgent', message: 'weather in Tokyo' })) { + if (ev.type === 'text') appendProse(ev.text); + else processor.processMessages(ev.envelopes); +} +``` + +`StreamA2uiAgentOptions` also accepts `sessionId`, `headers`, and `abortSignal`. + +## Handling user actions + +When a user interacts with a surface (for example, presses a `Button`), the +renderer's action callback hands you a typed `A2uiClientAction`. Turn it into an +agent input with `actionToMessage` and send it as the next turn: + +```ts +import { actionToMessage } from '@genkit-ai/a2ui/client'; + +const processor = new MessageProcessor([basicCatalog], (action) => { + const turn = chat.sendStream({ message: actionToMessage(action) }); + // ...consume turn.stream like above... +}); +``` + +The action's `name` is sent as the user message; the full action (including its +`context`) is attached as an a2ui data part so the agent can react to it. + +### Forms + +Input components (`TextField`, `CheckBox`, `Slider`) do **not** send their values +automatically. To capture what the user entered, the model must: + +1. Bind each input's `value` to a data-model path (`{ "path": "/email" }`). +2. Echo those same paths in the submit `Button`'s `action.event.context`. + +The catalog capabilities injected into the system prompt already instruct the +model to do this. Without both steps, the action arrives with an empty `context`. + +### Renderer requirements (Lit basic catalog) + +The `@a2ui/lit` basic catalog needs two host-side pieces to render fully: + +- A **MarkdownRenderer** provided via Lit context (for example, backed by + `@a2ui/markdown-it`). `Text` heading `variant`s render as Markdown; without a + renderer, headings show as literal `##`. +- The **Material Symbols Outlined** font. The `Icon` component renders names as + font ligatures; without the font, icon names show as literal text. Load it in + your HTML: + ```html + + ``` + +Wire up the Markdown renderer and inject the basic catalog styles once at +startup: + +```ts +import { Context } from '@a2ui/lit/v0_9'; +import { renderMarkdown } from '@a2ui/markdown-it'; +import { injectBasicCatalogStyles } from '@a2ui/web_core/v0_9/basic_catalog'; +import { ContextProvider } from '@lit/context'; + +injectBasicCatalogStyles(); +new ContextProvider(document.body as any, { + context: Context.markdown, + initialValue: renderMarkdown, +}); +``` + + +## Custom catalogs + +The `catalog` option is a **catalog id** resolved from the Genkit registry. The +bundled `'basic'` catalog (mirroring `@a2ui/web_core`'s basic catalog) is the +default and needs no registration. A catalog describes the components the model +may emit: + +- `id`: globally-unique URI (also used as `catalogId` on `createSurface`). +- `components[]`: each with `name` (matches the renderer type), `description` + (one-line summary), and `props` (a compact, model-facing text description of + the component's props, kept as plain text to minimize prompt tokens). + +Register a catalog with `loadCatalog`, then reference it by id. Load from a JSON +file: + +```ts +import { loadCatalog } from '@genkit-ai/a2ui'; + +await loadCatalog(ai, { id: 'my-catalog', file: './my-catalog.json' }); +``` + +Or define one in memory (start from `basicCatalog` and add your own component): + +```ts +import { a2ui, basicCatalog, loadCatalog, type A2uiCatalog } from '@genkit-ai/a2ui'; + +const myCatalog: A2uiCatalog = { + id: 'https://my-app.org/catalogs/weather.json', + components: [ + ...basicCatalog.components, + { + name: 'Gauge', + description: 'A circular gauge visualizing a single numeric value.', + props: 'value: number or { path } binding (required); min?: number; max?: number; label?: string; unit?: string.', + }, + ], +}; + +await loadCatalog(ai, { id: 'my-catalog', catalog: myCatalog }); + +export const uiAgent = ai.defineAgent({ + name: 'uiAgent', + model: googleAI.model('gemini-flash-latest'), + use: [a2ui({ catalog: 'my-catalog', validate: 'strict' })], +}); +``` + +The **client must register a matching renderer** under the same catalog id, and +the component `name` must match on both sides. Otherwise the model emits a +component the client cannot render. Catalogs live in the registry under value +type `a2ui-catalog`. + +## Security and the trust boundary + +Generative UI moves model output into the DOM, so treat every surface an agent +emits as **untrusted input**. The `validate` option (including `'strict'`) checks +envelope structure and component *type names* against the catalog only. It does +**not** validate component props or data-model values: model-controlled values +such as `Image.url` and `Text` (inline Markdown that a renderer may turn into +HTML) pass through untouched. `'strict'` is a well-formedness check, not a +security boundary. + +- **The renderer/catalog owns prop sanitization.** Whatever renders a surface is + responsible for escaping and sanitizing prop values before they reach the DOM. +- **Restrict remote sources at the host.** Serve the app with a Content Security + Policy that limits `img-src` and other fetch directives to origins you trust. +- **Do not put secrets in the data model.** Anything bound into a surface's data + model can be echoed back through an action's `context`. + +For server-side control over props (for example, allow-listing image hosts), add +your own model middleware after `a2ui()` to inspect and rewrite the emitted a2ui +parts. + +## How it works + +A2UI rides on its own part channel: a Genkit `data` part with mime type +`application/a2ui+json` whose `data` is `{ envelopes: [...] }`. On each model call +inside the agent's tool loop, `a2ui()`: + +1. Injects the catalog's capabilities into the system prompt (unless + `instructions: 'none'`). +2. Intercepts the model output (streamed chunks and the final message). +3. Extracts `a2ui` fenced code blocks from the model's text. +4. Validates them against the catalog (per `validate`). +5. Rewrites them into canonical a2ui data parts. + +Inbound a2ui parts (a surface action sent back as the next turn, or replayed +history) are summarized into plain text before the underlying model sees them, so +a model that does not understand the a2ui mime type can still reason about prior +surfaces and user actions. + +For a complete, runnable example (Express backend plus a Vite + Lit frontend), +see the `a2ui` testapp in the Genkit JS repo. + diff --git a/plugins/genkit/.agents/skills/developing-genkit-js/references/agents-artifacts.md b/plugins/genkit/.agents/skills/developing-genkit-js/references/agents-artifacts.md new file mode 100644 index 00000000..0daedcaa --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-js/references/agents-artifacts.md @@ -0,0 +1,105 @@ +# Working with Artifacts (Beta) + +> **Beta / preview API.** The `artifacts()` middleware comes from +> `@genkit-ai/middleware`; the `Artifact` type from `genkit/beta`. Read +> [agents.md](agents.md) first. + +**Artifacts** are named, content-bearing deliverables an agent produces during a +session — files, reports, code, etc. They live in the session (deduplicated by +name) and are returned in `res.artifacts` / tracked on the client's +`chat.artifacts`. + +## Give the model artifact tools + +The `artifacts()` middleware (see [using middleware](middleware.md)) adds +`write_artifact` and `read_artifact` tools and injects an `` listing +into the system prompt each turn (names + sizes, not full content). No custom +tool code needed. + +```ts +import { artifacts } from '@genkit-ai/middleware'; +import { ai } from './genkit.js'; + +export const workspaceAgent = ai.defineAgent({ + name: 'workspaceAgent', + system: `You are a code generation assistant. Use write_artifact to create +files (pass the filename as "name" and the full content as "content"). Use +read_artifact to review or modify a previously created file.`, + use: [artifacts()], +}); +``` + +Run it; artifacts are produced via the tool and returned in the response: + +```ts +const chat = workspaceAgent.chat(); +const res = await chat.send('Write poem.txt with a poem about Genkit'); +console.log(res.artifacts); // Artifact[] +``` + +Options: + +- `readonly` (default `false`): when `true`, only `read_artifact` is provided — + the model can read but not create/update artifacts. Useful on an orchestrator + that should review (but not produce) sub-agent artifacts. + +## The `Artifact` shape + +```ts +import type { Artifact } from 'genkit/beta'; + +// An artifact's content lives in `parts` (text parts). `metadata` is optional. +const artifact: Artifact = { + name: 'poem.txt', + parts: [{ text: 'Roses are red…' }], + metadata: { source: 'workspaceAgent' }, // optional +}; +``` + +Writing the same `name` again **replaces** the artifact (dedup by name). + +## Programmatic access (inside tools / custom agents) + +Use the active session. `ai.currentSession()` throws when there's no active +session, so only call it inside an agent turn. + +```ts +const session = ai.currentSession(); + +// Read all artifacts: +const all = session.getArtifacts(); // Artifact[] +const found = all.find((a) => a.name === 'poem.txt'); + +// Create / replace artifacts: +session.addArtifacts([{ name: 'notes.md', parts: [{ text: '# Notes' }] }]); +``` + +## Reading artifacts on the client + +The `remoteAgent` client tracks artifacts on `chat.artifacts` (and each response +exposes `res.artifacts`): + +```ts +import { remoteAgent } from 'genkit/beta/client'; + +const agent = remoteAgent({ url: '/api/workspaceAgent' }); +const chat = agent.chat(); +const res = await chat.send('Create index.html and styles.css'); +console.log(res.artifacts); // Artifact[] produced this turn +console.log(chat.artifacts); // all artifacts tracked for the session +``` + +## Sharing artifacts across agents + +In [multi-agent orchestration](agents-multi-agent.md), set the `agents()` +middleware's `artifactStrategy: 'session'` so sub-agent artifacts are merged +into the parent session (namespaced by invocation ID), and add +`artifacts({ readonly: true })` to the orchestrator so it can `read_artifact` +them: + +```ts +use: [ + agents({ agents: ['researcher', 'coder'], artifactStrategy: 'session' }), + artifacts({ readonly: true }), +]; +``` diff --git a/plugins/genkit/.agents/skills/developing-genkit-js/references/agents-background.md b/plugins/genkit/.agents/skills/developing-genkit-js/references/agents-background.md new file mode 100644 index 00000000..1c61357c --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-js/references/agents-background.md @@ -0,0 +1,109 @@ +# Background Agents / Detaching (Beta) + +> **Beta / preview API.** Detaching **requires a [session store](agents-sessions.md)** — +> the server needs somewhere to write the result when background work finishes. +> Read [agents.md](agents.md) first. + +Detaching runs a turn in the background: the server saves a `pending` snapshot +and returns a `snapshotId` **immediately**, keeps processing, then updates the +snapshot to a terminal status (`completed` / `failed` / `aborted` / `expired`). +The client polls for completion and can abort. + +## Define the agent (store required) + +```ts +import { InMemorySessionStore } from 'genkit/beta'; +import { ai } from './genkit.js'; + +export const backgroundAgent = ai.defineAgent({ + name: 'backgroundAgent', + system: + 'You are a senior research analyst. Produce a comprehensive markdown report.', + store: new InMemorySessionStore(), // REQUIRED for detach +}); +``` + +Expose its companion actions so the client can poll/abort (see +[agents.md](agents.md#serve-an-agent-over-http)): + +```ts +app.post('/api/backgroundAgent', expressHandler(backgroundAgent)); +app.post( + '/api/backgroundAgent/getSnapshot', + expressHandler(backgroundAgent.getSnapshotDataAction) +); +app.post( + '/api/backgroundAgent/abort', + expressHandler(backgroundAgent.abortAgentAction) +); +``` + +## Server-side: detach + wait + +`chat.detach(message)` returns a `DetachedTask` carrying the `snapshotId`. +`task.wait()` polls the store until a terminal state and resolves with the final +snapshot. + +```ts +const chat = backgroundAgent.chat(); +const task = await chat.detach('Write a report on renewable energy trends'); +console.log(task.snapshotId); // available immediately + +const snapshot = await task.wait({ intervalMs: 2000 }); +console.log(snapshot?.status); // 'completed' | 'failed' | 'aborted' | 'expired' +``` + +## Client-side: detach + poll + abort + +On the client (`genkit/beta/client`), `task.poll()` yields snapshots until a +terminal status; `task.abort()` cancels the background work. + +```ts +import { remoteAgent, type DetachedTask } from 'genkit/beta/client'; +import type { MessageData, Part } from 'genkit/beta'; + +const agent = remoteAgent({ url: '/api/backgroundAgent' }); + +// Submit — resolves immediately with a handle. +const task: DetachedTask = await agent.chat().detach('Quantum computing impact'); +console.log(task.snapshotId); + +// Poll until a terminal status. +for await (const snap of task.poll({ intervalMs: 2000 })) { + if (snap.status === 'completed') { + const messages: MessageData[] = snap.state?.messages ?? []; + const lastModel = messages.filter((m) => m.role === 'model').at(-1); + const text = (lastModel?.content ?? []) + .filter((p: Part) => p.text) + .map((p: Part) => p.text) + .join(''); + console.log(text); + break; + } else if (snap.status === 'failed') { + throw new Error('Background task failed on the server.'); + } else if (snap.status === 'aborted') { + break; + } else if (snap.status === 'expired') { + // Worker stopped sending heartbeats (e.g. the server restarted) so the + // task can never complete — treat as terminal. + break; + } + // 'pending' → keep polling +} + +// Abort an in-flight task at any time: +await task.abort(); +``` + +## Status values + +- `pending` — still processing. +- `completed` — completed successfully (read the result from `snapshot.state.messages`). +- `failed` — error during processing. +- `aborted` — cancelled by the client via `abort()`. +- `expired` — the background worker stopped responding (e.g. server restart); + terminal, the task can never complete. + +> Equivalent low-level wire protocol: the client sends `{ detach: true }` with +> the message; `poll`/`wait` hit the agent's `getSnapshot` action, and `abort` +> hits the `abort` action. `remoteAgent` wraps all of this for you. diff --git a/plugins/genkit/.agents/skills/developing-genkit-js/references/agents-branching.md b/plugins/genkit/.agents/skills/developing-genkit-js/references/agents-branching.md new file mode 100644 index 00000000..c07d2b31 --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-js/references/agents-branching.md @@ -0,0 +1,98 @@ +# Agent Branching (Beta) + +> **Beta / preview API.** Requires a [session store](agents-sessions.md) so +> snapshots are persistent. Read [agents.md](agents.md) first. + +A `snapshotId` is an **immutable checkpoint**, like a git commit. You can fork as +many independent timelines as you want from the same snapshot — each turn from a +snapshot creates a new, independent snapshot; the original is unchanged. + +To branch, open a new `chat` attached to an earlier snapshot via +`chat({ snapshotId })`. + +## Server-side branching + +```ts +import { z } from 'genkit'; +import { InMemorySessionStore } from 'genkit/beta'; +import { ai } from './genkit.js'; + +export const assistant = ai.defineAgent({ + name: 'assistant', + system: 'You are a helpful assistant.', + store: new InMemorySessionStore(), +}); + +const root = assistant.chat(); +const res1 = await root.send('Hello!'); +const checkpoint = res1.snapshotId; // branch point + +// Branch A — forks from `checkpoint`. +const branchA = assistant.chat({ snapshotId: checkpoint }); +await branchA.send('My name is Bob.'); +const resA = await branchA.send('What is my name?'); // -> Bob + +// Branch B — forks from the SAME `checkpoint`, fully independent. +const branchB = assistant.chat({ snapshotId: checkpoint }); +await branchB.send('My name is John.'); +const resB = await branchB.send('What is my name?'); // -> John +``` + +## Client-side branching ("pick a variant") + +A common pattern: generate two variants from the same checkpoint in parallel, +let the user pick one, and continue from the chosen snapshot. + +```ts +import { remoteAgent } from 'genkit/beta/client'; + +const agent = remoteAgent({ url: '/api/branchingAgent' }); +let snapshotId: string | undefined; // current branch point + +async function twoVariants(text: string) { + // Each variant gets its own chat branching from the same snapshot + // (or a fresh session when there's no branch point yet). + const makeChat = () => + snapshotId ? agent.chat({ snapshotId }) : agent.chat(); + + const [a, b] = await Promise.all([ + makeChat().send(text), + makeChat().send(text), + ]); + // a.snapshotId !== b.snapshotId — both branch from the same point. + return { a, b }; +} + +// When the user picks a variant, its snapshotId becomes the new branch point: +function pick(chosenSnapshotId: string) { + snapshotId = chosenSnapshotId; +} +``` + +## Restoring history from a snapshot + +Use `agent.getSnapshot(snapshotId)` to read a snapshot's state without starting a +turn — handy for restoring a UI after a reload (e.g. snapshotId stored in the +URL). The server must expose the agent's `getSnapshotDataAction` (see +[agents.md](agents.md#serve-an-agent-over-http)). + +```ts +import type { Part } from 'genkit/beta'; +import { remoteAgent } from 'genkit/beta/client'; + +const agent = remoteAgent({ url: '/api/branchingAgent' }); + +const snapshot = await agent.getSnapshot(snapshotId); +const history = (snapshot?.state?.messages ?? []) + .filter((m) => m.role === 'user' || m.role === 'model') + .map((m) => ({ + role: m.role, + text: (m.content ?? []) + .filter((p: Part) => p.text) + .map((p: Part) => p.text) + .join(''), + })); +``` + +> Abandoned branches simply remain in the store as immutable snapshots; nothing +> is overwritten when you branch. diff --git a/plugins/genkit/.agents/skills/developing-genkit-js/references/agents-custom.md b/plugins/genkit/.agents/skills/developing-genkit-js/references/agents-custom.md new file mode 100644 index 00000000..666e7f13 --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-js/references/agents-custom.md @@ -0,0 +1,141 @@ +# Advanced Custom Agents — `defineCustomAgent` (Beta) + +> **Beta / preview API.** `ai.defineCustomAgent` comes from `genkit/beta`. Read +> [agents.md](agents.md) and [agent state](agents-state.md) first. + +`defineAgent` runs a single prompt + tool loop. When you need **full control of +the turn** — multiple sequential model calls, custom logic between them, manual +message/state management, or custom progress streaming — use +`ai.defineCustomAgent`. You provide the handler that runs the turn. + +## When to use it + +Reach for `defineCustomAgent` when a turn needs to: + +- make **multiple model calls** with your own orchestration between them; +- run **multi-step workflows** (decompose → research → synthesize); +- **manually manage** messages and custom state; +- **stream custom status** updates to the client mid-turn. + +Otherwise prefer `defineAgent` (simpler; custom state still works — see +[agent state](agents-state.md)). + +## Signature + +```ts +ai.defineCustomAgent( + config: { + name: string; + description?: string; + stateSchema?: z.ZodType; + store?: SessionStore; + }, + fn: async (sess, { sendChunk }) => { message: MessageData } +); +``` + +The handler receives a session runner `sess` and a turn context (`sendChunk` to +stream chunks to the client). It must return `{ message }` — the final model +message for the turn. + +Key `sess` methods: + +- `sess.run(async (input) => {...})` — runs the turn; adds `input.message` (the + incoming user message) to the session before calling your callback, so + `sess.getMessages()` includes it. `input.message?.content` holds the parts. +- `sess.getMessages()` — the full message history. +- `sess.addMessages([...])` — append messages (e.g. your final model response). + +## Example: multi-step research agent + +```ts +import { z } from 'genkit'; +import { ai, liteModel } from './genkit.js'; + +interface ResearchState { + status?: string; // live progress shown to the client + subQuestions: string[]; + subAnswers: { question: string; answer: string }[]; +} + +export const researchAgent = ai.defineCustomAgent( + { name: 'researchAgent' }, + async (sess, { sendChunk }) => { + let lastMessage: any; + const session = ai.currentSession(); + + await sess.run(async (input) => { + const userText = input.message?.content[0]?.text ?? ''; + + // Step 1: decompose (a fast model). Mutating custom state auto-emits a + // `customPatch` chunk so the client's tracked state stays live. + session.updateCustom((s) => ({ + ...s!, + status: 'Decomposing question…', + })); + const decompose = await ai.generate({ + model: liteModel, + prompt: `Break this into 2-3 sub-questions (JSON array): "${userText}"`, + output: { format: 'json', schema: z.array(z.string()).min(2).max(3) }, + }); + const subQuestions = decompose.output ?? [userText]; + session.updateCustom((s) => ({ ...s!, subQuestions, subAnswers: [] })); + + // Step 2: research each sub-question (main model). + const subAnswers: { question: string; answer: string }[] = []; + for (let i = 0; i < subQuestions.length; i++) { + session.updateCustom((s) => ({ + ...s!, + status: `Researching (${i + 1}/${subQuestions.length})`, + })); + const research = await ai.generate({ prompt: subQuestions[i] }); + subAnswers.push({ question: subQuestions[i], answer: research.text }); + } + session.updateCustom((s) => ({ ...s!, subAnswers })); + + // Step 3: synthesize and STREAM the final answer to the client. + session.updateCustom((s) => ({ ...s!, status: 'Synthesizing…' })); + const synthesis = ai.generateStream({ + prompt: `Synthesize a unified answer from:\n${JSON.stringify(subAnswers)}`, + }); + for await (const chunk of synthesis.stream) { + sendChunk({ modelChunk: chunk }); // stream model output to the client + } + const final = await synthesis.response; + lastMessage = final.message; + + // Record the final response in the session history. + if (lastMessage) sess.addMessages([lastMessage]); + session.updateCustom((s) => ({ ...s!, status: 'Done' })); + }); + + return { + message: lastMessage ?? { + role: 'model' as const, + content: [{ text: 'Research complete.' }], + }, + }; + } +); +``` + +## Custom status streaming + +Calling `session.updateCustom(...)` during the turn automatically emits a +`customPatch` chunk, so the `remoteAgent` client's tracked +[custom state](agents-state.md) (e.g. the `status` field) stays live **mid-stream** +without any extra wiring. Stream model output separately with +`sendChunk({ modelChunk })`. + +Seed and run a custom agent exactly like a regular one: + +```ts +const chat = researchAgent.chat({ + state: { custom: { subQuestions: [], subAnswers: [] }, messages: [], artifacts: [] }, +}); +const turn = chat.sendStream('Impacts of electric vehicles?'); +for await (const chunk of turn.stream) { + /* chunk.text for model output; chat.state.status for live progress */ +} +await turn.response; +``` diff --git a/plugins/genkit/.agents/skills/developing-genkit-js/references/agents-deployment.md b/plugins/genkit/.agents/skills/developing-genkit-js/references/agents-deployment.md new file mode 100644 index 00000000..9f5d78ce --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-js/references/agents-deployment.md @@ -0,0 +1,190 @@ +# Deploying / Serving Agents over HTTP (Beta) + +> **Beta / preview API.** Uses `expressHandler` from `@genkit-ai/express` and the +> agent's beta companion actions. Read [agents.md](agents.md) first. + +An agent is served as an HTTP endpoint with `expressHandler`. Most agents also +expose two companion actions: + +- `agent.getSnapshotDataAction` → `POST /api//getSnapshot` — read a + snapshot's state. Needed for [snapshot restore](agents-branching.md) and + [background](agents-background.md) polling. +- `agent.abortAgentAction` → `POST /api//abort` — cancel a + [background](agents-background.md) turn. + +These paths match the `remoteAgent` client defaults (`${url}/getSnapshot`, +`${url}/abort`), so a client only needs the base `url`. + +## A reusable `exposeAgent` helper + +When serving several agents, a small helper keeps registration consistent. + +```ts +import { expressHandler } from '@genkit-ai/express'; +import { Agent } from 'genkit/beta'; +import express from 'express'; + +const app = express(); +app.use(express.json()); + +// Register an agent at `/api/`, optionally wiring up its companion +// `/getSnapshot` and `/abort` sub-actions. +function exposeAgent( + name: string, + agent: Agent, + opts: { snapshot?: boolean; abort?: boolean } = {} +) { + app.post(`/api/${name}`, expressHandler(agent)); + if (opts.snapshot) { + app.post( + `/api/${name}/getSnapshot`, + expressHandler(agent.getSnapshotDataAction) + ); + } + if (opts.abort) { + app.post(`/api/${name}/abort`, expressHandler(agent.abortAgentAction)); + } +} + +// Plain conversational agent — no companions needed: +exposeAgent('weatherAgent', weatherAgent); + +// Snapshot restore / branching — needs getSnapshot: +exposeAgent('branchingAgent', branchingAgent, { snapshot: true }); + +// Background agent — needs both getSnapshot (polling) and abort: +exposeAgent('backgroundAgent', backgroundAgent, { snapshot: true, abort: true }); + +app.listen(process.env.PORT ? parseInt(process.env.PORT) : 8080); +``` + +Which companions to enable: + +| Agent capability | `snapshot` | `abort` | +| ----------------------------------------- | ---------- | ------- | +| Plain chat (client- or server-state) | – | – | +| Snapshot restore / [branching](agents-branching.md) | ✓ | – | +| [Background](agents-background.md) / detach | ✓ | ✓ | + +## CORS for browser clients + +A browser `remoteAgent` calling a different origin (e.g. a Vite dev server) +needs CORS. **Streaming requires the `X-Genkit-Stream-Id` header** to be allowed +on the request and exposed on the response. + +```ts +app.use((req, res, next) => { + res.header('Access-Control-Allow-Origin', '*'); + res.header( + 'Access-Control-Allow-Headers', + 'Content-Type, Accept, X-Genkit-Stream-Id' + ); + res.header('Access-Control-Expose-Headers', 'X-Genkit-Stream-Id'); + res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); + if (req.method === 'OPTIONS') { + res.sendStatus(204); + return; + } + next(); +}); +``` + +## Serving a static web UI (optional) + +To ship a single server that hosts both the API and a built SPA, serve the +static bundle and add a non-`/api` fallback so client-side routing works on deep +links / reloads. + +```ts +import { existsSync } from 'node:fs'; +import { join } from 'node:path'; + +const webDist = join(__dirname, '..', 'web', 'dist'); +if (existsSync(webDist)) { + app.use(express.static(webDist)); + // SPA fallback — send index.html for any non-API GET request. + app.get(/^\/(?!api\/).*/, (_req, res) => { + res.sendFile(join(webDist, 'index.html')); + }); +} +``` + +## Registering agents/flows + +Agents and flows must be imported so they register with Genkit. Side-effect +imports work, but explicitly referencing them makes the available actions clear: + +```ts +import { weatherAgent } from './weather-agent.js'; +import { backgroundAgent } from './background-agent.js'; + +// Force-reference so the modules' top-level defineAgent/defineFlow run. +void [weatherAgent, backgroundAgent]; +``` + +You can also expose plain flows the same way (e.g. helper endpoints): + +```ts +app.post('/api/workspace/files', expressHandler(listWorkspaceFiles)); +``` + +## Other host frameworks + +`expressHandler` is one of several adapters — an agent is just an action, so any +of these works. Expose the companion actions +(`agent.getSnapshotDataAction`, `agent.abortAgentAction`) at the matching +sub-paths the same way when you need snapshot restore / background. + +### Next.js (`@genkit-ai/next`) + +`appRoute` turns an agent into an App Router route handler. Put companions in +their own route files. + +```ts +// app/api/weatherAgent/route.ts +import { appRoute } from '@genkit-ai/next'; +import { weatherAgent } from '@/lib/agents'; + +export const POST = appRoute(weatherAgent); + +// app/api/weatherAgent/getSnapshot/route.ts +export const POST = appRoute(weatherAgent.getSnapshotDataAction); +``` + +### Web Fetch (`@genkit-ai/fetch`) + +Works with any Fetch-API runtime (Hono, Bun, Deno, Cloudflare Workers, Vercel/ +Netlify Edge, etc.). `fetchHandler(action)` returns `(request) => Promise`; +`fetchHandlers(actions, prefix)` path-routes multiple actions by name. + +```ts +import { fetchHandler } from '@genkit-ai/fetch'; +import { Hono } from 'hono'; +import { weatherAgent } from './agents.js'; + +const app = new Hono(); +app.all('/api/weatherAgent', (c) => fetchHandler(weatherAgent)(c.req.raw)); +app.all('/api/weatherAgent/getSnapshot', (c) => + fetchHandler(weatherAgent.getSnapshotDataAction)(c.req.raw) +); +``` + +### Fastify (`@genkit-ai/fastify`) + +```ts +import Fastify from 'fastify'; +import { fastifyHandler } from '@genkit-ai/fastify'; +import { weatherAgent } from './agents.js'; + +const app = Fastify(); +app.post('/api/weatherAgent', fastifyHandler(weatherAgent)); +app.post( + '/api/weatherAgent/getSnapshot', + fastifyHandler(weatherAgent.getSnapshotDataAction) +); +await app.listen({ port: 8080 }); +``` + +> The `@genkit-ai/vercel-ai` plugin additionally offers a `GenkitChatTransport` +> for the Vercel AI SDK's `useChat`, which tracks the `chatId → snapshotId` +> mapping for you (no client `SessionStore` needed). diff --git a/plugins/genkit/.agents/skills/developing-genkit-js/references/agents-human-in-the-loop.md b/plugins/genkit/.agents/skills/developing-genkit-js/references/agents-human-in-the-loop.md new file mode 100644 index 00000000..ad9383a8 --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-js/references/agents-human-in-the-loop.md @@ -0,0 +1,173 @@ +# Agent Human-in-the-Loop / Interrupts (Beta) + +> **Beta / preview API.** Read [agents.md](agents.md) first. + +An **interrupt** pauses an agent mid-turn and hands control back to your code (or +a human) — e.g. to approve a sensitive action, collect missing input, or confirm +a plan. Internally it's a **tool call used as control flow**: the interrupt tool +never executes on the server; it exists only to pause the turn. You then +**resume** from the exact point it paused. + +Interrupts are **orthogonal to persistence** — they work the same whether the +agent uses a [session store](agents-sessions.md) or +[client-managed state](agents.md#client-managed-state-no-server-store). The +paused turn just needs to be carried back into the resume call: with a store the +snapshot does it; without one the client round-trips the state blob (the +`remoteAgent` client handles this for you). + +Flow: `chat.send(...)` → response has `res.interrupts` → collect human input → +`chat.resume({ respond: [...] })`. + +## Define an interrupt + +Define it like a tool (with `inputSchema`/`outputSchema`) and add it to the +agent's `tools`. No store is required; this example uses one so the multi-turn +conversation also persists server-side. + +```ts +import { z } from 'genkit'; +import { InMemorySessionStore } from 'genkit/beta'; +import { ai } from './genkit.js'; + +export const userApproval = ai.defineInterrupt({ + name: 'userApproval', + description: 'Ask the user for approval before a sensitive action.', + // What the model passes in when it pauses (shown to the human): + inputSchema: z.object({ action: z.string(), details: z.string() }), + // What the human/your code returns to resume: + outputSchema: z.object({ + approved: z.boolean(), + feedback: z.string().optional(), + }), +}); + +export const transferMoney = ai.defineTool( + { + name: 'transferMoney', + description: 'Transfer money to a specified account.', + inputSchema: z.object({ amount: z.number(), toAccount: z.string() }), + outputSchema: z.object({ success: z.boolean(), transactionId: z.string() }), + }, + async ({ amount, toAccount }) => ({ + success: true, + transactionId: `txn-${Date.now()}`, + }) +); + +export const bankingAgent = ai.defineAgent({ + name: 'bankingAgent', + system: + 'You are a banking assistant. ALWAYS use the userApproval interrupt to ' + + 'confirm before executing transferMoney.', + tools: [userApproval, transferMoney], + store: new InMemorySessionStore(), +}); +``` + +## Detect and resume (server-side) + +`res.interrupts` is non-empty when the agent paused. Each entry exposes: + +- `.name` — the interrupt's name. +- `.input` — the data the model passed in (typed by the interrupt's `inputSchema`). +- `.respond(output)` — **builder** returning a `toolResponse` part to resume with + (provides the tool's output without executing it). Does **not** send. +- `.restart()` — **builder** re-issuing the original tool request (use to retry / + let the tool actually run). Does **not** send. + +Resume the **same** chat with `chat.resume(...)` (or `chat.resumeStream(...)`), +which is sugar for `send({ resume })`: + +```ts +const chat = bankingAgent.chat(); +let res = await chat.send('Transfer $500 to my savings account.'); + +const approval = res.interrupts.find((i) => i.name === 'userApproval'); +if (approval) { + console.log(approval.input); // { action, details } — show this to the human + + // Collect the human decision, then resume with the interrupt's output: + res = await chat.resume({ + respond: [approval.respond({ approved: true, feedback: 'Looks good' })], + }); +} +console.log(res.text); // final confirmation +``` + +Streaming variant: + +```ts +const turn = chat.resumeStream({ + respond: [approval.respond({ approved: true })], +}); +for await (const chunk of turn.stream) process.stdout.write(chunk.text ?? ''); +const res = await turn.response; +``` + +You can resume multiple interrupts at once by passing several builders, and mix +`respond` (supply output) with `restart` (re-run the tool): + +```ts +await chat.resume({ + respond: [a.respond({ approved: true })], + restart: [b.restart()], +}); +``` + +## Client-side (browser) interrupts + +The same pattern works over HTTP with `remoteAgent`. Types come from +`genkit/beta/client`. The client tracks the snapshot, so resuming the same `chat` +continues exactly where it paused. + +```ts +import { + remoteAgent, + type AgentChat, + type AgentInterrupt, + type AgentResponse, +} from 'genkit/beta/client'; + +const agent = remoteAgent({ url: '/api/bankingAgent' }); +const chat: AgentChat = agent.chat(); + +// 1. Send and detect the pause. +const res: AgentResponse = await chat.send('Transfer $500 to savings.'); +const pending: AgentInterrupt | undefined = res.interrupts.find( + (i) => i.name === 'userApproval' +); + +if (pending) { + // pending.input → { action, details }; render an approval dialog. + + // 2. After the human approves/denies, resume the SAME chat. + const respond = pending.respond({ approved: true, feedback: 'ok' }); + const turn = chat.resumeStream({ respond: [respond] }); + for await (const chunk of turn.stream) { + /* render chunk.text */ + } + const final = await turn.response; + // If final.interrupts is non-empty, the agent paused again — repeat. +} +``` + +> UX tip: don't render a model message bubble for an interrupted turn +> (`res.interrupts.length > 0`); show the approval UI from `interrupt.input` +> instead, then render the model's reply after resuming. + +## Notes & gotchas + +- **No store required.** Interrupts work with either a [session store](agents-sessions.md) + or [client-managed state](agents.md#client-managed-state-no-server-store) — + persistence is orthogonal. Just resume on the same `chat` (or, for raw calls, + carry the returned state/snapshot back into the resume). +- **`respond`/`restart` are builders.** They return parts for the `resume` + payload; they do not send. You still call `chat.resume(...)`. +- **Resume validation.** The server validates every `respond`/`restart` entry + against the conversation history (name/ref must match; `restart` input must be + unchanged). A mismatch is rejected — always build entries from the interrupt + objects in the response, not hand-rolled parts. +- **Only `completed` snapshots are resumable.** A failed/aborted/pending snapshot + is kept for inspection but can't be resumed. +- **Re-pausing.** After resuming, the new response may interrupt again; loop + until `res.interrupts` is empty. diff --git a/plugins/genkit/.agents/skills/developing-genkit-js/references/agents-multi-agent.md b/plugins/genkit/.agents/skills/developing-genkit-js/references/agents-multi-agent.md new file mode 100644 index 00000000..5264a15d --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-js/references/agents-multi-agent.md @@ -0,0 +1,137 @@ +# Multi-Agent Orchestration / Sub-Agents (Beta) + +> **Beta / preview API.** Sub-agent delegation uses the `agents()` middleware +> from `@genkit-ai/middleware`. Read [agents.md](agents.md) first. + +A common pattern is an **orchestrator** agent that delegates tasks to +specialized **sub-agents** (e.g. a `researcher` and a `coder`). The `agents()` +middleware injects one delegation tool per sub-agent (`delegate_to_`), +appends a `` block to the orchestrator's system prompt, and — when +the model calls a delegation tool — runs the sub-agent and returns its response +as the tool result. + +## 1. Define the sub-agents + +Give each sub-agent a `description` — it's auto-discovered and shown to the +orchestrator so the model knows when to delegate. + +```ts +import { artifacts, retry } from '@genkit-ai/middleware'; +import { ai, defaultModel } from './genkit.js'; + +export const researcher = ai.defineAgent({ + name: 'researcher', + description: + 'A thorough research assistant that searches the web and provides ' + + 'well-sourced answers.', + model: defaultModel, + system: + 'You are a thorough research assistant. Save findings with write_artifact.', + maxTurns: 10, + use: [retry(), artifacts()], +}); + +export const coder = ai.defineAgent({ + name: 'coder', + description: 'An expert programmer that writes clean, well-commented code.', + system: 'You are an expert programmer. Save code with write_artifact.', + maxTurns: 10, + use: [artifacts(), retry()], +}); +``` + +## 2. Wire up the orchestrator + +Add `agents()` to the orchestrator's `use: [...]`. Each entry is a name string +(description auto-discovered) or `{ name, description }` to override. + +```ts +import { agents, artifacts, retry } from '@genkit-ai/middleware'; +import { ai } from './genkit.js'; + +export const orchestratorAgent = ai.defineAgent({ + name: 'orchestratorAgent', + system: `You are a helpful project assistant. Analyze the request and +delegate to the appropriate sub-agent. If it needs research AND code, call them +sequentially, then synthesize a final answer.`, + use: [ + agents({ + agents: [ + 'researcher', // auto-discovered description + { + name: 'coder', + description: 'Writes, debugs, and explains code. Use for programming.', + }, + ], + maxDelegations: 5, // guard rail against runaway delegation loops + historyLength: 4, // forward the last N user/model messages as context + artifactStrategy: 'session', // see "Sharing artifacts" below + }), + artifacts({ readonly: true }), // read sub-agent artifacts via read_artifact + retry(), + ], +}); +``` + +Run it like any other agent: + +```ts +const chat = orchestratorAgent.chat(); +const turn = chat.sendStream( + 'Research the best sorting algorithms, then write a TypeScript quicksort.' +); +for await (const chunk of turn.stream) process.stdout.write(chunk.text ?? ''); +const res = await turn.response; +``` + +## `agents()` options + +- `agents` (required): array of sub-agent refs. A `string` (name, description + auto-discovered from the registry) or `{ name, description? }` (explicit + override). +- `toolPrefix`: prefix for generated tool names. Default `"delegate_to"` → + `delegate_to_`. Empty string uses bare agent names. +- `maxDelegations`: max delegations per `generate` call. Prevents runaway loops. +- `historyLength`: number of recent user/model messages forwarded to sub-agents + as context. `0`/omitted sends only the task description. (Only client-managed + sub-agents — no `store` — accept ad-hoc seeded history; server-managed + sub-agents skip it and just receive the task.) +- `artifactStrategy`: `'inline'` (default) or `'session'` — see below. + +## Sharing artifacts between agents + +Sub-agents can produce [artifacts](agents-artifacts.md). `artifactStrategy` +controls how they reach the orchestrator: + +- `'inline'` (default): artifact content is included in the delegation tool + result (so the model sees it directly) **and** merged into the parent session. +- `'session'`: artifacts are merged into the parent session only; the tool + result lists artifact names, not content. Pair with the `artifacts()` + middleware so the orchestrator can `read_artifact` on demand (the pattern + shown above). Merged artifacts are namespaced by an invocation ID + (`_/`). + +## Other middleware + +`@genkit-ai/middleware` also exports `retry`, `fallback`, `filesystem`, +`skills`, and `toolApproval`. They attach the same way via `use: [...]` on an +agent (or on `ai.generate`) — see [using middleware](middleware.md). `retry()` +is commonly paired with delegation: + +```ts +import { retry } from '@genkit-ai/middleware'; + +retry({ + maxRetries: 3, // default 3 + initialDelayMs: 1000, // default 1000 + maxDelayMs: 60000, // default 60000 + backoffFactor: 2, // default 2 (exponential backoff) + // statuses defaults to UNAVAILABLE, DEADLINE_EXCEEDED, RESOURCE_EXHAUSTED, + // ABORTED, INTERNAL +}); +``` + +> Note: if a sub-agent triggers an [interrupt](agents-human-in-the-loop.md), it +> is reported back to the orchestrator as a normal tool response (not propagated +> as a resumable interrupt). Interactive, stateful sub-agent interrupts are a +> future feature — delegate self-contained tasks. diff --git a/plugins/genkit/.agents/skills/developing-genkit-js/references/agents-sessions.md b/plugins/genkit/.agents/skills/developing-genkit-js/references/agents-sessions.md new file mode 100644 index 00000000..8a170e2c --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-js/references/agents-sessions.md @@ -0,0 +1,147 @@ +# Agent Sessions & Persistence (Beta) + +> **Beta / preview API.** Session stores are imported from `genkit/beta` +> (`InMemorySessionStore`, `FileSessionStore`) and `@genkit-ai/google-cloud/beta` +> (`FirestoreSessionStore`). See [agents.md](agents.md) for the basics first. + +When an agent has a `store`, the **server** owns the session history. Each turn +produces an immutable **snapshot**; the snapshot chain is what carries +conversation state forward. A store also enables [branching](agents-branching.md) +and [background execution](agents-background.md). (Interrupts work with or without +a store — see [human-in-the-loop](agents-human-in-the-loop.md).) + +## Pick a store + +```ts +import { InMemorySessionStore, FileSessionStore } from 'genkit/beta'; + +// In-memory: great for tests/dev; lost on restart. +const memStore = new InMemorySessionStore(); + +// File-backed: snapshots persisted under /global/.json +const fileStore = new FileSessionStore('./.snapshots'); + +// File store with chain pruning — keep only the last N snapshots in a chain. +const pruning = new FileSessionStore('./.snapshots', { + maxPersistedChainLength: 3, +}); +``` + +Attach it to the agent: + +```ts +import { ai } from './genkit.js'; + +export const logbookAgent = ai.defineAgent({ + name: 'logbookAgent', + system: 'You are a personal logbook assistant.', + store: fileStore, +}); +``` + +A single `chat` persists to the store and threads the snapshot forward +automatically: + +```ts +const chat = logbookAgent.chat(); +const res1 = await chat.send('Log this: I started studying Genkit today.'); +const res2 = await chat.send('What did I study today?'); // remembers turn 1 +console.log(res1.snapshotId, res2.snapshotId); +``` + +Resume a prior conversation by snapshot id: + +```ts +// Continue an existing session from its latest snapshot. +const resumed = logbookAgent.chat({ snapshotId: res2.snapshotId }); +await resumed.send('Add another note.'); +``` + +## Typed session state + +Use `stateSchema` to attach typed custom state to the session. `State` is +inferred from the schema and validated when a snapshot is loaded. + +```ts +import { z } from 'genkit'; + +const Profile = z.object({ name: z.string(), tier: z.enum(['free', 'pro']) }); + +export const profileAgent = ai.defineAgent({ + name: 'profileAgent', + system: 'Greet the user by name and tailor answers to their tier.', + store: new InMemorySessionStore>(), + stateSchema: Profile, +}); + +// Seed custom state when opening the chat. Custom state lives under `.custom` +// of the `SessionState` (that's what `stateSchema` validates). +const chat = profileAgent.chat({ + state: { custom: { name: 'Ada', tier: 'pro' } }, +}); +``` + +## Interrupts (human-in-the-loop) + +Interrupts pause a turn so a human can approve/provide input, then resume from +the exact pause point. They work with a store or with client-managed state +(persistence is orthogonal). See the dedicated reference: +[Human-in-the-loop / interrupts](agents-human-in-the-loop.md). + +## Firestore session store (scalable, Beta) + +For production, `FirestoreSessionStore` (from `@genkit-ai/google-cloud/beta`) +persists each turn as an incremental JSON Patch diff anchored to periodic sharded +checkpoints — no single document approaches Firestore's 1 MiB limit, and +reads/writes per turn are bounded by `checkpointInterval` rather than total +session length (scales to long-lived chat/coding agents). + +```ts +import { genkit } from 'genkit/beta'; +import { FirestoreSessionStore } from '@genkit-ai/google-cloud/beta'; + +const ai = genkit({ plugins: [/* ... */] }); + +const myAgent = ai.defineAgent({ + name: 'myAgent', + system: 'You are a helpful assistant.', + // Defaults to a new Firestore() using Application Default Credentials. + store: new FirestoreSessionStore(), +}); +``` + +Options: + +- `db`: explicit Firestore instance (defaults to a new `Firestore()`, honoring + `FIRESTORE_EMULATOR_HOST`). +- `collection`: snapshot collection (default `"genkit-sessions"`). Companion + collections `"-pointers"` and `"-shards"` are derived. +- `checkpointInterval`: turns between full-state checkpoints (default `25`). + Lower for small, read-heavy state; raise for large per-turn state. +- `shardSize`: max bytes per shard/diff document (default `512 KiB`). + +> On Firebase, `@genkit-ai/firebase` re-exports this store with Firebase app +> setup (a `firebaseApp` option). See its README. + +## Implementing a custom `SessionStore` + +```ts +import type { SessionStore } from 'genkit/beta'; + +// S is the custom state type. +const store: SessionStore = { + // Load a snapshot by snapshotId OR sessionId (exactly one). + async getSnapshot(opts) { + /* ... */ return undefined; + }, + // Atomically read → mutate → persist. Returns the snapshotId used, + // or null when the mutator returns null. + async saveSnapshot(snapshotId, mutator, options) { + /* ... */ return snapshotId ?? 'new-id'; + }, + // Optional: subscribe to snapshot state changes (used by background agents). + onSnapshotStateChange(snapshotId, callback, options) { + return () => {}; // unsubscribe + }, +}; +``` diff --git a/plugins/genkit/.agents/skills/developing-genkit-js/references/agents-state.md b/plugins/genkit/.agents/skills/developing-genkit-js/references/agents-state.md new file mode 100644 index 00000000..f75f0e93 --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-js/references/agents-state.md @@ -0,0 +1,133 @@ +# Working with Agent State (Beta) + +> **Beta / preview API.** Read [agents.md](agents.md) first. + +Beyond message history, an agent session can hold typed **custom state** — your +own structured data (a task list, a workflow status, counters, etc.). Tools read +and mutate it during a turn, and it is automatically synced to the +[`remoteAgent`](agents.md#consume-an-agent-from-a-client-remoteagent) client. + +## Declare the state shape + +Pass a `stateSchema` (Zod) to `defineAgent`. `State` is inferred from it and +validated when a snapshot is loaded. + +```ts +import { z } from 'genkit'; +import { ai } from './genkit.js'; + +const TaskItem = z.object({ + id: z.number(), + title: z.string(), + done: z.boolean(), +}); + +const TaskState = z.object({ + tasks: z.array(TaskItem), + nextId: z.number(), +}); + +export const taskAgent = ai.defineAgent({ + name: 'taskAgent', + stateSchema: TaskState, + system: 'You manage the user\'s task list. Use the tools to modify it.', + tools: [ + /* addTask, toggleTask, removeTask — below */ + ], +}); +``` + +## Read & mutate state inside tools + +Tools call `ai.currentSession()` to access the live session, then +`session.getCustom()` / `session.updateCustom(mutator)`. `updateCustom` takes +`(custom?: S) => S` and returns the new state. + +```ts +type TaskState = z.infer; + +const addTask = ai.defineTool( + { + name: 'addTask', + description: 'Add a new task. Returns the created task.', + inputSchema: z.object({ title: z.string() }), + outputSchema: z.object({ + id: z.number(), + title: z.string(), + done: z.boolean(), + }), + }, + async (input) => { + const session = ai.currentSession(); + let created!: { id: number; title: string; done: boolean }; + session.updateCustom((state) => { + const s = state ?? { tasks: [], nextId: 1 }; + created = { id: s.nextId, title: input.title, done: false }; + s.tasks.push(created); + s.nextId++; + return s; + }); + return created; + } +); +``` + +`ai.currentSession()` throws if called outside an active session (e.g. a tool +invoked without a running agent turn), so only use it inside agent tools. + +## Seed and read state (server-side) + +Seed initial custom state when opening a chat. The `state` argument is a +`SessionState`: custom data goes under `.custom` (alongside `messages` and +`artifacts`). + +```ts +const chat = taskAgent.chat({ + state: { + custom: { tasks: [], nextId: 1 }, + messages: [], + artifacts: [], + }, +}); + +const res = await chat.send('Add a task: buy groceries'); +console.log(res.state); // res.state returns the custom state directly +``` + +## Auto-sync to the `remoteAgent` client + +When you talk to the agent over HTTP, the `remoteAgent` client tracks custom +state for you. Parameterize the client with your state type, seed it the same +way (`state.custom`), and after a turn read it off `chat.state`. + +**Important:** on the client, custom fields are **flattened directly onto +`chat.state`** (e.g. `chat.state.tasks`) — not under `chat.state.custom`. + +```ts +import { remoteAgent, type AgentChat } from 'genkit/beta/client'; + +interface TaskState { + tasks: { id: number; title: string; done: boolean }[]; + nextId: number; +} + +const agent = remoteAgent({ url: '/api/taskAgent' }); +const chat: AgentChat = agent.chat({ + state: { custom: { tasks: [], nextId: 1 }, messages: [], artifacts: [] }, +}); + +const turn = chat.sendStream('Add buy groceries, then mark it done'); +for await (const chunk of turn.stream) { + /* render chunk.text */ +} +await turn.response; + +// Custom state is flattened onto chat.state: +console.log(chat.state?.tasks); +``` + +There is **no `onStateChange` subscription** — state updates ride on the +streamed chunks. Read the authoritative state from `chat.state` after +`await turn.response`. (For live mid-stream status updates from a custom agent, +see [advanced custom agents](agents-custom.md), which emit `customPatch` chunks +as state changes.) diff --git a/plugins/genkit/.agents/skills/developing-genkit-js/references/agents.md b/plugins/genkit/.agents/skills/developing-genkit-js/references/agents.md new file mode 100644 index 00000000..3a821dbc --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-js/references/agents.md @@ -0,0 +1,278 @@ +# Agents (Beta) + +> **Beta / preview API.** Agents are not yet stable. Server APIs come from +> `genkit/beta`; the browser client comes from `genkit/beta/client`. Import paths +> and signatures may change. Always use `genkit/beta`, not `genkit`, for agents. +> +> **Requires `genkit` >= 1.39.0.** + +An **agent** is a persistent, multi-turn conversation primitive built on top of +prompts + tools. Compared to a bare `ai.generate`/`ai.definePrompt` loop, an +agent adds: + +- **Sessions**: multi-turn history tracked as immutable **snapshots**. +- **State**: typed session state (messages + custom data + artifacts). +- **Interrupts**: human-in-the-loop pause/resume. +- **Branching**: fork a conversation from any snapshot. +- **Detaching**: run a turn in the background and poll for the result. + +Progressive disclosure — read the file for the level you need: + +- This file: defining an agent, serving it, and **client-managed state** (no store). +- [Sessions & persistence](agents-sessions.md): `SessionStore`, `InMemorySessionStore`, `FileSessionStore`, `FirestoreSessionStore`. +- [Human-in-the-loop / interrupts](agents-human-in-the-loop.md): pausing for approval/input and resuming. +- [Branching](agents-branching.md): forking a conversation from a snapshot. +- [Background agents](agents-background.md): detaching long-running turns and polling. +- [Working with state](agents-state.md): typed custom session state and client auto-sync. +- [Artifacts](agents-artifacts.md): producing/reading named deliverables. +- [Multi-agent orchestration](agents-multi-agent.md): delegating to sub-agents. +- [Advanced custom agents](agents-custom.md): `defineCustomAgent` for full turn control. +- [Deploying agents](agents-deployment.md): serving multiple agents over HTTP, CORS, web UI, other frameworks. + +## Setup + +```ts +// genkit.ts — note the `genkit/beta` import (required for agents) +import { genkit } from 'genkit/beta'; +import { googleAI } from '@genkit-ai/google-genai'; + +export const ai = genkit({ + plugins: [googleAI()], + model: googleAI.model('gemini-flash-latest'), +}); +``` + +## Define an agent + +`ai.defineAgent` combines prompt + tool config + (optional) session store into a +single registered action. + +```ts +import { z } from 'genkit'; +import { ai } from './genkit.js'; + +const getWeather = ai.defineTool( + { + name: 'getWeather', + description: 'Look up the current weather for a city.', + inputSchema: z.object({ city: z.string() }), + outputSchema: z.object({ tempC: z.number(), summary: z.string() }), + }, + async ({ city }) => ({ tempC: 21, summary: 'Sunny' }) +); + +export const weatherAgent = ai.defineAgent({ + name: 'weatherAgent', + system: + 'You are a helpful weather assistant. Use the getWeather tool. Be concise.', + tools: [getWeather], + // `store` is optional. Omit it for client-managed state (see below). +}); +``` + +Common `defineAgent` options: + +- `name` (required): action name. +- `system` / `prompt` / dotprompt fields: same as `definePrompt`. +- `tools`: tools and interrupts available to the agent. +- `model`: override the default model. +- `store`: a `SessionStore` for server-side persistence. See [sessions](agents-sessions.md). +- `stateSchema`: a `z.ZodType` describing custom session state. When set, + `State` is inferred and validated at load time. +- `input` / `inputSchema`: input variables for the prompt template. + +## Agents and middleware go hand in hand + +Agents and [middleware](middleware.md) are built for each other: the `use: [...]` +array is where you layer in sophisticated behavior without writing it yourself. +Most agent capabilities — sub-agent delegation, artifacts, filesystem access, +skill loading, tool approval, retries — are just middleware you drop in. + +For example, a full coding assistant is mostly configuration: + +```ts +import { filesystem, retry, skills, toolApproval } from '@genkit-ai/middleware'; +import { FileSessionStore } from 'genkit/beta'; +import { ai } from './genkit.js'; + +export const codingAgent = ai.defineAgent({ + name: 'codingAgent', + system: 'You are an expert AI coding assistant working in a sandboxed workspace.', + tools: [runShell, askUser], // your own custom tools/interrupts + use: [ + // Require user approval (interrupt) before risky tools; reads auto-approved. + // Order matters: keep toolApproval before filesystem. + toolApproval({ + approved: ['list_files', 'read_file', 'use_skill', 'run_shell', 'ask_user'], + }), + // list_files / read_file / write_file / search_and_replace, sandboxed. + filesystem({ rootDirectory: WORKSPACE_DIR, allowWriteAccess: true }), + // Load coding conventions on demand via a use_skill tool. + skills({ skillPaths: [SKILLS_DIR] }), + // Automatic retry on transient model errors. + retry(), + ], + store: new FileSessionStore('./.snapshots-coding'), // needed for tool approval + maxTurns: 30, +}); +``` + +That single `use` array gives the agent filesystem tools, an on-demand skills +library, human-in-the-loop approval for writes, and retries — each is one line. +See [using middleware](middleware.md) for the full catalog and +[building custom middleware](middleware-custom.md) to write your own. + +## Chat with an agent (server-side) + +`agent.chat()` opens a conversation. A single `chat` carries state forward +automatically across turns. + +```ts +const chat = weatherAgent.chat(); + +// Non-streaming turn: +const res = await chat.send('Weather in Tokyo?'); +console.log(res.text); +console.log(res.snapshotId); // immutable checkpoint id for this turn + +// Follow-up turn — history is carried automatically: +const res2 = await chat.send('What about Paris?'); + +// Streaming turn: +const turn = chat.sendStream('And London?'); +for await (const chunk of turn.stream) { + process.stdout.write(chunk.text ?? ''); +} +const final = await turn.response; +``` + +## Verify an agent from the CLI (`flow:run`) + +`genkit flow:run` only runs **flows**, not agents, so you can't `flow:run` an +agent directly. To exercise an agent from the CLI (e.g. a quick, self-terminating +check), wrap one turn in a throwaway flow and run that: + +```ts +import { z } from 'genkit'; +import { weatherAgent } from './weather-agent.js'; +import { ai } from './genkit.js'; + +export const tryWeatherAgent = ai.defineFlow( + { name: 'tryWeatherAgent', inputSchema: z.string(), outputSchema: z.string() }, + async (message) => (await weatherAgent.chat().send(message)).text +); +// genkit flow:run tryWeatherAgent '"Weather in Tokyo?"' -- npx tsx src/index.ts +``` + +## Serve an agent over HTTP + +Use `expressHandler` from `@genkit-ai/express`. Optionally expose the agent's +companion `getSnapshotDataAction` (for state lookup/restore) and +`abortAgentAction` (for background aborts). + +```ts +import { expressHandler } from '@genkit-ai/express'; +import express from 'express'; +import { weatherAgent } from './weather-agent.js'; + +const app = express(); +app.use(express.json()); + +// Main turn endpoint: +app.post('/api/weatherAgent', expressHandler(weatherAgent)); + +// Optional companions (needed for snapshot restore / branching / background): +app.post( + '/api/weatherAgent/getSnapshot', + expressHandler(weatherAgent.getSnapshotDataAction) +); +app.post( + '/api/weatherAgent/abort', + expressHandler(weatherAgent.abortAgentAction) +); + +app.listen(8080); +``` + +For serving multiple agents, CORS/streaming headers for browser clients, a static +web UI, and other host frameworks (Next.js, Firebase), see +[Deploying agents](agents-deployment.md). + +## Consume an agent from a client (`remoteAgent`) + +The browser/Node client lives in `genkit/beta/client`. `remoteAgent` returns a +typed HTTP client; `getSnapshotUrl`/`abortUrl` default to `${url}/getSnapshot` +and `${url}/abort`. + +```ts +import { remoteAgent, AgentError } from 'genkit/beta/client'; + +const weather = remoteAgent({ url: 'http://localhost:8080/api/weatherAgent' }); + +const chat = weather.chat(); +const turn = chat.sendStream('Weather in Tokyo?'); +for await (const chunk of turn.stream) { + process.stdout.write(chunk.text ?? ''); +} +const res = await turn.response; +console.log(res.snapshotId, chat.snapshotId, chat.state); + +// Multi-turn — the client carries state forward automatically: +await chat.send('What about Paris?'); + +// Errors surface as AgentError with an HTTP-ish status: +try { + await remoteAgent({ url: '/api/nope' }).chat().send('hi'); +} catch (err) { + if (err instanceof AgentError) console.log(err.status); +} +``` + +## Client-managed state (no server store) + +If the agent has **no `store`**, the server is fully stateless and the session +state blob (messages + custom + artifacts) is owned by the caller. The +`remoteAgent` client tracks it and round-trips it on every turn automatically — +no `SessionStore`, no snapshot ids to manage. + +```ts +// Server: no `store` → stateless. Client owns the state blob. +export const weatherAgentStateless = ai.defineAgent({ + name: 'weatherAgentStateless', + system: 'You are a helpful weather assistant. Use getWeather. Be concise.', + tools: [getWeather], +}); +``` + +```ts +// Client: reuse one `chat` and the state threads automatically. +import { remoteAgent, type AgentChat } from 'genkit/beta/client'; + +const agent = remoteAgent({ url: '/api/weatherAgentStateless' }); +const chat: AgentChat = agent.chat(); + +await chat.send('Weather in London?'); +await chat.send('Is it sunny in Tokyo?'); // remembers prior turns + +// The full state blob is available after each turn: +const res = await chat.send('And Paris?'); +console.log(JSON.stringify(res.raw.state, null, 2)); +``` + +If you call the stateless agent directly (e.g. from a flow) instead of via +`remoteAgent`, you must round-trip the state yourself: + +```ts +async function turn(state: unknown | undefined, text: string) { + // Resume from prior state, or start fresh on the first turn. + const chat = weatherAgentStateless.chat(state ? { state } : undefined); + const res = await chat.send(text); + // Return the updated state so the caller can pass it back next turn. + return { state: res.raw.state, text: res.text }; +} +``` + +Use client-managed state when you don't want to run server-side storage (e.g. +the client persists history itself). Use a [session store](agents-sessions.md) +when the server should own history, or when you need branching or background +execution. ([Interrupts](agents-human-in-the-loop.md) work either way.) diff --git a/plugins/genkit/.agents/skills/developing-genkit-js/references/best-practices.md b/plugins/genkit/.agents/skills/developing-genkit-js/references/best-practices.md new file mode 100644 index 00000000..dee23eb4 --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-js/references/best-practices.md @@ -0,0 +1,30 @@ +# Genkit Best Practices + +## Project Structure +- **Organized Layout**: Keep flows and tools in separate directories (e.g., `src/flows`, `src/tools`) to maintain a clean codebase. +- **Index Exports**: Use `index.ts` files to export flows and tools, making it easier to import them into your main configuration. + +## Model Selection (Google AI) +- **Gemini Models**: If using Google AI, ALWAYS use the latest alias (`gemini-flash-latest` or `gemini-pro-latest`). + - **Recommended**: `gemini-flash-latest` for general use, `gemini-pro-latest` for complex tasks. + +## Model Selection (Other Providers) +- **Consult Documentation**: For other providers (OpenAI, Anthropic, etc.), refer to the provider's official documentation for the latest recommended model versions. + +## Schema Definition +- **Use `z` from `genkit`**: Always import `z` from the `genkit` package to ensure compatibility. + ```ts + import { z } from "genkit"; + ``` +- **Descriptive Schemas**: Use `.describe()` on Zod fields. LLMs use these descriptions to understand how to populate the fields. + +## Flow & Tool Design +- **Modularize**: Keep flows and tools in separate files/modules and import them into your main Genkit configuration. +- **Single Responsibility**: Tools should do one thing well. Complex logic should be broken down. + +## Configuration +- **Environment Variables**: Store sensitive keys (like API keys) in environment variables or `.env` files. Do not hardcode them. + +## Development +- **Use Dev Mode**: Run your app with `genkit start -- ` to enable the Developer UI. +- It is recommended to configure a watcher to auto-reload your app (e.g. `node --watch` or `tsx --watch`) diff --git a/plugins/genkit/.agents/skills/developing-genkit-js/references/common-errors.md b/plugins/genkit/.agents/skills/developing-genkit-js/references/common-errors.md new file mode 100644 index 00000000..05b99ccc --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-js/references/common-errors.md @@ -0,0 +1,132 @@ +# Common Errors & Pitfalls + +## When Typecheck Fails + +**Before searching source code or docs**, check the sections below. Many type errors are caused by deprecated APIs or incorrect imports. + +## Genkit v1.x vs Pre-1.0 Migration + +Genkit v1.x introduced significant API changes. This section covers critical syntax updates. + +### Package Imports + +- **Correct (v1.x)**: Import core functionality (zod, genkit) from the main `genkit` package and plugins from their specific packages. + ```ts + import { z, genkit } from 'genkit'; + import { googleAI } from '@genkit-ai/google-genai'; + ``` + +- **Incorrect (Pre-1.0)**: Importing from `@genkit-ai/ai`, `@genkit-ai/core`, or `@genkit-ai/flow`. These packages are internal/deprecated for direct use. + ```ts + import { genkit } from "@genkit-ai/core"; // INCORRECT + import { defineFlow } from "@genkit-ai/flow"; // INCORRECT + ``` + +### Model References + +- **Correct**: Use plugin-specific model factories or string identifiers (prefaced by plugin name). + ```ts + // Using model factory (v1.x - Preferred) + await ai.generate({ model: googleAI.model('gemini-flash-latest'), ... }); + + // Using string identifier + await ai.generate({ model: 'googleai/gemini-flash-latest', ...}); + // Or + await ai.generate({ model: 'vertexai/gemini-flash-latest', ...}); + ``` +- **Incorrect**: Using imported model objects directly or string identifiers without plugin name. + ```ts + await ai.generate({ model: gemini15Pro, ... }); // INCORRECT (Pre-1.0) + await ai.generate({ model: 'gemini-flash-latest', ... }); // INCORRECT (No plugin prefix) + ``` + +### Model Selection (Gemini) + +- **Preferred**: Use latest model aliases (e.g. `gemini-flash-latest`) for best performance and features. + ```ts + model: googleAI.model('gemini-flash-latest') // PREFERRED + ``` +- **DEPRECATED**: Versioned legacy models (like `gemini-1.5-flash` or `gemini-2.5-flash`) should be updated to latest aliases. + ```ts + model: googleAI.model('gemini-1.5-flash') // DEPRECATED (Use gemini-flash-latest instead) + ``` + +### Response Access + +- **Correct (v1.x)**: Access properties directly. + ```ts + response.text; // CORRECT + response.output; // CORRECT + ``` +- **Incorrect (Pre-1.0)**: Calling as methods. + ```ts + response.text(); // INCORRECT + response.output(); // INCORRECT + ``` + +### Streaming Generation + +- **Correct (v1.x)**: Do NOT await `generateStream`. Iterate over `stream` directly. Await `response` property for final result. + ```ts + const {stream, response} = ai.generateStream(...); // NO await here + for await (const chunk of stream) { ... } // Iterate stream + const finalResponse = await response; // Await response property + ``` +- **Incorrect (Pre-1.0)**: Calling stream as a function or awaiting the generator incorrectly. + ```ts + for await (const chunk of stream()) { ... } // INCORRECT + await response(); // INCORRECT + ``` + +### Initialization + +- **Correct (v1.x)**: Instantiate `genkit`. + ```ts + const ai = genkit({ plugins: [...] }); + ``` +- **Incorrect (Pre-1.0)**: Global configuration. + ```ts + configureGenkit({ plugins: [...] }); // INCORRECT + ``` + +### Flow Definitions + +- **Correct (v1.x)**: Define flows on the `ai` instance. + ```ts + ai.defineFlow({...}, (input) => {...}); + ``` +- **Incorrect (Pre-1.0)**: Importing `defineFlow` globally. + ```ts + import { defineFlow } from "@genkit-ai/flow"; // INCORRECT + +You should never import `@genkit-ai/flow`, `@genkit-ai/ai` or `@genkit-ai/core` packages directly. + +## Zod & Schema Errors + +- **Import Source**: ALWAYS use `import { z } from "genkit"`. + - Using `zod` directly from `zod` package may cause instance mismatches or compatibility issues. +- **Supported Types**: Stick to basic types: scalar (`string`, `number`, `boolean`), `object`, and `array`. + - Avoid complex Zod features unless strictly necessary and verified. +- **Descriptions**: Always use `.describe('...')` for fields in output schemas to guide the LLM. + +## Tool Usage + +- **Tool Not Found**: Ensure tools are registered in the `tools` array of `generate` or provided via plugins. +- **MCP Tools**: Use the `ServerName:tool_name` format when referencing MCP tools. + +## Multimodal & Image Generation + +- **Missing responseModalities**: When using image generation models (like `gemini-2.5-flash-image`), you **MUST** specify the response modalities in the config. + ```ts + config: { + responseModalities: ["TEXT", "IMAGE"] + } + ``` + Failure to do so will result in errors or incorrect output format. + +## Audio & Speech Generation + +- **Raw PCM Data vs MP3**: Some providers (e.g., Google GenAI) return raw PCM data, while others (e.g., OpenAI) return MP3. + - **DO NOT assume MP3 format.** + - **DO NOT embed raw PCM in HTML audio tags.** + - **Action**: Run `genkit docs:search "speech audio"` to find provider-specific conversion steps (e.g., PCM to WAV). diff --git a/plugins/genkit/.agents/skills/developing-genkit-js/references/docs-and-cli.md b/plugins/genkit/.agents/skills/developing-genkit-js/references/docs-and-cli.md new file mode 100644 index 00000000..4fe4467d --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-js/references/docs-and-cli.md @@ -0,0 +1,84 @@ +# Genkit Documentation & CLI + +This reference lists common tasks and workflows using the `genkit` CLI. For authoritative command details, always run `genkit --help` or `genkit --help`. + +## Prerequisites: + +Ensure that the CLI is on `genkit-cli` version >= 1.29.0. If not, or if an older version (1.x < 1.29.0) is present, update the Genkit CLI version. Alternatively, to run commands with a specific version or without global installation, prefix them with `npx -y genkit-cli@^1.29.0`. + +## Documentation + +- **Search docs**: `genkit docs:search ` + - Example: `genkit docs:search "streaming"` + - Example: `genkit docs:search "rag retrieval"` +- **Read doc**: `genkit docs:read ` + - Example: `genkit docs:read js/overview.md` +- **List docs**: `genkit docs:list` + +## Development Workflow (recommended) + +`genkit start` unintrusively wraps any Node.js program that uses the Genkit library, running it unchanged while capturing traces from every Genkit action so you can prove tools were actually called and inspect model I/O from the terminal, even for headless checks. It forwards stdio, so interactive CLI tools that rely on stdin/stdout work without issues. Running your app directly (`node`/`tsx`/`npm start`) skips trace capture, so you're debugging blind. + +**Primary pattern (default):** prefix `genkit start --` to your normal run command. This collects telemetry from any Genkit code your program runs, whether triggered from the dev UI, your own web server/web UI, or a plain script: + +- **Node.js (TypeScript)**: + ```bash + genkit start -- npx tsx --watch src/index.ts + ``` +- **Next.js**: + ```bash + genkit start -- npx next dev + ``` +- **Without the Dev UI** (still a persistent server): + ```bash + genkit start --noui -- npx tsx src/index.ts + ``` + +`genkit start` runs until you stop it with Ctrl+C. That is expected and correct for the common cases: a server your web/mobile app calls, or an interactive CLI you exit yourself. `--noui` only drops the Dev UI; it is **not** a one-shot command and will not exit on its own. Do **not** use `genkit start` as a blocking step in automated/non-interactive contexts; use `flow:run` (below) for that. + +For non-interactive/agent/CI use, add the global `--non-interactive` flag before `--` so the CLI uses defaults and never blocks on a prompt (e.g. the first-run analytics notice), e.g. `genkit flow:run myFlow '' --non-interactive -- npx tsx src/index.ts`. + +## Flow Execution (secondary) + +- **Run a flow**: `genkit flow:run '' -- ` + - Invokes a specific flow by name from the CLI. Append your run command after `--` to spin up the runtime for this run (the command runs as-is to register your flows): it runs once, prints a `Trace ID`, then exits, so it's the right choice for a quick, non-interactive check that must self-terminate (unlike `genkit start`). Note: `flow:run` runs **flows** (`ai.defineFlow`), not agents; you can't `flow:run` an agent (`ai.defineAgent`) directly. To verify an agent, wrap one turn in a throwaway flow and run that. Traces for the run can be inspected with the tracing commands below. + - **Always pass input JSON explicitly.** `flow:run` sends `undefined` when the input is omitted and does **not** fall back to a schema `.default()`, so a flow with a defaulted input will fail validation unless you pass the value. + - **Simple Input**: + ```bash + genkit flow:run tellJoke '"chicken"' -- npx tsx src/index.ts + ``` + - **Object Input**: + ```bash + genkit flow:run generateStory '{"subject": "robot", "genre": "sci-fi"}' -- npx tsx src/index.ts + ``` + +## Evaluation + + +- **Evaluate a flow**: `genkit eval:flow [data] -- ` + - Runs a flow and evaluates the output against configured evaluators. As with `flow:run`, append your run command after `--` to spin up the runtime for the run. + - **Example (Single Input)**: + ```bash + genkit eval:flow answerQuestion '[{"testCaseId": "1", "input": {"question": "What is Genkit?"}}]' -- npx tsx src/index.ts + ``` + - **Example (Batch Input)**: + ```bash + genkit eval:flow answerQuestion --input inputs.json -- npx tsx src/index.ts + ``` + +- **Run Evaluation**: `genkit eval:run ` + - Evaluates a dataset against configured evaluators. + - **Example**: + ```bash + genkit eval:run dataset.json --output results.json + ``` + +## Tracing + +- **Get a trace**: `genkit trace:get ` + - Retrieves detailed information for a specific trace by its ID. This is particularly useful for debugging failed model calls, inspecting tool execution, or analyzing the exact inputs and outputs of a specific step in your flow. + - **Machine-readable output**: add `--format json` (e.g. `genkit trace:get --format json`) to get clean JSON you can pipe into `jq` or other parsers. +- **List traces**: `genkit trace:list [options]` + - Lists recent traces. Use this to find trace IDs from recent executions. +- **Piping to parsers**: the **default** trace output is human-oriented (extra banner/log lines, possible truncation on large traces), so don't pipe that form directly into `jq`. Use `--format json` for clean JSON, or the Dev UI trace viewer (`genkit start`) for complex traces. + diff --git a/plugins/genkit/.agents/skills/developing-genkit-js/references/dotprompt.md b/plugins/genkit/.agents/skills/developing-genkit-js/references/dotprompt.md new file mode 100644 index 00000000..1c728ec4 --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-js/references/dotprompt.md @@ -0,0 +1,207 @@ +# Dotprompt (.prompt files) — Genkit JS + +## What it is + +`.prompt` files combine YAML frontmatter (model, config, schemas, tools, +middleware) with a Handlebars template. They keep prompt logic out of your +TypeScript code and make variants and iteration easy. + +## Where files live + +By default Genkit loads `.prompt` files from `./prompts`. Configure with the +`promptDir` option (set to `null` to disable auto-loading): + +```ts +import { genkit } from 'genkit'; +import { googleAI } from '@genkit-ai/google-genai'; + +const ai = genkit({ + plugins: [googleAI()], + promptDir: './prompts', // default +}); +``` + +## File format + +`prompts/recipe.prompt`: +``` +--- +model: googleai/gemini-pro-latest +input: + schema: + food: string + ingredients?(array): string # ? = optional +output: + schema: Recipe # references a schema registered via ai.defineSchema +--- +You are a chef famous for creative recipes. + +Generate a recipe for {{food}}. + +{{#if ingredients}} +Make sure to include the following ingredients: +{{list ingredients}} +{{/if}} +``` + +Schema fields use Picoschema (the compact form above) or you can reference a +named schema registered with `ai.defineSchema`. + +## Loading and calling a prompt + +`ai.prompt(name, { variant? })` returns an `ExecutablePrompt`. The returned +object is **callable** and also has `.stream()`, `.render()`, and `.asTool()`. + +```ts +// Non-streaming: call it like a function with the input object. +const recipePrompt = ai.prompt('recipe'); +const { output } = await recipePrompt({ food: 'banana bread' }); + +// With a typed output schema +const RecipeSchema = ai.defineSchema('Recipe', z.object({ + title: z.string(), + steps: z.array(z.string()), +})); +const result = await ai.prompt('recipe')({ food: 'banana bread' }); +result.output; // typed as Recipe +``` + +### Streaming + +```ts +const storyPrompt = ai.prompt('story'); +const { response, stream } = storyPrompt.stream({ subject: 'a robot' }); +for await (const chunk of stream) { + console.log(chunk.text); +} +const final = await response; +``` + +### Render without generating + +Useful for building `ai.generate` calls or LLM-judge evals: + +```ts +const rendered = await ai.prompt('recipe').render({ food: 'banana bread' }); +// rendered is a GenerateOptions object (messages, model, config, ...) +``` + +## Registering named schemas + +Reference a schema by name in `.prompt` frontmatter (`input.schema` / +`output.schema`) after registering it: + +```ts +import { z } from 'genkit'; + +const RecipeSchema = ai.defineSchema( + 'Recipe', + z.object({ + title: z.string().describe('recipe title'), + ingredients: z.array(z.object({ name: z.string(), quantity: z.string() })), + steps: z.array(z.string()).describe('the steps required'), + }) +); +``` + +## Variants + +Name the file `..prompt` — e.g. `recipe.robot.prompt`. Call with +the `variant` option: + +```ts +await ai.prompt('recipe', { variant: 'robot' })({ food: 'oil cake' }); +``` + +## Partials + +Reusable template fragments. Name a partial file `_.prompt` and include it +with `{{>name param=value}}`. + +`prompts/_style.prompt`: +``` +{{ role "system" }} +You should speak as if you are a {{#if personality}}{{personality}}{{else}}pirate{{/if}}. +{{role "user"}} +``` + +`prompts/story.prompt`: +``` +--- +model: googleai/gemini-pro-latest +input: + schema: + subject: string + personality?: string +--- +{{>style personality=personality}} + +Tell me a story about {{subject}}. +``` + +## Helpers + +Register a function callable inside templates: + +```ts +ai.defineHelper('list', (data: any) => + Array.isArray(data) ? data.map((item) => `- ${item}`).join('\n') : '' +); +``` + +Then use `{{list ingredients}}` in the template. + +## Tools, tool-loop control, and middleware + +`.prompt` frontmatter can configure tool calling and attach middleware, so an +agent-style prompt is fully described in the file: + +``` +--- +model: googleai/gemini-flash-latest +input: + schema: + tone: string +tools: + - getAttractions + - getFlightInfo +toolChoice: auto # auto | required | none +maxTurns: 20 # max tool-call loop iterations +returnToolRequests: false # return tool requests instead of running them +use: + - name: retry # bare string also works: `- retry` + config: + maxRetries: 4 +--- +{{role "system"}} +You are a friendly trip planning assistant. Help users plan trips by suggesting +attractions and looking up flight information. Keep your tone {{tone}}. + +{{history}} +``` + +- `tools`: list of registered tool names. +- `toolChoice`, `maxTurns`, `returnToolRequests`: same semantics as the + equivalent `ai.generate` options. +- `use`: list of middleware refs. Each entry is a bare string (middleware name) + or a map with `name` and optional `config`. Names resolve against middleware + registered on the Genkit instance — register the middleware plugin so the + name is available: + +```ts +import { retry } from '@genkit-ai/middleware'; + +const ai = genkit({ + plugins: [googleAI(), retry.plugin()], + promptDir: './prompts', +}); +``` + +See [Using middleware](middleware.md) for the full list of built-in middleware. + +## Relationship to agents + +Agents (`defineAgent`) share the same dotprompt frontmatter — `system`/`prompt`, +`tools`, `maxTurns`, `returnToolRequests`, and `use`. A `.prompt` file with +`{{history}}` and tools can back an agent directly. See [Agents](agents.md). + diff --git a/plugins/genkit/.agents/skills/developing-genkit-js/references/examples.md b/plugins/genkit/.agents/skills/developing-genkit-js/references/examples.md new file mode 100644 index 00000000..e6383c74 --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-js/references/examples.md @@ -0,0 +1,157 @@ +# Genkit Examples + +This reference contains minimal, reproducible examples (MREs) for common Genkit patterns. + +> **Disclaimer**: These examples use **Google AI** models (`googleAI`, `gemini-*`) for demonstration. The patterns apply to **any provider**. To use a different provider: +> 1. Search the docs for the correct plugin: `genkit docs:search "plugins"`. +> 2. Install and configure the plugin. +> 3. Swap the model reference in the code. + +## Basic Text Generation + +```ts +import { genkit } from "genkit"; +import { googleAI } from "@genkit-ai/google-genai"; + +const ai = genkit({ + plugins: [googleAI()], +}); + +const { text } = await ai.generate({ + model: googleAI.model('gemini-flash-latest'), + prompt: 'Tell me a story in a pirate accent', +}); +``` + +## Structured Output + +```ts +import { z } from 'genkit'; + +const JokeSchema = z.object({ + setup: z.string().describe('The setup of the joke'), + punchline: z.string().describe('The punchline'), +}); + +const response = await ai.generate({ + model: googleAI.model('gemini-flash-latest'), + prompt: 'Tell me a joke about developers.', + output: { schema: JokeSchema }, +}); + +// response.output is strongly typed +const joke = response.output; +if (joke) { + console.log(`${joke.setup} ... ${joke.punchline}`); +} +``` + +## Streaming + +```ts +const { stream, response } = ai.generateStream({ + model: googleAI.model('gemini-flash-latest'), + prompt: 'Tell a long story about a developer using Genkit.', +}); + +for await (const chunk of stream) { + console.log(chunk.text); +} + +// Await the final response +const finalResponse = await response; +console.log('Complete:', finalResponse.text); +``` + +## Advanced Configuration + +### Thinking Mode + +Enable "thinking" process for complex reasoning tasks. + +```ts +const response = await ai.generate({ + model: googleAI.model('gemini-pro-latest'), + prompt: 'what is heavier, one kilo of steel or one kilo of feathers', + config: { + thinkingConfig: { + thinkingLevel: 'HIGH', // or 'LOW' + includeThoughts: true, // Returns thought process in response + }, + }, +}); +``` + +### Google Search Grounding + +Enable models to access current information via Google Search. + +```ts +const response = await ai.generate({ + model: googleAI.model('gemini-flash-latest'), + prompt: 'What are the top tech news stories this week?', + config: { + googleSearchRetrieval: true, + }, +}); + +// Access grounding metadata (sources) +const groundingMetadata = (response.custom as any)?.candidates?.[0]?.groundingMetadata; +if (groundingMetadata) { + console.log('Sources:', groundingMetadata.groundingChunks); +} +``` + +## Multimodal Generation + +### Image Generation / Editing + +**Critical**: You MUST set `responseModalities: ['TEXT', 'IMAGE']` when using image generation models. + +```ts +// Generate an image +const { media } = await ai.generate({ + model: googleAI.model('gemini-2.5-flash-image'), + config: { responseModalities: ['TEXT', 'IMAGE'] }, + prompt: "generate a picture of a unicorn wearing a space suit on the moon", +}); +// media.url contains the data URI +``` + +```ts +// Edit an image +const { media } = await ai.generate({ + model: googleAI.model('gemini-2.5-flash-image'), + config: { responseModalities: ['TEXT', 'IMAGE'] }, + prompt: [ + { text: "change the person's outfit to a banana costume" }, + { media: { url: "https://example.com/photo.jpg" } }, + ], +}); +``` + +### Speech Generation (TTS) + +Generate audio from text. + +```ts +import { writeFile } from 'node:fs/promises'; + +const { media } = await ai.generate({ + model: googleAI.model('gemini-2.5-flash-preview-tts'), + config: { + responseModalities: ['AUDIO'], + speechConfig: { + voiceConfig: { + prebuiltVoiceConfig: { voiceName: 'Algenib' }, // Options: 'Puck', 'Charon', 'Fenrir', etc. + }, + }, + }, + prompt: 'Genkit is an amazing library', +}); + +// The response contains raw PCM data in media.url (base64 encoded). +// CAUTION: This is NOT an MP3/WAV file. It requires conversion (e.g., PCM to WAV). +// DO NOT GUESS. Run `genkit docs:search "speech audio"` to find the correct +// conversion code for your provider. +``` diff --git a/plugins/genkit/.agents/skills/developing-genkit-js/references/middleware-custom.md b/plugins/genkit/.agents/skills/developing-genkit-js/references/middleware-custom.md new file mode 100644 index 00000000..210b4033 --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-js/references/middleware-custom.md @@ -0,0 +1,96 @@ +# Building Custom Middleware + +Write reusable, named middleware with `generateMiddleware`. It returns a factory +you call in the `use: [...]` array (see [using middleware](middleware.md)) — +exactly how the `@genkit-ai/middleware` package's middleware are built. The +factory can take optional Zod-validated config and gets access to the `ai` +instance. + +```ts +// timing.ts +import { generateMiddleware, z } from 'genkit'; + +const OptionsSchema = z.object({ label: z.string().optional() }); + +export const timing = generateMiddleware( + { + name: 'timing', + description: 'Logs how long the model call takes.', + configSchema: OptionsSchema, + }, + ({ config, ai }) => { + // Runs once per generate() call. Return any of the hooks below. + return { + model: async (req, ctx, next) => { + const start = Date.now(); + const res = await next(req, ctx); + console.log(`[${config?.label ?? 'timing'}] ${Date.now() - start}ms`); + return res; + }, + }; + } +); +``` + +## Register it as a plugin + +Register custom middleware with Genkit via its `.plugin()` method. This is the +recommended setup — without it the middleware still works in `use: [...]`, but it +is **not visible to the Genkit Dev UI**. + +```ts +import { genkit } from 'genkit'; +import { googleAI } from '@genkit-ai/google-genai'; +import { timing } from './timing.js'; + +export const ai = genkit({ + plugins: [googleAI(), timing.plugin()], +}); + +// Then use the factory in `use: [...]`: +await ai.generate({ + model: googleAI.model('gemini-flash-latest'), + prompt: 'Hi', + use: [timing({ label: 'gen' })], +}); +``` + +## Available hooks + +The instantiate callback returns a `GenerateMiddlewareDef` with any subset of: + +- `generate(envelope, ctx, next)` — wrap the **whole generate action**. The + `envelope` carries `{ request, currentTurn, messageIndex }`. Use it to inject + request params, post-process the response, or catch errors across the tool loop. +- `model(req, ctx, next)` — wrap the **underlying model call** (caching, retry, + request/response rewriting). `req` is a `GenerateRequest`. +- `tool(req, ctx, next)` — wrap **individual tool calls** (validate inputs, + cache or override tool output). `req` is a `ToolRequestPart`; return a + `ToolResponsePart | undefined`. +- `tools: ToolAction[]` — **statically inject tools** whenever the middleware is + active (how `artifacts()` / `filesystem()` add their tools). + +```ts +generateMiddleware({ name: 'example' }, ({ ai }) => ({ + generate: async (envelope, ctx, next) => next(envelope, ctx), + model: async (req, ctx, next) => next(req, ctx), + tool: async (req, ctx, next) => next(req, ctx), + tools: [ + /* ToolAction[] */ + ], +})); +``` + +Call `next(...)` to continue the chain (optionally with a modified +request/envelope) and transform the result before returning it. + +## Choosing a hook + +- Transform prompt/messages or the final result across the whole turn → `generate`. +- Cache/retry/rewrite a single model round-trip → `model`. +- Gate or memoize tool execution → `tool`. +- Make extra capabilities available to the model → `tools`. + +> Reminder: register custom middleware via `.plugin()` (see above). It works in +> `use: [...]` without registering, but unregistered middleware is not visible to +> the Genkit Dev UI. diff --git a/plugins/genkit/.agents/skills/developing-genkit-js/references/middleware.md b/plugins/genkit/.agents/skills/developing-genkit-js/references/middleware.md new file mode 100644 index 00000000..2812e9f0 --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-js/references/middleware.md @@ -0,0 +1,170 @@ +# Using Middleware + +Middleware wraps generation to add cross-cutting behavior — retries, fallback, +extra tools, request/response transforms, etc. You attach middleware with the +`use: [...]` array, which is supported on `ai.generate` / `ai.generateStream`, +executable prompts (`definePrompt`), and agents (`defineAgent`). + +```ts +import { retry } from '@genkit-ai/middleware'; + +const res = await ai.generate({ + model: googleAI.model('gemini-flash-latest'), + prompt: 'Say hello', + use: [retry({ maxRetries: 2 })], +}); +``` + +The same array works on prompts and agents: + +```ts +const myPrompt = ai.definePrompt({ name: 'p', prompt: '...', use: [retry()] }); + +const myAgent = ai.defineAgent({ name: 'a', system: '...', use: [retry()] }); +``` + +Middleware is a configurable factory like `retry()` / `artifacts()` that returns +a reference for `use: [...]`. The `@genkit-ai/middleware` package ships a set of +ready-made ones (covered below); to write your own see +[building custom middleware](middleware-custom.md). + +## Register middleware as a plugin + +Register each middleware with Genkit via its `.plugin()` method. This is the +recommended setup — middleware still works in `use: [...]` without registering, +but **unregistered middleware is not visible to the Genkit Dev UI**. + +```ts +import { genkit } from 'genkit'; +import { googleAI } from '@genkit-ai/google-genai'; +import { retry, artifacts } from '@genkit-ai/middleware'; + +export const ai = genkit({ + plugins: [googleAI(), retry.plugin(), artifacts.plugin()], +}); + +// Then reference the factory in `use: [...]`: +await ai.generate({ + model: googleAI.model('gemini-flash-latest'), + prompt: 'Say hello', + use: [retry({ maxRetries: 2 })], +}); +``` + +## The `@genkit-ai/middleware` package + +```bash +npm i @genkit-ai/middleware +``` + +Exports seven middleware factories. Register each via `.plugin()` (above), then +call `name(options)` in `use: [...]`. + +### `retry(options?)` + +Retries on transient errors with exponential backoff. + +```ts +import { retry } from '@genkit-ai/middleware'; + +retry({ + maxRetries: 3, // default 3 + initialDelayMs: 1000, // default 1000 + maxDelayMs: 60000, // default 60000 + backoffFactor: 2, // default 2 + noJitter: false, // default false + // statuses defaults to UNAVAILABLE, DEADLINE_EXCEEDED, RESOURCE_EXHAUSTED, + // ABORTED, INTERNAL +}); +``` + +### `fallback(options)` + +Falls back to other models when the primary fails. + +```ts +import { fallback } from '@genkit-ai/middleware'; +import { googleAI } from '@genkit-ai/google-genai'; + +fallback({ + models: [googleAI.model('gemini-flash-latest')], // tried in order + // statuses defaults to UNAVAILABLE, DEADLINE_EXCEEDED, RESOURCE_EXHAUSTED, + // ABORTED, INTERNAL, NOT_FOUND, UNIMPLEMENTED + isolateConfig: false, // default false — fallback inherits the request config +}); +``` + +### `artifacts(options?)` + +Adds `write_artifact` / `read_artifact` tools for session artifacts. + +```ts +artifacts({ readonly: false }); // readonly true → read_artifact only +``` + +See [working with artifacts](agents-artifacts.md). + +### `agents(options)` + +Sub-agent delegation — injects a `delegate_to_` tool per sub-agent. + +```ts +agents({ agents: ['researcher', 'coder'], maxDelegations: 5 }); +``` + +See [multi-agent orchestration](agents-multi-agent.md). + +### `filesystem(options)` + +Grants the model `list_files`, `read_file`, `write_file`, and +`search_and_replace` tools, sandboxed to a root directory. + +```ts +filesystem({ + rootDirectory: './workspace', // required; all access is restricted to it + allowWriteAccess: false, // default false (read-only) + toolNamePrefix: '', // optional prefix for the injected tool names +}); +``` + +### `skills(options?)` + +Scans directories for skill files (frontmatter `name`/`description`), injects a +listing into the system prompt, and provides a `use_skill` tool. + +```ts +skills({ skillPaths: ['skills'] }); // default ['skills'] +``` + +### `toolApproval(options)` + +Restricts tool execution to an approved list; throws a `ToolInterruptError` for +anything else (resumable via [interrupts](agents-human-in-the-loop.md)). + +```ts +toolApproval({ approved: ['getWeather', 'search'] }); +``` + +## Built-in model middleware (in core) + +Some middleware ships in the `genkit` core (no extra package) — import from +`genkit/model/middleware`: + +- `downloadRequestMedia({ maxBytes? })` — fetch URL media and inline it. +- `validateSupport({ name, ... })` — assert a model supports requested features. +- `simulateSystemPrompt({ preface? })` — emulate a system prompt for models + without native support. +- `augmentWithContext(options?)` — inject retrieved context documents. +- `simulateConstrainedGeneration(options?)` — emulate constrained/JSON output. + +```ts +import { simulateConstrainedGeneration } from 'genkit/model/middleware'; + +await ai.generate({ + model: someModel, + prompt: '...', + use: [simulateConstrainedGeneration()], +}); +``` + +To write your own, see [building custom middleware](middleware-custom.md). diff --git a/plugins/genkit/.agents/skills/developing-genkit-js/references/setup.md b/plugins/genkit/.agents/skills/developing-genkit-js/references/setup.md new file mode 100644 index 00000000..0c8cea8e --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-js/references/setup.md @@ -0,0 +1,47 @@ +# Genkit JS Setup + +Follow these instructions to set up Genkit in the current codebase. These instructions are general-purpose and lack specific codebase knowledge, so use your best judgment when following them. + +0. Tell the user "I'm going to check out your workspace and set you up to use Genkit for GenAI workflows." +1. If the current workspace is empty or is a starter template, your goal will be to create a simple image generation flow that allows someone to generate an image based on a prompt and selectable style. If the current workspace is not empty, you will create a simple example flow to help get the user started. +2. Check to see if any Genkit provider plugin (such as `@genkit-ai/google-genai` or `@genkit-ai/oai-compat` or others, may start with `genkitx-*`) is installed. + - If not, ask the user which provider they want to use. + - **For non-Google providers**: Use `genkit docs:search "plugins"` to find the correct package and installation instructions. + - If they have no preference, default to `@genkit-ai/google-genai` for a quick start. + - If this is a Next.js app, install `@genkit-ai/next` as well. +3. Search the codebase for the exact string `genkit(` (remember to escape regexes properly) which indicates that the user has already set up Genkit in the codebase. If found, no need to set it up again; tell the user "Genkit is already configured in this app." and exit this workflow. +4. Create an `ai` directory in the primary source directory of the project (this may be e.g. `src` but is project-dependent). Adapt this path if your project uses a different structure. +5. Create `{sourceDir}/ai/genkit.ts` and populate it using the example below. DO NOT add a `next` plugin to the file, ONLY add a model provider plugin to the plugins array: + +```ts +import { genkit, z } from 'genkit'; +// Import your chosen provider plugin here. Example: +import { googleAI } from '@genkit-ai/google-genai'; + +export const ai = genkit({ + plugins: [ + googleAI(), // Add your provider plugin here + ], + model: googleAI.model('gemini-flash-latest'), // Set your provider's model here +}); + +export { z }; +``` + +6. Create `{sourceDir}/ai/tools` and `{sourceDir}/ai/flows` directories, but leave them empty for now. +7. Create `{sourceDir}/ai/index.ts` and populate it with the following (change the import to match import aliases in `tsconfig.json` as needed): + +```ts +import './genkit.js'; +// import each created flow, tool, etc. here for use in the Genkit Dev UI +``` + +8. Add the `genkit-cli` to the project's `devDependencies` (e.g. `npm install -D genkit-cli`, or the pnpm/yarn/bun equivalent) so the CLI version is pinned per-project and works in CI, then add a `genkit:dev` script to `package.json` that runs `genkit start -- npx tsx --watch {sourceDir}/ai/index.ts`. This is the recommended way to run the app during development: `genkit start` wraps your program and captures a trace of every Genkit action (prompts, model I/O, tool calls) while serving the Dev UI at http://localhost:4000. Running the app directly (`tsx`/`node`/`npm start`) skips trace capture, so you're debugging blind. DO NOT try to run the script now. +9. Tell the user "Genkit is now configured and ready for use." Let them know they can start developing with `npm run genkit:dev` (or the pnpm/yarn/bun equivalent), which stays running until stopped with Ctrl+C and serves the Dev UI at http://localhost:4000. Also remind them to set appropriate env variables (e.g. `GEMINI_API_KEY` for Google providers). Wait for the user to prompt further before creating any specific flows. + +## Next Steps & Troubleshooting + +- **Documentation**: Use the [CLI](docs-and-cli.md) to access documentation (e.g., `genkit docs:search`). +- **Building Flows**: See [examples.md](examples.md) for patterns on creating flows, adding tools, and advanced configuration. +- **Running & Verifying**: `npm run genkit:dev` is the default dev loop (persistent, serves the Dev UI). For a quick, non-interactive check that exits on its own, run a single flow with `genkit flow:run '' -- npx tsx {sourceDir}/ai/index.ts` and inspect it with `genkit trace:get `. See [docs-and-cli.md](docs-and-cli.md). +- **Troubleshooting**: If you encounter issues during setup or initialization, check [common-errors.md](common-errors.md) for solutions. diff --git a/plugins/genkit/.agents/skills/developing-genkit-python/SKILL.md b/plugins/genkit/.agents/skills/developing-genkit-python/SKILL.md new file mode 100644 index 00000000..e84864a0 --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-python/SKILL.md @@ -0,0 +1,123 @@ +--- +name: developing-genkit-python +description: Develop AI-powered applications using Genkit in Python. Use when the user asks about Genkit, AI agents, flows, or tools in Python, or when encountering Genkit errors, import issues, or API problems. +metadata: + category: AiAndMachineLearning +--- + +# Genkit Python + +Build AI features in Python — generate, stream, tools, flows, and multi-turn +agents — with one SDK. + +## Prerequisites + +- Python **3.10+** and **`uv`** ([install](https://docs.astral.sh/uv/getting-started/installation/)) +- Genkit CLI: `npm install -g genkit-cli` if `genkit --version` is missing + +New app? [Setup](references/setup.md). Patterns? [Examples](references/examples.md). + +## Hello World + +```python +from genkit import Genkit +from genkit_google_genai import GoogleAI + +ai = Genkit( + plugins=[GoogleAI()], + model='googleai/gemini-flash-latest', +) + +async def main(): + response = await ai.generate(prompt='Tell me a joke about Python.') + print(response.text) + +if __name__ == '__main__': + ai.run_main(main()) +``` + +## Agents (Beta) + +Multi-turn chats with history, typed state, human approval, branching, and +background work. Start here: [Agents](references/agents.md). + +```python +chat = agent.chat() +res = await chat.send('Hello') # AgentResponse +turn = chat.send_stream('Hello') # AgentTurn — .stream / .response +``` + +More: [sessions](references/agents-sessions.md) · +[HITL](references/agents-human-in-the-loop.md) · +[branching](references/agents-branching.md) · +[background](references/agents-background.md) · +[state](references/agents-state.md) · +[artifacts](references/agents-artifacts.md) · +[custom](references/agents-custom.md) · +[HTTP](references/agents-http.md) + +## Imports + +- Google AI: `from genkit_google_genai import GoogleAI` +- Agents: `from genkit.agent import InMemorySessionStore, ...` +- Middleware: `from genkit_middleware import Middleware, ToolApproval, ...` +- FastAPI: `from genkit_fastapi import serve_agent, serve_flow` +- Evals: `from genkit_evaluators import register_genkit_evaluators` + +## Workflow + +1. **Agent or flow?** If the task is conversational, multi-turn, or described as + "an agent", "assistant", or "chatbot", build it with `ai.define_agent` (see + [Agents](references/agents.md)) rather than hand-rolling a `generate` + tools + loop inside a flow. Reach for a plain flow only for single-shot, stateless + generation. +2. Set **`GEMINI_API_KEY`**. Use prefixed model ids (`googleai/gemini-flash-latest`). +3. Enter via **`ai.run_main(main())`** for Genkit apps (especially under + `genkit start`). See [Common Errors](references/common-errors.md). +4. Run with [Dev Workflow](references/dev-workflow.md) (`genkit start` + Dev UI). +5. Verify with traces, not a blind run. Running the app directly (`uv run`) + does **not** capture dev traces. See [Genkit CLI](#genkit-cli-recommended) + for how to run your app and capture traces. +6. Stuck? [Common Errors](references/common-errors.md) first. + +## Genkit CLI (recommended) + +`genkit start` unintrusively wraps any Python program that uses the Genkit library, running it unchanged while capturing traces from every Genkit action so you can prove tools were actually called and inspect model I/O from the terminal, even for headless checks. It forwards stdio, so interactive CLI tools that rely on stdin/stdout work without issues. Running the app directly (`uv run`) skips trace capture, so you're debugging blind. + +**Primary pattern (default):** prefix `genkit start --` to your normal run command. This collects telemetry from any Genkit code your program runs, whether triggered from the dev UI, your own web server/web UI, or a plain script: +```bash +genkit start -- uv run src/main.py +genkit start --noui -- uv run src/main.py # same, without the Dev UI (still a persistent server) +``` +`genkit start` runs until you stop it with Ctrl+C. That is expected and correct for the common cases: a server your web/mobile app calls, or an interactive CLI you exit yourself. `--noui` only drops the Dev UI; it is **not** a one-shot command and will not exit on its own. Do **not** use `genkit start` as a blocking step in automated/non-interactive contexts; use `flow:run` (below) for that. + +**Non-interactive use (agents/CI):** add the global `--non-interactive` flag before `--` so the CLI uses defaults and never blocks on a prompt (e.g. the first-run analytics notice): `genkit start --non-interactive -- uv run src/main.py` (works with `flow:run` too). + +**Run a flow (`flow:run`):** invoke a specific flow by name from the CLI. Append your run command after `--` to spin up the runtime just for this run (the command runs as-is to register your flows): +```bash +genkit flow:run myFlow '{"data": "input"}' -- uv run src/main.py +``` +This is **self-terminating**: it runs the flow once, prints a `Trace ID`, then exits, so it's the right choice for a quick, non-interactive check (unlike `genkit start`). Note: `flow:run` runs **flows** (`@ai.flow()`), not agents; you can't `flow:run` an agent (`ai.define_agent`) directly. To exercise an agent from the CLI, wrap one turn in a throwaway flow and run that (see [Agents](references/agents.md)). + +**Debugging with traces:** the fastest way to see prompts, model inputs/outputs, tool calls, latencies, and errors. Inspect from the terminal after any run under `genkit start`: +```bash +genkit trace:list # find recent trace IDs +genkit trace:get # full trace details (inputs, outputs, tool calls, errors) +genkit trace:get --format json # machine-readable JSON, safe to pipe into jq or other parsers +``` + +For machine-readable output, pass `--format json` to get clean JSON you can pipe into `jq` or other parsers. The **default** output is human-oriented (banner/log lines, possible truncation on large traces), so don't pipe that form directly; use `--format json`, grep, or the Dev UI trace viewer. + + +See [Dev Workflow](references/dev-workflow.md) for the full checklist and Dev UI walkthrough. + +## References + +- [Examples](references/examples.md): Structured output, streaming, flows, tools, embeddings. +- [Setup](references/setup.md): New project bootstrap and plugins. +- [Common Errors](references/common-errors.md): Read first when something breaks. +- [FastAPI](references/fastapi.md): HTTP, `genkit_fastapi_handler`, parallel flows. +- [Dotprompt](references/dotprompt.md): `.prompt` files and helpers. +- [Evals](references/evals.md): Evaluators and datasets. +- [Dev Workflow](references/dev-workflow.md): `genkit start`, Dev UI, checklist. +- [Agents (Beta)](references/agents.md): Multi-turn API. diff --git a/plugins/genkit/.agents/skills/developing-genkit-python/references/agents-artifacts.md b/plugins/genkit/.agents/skills/developing-genkit-python/references/agents-artifacts.md new file mode 100644 index 00000000..3b5e66fa --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-python/references/agents-artifacts.md @@ -0,0 +1,64 @@ +# Artifacts (Beta) + +> See [agents.md](agents.md). + +Named deliverables on the session — reports, files, code — available as +`chat.artifacts` and `res.artifacts`. + +## Middleware + +`Artifacts()` gives the model `write_artifact` and `read_artifact`. Writing the +same name replaces the previous version. + +Store-backed agents cannot seed with `chat(artifacts=...)`. Have the model +write on the first turn, or call `add_artifacts` from a custom agent. + +```python +from genkit_google_genai import GoogleAI +from genkit_middleware import Artifacts, Middleware + +from genkit import Genkit +from genkit.agent import InMemorySessionStore + +ai = Genkit(plugins=[GoogleAI(), Middleware()]) + +agent = ai.define_agent( + name='workspaceAgent', + model='googleai/gemini-flash-latest', + system='Use write_artifact for files. Use read_artifact to review them.', + use=[Artifacts()], + store=InMemorySessionStore(), +) + +chat = agent.chat() +await chat.send('Write poem.txt with a short poem about Python agents.') +print([a.name for a in chat.artifacts]) +``` + +## From a custom agent + +Pass a list: + +```python +from genkit import Part, TextPart +from genkit.agent import Artifact + +await sess.add_artifacts( + [Artifact(name='report.md', parts=[Part(TextPart(text=body))])] +) +``` + +## Reading text + +Parts use a RootModel — read text from `.root` when it is a `TextPart`: + +```python +from genkit import TextPart + +def artifact_text(artifact) -> str: + return ''.join( + p.root.text + for p in (artifact.parts or []) + if isinstance(p.root, TextPart) + ) +``` diff --git a/plugins/genkit/.agents/skills/developing-genkit-python/references/agents-background.md b/plugins/genkit/.agents/skills/developing-genkit-python/references/agents-background.md new file mode 100644 index 00000000..5fa59b45 --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-python/references/agents-background.md @@ -0,0 +1,69 @@ +# Background Agents / Detaching (Beta) + +> Needs a [session store](agents-sessions.md). See [agents.md](agents.md). + +Hand work to the server and keep going. `detach` returns a task handle with a +`snapshot_id` right away. When you need the reply, load that snapshot. + +```python +from genkit.agent import InMemorySessionStore + +agent = ai.define_agent( + name='backgroundAgent', + model='googleai/gemini-flash-latest', + system='Senior research analyst. Produce a comprehensive markdown report.', + store=InMemorySessionStore(), +) + +chat = agent.chat() +task = await chat.detach('Write a report on renewable energy trends') +print(task.snapshot_id) + +snapshot = await task.wait(interval=2.0) +print(snapshot.status) + +await task.abort() + +done = await agent.load_chat(snapshot_id=task.snapshot_id) +print(done.messages) +``` + +## Stopping work + +Always `await` aborts — otherwise nothing happens. + +**Client stop** — `await turn.abort()` (or `asyncio.timeout` around the stream) +stops listening. The server may still finish. Use this mid-stream when you do +not have a snapshot id yet. + +**Server cancel** — `await chat.abort()` or `await task.abort()`. Needs a store +and an existing snapshot (after a completed turn or after `detach`). + +Aborted snapshots are not resume points. Reload the last good leaf. + +On the first turn, a client abort can leave this chat without ids even if the +server later saves. Prefer `detach` + `task.abort()` when you need a +recoverable cancel. + +## Parallel research + +One `chat` can only host one detach. Fork a leaf from a shared checkpoint for +each branch: + +```python +root = agent.chat() +await root.send('Context both researchers should know.') +checkpoint = root.snapshot_id + +async def research(topic: str): + leaf = await agent.load_chat(snapshot_id=checkpoint) + task = await leaf.detach(f'Research {topic} in depth.') + await task.wait(interval=2.0) + done = await agent.load_chat(snapshot_id=task.snapshot_id) + return topic, done.messages[-1] + +import asyncio +results = await asyncio.gather(research('Postgres'), research('SQLite')) +``` + +Check the loaded messages after wait — status alone is not enough. diff --git a/plugins/genkit/.agents/skills/developing-genkit-python/references/agents-branching.md b/plugins/genkit/.agents/skills/developing-genkit-python/references/agents-branching.md new file mode 100644 index 00000000..b17be4f8 --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-python/references/agents-branching.md @@ -0,0 +1,43 @@ +# Agent Branching (Beta) + +> **Beta / preview API.** Needs a [session store](agents-sessions.md). +> See [agents.md](agents.md). + +A `snapshot_id` is an immutable checkpoint. Load it with +`load_chat(snapshot_id=...)` and send again — each turn creates a new leaf; +the original stays put. + +```python +from genkit.agent import InMemorySessionStore + +# After a fork, session_id is ambiguous — raise instead of guessing. +store = InMemorySessionStore(reject_ambiguous_session=True) + +agent = ai.define_agent( + name='designer', + model='googleai/gemini-flash-latest', + system='You help design a product landing page. Reply briefly.', + store=store, +) + +root = agent.chat() +await root.send('Plan a landing page for a note-taking app.') +checkpoint = root.snapshot_id + +minimal = await agent.load_chat(snapshot_id=checkpoint) +await minimal.send('Direction: minimal.') + +bold = await agent.load_chat(snapshot_id=checkpoint) +await bold.send('Direction: bold.') + +resumed = await agent.load_chat(snapshot_id=bold.snapshot_id) +await resumed.send('Add a pricing section.') +``` + +Same pattern for time travel: reopen an earlier snapshot and send a different +follow-up. After forks, resume by `snapshot_id`, not `session_id`: + +```python +# Ambiguous when reject_ambiguous_session=True — pick a leaf instead: +resumed = await agent.load_chat(snapshot_id=bold.snapshot_id) +``` diff --git a/plugins/genkit/.agents/skills/developing-genkit-python/references/agents-custom.md b/plugins/genkit/.agents/skills/developing-genkit-python/references/agents-custom.md new file mode 100644 index 00000000..f93b4dc6 --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-python/references/agents-custom.md @@ -0,0 +1,116 @@ +# Advanced Custom Agents — `define_custom_agent` (Beta) + +> **Beta / preview API.** See [agents.md](agents.md). + +`define_agent` runs a fixed prompt + tool loop. For your own orchestration +(multi-step, custom streaming, manual messages/state/artifacts), use +`ai.define_custom_agent`. Callers still use `chat.send` / `send_stream`. + +## Anatomy + +Register one async `fn`. Inside it, define `handle_turn`, hand it to +`sess.run`, return `sess.result()`: + +```python +async def fn(sess: SessionRunner, ctx: ActionRunContext) -> AgentResult: + async def handle_turn(inp: AgentInput, turn_ctx: TurnContext) -> TurnResult | None: + # user message already in sess.get_messages() + # model / tools / logic… + # ctx.send_chunk(AgentStreamChunk(...)) to stream + # sess.add_messages(...) to persist replies + return TurnResult(finish_reason=AgentFinishReason.STOP) + + await sess.run(handle_turn) + return await sess.result() + + +agent = ai.define_custom_agent(name='customCoder', fn=fn, store=store) +``` + +`handle_turn` runs once per user turn. Before it runs, the runtime appends +`inp.message` to history. Return a `TurnResult` (or `None`); common finish +reasons: `STOP`, `INTERRUPTED`, `FAILED`. Raising ends the invocation as +`FAILED`. + +`turn_ctx` (store only): `snapshot_id` (name external worktrees/dirs under +this), `parent_snapshot_id`, `turn_index`. Without a store, `snapshot_id` is +`None`. + +From `sess`: `get_messages` / `add_messages` / `set_messages`, +`get_custom` / `update_custom`, `get_artifacts` / `add_artifacts`. +`add_messages` and `add_artifacts` each take a **list**. Stream with +`ctx.send_chunk(AgentStreamChunk(...))`. + +## Example + +```python +from genkit import ActionRunContext, FinishReason, Genkit, Message +from genkit.agent import ( + AgentFinishReason, + AgentInput, + AgentResult, + AgentStreamChunk, + InMemorySessionStore, + SessionRunner, + TurnContext, + TurnResult, +) +from genkit_google_genai import GoogleAI + +ai = Genkit(plugins=[GoogleAI()]) +store = InMemorySessionStore() + + +async def custom_coder_fn(sess: SessionRunner, ctx: ActionRunContext) -> AgentResult: + async def handle_turn(inp: AgentInput, turn_ctx: TurnContext) -> TurnResult | None: + history = await sess.get_messages() + messages = [Message(m) for m in history] if history else None + + stream_resp = ai.generate_stream( + model='googleai/gemini-flash-latest', + system='Concise coding assistant.', + messages=messages, + ) + async for chunk in stream_resp.stream: + ctx.send_chunk(AgentStreamChunk(model_chunk=chunk)) + + res = await stream_resp.response + if res.message: + await sess.add_messages([res.message]) + + fr = ( + AgentFinishReason.STOP + if res.finish_reason == FinishReason.STOP + else AgentFinishReason.UNKNOWN + ) + return TurnResult(finish_reason=fr) + + await sess.run(handle_turn) + return await sess.result() + + +agent = ai.define_custom_agent(name='customCoder', fn=custom_coder_fn, store=store) + +chat = agent.chat() +await chat.send('What is a Python list comprehension?') +``` + +Persist model replies with `sess.add_messages` or the next turn won't see them. +Custom state → [state](agents-state.md). Artifacts → [artifacts](agents-artifacts.md). + + +## Non-streaming `handle_turn` with tools + +```python +async def handle_turn(inp: AgentInput, ctx: TurnContext) -> TurnResult | None: + history = [Message(m) for m in await sess.get_messages()] + # ai.generate runs the tool loop for you + res = await ai.generate( + messages=history + list(inp.messages or []), + tools=[get_time], + system='You are helpful.', + ) + return TurnResult(messages=res.messages, finish_reason=AgentFinishReason.STOP) +``` + +Tool inputs: [agents.md](agents.md#tool-inputs). diff --git a/plugins/genkit/.agents/skills/developing-genkit-python/references/agents-http.md b/plugins/genkit/.agents/skills/developing-genkit-python/references/agents-http.md new file mode 100644 index 00000000..fd70c50d --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-python/references/agents-http.md @@ -0,0 +1,113 @@ +# Agent HTTP (Beta) + +> See [agents.md](agents.md). + +Serve an agent over FastAPI. Talk to it from another process with +`remote_agent`. Flows use `serve_flow` — [FastAPI](fastapi.md). + +## Serve + +```python +import uvicorn +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from genkit import Genkit +from genkit.agent import InMemorySessionStore +from genkit_fastapi import serve_agent +from genkit_google_genai import GoogleAI + +ai = Genkit(plugins=[GoogleAI()], model='googleai/gemini-flash-latest') + +agent = ai.define_agent( + name='weatherAgent', + system='Weather assistant. Be concise.', + store=InMemorySessionStore(), +) + +app = FastAPI() +app.add_middleware( + CORSMiddleware, + allow_origins=['*'], + allow_methods=['*'], + allow_headers=['*'], + expose_headers=['X-Genkit-Stream-Id'], +) + +app.include_router(serve_agent(agent), prefix='/api') + +if __name__ == '__main__': + uvicorn.run(app, host='0.0.0.0', port=8080) +``` + +Routes land at `/{agent.name}` by default. Override with `base_path=...`. + +```bash +genkit start -- uv run server.py +``` + +## Remote client + +```python +from genkit.agent import remote_agent + +client = remote_agent( + url='http://127.0.0.1:8080/api/weatherAgent', + state_management='server', # 'client' if the server has no store +) +chat = client.chat() +res = await chat.send('Weather in Tokyo?') +print(res.text, res.snapshot_id) + +turn = chat.send_stream('And humidity?') +async for chunk in turn.stream: + if chunk.text: + print(chunk.text, end='', flush=True) +final = await turn.response + +resumed = await client.load_chat(snapshot_id=final.snapshot_id) +await resumed.send('What city was that?') +``` + +Keep one chat for multi-turn. Match `state_management` to the server. Skip the +trailing slash on the URL. + +## Auth + +Resolve identity on the **server**. Mount the agent with a FastAPI dependency +that returns a plain dict. Tools read it with `Genkit.current_context()` for +the whole turn. + +```python +from fastapi import FastAPI, Header, HTTPException +from genkit_fastapi import serve_agent + +async def resolve_user(x_user: str | None = Header(default=None)) -> dict: + if not x_user: + raise HTTPException(status_code=401, detail='X-User header required') + return {'sub': x_user, 'role': 'engineer'} + +app = FastAPI() +app.include_router( + serve_agent(agent, context_dependency=resolve_user), + prefix='/api', +) + +@ai.tool() +async def who_am_i() -> str: + ctx = Genkit.current_context() or {} + return f"user={ctx.get('sub')} role={ctx.get('role')}" +``` + +The remote client only forwards credentials: + +```python +client = remote_agent( + url='http://127.0.0.1:8080/api/supportAgent', + state_management='server', + headers={'X-User': 'alice'}, +) +``` + +Return a `dict` from the dependency — Pydantic models are dropped. Custom +routes can pass context through `handle_genkit_request` ([FastAPI](fastapi.md)). +One-shot calls outside chat can use `ai.generate(..., context={...})`. diff --git a/plugins/genkit/.agents/skills/developing-genkit-python/references/agents-human-in-the-loop.md b/plugins/genkit/.agents/skills/developing-genkit-python/references/agents-human-in-the-loop.md new file mode 100644 index 00000000..e6a2d411 --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-python/references/agents-human-in-the-loop.md @@ -0,0 +1,109 @@ +# Agent Human-in-the-Loop / Interrupts (Beta) + +> **Beta / preview API.** See [agents.md](agents.md). + +An **interrupt** pauses mid-turn so your code (or a human) can decide, then +**resume**. Works with or without a [store](agents-sessions.md). + +Flow: `send` → `finish_reason == INTERRUPTED` + `res.interrupts` → human input +→ `resume`. + +## ToolApproval + +Tools in `allowed_tools` run freely; everything else pauses. Empty list ⇒ every +tool needs approval. When you also mount `Filesystem` / `Artifacts`, remember +to allow-list (or deliberately interrupt on) `write_file` / `edit_file` / +`write_artifact` — the coding-agent example often auto-approves **reads only**. + +```python +from uuid import uuid4 + +from pydantic import BaseModel, Field +from genkit_google_genai import GoogleAI +from genkit_middleware import Middleware, ToolApproval + +from genkit import Genkit, ToolRequestPart +from genkit.agent import AgentFinishReason, InMemorySessionStore + +ai = Genkit(plugins=[GoogleAI(), Middleware()]) +tool_approval = ToolApproval(allowed_tools=[]) + + +class TransferInput(BaseModel): + amount: float + to_account: str = Field(alias='toAccount') + + +class TransferOutput(BaseModel): + success: bool + transaction_id: str = Field(alias='transactionId') + + +@ai.tool(name='transferMoney', description='Transfer money between accounts.') +async def transfer_money(input: TransferInput) -> TransferOutput: + return TransferOutput(success=True, transactionId=f'txn-{uuid4().hex[:12]}') + + +agent = ai.define_agent( + name='bankingAgent', + model='googleai/gemini-flash-latest', + system='Banking assistant. Call transferMoney when the user asks to transfer money.', + tools=[transfer_money], + use=[tool_approval], + store=InMemorySessionStore(), +) +``` + +## Detect and resume + +```python +chat = agent.chat() + +out1 = await chat.send('Transfer $500 to account 12345 for rent.') +# finish_reason == INTERRUPTED; transferMoney has NOT executed yet. + +restart_parts: list[ToolRequestPart] = [ + intr.restart(resumed_metadata={'tool_approved': True}) for intr in out1.interrupts +] +out2 = await chat.resume(restart=restart_parts) +``` + +Resume builders (return parts; you still call `chat.resume`): + +- `interrupt.restart(...)` — re-issue the tool request (ToolApproval after OK). + `resumed_metadata={'tool_approved': True}` (camelCase `toolApproved` also ok) +- `interrupt.respond(output)` — supply a tool response without running the tool + (use this to **deny**: e.g. `respond({'ok': False, 'error': 'denied by policy'})`) + +```python +# Approve (runs the tool): +await chat.resume( + restart=[intr.restart(resumed_metadata={'tool_approved': True}) for intr in out1.interrupts] +) + +# Deny (model sees the respond payload; tool does not execute): +await chat.resume( + respond=[intr.respond({'ok': False, 'error': 'export denied'}) for intr in out1.interrupts] +) + +await chat.resume( + respond=[a.respond({'approved': True})], + restart=[b.restart(resumed_metadata={'tool_approved': True})], +) + +turn = chat.resume_stream(restart=restart_parts) +async for chunk in turn.stream: + if chunk.text: + print(chunk.text, end='', flush=True) +final = await turn.response +``` + +Without a store: keep the same `chat`, or round-trip messages/state/artifacts +([client-managed state](agents.md#client-managed-state-no-store)). + +Build resume parts from `res.interrupts` only. Loop until finish reason is no +longer `INTERRUPTED`. Don't treat an interrupted turn as the final reply. + +For approval UIs, each interrupt exposes `.name`, `.ref`, and `.input` (the +pending tool call). Use those to render the prompt; then `.restart(...)` or +`.respond(...)`. diff --git a/plugins/genkit/.agents/skills/developing-genkit-python/references/agents-sessions.md b/plugins/genkit/.agents/skills/developing-genkit-python/references/agents-sessions.md new file mode 100644 index 00000000..16a24151 --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-python/references/agents-sessions.md @@ -0,0 +1,61 @@ +# Sessions & Persistence (Beta) + +> See [agents.md](agents.md). + +With a store, the server owns history. Each turn writes an immutable snapshot — +the backbone for branching and [background work](agents-background.md). +Approvals work with or without a store ([HITL](agents-human-in-the-loop.md)). + +## Choose a store + +```python +from genkit.agent import InMemorySessionStore, FileSessionStore + +mem_store = InMemorySessionStore() # gone on restart +file_store = FileSessionStore('./.snapshots') # one JSON file per snapshot + +# Optional: prune long chains; refuse ambiguous session_id after forks +pruning = FileSessionStore( + './.snapshots', + max_persisted_chain_length=3, + reject_ambiguous_session=True, +) +``` + +```python +from genkit import Genkit +from genkit_google_genai import GoogleAI + +ai = Genkit(plugins=[GoogleAI()], model='googleai/gemini-flash-latest') + +agent = ai.define_agent( + name='logbookAgent', + system='You are a personal logbook assistant.', + store=file_store, +) + +chat = agent.chat() +res = await chat.send('Log this: I started studying Genkit today.') +print(res.snapshot_id) + +resumed = await agent.load_chat(snapshot_id=res.snapshot_id) +await resumed.send('Add another note.') +``` + +## Typed state + +Pass `state_schema`. Seed with `chat(state=...)` only when there is no store. +With a store, update fields from tools via `ai.current_session()` — +[state](agents-state.md). + +## What a store unlocks + +Without: multi-turn on one chat, interrupts. + +With: durable ids, `load_chat`, branching, detach / background tasks, server +abort. + +## Many users, one agent + +Each `agent.chat()` gets its own session on a shared store. Resume with the +right `session_id` or `snapshot_id` per user. diff --git a/plugins/genkit/.agents/skills/developing-genkit-python/references/agents-state.md b/plugins/genkit/.agents/skills/developing-genkit-python/references/agents-state.md new file mode 100644 index 00000000..04bdb00e --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-python/references/agents-state.md @@ -0,0 +1,135 @@ +# Agent State (Beta) + +> See [agents.md](agents.md) · [sessions](agents-sessions.md). + +A session carries three layers: messages, custom state (`state_schema`), and +artifacts. With a schema, `chat.state` and streamed `chunk.custom` come back as +that model. + +## Two modes + +**With a store** — Genkit owns history. Resume by `snapshot_id` or +`session_id`. You cannot seed with `chat(state=...)`. + +**Without a store** — your app owns history. Seed and round-trip yourself: + +```python +chat = agent.chat(state=Profile(name='Ada', tier='pro')) +await chat.send('Hello') +resumed = agent.chat( + messages=chat.messages, state=chat.state, artifacts=chat.artifacts +) +``` + +Custom state is for your product (routing, UI). It is not injected into the +model unless you put it in the system prompt or messages. + +`state_schema` alone does not fill `chat.state`. Something in the turn must call +`update_custom`. Prefer tools on a normal agent so you keep middleware. + +## Client-managed typed state + +```python +from pydantic import BaseModel + +from genkit import Genkit +from genkit_google_genai import GoogleAI + +ai = Genkit(plugins=[GoogleAI()], model='googleai/gemini-flash-latest') + + +class Profile(BaseModel): + name: str + tier: str = 'free' + + +agent = ai.define_agent( + name='profileAgent', + system='Greet the user by name when you know it.', + state_schema=Profile, +) + +chat = agent.chat(state=Profile(name='Ada', tier='pro')) +await chat.send('Hello') +print(chat.state.name) +``` + +## Store + middleware + +Update custom state from tools with `ai.current_session()`. Mutators are +async. Coerce to your model if the callback receives a dict: + +```python +from pydantic import BaseModel, Field + +from genkit import Genkit +from genkit.agent import InMemorySessionStore +from genkit_google_genai import GoogleAI +from genkit_middleware import Middleware, ToolApproval + + +class CaseState(BaseModel): + case_id: str = '' + status: str = 'open' + + +class OpenCaseInput(BaseModel): + case_id: str = Field(description='Support case id') + + +ai = Genkit(plugins=[GoogleAI(), Middleware()], model='googleai/gemini-flash-latest') + + +@ai.tool(name='openCase', description='Open or update the support case id.') +async def open_case(input: OpenCaseInput) -> dict: + sess = ai.current_session() + if sess is None: + return {'ok': False, 'error': 'no session'} + + async def mutate(c: object) -> CaseState: + base = c if isinstance(c, CaseState) else CaseState.model_validate(c or {}) + return CaseState(case_id=input.case_id, status=base.status) + + await sess.update_custom(mutate) + return {'ok': True, 'case_id': input.case_id} + + +agent = ai.define_agent( + name='supportOps', + system='Support ops. Call openCase when asked. Be brief.', + tools=[open_case], + state_schema=CaseState, + use=[ToolApproval(allowed_tools=['openCase'])], + store=InMemorySessionStore(), +) +``` + +## Live patches + +In a [custom agent](agents-custom.md), `update_custom` streams to +`chunk.custom`: + +```python +async def bump(c): + return {'turns': (c or {}).get('turns', 0) + 1} + +await sess.update_custom(bump) +``` + +## Redacting on the way out + +`state_transform` and `chunk_transform` shape what clients see. They do not +change what the store keeps. Return a full `SessionState` from +`state_transform`. Returning `None` from `chunk_transform` drops that chunk. + +```python +from genkit.agent import SessionState + +def redact(state: SessionState) -> SessionState: + custom = dict(state.custom or {}) + if 'api_key' in custom: + custom['api_key'] = 'REDACTED' + return SessionState(messages=state.messages, custom=custom, artifacts=state.artifacts) + +agent = ai.define_agent(..., state_schema=SecretState, state_transform=redact, store=...) +``` diff --git a/plugins/genkit/.agents/skills/developing-genkit-python/references/agents.md b/plugins/genkit/.agents/skills/developing-genkit-python/references/agents.md new file mode 100644 index 00000000..fd114e91 --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-python/references/agents.md @@ -0,0 +1,170 @@ +# Agents (Beta) + +> Preview API under `genkit.agent`. + +Turn a model + tools into a durable conversation — history, typed state, +approvals, branches, and background work. + +Deeper topics: [sessions](agents-sessions.md) · +[HITL](agents-human-in-the-loop.md) · [branching](agents-branching.md) · +[background](agents-background.md) · [state](agents-state.md) · +[artifacts](agents-artifacts.md) · [custom](agents-custom.md) · +[HTTP](agents-http.md) + +## Define an agent + +```python +from pydantic import BaseModel + +from genkit import Genkit +from genkit_google_genai import GoogleAI +from genkit.agent import InMemorySessionStore + +ai = Genkit(plugins=[GoogleAI()]) + + +class WeatherInput(BaseModel): + location: str + + +class WeatherOutput(BaseModel): + weather: str + temperature: str + + +@ai.tool(name='getWeather', description='Get weather for a city.') +async def get_weather(input: WeatherInput) -> WeatherOutput: + return WeatherOutput(weather='Sunny', temperature='21°C') + + +agent = ai.define_agent( + name='weatherAgent', + model='googleai/gemini-flash-latest', + system='Weather assistant. Use getWeather for weather questions.', + tools=[get_weather], + store=InMemorySessionStore(), # omit to manage history yourself +) +``` + +Options worth knowing: `use` (middleware), `state_schema`, `max_turns` (tool +loop depth per user message), transforms. Dotprompt agents: +`ai.define_prompt_agent`. Full control: [`define_custom_agent`](agents-custom.md). + +Tool parameters should be a small Pydantic model — even for one field. Empty +inputs need an empty subclass, not bare `BaseModel`. + +## Middleware + +Register `Middleware()` once, then pass instances in `use=[...]`. + +- **Filesystem** — `list_files` / `read_file`. Writes stay off until + `allow_write_access=True`. Paths are relative to `root_dir`. +- **ToolApproval** — tools in `allowed_tools` run; everything else pauses for + a human. List write/artifact tools if they should run freely. +- **Skills** — load `skills//SKILL.md` and expose `use_skill`. +- **Retry** — retries flaky model HTTP calls. Tool failures are different: + return a soft-error payload the model can read. + +```python +from genkit_middleware import Middleware, ToolApproval, Filesystem, Skills, Retry +from genkit.agent import FileSessionStore + +ai = Genkit(plugins=[GoogleAI(), Middleware()]) + +coding_agent = ai.define_agent( + name='codingAgent', + model='googleai/gemini-flash-latest', + system='Coding assistant. Use list_files/read_file/write_file/edit_file.', + use=[ + ToolApproval(allowed_tools=['list_files', 'read_file', 'use_skill']), + Filesystem(root_dir='./workspace', allow_write_access=True), + Skills(skill_paths=['./skills']), + Retry(), + ], + store=FileSessionStore('./.snapshots'), + max_turns=30, +) +``` + +For typed session fields with middleware, update state from tools via +`ai.current_session()` — [state](agents-state.md). Declaring `state_schema` +alone does not fill `chat.state`. + +## Chat + +```python +chat = agent.chat() + +res = await chat.send('Weather in Tokyo?') +print(res.text, res.snapshot_id) + +turn = chat.send_stream('What about Paris?') +async for chunk in turn.stream: + if chunk.text: + print(chunk.accumulated_text, end='\r', flush=True) +final = await turn.response +``` + +Send returns a response. Stream returns a turn with `.stream` and `.response`. +After an interrupt, use `resume` / `resume_stream`. + +You can await the response without reading every chunk. One send at a time per +`chat`. + +To reopen a stored conversation, use `load_chat` (not `chat(snapshot_id=...)`, +which only attaches a resume handle): + +```python +resumed = await agent.load_chat(snapshot_id=res.snapshot_id) +await resumed.send('What city did I ask about?') +``` + +## Verify an agent from the CLI (`flow:run`) + +`genkit flow:run` only runs **flows**, not agents, so you can't `flow:run` an +agent directly. To exercise an agent from the CLI (e.g. a quick, self-terminating +check), wrap one turn in a throwaway flow and run that: + +```python +@ai.flow() +async def try_weather_agent(message: str) -> str: + return (await agent.chat().send(message)).text + +# genkit flow:run try_weather_agent '"Weather in Tokyo?"' -- uv run src/main.py +``` + +## Without a store + +Skip `store` when your app owns history. Ids stay `None` — pass messages, +state, and artifacts into the next `chat(...)`: + +```python +agent = ai.define_agent( + name='echoNoStore', + model='googleai/gemini-flash-latest', + system='Echo assistant. Answer briefly and remember context.', +) + +chat = agent.chat() +await chat.send('My name is Ada. Remember it.') + +resumed = agent.chat( + messages=chat.messages, state=chat.state, artifacts=chat.artifacts +) +await resumed.send('What is my name? One word.') +``` + +Add a [store](agents-sessions.md) when the server should own history, or when +you need branching and detach. [Interrupts](agents-human-in-the-loop.md) work +either way. + +## Auth + +Put identity on the server with `serve_agent` and a FastAPI dependency. The +remote client only forwards credentials. See [HTTP](agents-http.md). + +## Prompt agents + +`define_prompt_agent(name=...)` uses the same name for the agent and the +`.prompt` file stem. Keep preamble inputs stable for the chat — put dynamic +fields in user messages or tools. diff --git a/plugins/genkit/.agents/skills/developing-genkit-python/references/common-errors.md b/plugins/genkit/.agents/skills/developing-genkit-python/references/common-errors.md new file mode 100644 index 00000000..fba68af8 --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-python/references/common-errors.md @@ -0,0 +1,133 @@ +# Common Errors + +Quick fixes. Longer recipes live in the linked guides. + +--- + +## Wrong Google AI import + +```python +from genkit_google_genai import GoogleAI +``` + +```bash +uv add genkit genkit-google-genai +``` + +--- + +## Streaming TypeError + +Do not await the stream handle — await the response: + +```python +turn = chat.send_stream('hi') +async for chunk in turn.stream: ... +res = await turn.response + +sr = ai.generate_stream(prompt='...') +async for chunk in sr.stream: ... +final = await sr.response +``` + +--- + +## Tool schema must be an object + +Wrap tool params in a Pydantic model. Bare `str` / `float` arguments fail on +Gemini. See [agents.md](agents.md). + +```python +class WeatherInput(BaseModel): + city: str + +@ai.tool() +async def get_weather(input: WeatherInput) -> str: ... +``` + +--- + +## `define_tool` missing + +Use `@ai.tool()`. + +--- + +## Model id without a prefix + +Use `googleai/gemini-flash-latest`, not bare `gemini-flash-latest`. + +--- + +## Missing `.json` / `.message` on the response + +Plain text → `response.text`. Structured output → `response.output`. + +--- + +## Event loop issues under `genkit start` + +Use `ai.run_main(main())` for long-lived apps. See [fastapi.md](fastapi.md) +for serving under Dev UI. + +--- + +## No `snapshot_id` after a turn + +Attach a `store`. Without one, pass messages / state / artifacts yourself — +[agents.md](agents.md). + +--- + +## Cannot send state to a server-managed agent + +Store-backed chats resume by id. Update fields inside the turn — +[agents-state.md](agents-state.md). + +--- + +## Ambiguous session after a fork + +Resume with a leaf `snapshot_id`, not `session_id` — +[agents-branching.md](agents-branching.md). + +--- + +## Approval resume does nothing + +Pass approval metadata on the restart part: + +```python +await chat.resume( + restart=[ + i.restart(resumed_metadata={'tool_approved': True}) + for i in out.interrupts + ] +) +``` + +Build from `res.interrupts`. Full flow: +[agents-human-in-the-loop.md](agents-human-in-the-loop.md). + +--- + +## Abort seems to do nothing + +Await it. Client stop: `turn.abort()` (or a timeout around the stream). +Server cancel: `chat.abort()` / `task.abort()` after you have a snapshot. +Details: [agents-background.md](agents-background.md). + +--- + +## Tool raise becomes `AgentError` + +A raised tool ends the turn. Catch `AgentError` on `chat.send`. Prefer +returning a soft-error dict so the model can recover. History stays usable — +the next send continues from the last good snapshot. + +--- + +## Remote client oddities + +Match `state_management` to the server. No trailing slash on the URL. Check +server logs. Auth: [agents-http.md](agents-http.md). diff --git a/plugins/genkit/.agents/skills/developing-genkit-python/references/dev-workflow.md b/plugins/genkit/.agents/skills/developing-genkit-python/references/dev-workflow.md new file mode 100644 index 00000000..74681b1e --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-python/references/dev-workflow.md @@ -0,0 +1,127 @@ +# Dev Workflow — Genkit Python + +## Agent responsibility + +After generating code, always give the developer: +1. The full pre-run checklist with copy-paste commands using absolute paths +2. The `genkit start` command to run in their terminal (foreground — it's expected to block) +3. Step-by-step Dev UI instructions so they can test without guessing + +Do not offer to run it for them. Give them the commands and let them run it. + +--- + +## Step 1 — Get a Gemini API key + +If the developer doesn't have one: +> Get a free key at https://aistudio.google.com/apikey — click **"Create API key"**, copy it. + +--- + +## Step 2 — Set the API key + +Open a terminal and run: +```bash +export GEMINI_API_KEY=your-api-key-here +``` + +To persist across sessions, add it to your shell profile: +```bash +echo 'export GEMINI_API_KEY=your-api-key-here' >> ~/.zshrc && source ~/.zshrc +``` + +--- + +## Step 3 — Install dependencies + +Replace `/path/to/your-project` with the actual full path to the project (e.g. `/Users/yourname/projects/my-genkit-app`): + +```bash +cd /path/to/your-project +uv add genkit genkit-google-genai +``` + +(Requires a project with `pyproject.toml` — run `uv init` in an empty directory first if needed.) + +--- + +## Step 4 — Start the Dev UI + +Run this in your terminal. **It will block — that's expected.** Leave this terminal open while you use the Dev UI. + +```bash +cd /path/to/your-project +GEMINI_API_KEY=your-api-key-here genkit start -- uv run src/main.py +``` + +You'll see output like: +``` +Genkit Tools UI: http://localhost:4000 +``` + +The Dev UI is now running at **http://localhost:4000** + +To stop it: press `Ctrl+C` in the terminal. + +--- + +## Step 5 — Test in the Dev UI + +1. Open **http://localhost:4000** in your browser +2. Click **"Run"** in the left sidebar +3. Find your flow by name (e.g. `summarize`, `chat`, `joke_generator`) +4. In the input box, paste your input as JSON — e.g: + ```json + {"text": "hello world"} + ``` +5. Click the **"Run"** button — the output appears on the right +6. Click **"Traces"** in the left sidebar to inspect every step, model call, token count, and latency + +--- + +## CLI Commands + +`genkit start` unintrusively wraps any Python program that uses the Genkit library, running it unchanged while capturing traces from every Genkit action so you can prove tools were actually called and inspect model I/O from the terminal, even for headless checks. It forwards stdio, so interactive CLI tools that rely on stdin/stdout work without issues. Running the app directly (`uv run`) skips trace capture, so you're debugging blind. + +**Primary pattern (default):** prefix `genkit start --` to your normal run command. This collects telemetry from any Genkit code your program runs, whether triggered from the dev UI, your own web server/web UI, or a plain script: +```bash +genkit start -- uv run src/main.py +genkit start --noui -- uv run src/main.py # same, without the Dev UI (still a persistent server) +``` +`genkit start` runs until you stop it with Ctrl+C. That is expected and correct for the common cases: a server your web/mobile app calls, or an interactive CLI you exit yourself. `--noui` only drops the Dev UI; it is **not** a one-shot command and will not exit on its own. Do **not** use `genkit start` as a blocking step in automated/non-interactive contexts; use `flow:run` (below) for that. + +**Non-interactive use (agents/CI):** add the global `--non-interactive` flag before `--` so the CLI uses defaults and never blocks on a prompt (e.g. the first-run analytics notice): `genkit start --non-interactive -- uv run src/main.py` (works with `flow:run` too). + +**Run a flow (`flow:run`):** invoke a specific flow by name from the CLI. Append your run command after `--` to spin up the runtime just for this run (the command runs as-is to register your flows): +```bash +genkit flow:run myFlow '{"data": "input"}' -- uv run src/main.py +``` +This is **self-terminating**: it runs the flow once, prints a `Trace ID`, then exits, so it's the right choice for a quick, non-interactive check (unlike `genkit start`). Note: `flow:run` runs **flows** (`@ai.flow()`), not agents; you can't `flow:run` an agent (`ai.define_agent`) directly. To exercise an agent from the CLI, wrap one turn in a throwaway flow and run that (see [Agents](agents.md)). Traces for this run can be inspected using the trace commands below. + +**Debugging with traces:** the fastest way to see prompts, model inputs/outputs, tool calls, latencies, and errors. Inspect from the terminal after any run under `genkit start`: +```bash +genkit trace:list # find recent trace IDs +genkit trace:get # full trace details (inputs, outputs, tool calls, errors) +genkit trace:get --format json # machine-readable JSON, safe to pipe into jq or other parsers +``` + +For machine-readable output, pass `--format json` to get clean JSON you can pipe into `jq` or other parsers. The **default** output is human-oriented (banner/log lines, possible truncation on large traces), so don't pipe that form directly; use `--format json`, grep, or the Dev UI trace viewer. + + +**Documentation:** +```bash +genkit docs:search "streaming" python +genkit docs:list python +genkit docs:read python/flows.md +``` + + +## Troubleshooting + +- `genkit: command not found` — `npm install -g genkit-cli` +- `GEMINI_API_KEY not set` — `export GEMINI_API_KEY=your-key` +- Port 4000 already in use — + `genkit start --port 4001 -- uv run src/main.py` +- `uv: command not found` — + `curl -LsSf https://astral.sh/uv/install.sh | sh` +- Flow not showing in Dev UI — check `genkit start` output for errors diff --git a/plugins/genkit/.agents/skills/developing-genkit-python/references/dotprompt.md b/plugins/genkit/.agents/skills/developing-genkit-python/references/dotprompt.md new file mode 100644 index 00000000..0bc91ad5 --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-python/references/dotprompt.md @@ -0,0 +1,166 @@ +# Dotprompt — Genkit Python + +## What it is + +`.prompt` files combine YAML frontmatter (model config, schemas) with Handlebars +templates. Keeps prompt logic out of Python code and makes variants easy. + +## File format + +```yaml +--- +model: googleai/gemini-flash-latest +input: + schema: + food: string + tone?: string # optional scalar — just `field?: type` + ingredients?(array): string # optional array/object needs the parenthetical +output: + schema: Recipe # references a schema registered with ai.define_schema() + format: json +--- +{{role "system"}} +You are a chef. Keep recipes practical. + +{{role "user"}} +Generate a recipe for {{food}}. +{{#if ingredients}} +Prefer these ingredients: {{ingredients}}. +{{/if}} +``` + +Use `{{role "system"}}` / `{{role "user"}}` (and friends) so the model sees a +real system instruction. A flat template body becomes a single **user** message. + +Place `.prompt` files in a `prompts/` directory and point `prompt_dir` at it. + +**Note:** `input.schema` describes the contract for humans and tooling. Today it +is **not** validated locally before the model call — missing required fields can +still render and hit the API. Validate in your flow if you need hard failures. + +## Python setup + +```python +from pathlib import Path +from pydantic import BaseModel +from genkit import Genkit +from genkit_google_genai import GoogleAI + +ai = Genkit( + plugins=[GoogleAI()], + model='googleai/gemini-flash-latest', + prompt_dir=Path(__file__).resolve().parent.parent / 'prompts', +) + +class Recipe(BaseModel): + title: str + steps: list[str] + +ai.define_schema('Recipe', Recipe) +``` + +## Calling a prompt + +There is **no** `prompt.execute()`. Non-streaming uses the callable +(`ExecutablePrompt.__call__`): + +```python +# Non-streaming — await the prompt itself (double-call: ai.prompt('name') then (...)) +prompt = ai.prompt('recipe') +response = await prompt(input={'food': 'banana bread'}) +# same as: await ai.prompt('recipe')(input={'food': 'banana bread'}) +result = Recipe.model_validate(response.output) + +# Variant (recipe.robot.prompt file) +response = await ai.prompt('recipe', variant='robot')(input={'food': 'banana bread'}) +``` + +## Streaming from a prompt + +`.stream(...)` is **not** awaited; then consume `.stream` / `.response`: + +```python +from genkit import ActionRunContext + +@ai.flow() +async def tell_story(subject: str, ctx: ActionRunContext) -> str: + result = ai.prompt('story').stream(input={'subject': subject}) + full = '' + async for chunk in result.stream: + if chunk.text: + ctx.send_chunk(chunk.text) + full += chunk.text + final = await result.response # completes even if you stop reading chunks early + return final.text or full +``` + +## Render without generating (for LLM-judge evals) + +```python +rendered = await ai.prompt('my_prompt').render(input={'key': 'value'}) +# Inspect roles/messages before spending tokens: +# print(rendered.messages) +response = await ai.generate(model='googleai/gemini-flash-latest', messages=rendered.messages) +``` + +## Helpers + +Handlebars helpers receive template args in a packed form. Prefer simple +string/list formatting in the template, or unpack carefully: + +```python +def list_helper(data: object, *args, **kwargs) -> str: + # Positional template args arrive packed in the first parameter. + items = data[0] if isinstance(data, (list, tuple)) and data else data + if not isinstance(items, list): + return '' + return '\n'.join(f'- {item}' for item in items) + +ai.define_helper('list', list_helper) +``` + +Then `{{list ingredients}}` in the `.prompt`. If output looks like a Python +`repr` of a list, the helper unpacked wrong — fix the helper, not the model. + +## Variants + +Name the file `..prompt` — e.g. `recipe.robot.prompt`. +Call with `ai.prompt('recipe', variant='robot')`. + +## Partials + +Use `{{>partial_name param=value}}` in templates. Partial files are named +`_partial_name.prompt`. + +## Prompts + tools + structured output + +Naming a tool in the template is not enough — pass tool objects at call time +(or list names in frontmatter `tools:` that match registered tools): + +```python +response = await ai.prompt('insurance_quote')( + input={'age': 35, 'zip': '94105', 'coverage_tier': 'plus'}, + tools=[lookup_rate], +) +quote = QuoteResult.model_validate(response.output) +``` + +Register output schemas with `ai.define_schema('QuoteResult', QuoteResult)` +when frontmatter references them by name. + +### Partials example + +``` +prompts/_greeting.prompt # partial body only +prompts/support_reply.prompt +``` + +In `support_reply.prompt`: +``` +{{>greeting}} +... main template ... +{{>disclaimer}} +``` + +Call `await ai.prompt('support_reply')(input={...})`. Parent input is visible +inside partials unless you override with `{{>greeting name=name}}`. diff --git a/plugins/genkit/.agents/skills/developing-genkit-python/references/evals.md b/plugins/genkit/.agents/skills/developing-genkit-python/references/evals.md new file mode 100644 index 00000000..9c1b7a77 --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-python/references/evals.md @@ -0,0 +1,152 @@ +# Evals — Genkit Python + +## Two types of evaluators + +1. **Built-in** — ship with `genkit-evaluators`, register with + `register_genkit_evaluators(ai)`. Regex / deep_equal do not need + `GoogleAI()` or `GEMINI_API_KEY`. +2. **BYO** — `ai.define_evaluator()` with your own scoring logic (including + LLM judges). + +## Install + +```bash +uv add genkit-evaluators +``` + +## Dataset format + +```json +[ + {"testCaseId": "case1", "input": "x", "output": "banana", "reference": "ba?a?a"}, + {"testCaseId": "case2", "input": "x", "output": "apple", "reference": "ba?a?a"} +] +``` + +Fields: `testCaseId`, `input`, `output`, `reference` (optional for some +evaluators). + +## Built-in evaluators + +```python +from genkit_evaluators import register_genkit_evaluators +register_genkit_evaluators(ai) +``` + +Registered: `genkitEval/regex`, `genkitEval/deep_equal`, `genkitEval/jsonata`. +`eval:run` needs a Genkit runtime: + +```bash +# terminal A +genkit start -- uv run src/main.py +# terminal B +genkit eval:run datasets/my_dataset.json --evaluators=genkitEval/regex +``` + +One-shot (starts the app as the runtime): + +```bash +genkit eval:run datasets/my_dataset.json --evaluators=genkitEval/regex -- uv run src/main.py +``` + +`No runtimes found` means nothing is registered — start the app or use `--`. + +Programmatic — one evaluator per call (`evaluator=`, singular): + +```python +result = await ai.evaluate( + dataset=my_dataset, + evaluator='byo/my_eval', # not evaluators=[...] +) +# EvalResponse — use result.root (not .results) +``` + +Add `--output=eval_out.json` on CLI for machine-readable pass/fail. + +## BYO evaluator + +```python +from genkit.evaluator import BaseDataPoint, Details, EvalFnResponse, EvalStatusEnum, Score + +async def my_eval(datapoint: BaseDataPoint, _options: dict | None = None) -> EvalFnResponse: + output = str(datapoint.output or '') + reference = str(datapoint.reference or '') + passed = output.strip() == reference.strip() + return EvalFnResponse( + test_case_id=datapoint.test_case_id or '', + evaluation=Score( + score=1.0 if passed else 0.0, + status=EvalStatusEnum.PASS if passed else EvalStatusEnum.FAIL, + details=Details(reasoning='Exact match check'), + ), + ) + +ai.define_evaluator( + name='byo/my_eval', + display_name='My Eval', + definition='Checks exact match of output vs reference.', + fn=my_eval, +) +``` + +## LLM-based judge + +Keep a judge prompt next to the app and parse a score from the model. + +`prompts/judge.prompt`: + +``` +--- +model: googleai/gemini-flash-latest +input: + schema: + output: string + reference: string +--- +Score how well the candidate matches the reference from 0.0 to 1.0. +Reply with ONLY a number (e.g. 0.8), nothing else. + +Reference: {{reference}} +Candidate: {{output}} +``` + +```python +import re + +async def llm_eval(datapoint: BaseDataPoint, _options: dict | None = None) -> EvalFnResponse: + rendered = await ai.prompt('judge').render( + input={'output': str(datapoint.output), 'reference': str(datapoint.reference)} + ) + response = await ai.generate( + model='googleai/gemini-flash-latest', + messages=rendered.messages, + ) + m = re.search(r'0?\.\d+|1\.0|[01]', (response.text or '').strip()) + score = float(m.group(0)) if m else 0.0 + return EvalFnResponse( + test_case_id=datapoint.test_case_id or '', + evaluation=Score( + score=score, + status=EvalStatusEnum.PASS if score >= 0.5 else EvalStatusEnum.FAIL, + ), + ) + +ai.define_evaluator( + name='byo/llm_judge', + display_name='LLM Judge', + definition='Scores candidate vs reference with a judge model.', + fn=llm_eval, +) +``` + +Built-in `genkitEval/regex` scores are `True`/`False`; BYO scores are usually +`0.0`/`1.0` — normalize before aggregating. + +## CLI + +```bash +genkit eval:run datasets/my_dataset.json --evaluators=byo/my_eval +genkit eval:run datasets/my_dataset.json --evaluators=genkitEval/regex,byo/my_eval +``` + +Results appear in the Dev UI under **Evaluate**. diff --git a/plugins/genkit/.agents/skills/developing-genkit-python/references/examples.md b/plugins/genkit/.agents/skills/developing-genkit-python/references/examples.md new file mode 100644 index 00000000..13a000fa --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-python/references/examples.md @@ -0,0 +1,212 @@ +# Genkit Python Examples + +Minimal patterns for common Genkit APIs. Examples use **Google AI** (`GoogleAI`, `googleai/...`); other providers use the same patterns with the right plugin and model prefix. + +## Public imports + +Use public packages only — `genkit`, `genkit_google_genai`, `genkit_fastapi`, +`genkit_middleware`, `genkit_evaluators`, `genkit.agent`, `genkit.embedder`, +`genkit.evaluator`, `genkit.model`, etc. Do not import internal modules +(`genkit._core`, …). + +```python +from genkit import Genkit, ActionRunContext +from genkit_google_genai import GoogleAI + +ai = Genkit(plugins=[GoogleAI()], model='googleai/gemini-flash-latest') +``` + +For agents, see [Agents](agents.md) (`from genkit.agent import ...`). + +--- + +## Structured output + +```python +from pydantic import BaseModel, TypeAdapter + +class CityInfo(BaseModel): + name: str + population: int + country: str + +response = await ai.generate( + prompt='Give facts about Tokyo.', + output_format='json', + output_schema=CityInfo, +) +city = response.output + +# Arrays +schema = TypeAdapter(list[CityInfo]).json_schema() +response = await ai.generate( + prompt='List 3 cities.', + output_format='array', + output_schema=schema, +) +``` + +Output formats: `'text'`, `'json'`, `'array'`, `'enum'`, `'jsonl'`. + +--- + +## Streaming (text) + +```python +sr = ai.generate_stream(prompt='Tell me a story.') +async for chunk in sr.stream: + if chunk.text: + print(chunk.text, end='', flush=True) +final = await sr.response # final.text +``` + +You do **not** need to drain `.stream` for the turn to finish — `await sr.response` +completes even if you break out of the loop early (same idea as agent +`send_stream`). + +--- + +## Text and media parts + +```python +# Non-streaming +response = await ai.generate(prompt='...') +for media in response.media: + print(media.content_type, (media.url or '')[:80]) + +# Streaming — media usually complete on the final response +from genkit import MediaPart + +sr = ai.generate_stream(prompt='...') +async for chunk in sr.stream: + if chunk.text: + print(chunk.text, end='', flush=True) +final = await sr.response +for media in final.media: + print(media.content_type, (media.url or '')[:80]) + +if final.message: + for part in final.message.content: + if isinstance(part.root, MediaPart) and part.root.media: + print(part.root.media.content_type) +``` + +--- + + +## Sending image / media input + +The section above covers **model-produced** media. To *send* an image to Gemini, +build a user `Message` with text + `MediaPart` (URL or data URI): + +```python +from genkit import Media, MediaPart, Message, Part, Role, TextPart + +image_url = 'https://example.com/cat.jpg' # or data:image/png;base64,... +msg = Message( + role=Role.USER, + content=[ + Part(root=TextPart(text='What is in this image?')), + Part(root=MediaPart(media=Media(url=image_url, content_type='image/jpeg'))), + ], +) +response = await ai.generate(messages=[msg]) +print(response.text) +``` + +Prefer a vision-capable model (`googleai/gemini-flash-latest`). URLs are +fetched **server-side** — hotlink-blocked or thumbnail URLs often fail. +Prefer a direct image URL or a data URI: + +```python +import base64 +b64 = base64.b64encode(Path('cat.png').read_bytes()).decode() +url = f'data:image/png;base64,{b64}' +``` + + +## Streaming + structured output + +```python +class StoryAnalysis(BaseModel): + title: str + genre: str + summary: str + +sr = ai.generate_stream( + prompt='Write a short story then analyze it.', + output_format='json', + output_schema=StoryAnalysis, +) +async for chunk in sr.stream: + if chunk.text: + print(chunk.text, end='', flush=True) +final = await sr.response +analysis = final.output +``` + +--- + +## Flows + +```python +class SummarizeInput(BaseModel): + text: str + +@ai.flow() +async def summarize(input: SummarizeInput) -> str: + response = await ai.generate(prompt=f'Summarize: {input.text}') + return response.text +``` + +--- + +## Streaming flows + +```python +@ai.flow() +async def stream_story(subject: str, ctx: ActionRunContext) -> str: + sr = ai.generate_stream(prompt=f'Story about {subject}.') + full = '' + async for chunk in sr.stream: + if chunk.text: + ctx.send_chunk(chunk.text) + full += chunk.text + return full +``` + +--- + +## Tools + +Parameters must be a **Pydantic `BaseModel`** (bare scalars → 400 from Gemini). Use **`@ai.tool()`**, not `@ai.define_tool()`. + +```python +class WeatherInput(BaseModel): + city: str + +@ai.tool() +async def get_weather(input: WeatherInput) -> str: + return f'Sunny in {input.city}' + +response = await ai.generate(prompt='Weather in Paris?', tools=[get_weather]) +``` + +--- + +## Embeddings + +```python +from genkit_google_genai import GeminiEmbeddingModels + +embedder = f'googleai/{GeminiEmbeddingModels.GEMINI_EMBEDDING_001}' +embeddings = await ai.embed(embedder=embedder, content='The sky is blue.') +vector = embeddings[0].embedding + +embeddings = await ai.embed_many( + embedder=embedder, + content=['The sky is blue.', 'Grass is green.'], +) +``` + +Common embedders: `googleai/gemini-embedding-001`, `googleai/gemini-embedding-exp-03-07`. diff --git a/plugins/genkit/.agents/skills/developing-genkit-python/references/fastapi.md b/plugins/genkit/.agents/skills/developing-genkit-python/references/fastapi.md new file mode 100644 index 00000000..a2aafd3e --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-python/references/fastapi.md @@ -0,0 +1,344 @@ +# FastAPI — Genkit Python + +## Install + +```bash +uv add genkit-fastapi fastapi uvicorn +``` + +Import from `genkit_fastapi`: + +- `serve_agent` / `serve_flow` — mount an agent or flow as a router +- `handle_genkit_request` — escape hatch for a custom `@app.post` that needs its own + `Depends` / auth wiring while still speaking the Genkit wire format + +For agents, see also [Agent HTTP](agents-http.md). + +--- + +## Serve an agent or flow + +```python +import uvicorn +from fastapi import FastAPI +from genkit import Genkit +from genkit.agent import InMemorySessionStore +from genkit_fastapi import serve_agent, serve_flow +from genkit_google_genai import GoogleAI + +ai = Genkit(plugins=[GoogleAI()], model='googleai/gemini-flash-latest') + +agent = ai.define_agent( + name='weatherAgent', + system='Weather assistant. Be concise.', + store=InMemorySessionStore(), +) + +@ai.flow() +async def hello(name: str) -> str: + return f'Hello, {name}' + +app = FastAPI() +# Routes use the action name as defined: agent `weatherAgent` → /api/weatherAgent; +# flow `hello` → /api/hello. Override with base_path=... if needed. +app.include_router(serve_agent(agent), prefix='/api') +app.include_router(serve_flow(hello), prefix='/api') + +# Under genkit start, drive uvicorn from ai.run_main (don't nest uvicorn.run inside it). +async def main() -> None: + import os + config = uvicorn.Config(app, host='0.0.0.0', port=int(os.environ.get('PORT', '8080'))) + await uvicorn.Server(config).serve() + +if __name__ == '__main__': + ai.run_main(main()) +``` + +--- + +## Streaming + +`serve_agent`, `serve_flow`, and `handle_genkit_request` all stream when the client +sends `Accept: text/event-stream` (same wire path). Otherwise they return a one-shot +`{"result": ...}` JSON body. + +**Wire format (SSE):** +``` +data: {"message": ""} ← one per ctx.send_chunk() call +data: {"message": ""} +data: {"result": } ← sent once when the action completes +``` + +**Frontend:** +```js +const res = await fetch('/api/chat', { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'Accept': 'text/event-stream' }, + body: JSON.stringify({ data: { topic: 'quantum computing' } }), +}); +const reader = res.body.getReader(); +// decode and parse each `data: {...}` line +``` + +**curl test:** +```bash +curl -N -X POST http://localhost:8080/api/chat \ + -H 'Content-Type: application/json' \ + -H 'Accept: text/event-stream' \ + -d '{"data": {"topic": "quantum computing"}}' +``` + +--- + +## Minimal streaming FastAPI app + +```python +import uvicorn +from pydantic import BaseModel +from fastapi import FastAPI +from genkit import Genkit, ActionRunContext +from genkit_fastapi import serve_flow +from genkit_google_genai import GoogleAI + +ai = Genkit(plugins=[GoogleAI()], model='googleai/gemini-flash-latest') + + +class ChatInput(BaseModel): + topic: str + + +@ai.flow() +async def chat(input: ChatInput, ctx: ActionRunContext) -> str: + sr = ai.generate_stream(prompt=f'Tell me about {input.topic}.') + full = '' + async for chunk in sr.stream: + if chunk.text: + ctx.send_chunk(chunk.text) # each chunk → SSE event on the wire + full += chunk.text + return full + + +app = FastAPI() +app.include_router(serve_flow(chat), prefix='/api') # POST /api/chat + +async def main() -> None: + import os + config = uvicorn.Config(app, host='0.0.0.0', port=int(os.environ.get('PORT', '8080'))) + await uvicorn.Server(config).serve() + +if __name__ == '__main__': + ai.run_main(main()) +``` + +**Key:** the flow must accept `ctx: ActionRunContext` and call `ctx.send_chunk(text)` +to emit SSE chunks. Without `ctx.send_chunk`, the flow runs but streams nothing — the +client waits for the final result. + +--- + +## Advanced use cases + +### Nested flow streaming + +Chain flows so a child's chunks surface on the parent's HTTP stream. Call the +child with `.run(..., on_chunk=ctx.send_chunk)` — do **not** pass `ctx` as a +second positional argument to `await child(input, ctx)` (that raises `TypeError`). + +```python +class ResearchInput(BaseModel): + topic: str + +@ai.flow() +async def research(input: ResearchInput, ctx: ActionRunContext) -> str: + sr = ai.generate_stream(prompt=f'Explain {input.topic} in depth.') + full = '' + async for chunk in sr.stream: + if chunk.text: + ctx.send_chunk(chunk.text) + full += chunk.text + return full + + +class HeadlineInput(BaseModel): + text: str + +@ai.flow() +async def make_headline(input: HeadlineInput) -> str: + response = await ai.generate(prompt=f'One-line headline for: {input.text}') + return response.text.strip() + + +class ReportInput(BaseModel): + topic: str + +@ai.flow() +async def report(input: ReportInput, ctx: ActionRunContext) -> str: + headline = await make_headline(HeadlineInput(text=input.topic)) + ctx.send_chunk(f'# {headline}\n\n') + + body = ( + await research.run( + ResearchInput(topic=input.topic), + on_chunk=ctx.send_chunk, + ) + ).response + + return f'# {headline}\n\n{body}' + + +app.include_router(serve_flow(report), prefix='/api') # POST /api/report +``` + +**Rules:** +- Streaming children accept `ctx: ActionRunContext` and call `ctx.send_chunk` +- Parents forward chunks with `child.run(input, on_chunk=ctx.send_chunk)` +- Non-streaming children: `await child(input)` is fine + +### Executing flows in parallel + +Use `asyncio.gather` to run multiple flows concurrently. Only makes sense when children don't need to stream. + +```python +import asyncio + +class AnalysisInput(BaseModel): + text: str + +class CheckResult(BaseModel): + issues: list[str] + +class CombinedAnalysis(BaseModel): + issues: list[str] + +@ai.flow() +async def check_security(input: AnalysisInput) -> CheckResult: + r = await ai.generate( + prompt=f'List security concerns as a short comma-separated line (or "none"): {input.text[:2000]}', + ) + raw = (r.text or '').strip() + issues = [s.strip() for s in raw.split(',') if s.strip() and s.strip().lower() != 'none'] + return CheckResult(issues=issues) + +@ai.flow() +async def check_bugs(input: AnalysisInput) -> CheckResult: + r = await ai.generate( + prompt=f'List likely bugs or correctness issues as a short comma-separated line (or "none"): {input.text[:2000]}', + ) + raw = (r.text or '').strip() + issues = [s.strip() for s in raw.split(',') if s.strip() and s.strip().lower() != 'none'] + return CheckResult(issues=issues) + +@ai.flow() +async def check_style(input: AnalysisInput) -> CheckResult: + r = await ai.generate( + prompt=f'List style or clarity issues as a short comma-separated line (or "none"): {input.text[:2000]}', + ) + raw = (r.text or '').strip() + issues = [s.strip() for s in raw.split(',') if s.strip() and s.strip().lower() != 'none'] + return CheckResult(issues=issues) + +@ai.flow() +async def analyze(input: AnalysisInput) -> CombinedAnalysis: + security, bugs, style = await asyncio.gather( + check_security(input), + check_bugs(input), + check_style(input), + ) + return CombinedAnalysis(issues=security.issues + bugs.issues + style.issues) + + +app.include_router(serve_flow(analyze), prefix='/api') # POST /api/analyze +``` + +--- + +## Structured output endpoint (non-streaming) + +```python +class SentimentResult(BaseModel): + sentiment: str # positive / negative / neutral + confidence: float # 0.0–1.0 + key_phrases: list[str] + +@ai.flow() +async def sentiment(input: AnalysisInput) -> SentimentResult: + response = await ai.generate( + prompt=f'Analyze sentiment: {input.text}', + output_format='json', + output_schema=SentimentResult, + ) + return response.output + + +app.include_router(serve_flow(sentiment), prefix='/api') # POST /api/sentiment +``` + +Client calls this without `Accept: text/event-stream` — gets `{"result": {...}}` back. + +--- + +## Custom route with `handle_genkit_request` + +When you need your own FastAPI dependencies (auth, tenancy, etc.) and still want +the Genkit wire format, call `handle_genkit_request` from a hand-written route: + +```python +from fastapi import Depends, Request +from genkit_fastapi import handle_genkit_request + + +async def current_user(request: Request) -> dict: + # Resolve auth however you like. + return {'uid': request.headers.get('x-user-id', 'anon')} + + +@app.post('/api/secure-chat') +async def secure_chat( + request: Request, + user: dict = Depends(current_user), +): + return await handle_genkit_request( + request, + action=chat, + context={'auth': user}, + ) +``` + +Prefer `serve_flow` / `serve_agent` with `context_dependency=` when a single +`Depends` is enough — use `handle_genkit_request` when the route shape itself +needs to be custom. + +--- + +## Run with Dev UI + +```bash +GEMINI_API_KEY=your-key genkit start -- uv run src/main.py +``` + +Leave the process running until the CLI prints something like: + +``` +Genkit Developer UI: http://localhost:4000 +``` + +Open that URL. Port may differ if 4000 is busy. + + +## Python HTTP client (mounted flows) + +```python +import httpx, asyncio + +async with httpx.AsyncClient(base_url='http://127.0.0.1:18524') as client: + async def call(path, data): + r = await client.post(path, json={'data': data}) + r.raise_for_status() + return r.json()['result'] + a, b = await asyncio.gather( + call('/api/summarize', {'text': '...'}), + call('/api/sentiment', {'text': '...'}), + ) +``` + +Wire shape is `{"data": ...}` in, `{"result": ...}` out (not raw flow args). diff --git a/plugins/genkit/.agents/skills/developing-genkit-python/references/setup.md b/plugins/genkit/.agents/skills/developing-genkit-python/references/setup.md new file mode 100644 index 00000000..136f3869 --- /dev/null +++ b/plugins/genkit/.agents/skills/developing-genkit-python/references/setup.md @@ -0,0 +1,60 @@ +# Setup — Genkit Python + +## New project + +**Always use a virtual environment** — never install Genkit into the system interpreter. With **uv**, the project’s **`.venv`** is created and used by `uv sync` / `uv run` automatically once you add dependencies. + +```bash +mkdir my-app && cd my-app +uv init +# uv may set requires-python to a very new floor (e.g. >=3.14). Genkit needs +# 3.10+ — edit pyproject.toml if your interpreter is older. +# uv init may also write .python-version (e.g. 3.14); delete or edit it +# if it fights an explicit `uv venv --python 3.12`. +uv venv --python 3.12 .venv +# Unix: source .venv/bin/activate +# Windows: .venv\Scripts\activate +uv add genkit genkit-google-genai +# Agents with ToolApproval / Filesystem / Artifacts / Retry also need: +# uv add genkit-middleware +# HTTP serve: uv add genkit-fastapi +# Evals: uv add genkit-evaluators +# Also add pydantic if you use BaseModel schemas with Dotprompt. +export GEMINI_API_KEY=your_key_here +``` + +Import the Google AI plugin as: + +```python +from genkit_google_genai import GoogleAI +``` + +`uv init` creates `pyproject.toml`. Add your app under something like `src/main.py` (or match whatever layout `uv` generated) and point `genkit start` at that entrypoint. + +## pyproject.toml + +Minimal `[project]` block with unpinned Genkit deps (resolver picks compatible releases): + +```toml +[project] +name = "my-app" +version = "0.1.0" +requires-python = ">=3.10" +dependencies = [ + "genkit", + "genkit-google-genai", +] +``` + +## Plugins + +Packages are **`genkit-*`** on PyPI, e.g. `genkit-google-genai`, `genkit-vertexai`, +`genkit-anthropic`, `genkit-fastapi`, `genkit-middleware`, `genkit-evaluators`. +Install with `uv add genkit-`. + +Import modules use underscores (e.g. `genkit_google_genai`, `genkit_middleware`). +See [Examples](examples.md) and [Agents](agents.md). + +## Python version + +**3.10+**. Prefer a project venv: `uv venv --python 3.12 .venv` (or newer) before you run commands. diff --git a/plugins/genkit/.claude-plugin/plugin.json b/plugins/genkit/.claude-plugin/plugin.json new file mode 100644 index 00000000..eb699b3d --- /dev/null +++ b/plugins/genkit/.claude-plugin/plugin.json @@ -0,0 +1,27 @@ +{ + "name": "genkit", + "version": "1.0.0", + "description": "Official Genkit skills: build AI-powered applications with the Genkit framework in JavaScript/TypeScript, Go, Dart/Flutter, and Python — flows, generation, prompts (Dotprompt), tool calling, agents, middleware, model providers, and common errors.", + "author": { + "name": "Genkit", + "url": "https://github.com/genkit-ai" + }, + "homepage": "https://genkit.dev", + "repository": "https://github.com/genkit-ai/skills", + "license": "NOASSERTION", + "keywords": [ + "genkit", + "ai", + "llm", + "agents", + "flows", + "dotprompt", + "firebase", + "typescript", + "go", + "dart", + "flutter", + "python" + ], + "skills": "./.agents/skills/" +} diff --git a/plugins/genkit/.codex-plugin/plugin.json b/plugins/genkit/.codex-plugin/plugin.json new file mode 100644 index 00000000..d9f70756 --- /dev/null +++ b/plugins/genkit/.codex-plugin/plugin.json @@ -0,0 +1,40 @@ +{ + "name": "genkit", + "version": "1.0.0", + "description": "Official Genkit skills: build AI-powered applications with the Genkit framework in JavaScript/TypeScript, Go, Dart/Flutter, and Python — flows, generation, prompts (Dotprompt), tool calling, agents, middleware, model providers, and common errors.", + "author": { + "name": "Genkit", + "url": "https://github.com/genkit-ai" + }, + "interface": { + "displayName": "Genkit", + "shortDescription": "Official Genkit skills: build AI-powered applications with the Genkit framework in JavaScript/TypeScript, Go, Dart/Flutter, and Python — flows, generation, p...", + "longDescription": "Official Genkit skills: build AI-powered applications with the Genkit framework in JavaScript/TypeScript, Go, Dart/Flutter, and Python — flows, generation, prompts (Dotprompt), tool calling, agents, middleware, model providers, and common errors.", + "developerName": "Genkit", + "category": "Development", + "capabilities": [ + "Skill" + ], + "defaultPrompt": [ + "Help me use Genkit for my current task." + ], + "websiteURL": "https://genkit.dev" + }, + "homepage": "https://genkit.dev", + "repository": "https://github.com/genkit-ai/skills", + "license": "NOASSERTION", + "keywords": [ + "genkit", + "ai", + "llm", + "agents", + "flows", + "dotprompt", + "firebase", + "typescript", + "go", + "dart", + "flutter", + "python" + ] +} diff --git a/plugins/genkit/.cursor-plugin/plugin.json b/plugins/genkit/.cursor-plugin/plugin.json new file mode 100644 index 00000000..024507a7 --- /dev/null +++ b/plugins/genkit/.cursor-plugin/plugin.json @@ -0,0 +1,31 @@ +{ + "name": "genkit", + "displayName": "Genkit", + "description": "Official Genkit skills: build AI-powered applications with the Genkit framework in JavaScript/TypeScript, Go, Dart/Flutter, and Python — flows, generation, prompts (Dotprompt), tool calling, agents, middleware, model providers, and common errors.", + "version": "1.0.0", + "author": { + "name": "Genkit" + }, + "category": "Development", + "homepage": "https://genkit.dev", + "repository": "https://github.com/genkit-ai/skills", + "license": "NOASSERTION", + "keywords": [ + "genkit", + "ai", + "llm", + "agents", + "flows", + "dotprompt", + "firebase", + "typescript", + "go", + "dart", + "flutter", + "python" + ], + "tags": [ + "ai", + "framework" + ] +} diff --git a/plugins/genkit/README.md b/plugins/genkit/README.md new file mode 100644 index 00000000..2e7f23a0 --- /dev/null +++ b/plugins/genkit/README.md @@ -0,0 +1,75 @@ +# genkit + +Bundles the **official [Genkit skills](https://github.com/genkit-ai/skills)** (maintained by the Genkit team) as a marketplace plugin — build AI-powered applications with the [Genkit](https://genkit.dev) framework in JavaScript/TypeScript, Go, Dart/Flutter, and Python. + +## Skills + +| Skill | Covers | +|-------|--------| +| `developing-genkit-js` | Genkit for Node.js/TypeScript — setup, flows, Dotprompt, middleware, agents (sessions, state, multi-agent, human-in-the-loop, deployment), CLI and docs lookup, best practices, and common errors | +| `developing-genkit-go` | Genkit for Go — getting started, generation, prompts, tool calling, flows and HTTP, middleware, model providers, and agents | +| `developing-genkit-dart` | Genkit for Dart/Flutter — core `genkit` package, Schemantic, Dotprompt, agents, middleware, MCP, and provider plugins (Google GenAI, Firebase AI, Anthropic, OpenAI, Chrome, Shelf) | +| `developing-genkit-python` | Genkit for Python — setup, dev workflow, Dotprompt, evals, FastAPI integration, agents, and common errors | + +## Install + +### Claude Code + +```bash +/plugin marketplace add pleaseai/claude-code-plugins +/plugin install genkit@pleaseai +``` + +### Codex CLI + +Install via the Codex marketplace using this repository, or manually copy the plugin contents into your Codex plugins directory. See the [Codex plugin docs](https://developers.openai.com/codex/plugins/build) for the local install layout. + +### Antigravity + +Antigravity recognises this directory as a plugin via the root `plugin.json` marker file: + +```bash +# Workspace scope (project-only) +mkdir -p .agents/plugins +cp -R .agents/plugins/genkit + +# Global scope (all projects) +mkdir -p ~/.gemini/antigravity/plugins +cp -R ~/.gemini/antigravity/plugins/genkit +``` + +See the [Antigravity plugins docs](https://antigravity.google/docs/plugins) for background. + +## What's inside + +``` +plugins/genkit/ +├── .claude-plugin/plugin.json # Claude Code manifest (source of truth) +├── .codex-plugin/plugin.json # Codex manifest (generated) +├── .cursor-plugin/plugin.json # Cursor manifest (generated) +├── plugin.json # Antigravity marker file (generated) +├── skills-lock.json # skills.sh lockfile — pins the upstream revision +├── README.md # this file +└── .agents/skills/ # vendor-managed — do not edit (see below) + ├── developing-genkit-js/ + ├── developing-genkit-go/ + ├── developing-genkit-dart/ + └── developing-genkit-python/ +``` + +> The Codex, Cursor, and Antigravity manifests are generated from the Claude manifest by +> `bun run plugins:multi-format` — edit `.claude-plugin/plugin.json` and re-run, do not hand-edit them. + +## Updating the skills + +The skills under `.agents/skills/` are **vendor-managed** and tracked by `skills-lock.json`. Do not edit them in place — changes are overwritten on the next sync. Fix issues upstream at [genkit-ai/skills](https://github.com/genkit-ai/skills) instead. + +To pull the latest upstream revision: + +```bash +bun run skills:update-locks # refresh every lock dir in the repo +bun run skills:update-locks plugins/genkit # or just this plugin +bun run skills:update-locks:check # report what would change, leave the tree clean +``` + +The `.github/workflows/update-skills.yml` workflow runs this weekly and opens a `fix:` PR when upstream has moved, so release-please bumps this plugin on merge. diff --git a/plugins/genkit/plugin.json b/plugins/genkit/plugin.json new file mode 100644 index 00000000..cffaa236 --- /dev/null +++ b/plugins/genkit/plugin.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://antigravity.google/schemas/v1/plugin.json", + "name": "genkit", + "version": "1.0.0", + "description": "Official Genkit skills: build AI-powered applications with the Genkit framework in JavaScript/TypeScript, Go, Dart/Flutter, and Python — flows, generation, prompts (Dotprompt), tool calling, agents, middleware, model providers, and common errors.", + "author": { + "name": "Genkit", + "url": "https://github.com/genkit-ai" + }, + "homepage": "https://genkit.dev", + "repository": "https://github.com/genkit-ai/skills", + "license": "NOASSERTION", + "keywords": [ + "genkit", + "ai", + "llm", + "agents", + "flows", + "dotprompt", + "firebase", + "typescript", + "go", + "dart", + "flutter", + "python" + ] +} diff --git a/plugins/genkit/skills-lock.json b/plugins/genkit/skills-lock.json new file mode 100644 index 00000000..5affc132 --- /dev/null +++ b/plugins/genkit/skills-lock.json @@ -0,0 +1,29 @@ +{ + "version": 1, + "skills": { + "developing-genkit-dart": { + "source": "genkit-ai/skills", + "sourceType": "github", + "skillPath": "skills/developing-genkit-dart/SKILL.md", + "computedHash": "5f55290370f086bd0d6add6d8f7b4ef31b330260359035bc15295c96af220a74" + }, + "developing-genkit-go": { + "source": "genkit-ai/skills", + "sourceType": "github", + "skillPath": "skills/developing-genkit-go/SKILL.md", + "computedHash": "edab0ca45e15c3ba44dcbddc221fa48970d990bc37a08c5b8f50ee384f862fbd" + }, + "developing-genkit-js": { + "source": "genkit-ai/skills", + "sourceType": "github", + "skillPath": "skills/developing-genkit-js/SKILL.md", + "computedHash": "e3e8c3091f093c9dcb19cc5eb183ea88ca9a16cc8524f8c04663036c9ba54fca" + }, + "developing-genkit-python": { + "source": "genkit-ai/skills", + "sourceType": "github", + "skillPath": "skills/developing-genkit-python/SKILL.md", + "computedHash": "a2c33c613520054bdc19c3778b5d38f9829fbda6397cb505d7a9ac59eb7c8a09" + } + } +} diff --git a/release-please-config.json b/release-please-config.json index 044a129a..e18707a2 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -1272,6 +1272,32 @@ } ] }, + "plugins/genkit": { + "release-type": "simple", + "component": "genkit", + "extra-files": [ + { + "type": "json", + "path": ".claude-plugin/plugin.json", + "jsonpath": "$.version" + }, + { + "type": "json", + "path": ".codex-plugin/plugin.json", + "jsonpath": "$.version" + }, + { + "type": "json", + "path": "plugin.json", + "jsonpath": "$.version" + }, + { + "type": "json", + "path": ".cursor-plugin/plugin.json", + "jsonpath": "$.version" + } + ] + }, "plugins/tanstack": { "release-type": "simple", "component": "tanstack", From 32835b0887dc57b5c2872687bcc421ed6add8539 Mon Sep 17 00:00:00 2001 From: Minsu Lee Date: Fri, 25 Sep 2026 01:37:38 +0900 Subject: [PATCH 2/2] chore(genkit): match pubspec.yaml genkit dependency on first line --- .claude-plugin/marketplace.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index f2a517f8..8f79892a 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -1353,7 +1353,7 @@ }, { "file": "[/\\\\]pubspec\\.yaml$", - "pattern": "\\n\\s*genkit(_[a-z_]+)?\\s*:" + "pattern": "(^|\\n)\\s*genkit(_[a-z_]+)?\\s*:" }, { "file": "[/\\\\](pyproject\\.toml|requirements[^/\\\\]*\\.txt)$",