Skip to content

feat(js): implemented defineAgent - #5251

Merged
pavelgj merged 195 commits into
mainfrom
pj/session-flow-take-2
Jun 24, 2026
Merged

feat(js): implemented defineAgent#5251
pavelgj merged 195 commits into
mainfrom
pj/session-flow-take-2

Conversation

@pavelgj

@pavelgjpavelgj commented May 6, 2026

Copy link
Copy Markdown
Member

Introduces defineAgent and friends: a first-class, multi-turn agent API for Genkit JS. An agent bundles a model/prompt, conversational state, tool loops, interrupts, persistence, streaming, and abort/resume into a single ergonomic, transport-agnostic surface that runs identically in-process on the server and over HTTP from a browser.

Highlights

  • Three ways to define an agent, from zero-config to full control.
  • Stateful, multi-turn chats that thread snapshotId/state for you.
  • Typed custom state with Zod validation (stateSchema).
  • Pluggable persistence via SessionStore (in-memory + file stores included), with server-managed sessions resumable by sessionId.
  • Streaming with live custom-state patches over the wire (JSON Patch).
  • Interrupts & resume (restart / respond) for human-in-the-loop tools.
  • Abort, detach (background turns) + heartbeats, and graceful failure with last-good state.
  • clientTransform to redact/reshape state and stream chunks before they leave the server.
  • Transport-agnostic: the same AgentAPI is returned by ai.defineAgent(...) on the server and remoteAgent(...) on the client.

1. Define an agent

The headline API — one call wires up a prompt and a multi-turn agent:

constagent=ai.defineAgent({name: 'assistant',model: 'googleai/gemini-flash-latest',system: 'You are a helpful assistant.',tools: [searchTool],});

Prompt-driven (reuse a registered .prompt), with typed promptInput:

