@jay-framework/gemini-agent-plugin 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,26 @@
1
+ name: sendMessage
2
+ description: Send a user message to the Gemini agent. Includes conversation history, page automation tool definitions, and current page state. Returns either a text response or pending tool calls.
3
+
4
+ inputSchema:
5
+ message: string
6
+ history:
7
+ - role: string
8
+ parts:
9
+ - {}
10
+ toolDefinitions:
11
+ - name: string
12
+ description: string
13
+ inputSchema: {}
14
+ category: string
15
+ pageState: {}
16
+
17
+ outputSchema:
18
+ type: string
19
+ message?: string
20
+ calls?:
21
+ - id: string
22
+ name: string
23
+ args: {}
24
+ category: string
25
+ history:
26
+ - {}
@@ -0,0 +1,29 @@
1
+ name: submitToolResults
2
+ description: Submit tool execution results back to continue the Gemini conversation. Called after the client executes page automation tools. Returns either a text response or more pending tool calls.
3
+
4
+ inputSchema:
5
+ results:
6
+ - callId: string
7
+ result: string
8
+ isError: boolean
9
+ history:
10
+ - role: string
11
+ parts:
12
+ - {}
13
+ toolDefinitions:
14
+ - name: string
15
+ description: string
16
+ inputSchema: {}
17
+ category: string
18
+ pageState: {}
19
+
20
+ outputSchema:
21
+ type: string
22
+ message?: string
23
+ calls?:
24
+ - id: string
25
+ name: string
26
+ args: {}
27
+ category: string
28
+ history:
29
+ - {}
@@ -0,0 +1,69 @@
1
+ name: gemini-chat
2
+ description: AI chat agent powered by Gemini with page automation capabilities
3
+ tags:
4
+ # Message list — repeated sub-contract for full chat history
5
+ - tag: messages
6
+ type: sub-contract
7
+ repeated: true
8
+ trackBy: index
9
+ phase: fast+interactive
10
+ tags:
11
+ - tag: index
12
+ type: data
13
+ dataType: number
14
+ - tag: role
15
+ type: [data, variant]
16
+ dataType: enum (user | assistant)
17
+ - tag: content
18
+ type: data
19
+ dataType: string
20
+
21
+ # Compact mode shortcuts — avoid iterating full list
22
+ - tag: lastUserMessage
23
+ type: data
24
+ dataType: string
25
+ phase: fast+interactive
26
+ - tag: lastAssistantMessage
27
+ type: data
28
+ dataType: string
29
+ phase: fast+interactive
30
+
31
+ # Chat input
32
+ - tag: messageInput
33
+ type: [data, interactive]
34
+ dataType: string
35
+ elementType: HTMLInputElement
36
+
37
+ # Send button
38
+ - tag: sendMessage
39
+ type: interactive
40
+ elementType: HTMLButtonElement
41
+
42
+ # Expand/collapse toggle for compact mode
43
+ - tag: toggleExpand
44
+ type: interactive
45
+ elementType: HTMLButtonElement
46
+
47
+ # State variants
48
+ - tag: isLoading
49
+ type: variant
50
+ dataType: boolean
51
+ phase: fast+interactive
52
+ - tag: isExpanded
53
+ type: variant
54
+ dataType: boolean
55
+ phase: fast+interactive
56
+ - tag: hasError
57
+ type: variant
58
+ dataType: boolean
59
+ phase: fast+interactive
60
+ - tag: hasMessages
61
+ type: variant
62
+ dataType: boolean
63
+ phase: fast+interactive
64
+
65
+ # Error message
66
+ - tag: errorMessage
67
+ type: data
68
+ dataType: string
69
+ phase: fast+interactive
@@ -0,0 +1,51 @@
1
+ import {HTMLElementCollectionProxy, HTMLElementProxy, JayContract} from "@jay-framework/runtime";
2
+
3
+
4
+ export enum Role {
5
+ user,
6
+ assistant
7
+ }
8
+
9
+ export interface MessageOfGeminiChatViewState {
10
+ index: number,
11
+ role: Role,
12
+ content: string
13
+ }
14
+
15
+ export interface GeminiChatViewState {
16
+ messages: Array<MessageOfGeminiChatViewState>,
17
+ lastUserMessage: string,
18
+ lastAssistantMessage: string,
19
+ messageInput: string,
20
+ isLoading: boolean,
21
+ isExpanded: boolean,
22
+ hasError: boolean,
23
+ hasMessages: boolean,
24
+ errorMessage: string
25
+ }
26
+
27
+ export type GeminiChatSlowViewState = {};
28
+
29
+ export type GeminiChatFastViewState = Pick<GeminiChatViewState, 'lastUserMessage' | 'lastAssistantMessage' | 'messageInput' | 'isLoading' | 'isExpanded' | 'hasError' | 'hasMessages' | 'errorMessage'> & {
30
+ messages: Array<GeminiChatViewState['messages'][number]>;
31
+ };
32
+
33
+ export type GeminiChatInteractiveViewState = Pick<GeminiChatViewState, 'lastUserMessage' | 'lastAssistantMessage' | 'messageInput' | 'isLoading' | 'isExpanded' | 'hasError' | 'hasMessages' | 'errorMessage'> & {
34
+ messages: Array<GeminiChatViewState['messages'][number]>;
35
+ };
36
+
37
+
38
+ export interface GeminiChatRefs {
39
+ messageInput: HTMLElementProxy<GeminiChatViewState, HTMLInputElement>,
40
+ sendMessage: HTMLElementProxy<GeminiChatViewState, HTMLButtonElement>,
41
+ toggleExpand: HTMLElementProxy<GeminiChatViewState, HTMLButtonElement>
42
+ }
43
+
44
+
45
+ export interface GeminiChatRepeatedRefs {
46
+ messageInput: HTMLElementCollectionProxy<GeminiChatViewState, HTMLInputElement>,
47
+ sendMessage: HTMLElementCollectionProxy<GeminiChatViewState, HTMLButtonElement>,
48
+ toggleExpand: HTMLElementCollectionProxy<GeminiChatViewState, HTMLButtonElement>
49
+ }
50
+
51
+ export type GeminiChatContract = JayContract<GeminiChatViewState, GeminiChatRefs, GeminiChatSlowViewState, GeminiChatFastViewState, GeminiChatInteractiveViewState>
@@ -0,0 +1,242 @@
1
+ import { createJayService, makeJayInit, makeJayStackComponent } from "@jay-framework/fullstack-component";
2
+ import { createSignal, createMemo } from "@jay-framework/component";
3
+ import { createActionCaller } from "@jay-framework/stack-client-runtime";
4
+ const GEMINI_SERVICE = createJayService("GeminiService");
5
+ const init = makeJayInit();
6
+ const callSendMessage = createActionCaller("geminiAgent.sendMessage", "POST");
7
+ const callSubmitToolResults = createActionCaller("geminiAgent.submitToolResults", "POST");
8
+ function buildSerializedTools(automation) {
9
+ const { interactions } = automation.getPageState();
10
+ const tools = [];
11
+ for (const group of interactions) {
12
+ const sample = group.items[0];
13
+ if (!sample)
14
+ continue;
15
+ const elementType = sample.element.constructor.name;
16
+ const isFillable = [
17
+ "HTMLInputElement",
18
+ "HTMLTextAreaElement",
19
+ "HTMLSelectElement"
20
+ ].includes(elementType);
21
+ const isCheckbox = elementType === "HTMLInputElement" && ["checkbox", "radio"].includes(sample.element.type);
22
+ const isForEach = group.items.length > 1 || sample.coordinate.length > 1;
23
+ const prefix = isCheckbox ? "toggle" : isFillable ? "fill" : "click";
24
+ const toolName = `${prefix}-${toKebab(group.refName)}`;
25
+ const humanName = toHumanReadable(group.refName);
26
+ const description = group.description || `${isCheckbox ? "Toggle" : isFillable ? "Fill" : "Click"} ${humanName}${isForEach ? " for a specific item" : ""}`;
27
+ const properties = {};
28
+ const required = [];
29
+ if (isForEach) {
30
+ properties.coordinate = {
31
+ type: "string",
32
+ description: `Item coordinate (e.g. "${sample.coordinate.join("/")}")`
33
+ };
34
+ required.push("coordinate");
35
+ }
36
+ if (isFillable && !isCheckbox) {
37
+ properties.value = {
38
+ type: "string",
39
+ description: `Value to set in ${humanName}`
40
+ };
41
+ required.push("value");
42
+ }
43
+ tools.push({
44
+ name: toolName,
45
+ description,
46
+ inputSchema: {
47
+ type: "object",
48
+ properties,
49
+ required: required.length > 0 ? required : void 0
50
+ },
51
+ category: "page-automation"
52
+ });
53
+ }
54
+ return tools;
55
+ }
56
+ function toKebab(s) {
57
+ return s.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
58
+ }
59
+ function toHumanReadable(s) {
60
+ return s.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[-_]/g, " ").toLowerCase();
61
+ }
62
+ function executePageAutomationTool(automation, call) {
63
+ try {
64
+ const { interactions } = automation.getPageState();
65
+ const toolName = call.name;
66
+ const args = call.args;
67
+ const parts = toolName.split("-");
68
+ const prefix = parts[0];
69
+ const refName = parts.slice(1).join("-");
70
+ const group = interactions.find((g) => toKebab(g.refName) === refName);
71
+ if (!group) {
72
+ return {
73
+ callId: call.id,
74
+ result: JSON.stringify({ error: `Interaction '${refName}' not found` }),
75
+ isError: true
76
+ };
77
+ }
78
+ let item = group.items[0];
79
+ if (args.coordinate && typeof args.coordinate === "string") {
80
+ const coord = args.coordinate.split("/");
81
+ item = group.items.find((i) => i.coordinate.join("/") === args.coordinate) || group.items[0];
82
+ }
83
+ if (!item) {
84
+ return {
85
+ callId: call.id,
86
+ result: JSON.stringify({ error: "Item not found" }),
87
+ isError: true
88
+ };
89
+ }
90
+ if (prefix === "fill" && args.value != null) {
91
+ const el = item.element;
92
+ const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value")?.set;
93
+ if (nativeInputValueSetter) {
94
+ nativeInputValueSetter.call(el, String(args.value));
95
+ } else {
96
+ el.value = String(args.value);
97
+ }
98
+ el.dispatchEvent(new Event("input", { bubbles: true }));
99
+ el.dispatchEvent(new Event("change", { bubbles: true }));
100
+ } else {
101
+ item.element.click();
102
+ }
103
+ const newState = automation.getPageState();
104
+ return {
105
+ callId: call.id,
106
+ result: JSON.stringify({ success: true, pageState: newState.viewState })
107
+ };
108
+ } catch (error) {
109
+ return {
110
+ callId: call.id,
111
+ result: JSON.stringify({ error: error.message }),
112
+ isError: true
113
+ };
114
+ }
115
+ }
116
+ function GeminiChatInteractive(_props, refs, fastViewState, _carryForward) {
117
+ const [getMessages, setMessages] = createSignal([]);
118
+ const [getHistory, setHistory] = createSignal([]);
119
+ const [getInputValue, setInputValue] = createSignal("");
120
+ const [getIsLoading, setIsLoading] = createSignal(false);
121
+ const [getIsExpanded, setIsExpanded] = createSignal(false);
122
+ const [getError, setError] = createSignal(null);
123
+ const hasMessages = createMemo(() => getMessages().length > 0);
124
+ const hasError = createMemo(() => getError() !== null);
125
+ const lastUserMessage = createMemo(() => {
126
+ const msgs = getMessages();
127
+ for (let i = msgs.length - 1; i >= 0; i--) {
128
+ if (msgs[i].role === "user")
129
+ return msgs[i].content;
130
+ }
131
+ return "";
132
+ });
133
+ const lastAssistantMessage = createMemo(() => {
134
+ const msgs = getMessages();
135
+ for (let i = msgs.length - 1; i >= 0; i--) {
136
+ if (msgs[i].role === "assistant")
137
+ return msgs[i].content;
138
+ }
139
+ return "";
140
+ });
141
+ const getAutomation = () => window.__jay?.automation || null;
142
+ function getToolsAndState() {
143
+ const automation = getAutomation();
144
+ if (!automation) {
145
+ return { toolDefinitions: [], pageState: {} };
146
+ }
147
+ return {
148
+ toolDefinitions: buildSerializedTools(automation),
149
+ pageState: automation.getPageState().viewState
150
+ };
151
+ }
152
+ async function handleToolCalls(output) {
153
+ while (output.type === "tool-calls") {
154
+ setHistory(output.history);
155
+ const automation = getAutomation();
156
+ if (!automation) {
157
+ setError("AutomationAPI not available");
158
+ return;
159
+ }
160
+ const results = output.calls.filter((c) => c.category === "page-automation").map((call) => executePageAutomationTool(automation, call));
161
+ const { toolDefinitions, pageState } = getToolsAndState();
162
+ const nextOutput = await callSubmitToolResults({
163
+ results,
164
+ history: output.history,
165
+ toolDefinitions,
166
+ pageState
167
+ });
168
+ output = nextOutput;
169
+ }
170
+ const finalOutput = output;
171
+ if (finalOutput.type === "response") {
172
+ setHistory(finalOutput.history);
173
+ setMessages((msgs) => [
174
+ ...msgs,
175
+ {
176
+ index: msgs.length,
177
+ role: "assistant",
178
+ content: finalOutput.message
179
+ }
180
+ ]);
181
+ }
182
+ }
183
+ async function sendMessage() {
184
+ const message = getInputValue().trim();
185
+ if (!message || getIsLoading())
186
+ return;
187
+ setError(null);
188
+ setIsLoading(true);
189
+ setInputValue("");
190
+ setMessages((msgs) => [...msgs, { index: msgs.length, role: "user", content: message }]);
191
+ try {
192
+ const { toolDefinitions, pageState } = getToolsAndState();
193
+ const output = await callSendMessage({
194
+ message,
195
+ history: getHistory(),
196
+ toolDefinitions,
197
+ pageState
198
+ });
199
+ await handleToolCalls(output);
200
+ } catch (error) {
201
+ setError(error.message || "Failed to send message");
202
+ } finally {
203
+ setIsLoading(false);
204
+ }
205
+ }
206
+ refs.sendMessage.onclick(sendMessage);
207
+ refs.toggleExpand.onclick(() => {
208
+ setIsExpanded((v) => !v);
209
+ });
210
+ refs.messageInput.oninput((jayEvent) => {
211
+ setInputValue(jayEvent.event.target.value);
212
+ });
213
+ refs.messageInput.onkeydown((jayEvent) => {
214
+ if (jayEvent.event.key === "Enter" && !jayEvent.event.shiftKey) {
215
+ jayEvent.event.preventDefault();
216
+ sendMessage();
217
+ }
218
+ });
219
+ return {
220
+ render: () => ({
221
+ messages: () => getMessages().map((m) => ({
222
+ index: m.index,
223
+ role: m.role,
224
+ content: m.content
225
+ })),
226
+ lastUserMessage,
227
+ lastAssistantMessage,
228
+ messageInput: getInputValue,
229
+ isLoading: getIsLoading,
230
+ isExpanded: getIsExpanded,
231
+ hasError,
232
+ hasMessages,
233
+ errorMessage: () => getError() || ""
234
+ })
235
+ };
236
+ }
237
+ const geminiChat = makeJayStackComponent().withProps().withInteractive(GeminiChatInteractive);
238
+ export {
239
+ GEMINI_SERVICE,
240
+ geminiChat,
241
+ init
242
+ };
@@ -0,0 +1,244 @@
1
+ import * as _jay_framework_fullstack_component from '@jay-framework/fullstack-component';
2
+ import { Signals } from '@jay-framework/fullstack-component';
3
+ import * as _google_genai from '@google/genai';
4
+ import { PluginSetupContext, PluginSetupResult, ActionMetadata } from '@jay-framework/stack-server-runtime';
5
+ import * as _jay_framework_component from '@jay-framework/component';
6
+ import { HTMLElementProxy } from '@jay-framework/runtime';
7
+
8
+ /**
9
+ * Shared type definitions for the Gemini agent plugin.
10
+ */
11
+ interface GeminiMessage {
12
+ role: 'user' | 'model';
13
+ parts: GeminiPart[];
14
+ }
15
+ type GeminiPart = GeminiTextPart | GeminiFunctionCallPart | GeminiFunctionResponsePart;
16
+ interface GeminiTextPart {
17
+ text: string;
18
+ }
19
+ interface GeminiFunctionCallPart {
20
+ functionCall: {
21
+ name: string;
22
+ args: Record<string, unknown>;
23
+ };
24
+ }
25
+ interface GeminiFunctionResponsePart {
26
+ functionResponse: {
27
+ name: string;
28
+ response: Record<string, unknown>;
29
+ };
30
+ }
31
+ interface SerializedToolDef {
32
+ name: string;
33
+ description: string;
34
+ inputSchema: {
35
+ type: 'object';
36
+ properties: Record<string, any>;
37
+ required?: string[];
38
+ };
39
+ category: 'page-automation';
40
+ }
41
+ interface SendMessageInput {
42
+ /** User's message text */
43
+ message: string;
44
+ /** Full conversation history (Gemini format) */
45
+ history: GeminiMessage[];
46
+ /** Page automation tool definitions (serialized from client) */
47
+ toolDefinitions: SerializedToolDef[];
48
+ /** Current page state snapshot */
49
+ pageState: object;
50
+ }
51
+ interface PendingToolCall {
52
+ id: string;
53
+ name: string;
54
+ args: Record<string, unknown>;
55
+ category: 'page-automation' | 'server-action';
56
+ }
57
+ type SendMessageOutput = {
58
+ type: 'response';
59
+ message: string;
60
+ history: GeminiMessage[];
61
+ } | {
62
+ type: 'tool-calls';
63
+ calls: PendingToolCall[];
64
+ history: GeminiMessage[];
65
+ };
66
+ interface SubmitToolResultsInput {
67
+ /** Tool execution results */
68
+ results: ToolCallResult[];
69
+ /** Full conversation history (including the tool call turn) */
70
+ history: GeminiMessage[];
71
+ /** Resend tool definitions (page state may have changed) */
72
+ toolDefinitions: SerializedToolDef[];
73
+ /** Fresh page state (may have changed after tool execution) */
74
+ pageState: object;
75
+ }
76
+ interface ToolCallResult {
77
+ callId: string;
78
+ /** JSON-stringified result */
79
+ result: string;
80
+ isError?: boolean;
81
+ }
82
+ type SubmitToolResultsOutput = SendMessageOutput;
83
+ interface GeminiFunctionDeclaration {
84
+ name: string;
85
+ description: string;
86
+ parameters: {
87
+ type: 'object';
88
+ properties: Record<string, any>;
89
+ required?: string[];
90
+ };
91
+ }
92
+ interface GeminiServiceConfig {
93
+ apiKey: string;
94
+ model: string;
95
+ systemPrompt?: string;
96
+ }
97
+
98
+ declare class GeminiService {
99
+ private config;
100
+ private client;
101
+ constructor(config: GeminiServiceConfig);
102
+ get model(): string;
103
+ get systemPromptPrefix(): string | undefined;
104
+ generateWithTools(messages: GeminiMessage[], tools: GeminiFunctionDeclaration[], systemPrompt: string): Promise<_google_genai.GenerateContentResponse>;
105
+ }
106
+
107
+ declare const GEMINI_SERVICE: _jay_framework_fullstack_component.ServiceMarker<GeminiService>;
108
+ declare const init: _jay_framework_fullstack_component.JayInitBuilderWithServer<{}>;
109
+
110
+ /**
111
+ * Setup handler for gemini-agent plugin.
112
+ *
113
+ * Creates config/.gemini.yaml template if missing, validates the API key.
114
+ */
115
+
116
+ declare function setupGeminiAgent(ctx: PluginSetupContext): Promise<PluginSetupResult>;
117
+
118
+ /**
119
+ * Main entry point for chat messages.
120
+ *
121
+ * Receives the user message, full conversation history, tool definitions,
122
+ * and page state. Returns either a text response or pending tool calls.
123
+ */
124
+ declare const sendMessage: _jay_framework_fullstack_component.JayAction<SendMessageInput, SendMessageOutput> & _jay_framework_fullstack_component.JayActionDefinition<SendMessageInput, SendMessageOutput, [GeminiService]>;
125
+ /**
126
+ * Continue after client executes page automation tools.
127
+ *
128
+ * Receives tool execution results, the updated history, and fresh page state.
129
+ * Returns either a text response or more pending tool calls.
130
+ */
131
+ declare const submitToolResults: _jay_framework_fullstack_component.JayAction<SubmitToolResultsInput, SendMessageOutput> & _jay_framework_fullstack_component.JayActionDefinition<SubmitToolResultsInput, SendMessageOutput, [GeminiService]>;
132
+
133
+ /**
134
+ * Configuration for the Gemini agent plugin.
135
+ */
136
+ interface GeminiAgentConfig {
137
+ /** Gemini API key */
138
+ apiKey: string;
139
+ /** Model name (default: gemini-2.0-flash) */
140
+ model: string;
141
+ /** Optional system prompt prefix prepended to the generated system prompt */
142
+ systemPrompt?: string;
143
+ }
144
+
145
+ declare enum Role {
146
+ user,
147
+ assistant
148
+ }
149
+
150
+ interface MessageOfGeminiChatViewState {
151
+ index: number,
152
+ role: Role,
153
+ content: string
154
+ }
155
+
156
+ interface GeminiChatViewState {
157
+ messages: Array<MessageOfGeminiChatViewState>,
158
+ lastUserMessage: string,
159
+ lastAssistantMessage: string,
160
+ messageInput: string,
161
+ isLoading: boolean,
162
+ isExpanded: boolean,
163
+ hasError: boolean,
164
+ hasMessages: boolean,
165
+ errorMessage: string
166
+ }
167
+
168
+ type GeminiChatSlowViewState = {};
169
+
170
+ type GeminiChatFastViewState = Pick<GeminiChatViewState, 'lastUserMessage' | 'lastAssistantMessage' | 'messageInput' | 'isLoading' | 'isExpanded' | 'hasError' | 'hasMessages' | 'errorMessage'> & {
171
+ messages: Array<GeminiChatViewState['messages'][number]>;
172
+ };
173
+
174
+ type GeminiChatInteractiveViewState = Pick<GeminiChatViewState, 'lastUserMessage' | 'lastAssistantMessage' | 'messageInput' | 'isLoading' | 'isExpanded' | 'hasError' | 'hasMessages' | 'errorMessage'> & {
175
+ messages: Array<GeminiChatViewState['messages'][number]>;
176
+ };
177
+
178
+
179
+ interface GeminiChatRefs {
180
+ messageInput: HTMLElementProxy<GeminiChatViewState, HTMLInputElement>,
181
+ sendMessage: HTMLElementProxy<GeminiChatViewState, HTMLButtonElement>,
182
+ toggleExpand: HTMLElementProxy<GeminiChatViewState, HTMLButtonElement>
183
+ }
184
+
185
+ interface GeminiChatProps {
186
+ }
187
+ interface ChatCarryForward {
188
+ }
189
+ declare const geminiChat: _jay_framework_fullstack_component.JayStackComponentDefinition<GeminiChatRefs, GeminiChatSlowViewState, GeminiChatFastViewState, GeminiChatInteractiveViewState, [], [Signals<GeminiChatFastViewState>, ChatCarryForward], GeminiChatProps, {}, _jay_framework_component.JayComponentCore<GeminiChatProps, GeminiChatInteractiveViewState>>;
190
+
191
+ /**
192
+ * Tool Bridge — converts jay-stack tool descriptors and action metadata
193
+ * to Gemini FunctionDeclarations.
194
+ *
195
+ * Only actions with .jay-action metadata are exposed to the AI agent.
196
+ * Page automation tools (from AutomationAPI) are always included.
197
+ */
198
+
199
+ /**
200
+ * Converts page automation tools and server actions into Gemini function declarations.
201
+ *
202
+ * - Client tools are included directly (already have schema).
203
+ * - Server actions are only included if they have .jay-action metadata.
204
+ * Actions without metadata are silently skipped (opt-in mechanism).
205
+ */
206
+ declare function toGeminiTools(clientTools: SerializedToolDef[], serverActions: Array<{
207
+ actionName: string;
208
+ metadata: ActionMetadata;
209
+ }>): GeminiFunctionDeclaration[];
210
+ /**
211
+ * Maps a Gemini function call name back to its original action name.
212
+ * Reverses the `action_` prefix and `_` → `.` replacement.
213
+ */
214
+ declare function resolveToolCallTarget(toolName: string, clientToolNames: Set<string>): {
215
+ category: 'page-automation';
216
+ name: string;
217
+ } | {
218
+ category: 'server-action';
219
+ name: string;
220
+ };
221
+
222
+ /**
223
+ * System Prompt Builder — constructs the system prompt for Gemini.
224
+ *
225
+ * Page state and available server actions are included as context
226
+ * (not tools), so the LLM always knows the current state without
227
+ * wasting tool calls.
228
+ */
229
+ interface ServerActionSummary {
230
+ name: string;
231
+ description?: string;
232
+ }
233
+ /**
234
+ * Builds the system prompt for a Gemini conversation turn.
235
+ *
236
+ * The prompt includes:
237
+ * 1. Custom prefix (from config) or default greeting
238
+ * 2. Current page state as JSON context
239
+ * 3. List of available server actions with descriptions
240
+ * 4. Instructions for tool use
241
+ */
242
+ declare function buildSystemPrompt(pageState: object, serverActions: ServerActionSummary[], customPrefix?: string): string;
243
+
244
+ export { GEMINI_SERVICE, type GeminiAgentConfig, type GeminiFunctionCallPart, type GeminiFunctionDeclaration, type GeminiFunctionResponsePart, type GeminiMessage, type GeminiPart, GeminiService, type GeminiServiceConfig, type GeminiTextPart, type PendingToolCall, type SendMessageInput, type SendMessageOutput, type SerializedToolDef, type ServerActionSummary, type SubmitToolResultsInput, type SubmitToolResultsOutput, type ToolCallResult, buildSystemPrompt, geminiChat, init, resolveToolCallTarget, sendMessage, setupGeminiAgent, submitToolResults, toGeminiTools };
package/dist/index.js ADDED
@@ -0,0 +1,376 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
+ var __publicField = (obj, key, value) => {
4
+ __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
5
+ return value;
6
+ };
7
+ import { createJayService, makeJayInit, makeJayAction, makeJayStackComponent, RenderPipeline } from "@jay-framework/fullstack-component";
8
+ import { registerService, actionRegistry } from "@jay-framework/stack-server-runtime";
9
+ import * as fs from "fs";
10
+ import * as path from "path";
11
+ import * as yaml from "js-yaml";
12
+ import { Type, GoogleGenAI } from "@google/genai";
13
+ const CONFIG_FILE_NAME$1 = ".gemini.yaml";
14
+ const DEFAULT_MODEL = "gemini-2.0-flash";
15
+ function loadConfig() {
16
+ const configPath = path.join(process.cwd(), "config", CONFIG_FILE_NAME$1);
17
+ if (!fs.existsSync(configPath)) {
18
+ throw new Error(
19
+ `Gemini config file not found at: ${configPath}
20
+ Run "jay-stack setup gemini-agent" to create it.`
21
+ );
22
+ }
23
+ const fileContents = fs.readFileSync(configPath, "utf8");
24
+ const config = yaml.load(fileContents);
25
+ if (!config) {
26
+ throw new Error("Gemini config file is empty or invalid");
27
+ }
28
+ if (config.apiKey === void 0 || config.apiKey === null) {
29
+ throw new Error('Config validation failed: "apiKey" is required in .gemini.yaml');
30
+ }
31
+ if (typeof config.apiKey !== "string" || config.apiKey.trim() === "") {
32
+ throw new Error('Config validation failed: "apiKey" must be a non-empty string');
33
+ }
34
+ if (config.apiKey.startsWith("<")) {
35
+ throw new Error(
36
+ 'Config validation failed: "apiKey" still has placeholder value. Replace it with your Gemini API key.'
37
+ );
38
+ }
39
+ return {
40
+ apiKey: config.apiKey,
41
+ model: config.model || DEFAULT_MODEL,
42
+ systemPrompt: config.systemPrompt || void 0
43
+ };
44
+ }
45
+ const TYPE_MAP = {
46
+ object: Type.OBJECT,
47
+ string: Type.STRING,
48
+ number: Type.NUMBER,
49
+ integer: Type.INTEGER,
50
+ boolean: Type.BOOLEAN,
51
+ array: Type.ARRAY
52
+ };
53
+ function toGeminiSchema(schema) {
54
+ const result = {};
55
+ if (schema.type) {
56
+ result.type = TYPE_MAP[schema.type] || Type.STRING;
57
+ }
58
+ if (schema.description) {
59
+ result.description = schema.description;
60
+ }
61
+ if (schema.enum) {
62
+ result.enum = schema.enum;
63
+ }
64
+ if (schema.items) {
65
+ result.items = toGeminiSchema(schema.items);
66
+ }
67
+ if (schema.properties) {
68
+ result.properties = {};
69
+ for (const [key, value] of Object.entries(schema.properties)) {
70
+ result.properties[key] = toGeminiSchema(value);
71
+ }
72
+ }
73
+ if (schema.required) {
74
+ result.required = schema.required;
75
+ }
76
+ return result;
77
+ }
78
+ function toSdkDeclaration(decl) {
79
+ return {
80
+ name: decl.name,
81
+ description: decl.description,
82
+ parameters: toGeminiSchema(decl.parameters)
83
+ };
84
+ }
85
+ class GeminiService {
86
+ constructor(config) {
87
+ __publicField(this, "client");
88
+ this.config = config;
89
+ this.client = new GoogleGenAI({ apiKey: config.apiKey });
90
+ }
91
+ get model() {
92
+ return this.config.model;
93
+ }
94
+ get systemPromptPrefix() {
95
+ return this.config.systemPrompt;
96
+ }
97
+ async generateWithTools(messages, tools, systemPrompt) {
98
+ const sdkDeclarations = tools.map(toSdkDeclaration);
99
+ const response = await this.client.models.generateContent({
100
+ model: this.config.model,
101
+ contents: messages,
102
+ config: {
103
+ systemInstruction: systemPrompt,
104
+ tools: sdkDeclarations.length > 0 ? [{ functionDeclarations: sdkDeclarations }] : void 0
105
+ }
106
+ });
107
+ return response;
108
+ }
109
+ }
110
+ const GEMINI_SERVICE = createJayService("GeminiService");
111
+ const init = makeJayInit().withServer(() => {
112
+ const config = loadConfig();
113
+ const service = new GeminiService({
114
+ apiKey: config.apiKey,
115
+ model: config.model,
116
+ systemPrompt: config.systemPrompt
117
+ });
118
+ registerService(GEMINI_SERVICE, service);
119
+ console.log(`[gemini-agent] Initialized (model: ${config.model})`);
120
+ return {};
121
+ });
122
+ const CONFIG_FILE_NAME = ".gemini.yaml";
123
+ const CONFIG_TEMPLATE = `# Gemini Agent Configuration
124
+ #
125
+ # This file contains credentials for the Gemini AI agent plugin.
126
+ # Get your API key from: https://aistudio.google.com/apikey
127
+ #
128
+ # IMPORTANT: This file contains secrets. Add config/.gemini.yaml to .gitignore.
129
+
130
+ # Required: Your Gemini API key
131
+ apiKey: "<your-gemini-api-key>"
132
+
133
+ # Optional: Model name (default: gemini-2.0-flash)
134
+ # model: gemini-2.0-flash
135
+
136
+ # Optional: Custom system prompt prefix (prepended to the generated system prompt)
137
+ # systemPrompt: "You are a helpful assistant for this web application."
138
+ `;
139
+ async function setupGeminiAgent(ctx) {
140
+ const configPath = path.join(ctx.configDir, CONFIG_FILE_NAME);
141
+ if (!fs.existsSync(configPath)) {
142
+ if (!fs.existsSync(ctx.configDir)) {
143
+ fs.mkdirSync(ctx.configDir, { recursive: true });
144
+ }
145
+ fs.writeFileSync(configPath, CONFIG_TEMPLATE, "utf-8");
146
+ return {
147
+ status: "needs-config",
148
+ configCreated: [`config/${CONFIG_FILE_NAME}`],
149
+ message: "Fill in your Gemini API key and re-run: jay-stack setup gemini-agent"
150
+ };
151
+ }
152
+ try {
153
+ const configContent = fs.readFileSync(configPath, "utf-8");
154
+ const config = yaml.load(configContent);
155
+ if (!config) {
156
+ return {
157
+ status: "error",
158
+ message: `Config file is empty: config/${CONFIG_FILE_NAME}`
159
+ };
160
+ }
161
+ const apiKey = config.apiKey || "";
162
+ const hasPlaceholder = apiKey.startsWith("<");
163
+ const isEmpty = !apiKey;
164
+ if (hasPlaceholder || isEmpty) {
165
+ return {
166
+ status: "needs-config",
167
+ message: `Config has placeholder value. Set your Gemini API key in config/${CONFIG_FILE_NAME}`
168
+ };
169
+ }
170
+ if (ctx.initError) {
171
+ return {
172
+ status: "error",
173
+ message: `Gemini initialization failed: ${ctx.initError.message}`
174
+ };
175
+ }
176
+ return {
177
+ status: "configured",
178
+ message: `Gemini agent configured (model: ${config.model || "gemini-2.0-flash"})`
179
+ };
180
+ } catch (error) {
181
+ return {
182
+ status: "error",
183
+ message: `Failed to read config: ${error.message}`
184
+ };
185
+ }
186
+ }
187
+ function toGeminiTools(clientTools, serverActions) {
188
+ const tools = [];
189
+ for (const tool of clientTools) {
190
+ tools.push({
191
+ name: tool.name,
192
+ description: tool.description,
193
+ parameters: tool.inputSchema
194
+ });
195
+ }
196
+ for (const { actionName, metadata } of serverActions) {
197
+ tools.push({
198
+ name: `action_${actionName.replace(/\./g, "_")}`,
199
+ description: metadata.description,
200
+ parameters: metadata.inputSchema
201
+ });
202
+ }
203
+ return tools;
204
+ }
205
+ function resolveToolCallTarget(toolName, clientToolNames) {
206
+ if (clientToolNames.has(toolName)) {
207
+ return { category: "page-automation", name: toolName };
208
+ }
209
+ if (toolName.startsWith("action_")) {
210
+ const actionName = toolName.slice("action_".length).replace(/_/g, ".");
211
+ return { category: "server-action", name: actionName };
212
+ }
213
+ return { category: "page-automation", name: toolName };
214
+ }
215
+ function buildSystemPrompt(pageState, serverActions, customPrefix) {
216
+ const parts = [
217
+ customPrefix || "You are a helpful assistant for this web application.",
218
+ "",
219
+ "## Current Page State",
220
+ JSON.stringify(pageState, null, 2),
221
+ ""
222
+ ];
223
+ if (serverActions.length > 0) {
224
+ parts.push("## Available Server Actions");
225
+ for (const action of serverActions) {
226
+ parts.push(`- ${action.name}${action.description ? `: ${action.description}` : ""}`);
227
+ }
228
+ parts.push("");
229
+ }
230
+ parts.push(
231
+ "Use the provided tools to interact with the page and call server actions.",
232
+ "After using tools, describe what you did to the user.",
233
+ "The page state above is refreshed each turn — use it to understand what the user sees."
234
+ );
235
+ return parts.join("\n");
236
+ }
237
+ async function processGeminiTurn(service, history, tools, systemPrompt, clientToolNames) {
238
+ const response = await service.generateWithTools(history, tools, systemPrompt);
239
+ const candidate = response.candidates?.[0];
240
+ if (!candidate?.content?.parts) {
241
+ return {
242
+ type: "response",
243
+ message: "I apologize, but I was unable to generate a response.",
244
+ history
245
+ };
246
+ }
247
+ const parts = candidate.content.parts;
248
+ const functionCalls = parts.filter(
249
+ (p) => p.functionCall != null
250
+ );
251
+ const responseParts = candidate.content.parts;
252
+ if (functionCalls.length === 0) {
253
+ const textParts = parts.filter((p) => p.text != null);
254
+ const message = textParts.map((p) => p.text).join("");
255
+ const updatedHistory2 = [
256
+ ...history,
257
+ { role: "model", parts: responseParts }
258
+ ];
259
+ return {
260
+ type: "response",
261
+ message,
262
+ history: updatedHistory2
263
+ };
264
+ }
265
+ const updatedHistory = [...history, { role: "model", parts: responseParts }];
266
+ const pendingClientCalls = [];
267
+ const serverCallResults = [];
268
+ for (const fc of functionCalls) {
269
+ const target = resolveToolCallTarget(fc.functionCall.name, clientToolNames);
270
+ if (target.category === "server-action") {
271
+ const result = await actionRegistry.execute(target.name, fc.functionCall.args);
272
+ serverCallResults.push({
273
+ functionResponse: {
274
+ name: fc.functionCall.name,
275
+ response: result.success ? { result: result.data } : { error: result.error?.message || "Unknown error" }
276
+ }
277
+ });
278
+ } else {
279
+ pendingClientCalls.push({
280
+ id: fc.functionCall.name,
281
+ name: fc.functionCall.name,
282
+ args: fc.functionCall.args || {},
283
+ category: "page-automation"
284
+ });
285
+ }
286
+ }
287
+ if (pendingClientCalls.length > 0) {
288
+ [
289
+ ...pendingClientCalls,
290
+ // Server actions already executed — include as completed calls
291
+ ...serverCallResults.map((r) => ({
292
+ id: r.functionResponse.name,
293
+ name: r.functionResponse.name,
294
+ args: {},
295
+ category: "server-action"
296
+ }))
297
+ ];
298
+ return {
299
+ type: "tool-calls",
300
+ calls: pendingClientCalls,
301
+ history: updatedHistory
302
+ };
303
+ }
304
+ const historyWithResults = [
305
+ ...updatedHistory,
306
+ { role: "user", parts: serverCallResults }
307
+ ];
308
+ return processGeminiTurn(service, historyWithResults, tools, systemPrompt, clientToolNames);
309
+ }
310
+ async function handleConversation(service, history, toolDefinitions, pageState) {
311
+ const serverActions = actionRegistry.getActionsWithMetadata();
312
+ const tools = toGeminiTools(toolDefinitions, serverActions);
313
+ const serverActionSummaries = serverActions.map((a) => ({
314
+ name: a.actionName,
315
+ description: a.metadata.description
316
+ }));
317
+ const systemPrompt = buildSystemPrompt(
318
+ pageState,
319
+ serverActionSummaries,
320
+ service.systemPromptPrefix
321
+ );
322
+ const clientToolNames = new Set(toolDefinitions.map((t) => t.name));
323
+ return processGeminiTurn(service, history, tools, systemPrompt, clientToolNames);
324
+ }
325
+ const sendMessage = makeJayAction("geminiAgent.sendMessage").withServices(GEMINI_SERVICE).withHandler(async (input, service) => {
326
+ const { message, history, toolDefinitions, pageState } = input;
327
+ const updatedHistory = [
328
+ ...history,
329
+ { role: "user", parts: [{ text: message }] }
330
+ ];
331
+ return handleConversation(service, updatedHistory, toolDefinitions, pageState);
332
+ });
333
+ const submitToolResults = makeJayAction("geminiAgent.submitToolResults").withServices(GEMINI_SERVICE).withHandler(async (input, service) => {
334
+ const { results, history, toolDefinitions, pageState } = input;
335
+ const functionResponses = results.map((r) => ({
336
+ functionResponse: {
337
+ name: r.callId,
338
+ response: r.isError ? { error: r.result } : JSON.parse(r.result)
339
+ }
340
+ }));
341
+ const updatedHistory = [
342
+ ...history,
343
+ { role: "user", parts: functionResponses }
344
+ ];
345
+ return handleConversation(service, updatedHistory, toolDefinitions, pageState);
346
+ });
347
+ async function fastRender() {
348
+ const Pipeline = RenderPipeline.for();
349
+ return Pipeline.ok({}).toPhaseOutput(() => ({
350
+ viewState: {
351
+ messages: [],
352
+ lastUserMessage: "",
353
+ lastAssistantMessage: "",
354
+ messageInput: "",
355
+ isLoading: false,
356
+ isExpanded: false,
357
+ hasError: false,
358
+ hasMessages: false,
359
+ errorMessage: ""
360
+ },
361
+ carryForward: {}
362
+ }));
363
+ }
364
+ const geminiChat = makeJayStackComponent().withProps().withFastRender(fastRender);
365
+ export {
366
+ GEMINI_SERVICE,
367
+ GeminiService,
368
+ buildSystemPrompt,
369
+ geminiChat,
370
+ init,
371
+ resolveToolCallTarget,
372
+ sendMessage,
373
+ setupGeminiAgent,
374
+ submitToolResults,
375
+ toGeminiTools
376
+ };
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@jay-framework/gemini-agent-plugin",
3
+ "version": "0.12.0",
4
+ "type": "module",
5
+ "license": "Apache-2.0",
6
+ "description": "Gemini AI agent plugin for jay-stack — embedded chat agent with page automation and server action calling",
7
+ "main": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "exports": {
10
+ ".": "./dist/index.js",
11
+ "./client": "./dist/index.client.js",
12
+ "./gemini-chat.jay-contract": "./dist/contracts/gemini-chat.jay-contract",
13
+ "./send-message.jay-action": "./dist/actions/send-message.jay-action",
14
+ "./submit-tool-results.jay-action": "./dist/actions/submit-tool-results.jay-action",
15
+ "./plugin.yaml": "./plugin.yaml"
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "plugin.yaml"
20
+ ],
21
+ "scripts": {
22
+ "build": "npm run definitions && npm run build:client && npm run build:server && npm run build:copy-contract && npm run build:types",
23
+ "build:client": "vite build",
24
+ "build:server": "vite build --ssr",
25
+ "build:copy-contract": "mkdir -p dist/contracts && cp lib/contracts/*.jay-contract* dist/contracts/ && mkdir -p dist/actions && cp lib/actions/*.jay-action dist/actions/",
26
+ "build:types": "tsup lib/index.ts --dts-only --format esm",
27
+ "build:check-types": "tsc",
28
+ "definitions": "jay-cli definitions lib",
29
+ "clean": "rimraf dist",
30
+ "confirm": "npm run clean && npm run build && npm run build:check-types && npm run test",
31
+ "test": "vitest run",
32
+ "test:watch": "vitest"
33
+ },
34
+ "dependencies": {
35
+ "@google/genai": "^0.14.0",
36
+ "@jay-framework/component": "^0.12.0",
37
+ "@jay-framework/fullstack-component": "^0.12.0",
38
+ "@jay-framework/runtime": "^0.12.0",
39
+ "@jay-framework/runtime-automation": "^0.12.0",
40
+ "@jay-framework/stack-client-runtime": "^0.12.0",
41
+ "@jay-framework/stack-server-runtime": "^0.12.0",
42
+ "js-yaml": "^4.1.0"
43
+ },
44
+ "devDependencies": {
45
+ "@jay-framework/compiler-jay-stack": "^0.12.0",
46
+ "@jay-framework/dev-environment": "^0.12.0",
47
+ "@jay-framework/jay-cli": "^0.12.0",
48
+ "@types/js-yaml": "^4.0.9",
49
+ "@types/node": "^22.15.21",
50
+ "rimraf": "^5.0.5",
51
+ "tsup": "^8.0.1",
52
+ "typescript": "^5.3.3",
53
+ "vite": "^5.0.11",
54
+ "vitest": "^1.2.1"
55
+ }
56
+ }
package/plugin.yaml ADDED
@@ -0,0 +1,15 @@
1
+ name: gemini-agent
2
+ global: true
3
+ contracts:
4
+ - name: gemini-chat
5
+ contract: gemini-chat.jay-contract
6
+ component: geminiChat
7
+ description: AI chat agent powered by Gemini with page automation capabilities
8
+ actions:
9
+ - name: sendMessage
10
+ action: send-message.jay-action
11
+ - name: submitToolResults
12
+ action: submit-tool-results.jay-action
13
+ setup:
14
+ handler: setupGeminiAgent
15
+ description: Configure Gemini API key