constagent=ai.definePromptAgent({promptName: 'assistant',promptInput: {role: 'pirate',tone: 'cheerful'},// type-checked vs prompt's input schema});

Full control via a custom turn handler:

constagent=ai.defineCustomAgent({name: 'custom'},async(runner,input)=>{// own the turn: call models, tools, mutate session state, return a result});

2. Chat: send & stream

chat() returns a stateful conversation that tracks state, messages, snapshotId, and sessionId so you don't thread them by hand:

constchat=agent.chat();constres=awaitchat.send('Hello!');console.log(res.text,res.finishReason);// 'Hi! ...', 'stop'// streamingconstturn=chat.sendStream('Write a poem');forawait(constchunkofturn.stream){process.stdout.write(chunk.text??'');}constfinal=awaitturn.response;

3. Typed custom state

constagent=ai.defineAgent({name: 'cart',model: 'googleai/gemini-flash-latest',stateSchema: z.object({items: z.array(z.string())}),});constchat=agent.chat({state: {items: []}});awaitchat.send('add milk');console.log(chat.state);// { items: ['milk'] } (validated against stateSchema)

4. Persistence & resuming sessions

Attach a SessionStore to make an agent server-managed: turns are persisted as snapshots and a chat can be resumed later by sessionId (no need to track snapshot IDs) or by an exact snapshotId:

import{FileSessionStore}from'@genkit-ai/ai/session-stores';constagent=ai.defineAgent({name: 'assistant',model: 'googleai/gemini-flash-latest',store: newFileSessionStore('./sessions'),});constchat=agent.chat();awaitchat.send('Remember my name is Ada.');const{ sessionId }=chat;// later, somewhere else:constresumed=awaitagent.loadChat({ sessionId });awaitresumed.send('What is my name?');// -> "Ada"

Built-in stores: InMemorySessionStore and FileSessionStore (with filesystem snapshot watching + configurable poll fallback). The SessionStore interface uses an atomic mutator-based saveSnapshot to make concurrent writes (e.g. an abort racing a completion) safe.

5. Interrupts & resume (human-in-the-loop)

When a tool interrupts, the turn finishes with finishReason: 'interrupted'. Resume by either re-running the tool (restart) or supplying the answer directly (respond):

letres=awaitchat.send('Transfer $100 to Bob');if(res.finishReason==='interrupted'){res=awaitchat.resume({respond: [confirmTransfer.respond(res,{approved: true})],});}

6. Abort, detach & graceful failure

// abort an in-flight turnconstturn=chat.sendStream('long task...');awaitchat.abort();// background / detached turns with heartbeat-based livenessconsttask=awaitchat.detach('run this in the background');forawait(conststatusoftask.watch()){/* pending -> completed/expired */}

Failures resolve gracefully (finishReason: 'failed') and preserve the last-good snapshot/state instead of corrupting the session; genuine API misuse throws AgentInitError.

7. Redact / reshape state for clients

constagent=ai.defineAgent({name: 'assistant',model: 'googleai/gemini-flash-latest',clientTransform: {state: (s)=>({ ...s,secret: undefined}),// redact at rest / in responseschunk: (c)=>c,// reshape or drop stream chunks in flight},});

8. Same API, server or browser

The server returns an AgentAPI; the client gets the exact same shape from remoteAgent(...), so UI code is identical whether it runs in-process or over HTTP:

import{remoteAgent}from'genkit/beta/client';constagent=remoteAgent({url: 'https://my-app/agents/assistant'});constchat=agent.chat();constturn=chat.sendStream('Hello from the browser');forawait(constchunkofturn.stream){render(chunk);}

What's inside

  • js/ai/src/agent.tsdefineAgent / definePromptAgent / defineCustomAgent, schemas, session runner, finish reasons, error handling.
  • js/ai/src/agent-core.ts — transport-agnostic AgentAPI / AgentChat / AgentResponse, in-process transport, custom-state patching.
  • js/ai/src/session.ts, js/ai/src/session-stores.tsSession, SessionStore, InMemorySessionStore, FileSessionStore, snapshot model + heartbeats.
  • js/ai/src/json-patch.ts — minimal, pollution-safe JSON Patch used for streaming custom-state updates.
  • js/genkit/src/client/*remoteAgent HTTP client.
  • js/genkit/src/beta.ts / genkit-beta.tsai.defineAgent etc. + ai.currentSession() exposed on the beta surface.
  • Extensive tests: agent_test.ts, agent_client_test.ts, session-stores_test.ts, json-patch_test.ts.

Finish reasons:stop, interrupted, failed, aborted, blocked, detached, unknown.
Snapshot statuses:pending, completed, failed, aborted, expired.

pavelgj added 30 commits March 11, 2026 21:07
…ectly into `ActionParams` and removing the separate `BidiActionParams` interface.
Add support for an `init` parameter in the action run pipeline to
establish long-running session states. This propagates the parameter
through the reflection server (v1 and v2), API schemas, and includes
tests for both streaming and non-streaming action invocations.
Add `init` parameter to `streamFlow` and `runFlow` client functions
and propagate it through the Express handler to action execution.
This allows passing initialization data (defined via `initSchema`)
from clients to flows and actions. Includes tests and documentation.
apascal07 added a commit that referenced this pull request Jun 22, 2026
Stamp the agent's root action span with the session ID under
"genkit:metadata:agent:sessionId" so traces from the same conversation
can be correlated. This mirrors the JS agent (PR #5251), which sets the
attribute once on the action span via setCustomMetadataAttributes; Go has
no such helper, so the genkit:metadata: prefix is inlined.
Align the per-turn span shape with JS's run() too: name "runTurn-N"
(1-indexed) with type flowStep and no subtype, replacing "agent/turn/N".
@pavelgj
pavelgj marked this pull request as ready for review June 22, 2026 23:30
Implement file-system snapshot watching for FileSessionStore using
fs.watch with a polling fallback to backstop missed events on
filesystems like network mounts. Callbacks are de-duplicated by
serialized content and transient read errors are swallowed.
Add configurable snapshotWatchPollIntervalMs option (defaulting to
2000ms) to control the polling fallback interval.
apascal07 added a commit that referenced this pull request Jun 23, 2026
Add two construction options to localstore.FileSessionStore, mirroring
the JS FileSessionStore (PR #5251):
- WithMaxPersistedChainLength(n): on each save, walk the new snapshot's
parentId chain and unlink rows past the newest n, capping
per-conversation disk use. n must be >= 1; 0 or negative is rejected.
- WithSnapshotPathPrefix(fn): derive a per-call subdirectory from
context for tenant isolation. The prefix may nest via "/" and is
sanitized against directory escape.
Snapshots remain flat by id within the prefix
(<dir>/<prefix>/<snapshotId>.json), so resume-by-snapshot, heartbeat,
finalize, abort, and status subscription stay O(1) direct opens, while
GetLatestSnapshot scans the prefix directory. With no options configured
the on-disk layout is unchanged.
Options follow the ai/option.go interface pattern (in a new option.go)
and error if set more than once.
Add a per-turn chunk buffer in SessionRunner that captures all stream
chunks emitted during a turn and stamps them onto the turn's trace span
as a serialized JSON array (agent:chunks). This ensures traces capture
everything that was streamed during each turn.
The recording is best-effort, absorbing serialization or span-context
failures so trace bookkeeping never fails an otherwise-successful turn.
Add `promptInput` option to `definePromptAgent` and `defineAgent` so a
single prompt definition can be reused and customized across multiple
agents (e.g. supplying different `role` or `tone` values). The new `I`
type parameter provides type-checking against the prompt's input schema.
Remove the per-turn chunk buffer and recordTurnChunks logic that
serialized streamed chunks onto the turn span. Instead, tag turn spans
with the persisted snapshotId and emit the resulting session state as
the turn span output, simplifying trace bookkeeping for both client- and
server-managed agents.
Add generic `Init` type parameter to fastify handlers, flow wrappers,
and helper types to support passing initialization data alongside input.
The init data is extracted from request body and forwarded to action
runs, including durable streaming.
Comment threadjs/ai/src/session-stores.ts Outdated
Comment threadjs/ai/src/json-patch.ts
Comment threadjs/ai/src/session-stores.ts
Comment threadjs/ai/src/agent.ts
Comment threadjs/ai/src/agent-core.ts
Comment threadjs/ai/src/session.ts Outdated
Comment threadjs/ai/src/agent.ts
Comment threadjs/ai/src/agent-core.ts
Comment threadjs/ai/src/agent-core.ts
Comment threadjs/ai/src/genkit-ai.ts
Comment threadjs/ai/src/session.ts Outdated
Comment threadjs/genkit/src/client/agent.ts Outdated
Comment threadjs/ai/src/agent.ts Outdated
Security:
- json-patch: reject __proto__/prototype/constructor pointer tokens to
prevent prototype pollution from server-sent patches.
- FileSessionStore: validate snapshotId is a plain basename and assert the
resolved path stays under the prefix dir (path traversal); serialize saves
with a per-file lock and write atomically (temp file + rename).
Correctness:
- agent: an aborted turn now reports finishReason 'aborted' and skips the
failed snapshot write instead of being reported as 'failed'.
- session: deep-clone initial state and return a copy from getMessages() so
handlers cannot reach back into caller/chat state.
- agent client: bail before pushing the message when the abort signal is
already aborted, and roll back the eagerly-pushed user message on a
failed/aborted turn so it isn't re-sent next turn.
Robustness:
- agent: guard the parentId walk against cyclic chains (FAILED_PRECONDITION).
- getSnapshot action now uses SessionSnapshotSchema instead of z.any().
Design:
- drop invocationEnd + the snapshot `event` field + SnapshotEventSchema to
align with the Go implementation (turn is the unit of persistence).
- rename the remote agent client path /state -> /getSnapshot (keep /abort).
- document the single-flight turn assumption and the in-order/no-loss
customPatch transport requirement.
The override was identical to the base GenkitAI.currentSession except it
threw SessionError instead of GenkitError(FAILED_PRECONDITION). Removing it
lets GenkitBeta inherit the base, collapsing to a single error type for one
method down the inheritance chain (per review feedback).
@pavelgj
pavelgj requested a review from apascal07June 24, 2026 02:57
Add a stable sessionId identifier to correlate snapshots and turns of a
conversation. The sessionId is tracked on AgentChatImpl and exposed on
both AgentChat and AgentResponse interfaces, with a fallback getter so
server-managed agents that omit sessionId on the wire still resolve it
from the chat's tracked value.
Allow specifying a human-readable description when defining a prompt
agent, which is surfaced on the agent action's metadata. The description
is now propagated from the prompt config through definePromptAgent and
defineAgent.
Base automatically changed from pj/remove-chat to mainJune 24, 2026 15:16
@pavelgj
pavelgj merged commit 510b447 into mainJun 24, 2026
13 checks passed
@pavelgj
pavelgj deleted the pj/session-flow-take-2 branch June 24, 2026 15:52
@cabljaccabljac mentioned this pull request Jul 20, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

configdocsImprovements or additions to documentationjsroot

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@pavelgj@apascal07