GASADK: Agent Development Kit for Google Apps Script. Build AI agents with Gemini API, MCP, A2A, Agent Skills, Human-in-the-Loop (HITL) suspension/resumption, token quota safeguards, and dynamic Google API MCP servers.
SaferSkills independently audited adk-gas (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
GASADK Diagram
Welcome to GASADK, the ultimate Agent Development Kit (ADK) designed specifically for the Google Apps Script (GAS) environment.
Operating within the strict constraints of Google Apps Script—specifically the absolute 6-minute execution limit and synchronous blocking networking—demands an architecture that discards the optimistic assumptions of traditional Node.js environments. GASADK is a highly engineered survival architecture. Inspired by the @google/adk, this initial release of GASADK introduces the heavily optimized LlmAgent. It replaces unbounded, recursive ReAct loops with a deterministic, phase-separated orchestration model, fundamentally solving context bloat, execution latency, and API quota exhaustion.
[!TIP] 🚀 Looking to turn your Google Workspace APIs into a Model Context Protocol (MCP) server? Check out our new sample project: Google API MCP Server Guide to instantly expose all Google Workspace APIs (Sheets, Drive, Docs, Calendar, Gmail, etc.) as dynamic MCP tools.
At the core of GASADK is the LlmAgent, powered by the Planner-Executor-Synthesizer (PES) architecture. It utilizes Directed Acyclic Graphs (DAGs) to orchestrate complex task delegations across external tools, Model Context Protocol (MCP) servers, Agent-to-Agent (A2A) networks, and file-based Agent Skills.
If the LLM Planner determines that external capabilities are unnecessary for a given prompt (e.g., standard conversational queries), the execution and synthesis phases are completely bypassed. The agent returns a direct response, aggressively slashing API latency and token consumption by avoiding redundant tool queueing.
Even when the Fast-Track attempts to bypass execution, if developers enforce an outputSchema (strict JSON formatting), GASADK intelligently intercepts the bypass. It routes the output through a dummy task directly into the Synthesizer to mathematically guarantee adherence to the requested JSON schema under all circumstances.
LLMs suffer from temporal blindness—they cannot natively resolve relative time like "tomorrow" or "last week". GASADK intercepts the system prompt and injects a hardcoded new Date() absolute anchor. The Planner autonomously converts relative requests into absolute ISO 8601 timestamps before pinging external tools (MCP/A2A), completely eliminating date-resolution errors on remote servers.
When external servers or massive Google Drive files return tens of thousands of characters, feeding them directly into the context window triggers a fatal 400 Payload Too Large error. GASADK enforces a strict maxResultLength threshold (default 20,000 chars), automatically truncating overflow data. It favors partial data over catastrophic runtime crashes.
Unlike standard ADKs that rely on a continuous ReAct loop for every step, GASADK plans an entire DAG upfront. Only if a node in the DAG execution fails does the system trigger a Re-Plan. It discards the unexecuted queue, analyzes the failure report, and generates an alternative DAG utilizing different tools.
Maintains and propagates conversation history dynamically to sub-agents, MCP servers, and remote A2A servers without polluting the core logic history. Massive internal intermediate LLM reasoning steps (function calls, planning thoughts) are filtered out, constructing a clean user/model role-based chat history to prevent token bloat and quota exhaustion.
Allows server functions to forcefully bypass the server-side LLM synthesis loop (by returning _gemini_halt: true). This prevents endless generative loops, eliminates unnecessary token usage, and guarantees instant response times for purely algorithmic/computational tool executions.
Bypasses the entire multi-phase LLM mock orchestration when directRouting is flagged and a single target card is assigned, dispatching the JSON-RPC request natively to slash network latency. It also supports local pre-fetched Agent Cards through a2aServerAgentCardJSONs to completely bypass remote HTTP fetches.
Supports explicit log propagation from the orchestrator down to sub-clients (MCPApp and A2AApp), storing logs inside dedicated, isolated Sheets (raw, MCP, A2A, MCPA2Aserver_log) dynamically. It guarantees thread-safe writes using script lock protection under high-concurrency environments.
Resolves compilation ReferenceError during global script initialization in Google Apps Script by removing unbound variables (accessKey, webAppsUrl) from the global context of agentCard_ToolsForMCPServer.js and instead using runtime shadow cloning for context safety.
@google/adk) Paradigm Shift| Feature | TypeScript ADK (@google/adk) | GASADK |
|---|---|---|
| Execution Model | Recursive ReAct Loop (Step-by-Step). | Phase-separated DAG execution. |
| I/O Networking | Asynchronous I/O, local stdio, WebSockets. | Synchronous, thread-blocking HTTP (UrlFetchApp). |
| Concurrency | Highly parallelized (Promise.all). | Strictly sequential execution to prevent quota burnout. |
| Failure Handling | Optimistic: Infinite loops possible. | Pessimistic: Hard aborts at 280s to prevent 6-min kill. |
| State Protection | In-memory session tracking. | Infrastructure-level locking via LockService. |
The execution lifecycle of LlmAgent is rigorously compartmentalized. The diagram below details the exact chronological flow from the moment agent.run() is invoked to the final synthesized response.
graph TD
%% Styling Definitions
classDef userReq fill:#f9f,stroke:#333,stroke-width:2px,color:#000;
classDef core fill:#bbf,stroke:#333,stroke-width:2px,color:#000;
classDef llm fill:#fbb,stroke:#333,stroke-width:2px,color:#000;
classDef external fill:#bfb,stroke:#333,stroke-width:2px,color:#000;
classDef decision fill:#ff9,stroke:#333,stroke-width:2px,color:#000;
classDef safeguard fill:#f66,stroke:#333,stroke-width:2px,color:#000;
%% Nodes
Start(["User: agent.run(prompt)"])
InitCaps["Initialize Capabilities<br>(Tools, MCP, A2A, Skills)"]
InjectTime["Inject Temporal Context<br>System Time Anchor"]
Planner["LLM: Planner Phase<br>Generate DAG & JSON Schema"]
FastTrackDec{"requires_<br>capabilities<br>== false?"}
SchemaDec{"outputSchema<br>defined?"}
ReturnDirect(["Return direct_answer<br>Execution Bypassed"])
PopTask["Pop Task from planQueue"]
CheckTime{"Elapsed Time ><br>timeoutMs<br>(280s)?"}
TimeoutAbort["Trigger Safe Abort<br>Stop Queue"]
InjectContext["Inject 'depends_on'<br>Context to Prompt"]
ExecRouter{"Capability<br>Type?"}
ExecMCP["MCP Server Client"]
ExecA2A["A2A Server Client"]
ExecSkill["Agent Skill LLM Call"]
ExecNative["Native Function Calling"]
CheckErr{"Execution<br>Error?"}
Truncate["Payload Truncation<br>> maxResultLength"]
SaveResult["Save to taskResults"]
CheckEmpty{"planQueue<br>Empty?"}
CheckReplan{"replanCount <<br>maxReplans?"}
DropQueue["Discard Remaining DAG"]
Replanner["LLM: Dynamic Re-Planner<br>Avoid Failed Method"]
SynthPhase["LLM: Final Synthesis<br>Analyze Gathered Data"]
End(["Return Final Answer"])
A2A_Registry[("A2A App")]
MCP_Servers[("MCP Servers")]
GDrive[("Google Drive")]
%% Edges
Start --> InitCaps
InitCaps -. "Fetch Agent Cards" .-> A2A_Registry
InitCaps -. "tools/list Request" .-> MCP_Servers
InitCaps -. "Read .md Skills" .-> GDrive
InitCaps --> InjectTime --> Planner
Planner --> FastTrackDec
FastTrackDec -- "Yes (Fast-Track)" --> SchemaDec
SchemaDec -- "No (Raw output fine)" --> ReturnDirect
SchemaDec -- "Yes (Intercept)" --> SynthPhase
FastTrackDec -- "No (Capabilities required)" --> PopTask
PopTask --> CheckTime
CheckTime -- "Timeout Exceeded" --> TimeoutAbort
TimeoutAbort --> SynthPhase
CheckTime -- "Safe" --> InjectContext
InjectContext --> ExecRouter
ExecRouter -- "MCP Server" --> ExecMCP
ExecRouter -- "A2A Server" --> ExecA2A
ExecRouter -- "Agent Skill" --> ExecSkill
ExecRouter -- "Native/Built-in" --> ExecNative
ExecMCP --> CheckErr
ExecA2A --> CheckErr
ExecSkill --> CheckErr
ExecNative --> CheckErr
CheckErr -- "Success" --> Truncate --> SaveResult
SaveResult --> CheckEmpty
CheckEmpty -- "No" --> PopTask
CheckEmpty -- "Yes" --> SynthPhase
CheckErr -- "Failed" --> CheckReplan
CheckReplan -- "Yes (Can Replan)" --> DropQueue --> Replanner
Replanner --> PopTask
CheckReplan -- "No (Max Replans)" --> SaveResult
SynthPhase --> End
%% Assign Classes
class Start,ReturnDirect,End userReq;
class InitCaps,InjectTime,PopTask,InjectContext,ExecNative,SaveResult core;
class Planner,ExecSkill,Replanner,SynthPhase llm;
class ExecMCP,ExecA2A,A2A_Registry,MCP_Servers,GDrive external;
class FastTrackDec,SchemaDec,CheckTime,ExecRouter,CheckErr,CheckEmpty,CheckReplan decision;
class TimeoutAbort,Truncate,DropQueue safeguard;GASADK acts as a universal adapter, normalizing disparate protocols into a unified schema for the Planner.
tools/list) and invoke tools on external servers using the Model Context Protocol.LlmAgent locally. Delegate complex cognitive sub-tasks without corrupting the main orchestrator's context..md) files inside Google Drive. A native GAS hack for distributed, RAG-like prompt injection.CodeExecutor and native GoogleSearch capabilities.GASADK integrates multiple high-performance GAS libraries under the hood. You can use it as a standalone library or copy the source code directly.
GASADK is constructed by the following scripts.
1w2mwhWQd4_6rom-UBRPD8gayBoqGH_87awSBVqGI8DdaQI_pOeSuGYDuGASADK.After GASADK was installed, you can use it as follows.
const { LlmAgent, MCPA2Aserver, FileSearch } = GASADK;All objects are the class objects.
You can also directly use GeminiWithFiles, A2AApp, and MCPApp like const { LlmAgent, GeminiWithFiles, MCPA2Aserver, FileSearch, GeminiWithFiles, A2AApp, MCPApp } = GASADK.
If you want to directly use GASADK by including all scripts in a Google Apps Script project, please copy and paste the following script. The following script includes all required scripts for using GASADK.
https://github.com/tanaikech/adk-gas/blob/master/dist/GASADK.js
In this case, you can directly use the class objects. So, you are not required to set const { LlmAgent, GeminiWithFiles, MCPA2Aserver, FileSearch, GeminiWithFiles, A2AApp, MCPApp } = GASADK.
LlmAgent Configuration APIThe new LlmAgent(config) constructor accepts an extensive configuration object to dictate agent behavior and safety parameters.
| Parameter | Type | Required | Description |
|---|---|---|---|
apiKey | String | Yes | Your Gemini API Key. |
name | String | No | Internal name of the agent. Defaults to "Agent". |
description | String | No | Agent description. Critical for parent orchestrators utilizing Sub-Agents. |
model | String | No | The Gemini model. Defaults to "models/gemini-3.1-flash-lite". |
instruction | String/Object | No | Global system instruction. Supports {var_name} interpolation. |
state | Object | No | Key-value mapping for dynamic state variables. Replaces {var_name}. |
tools | Array | No | Array of native GAS functions mapped to the tool schema. |
mcpServers | Array | No | Array of external MCP Server URLs or JSON objects (for custom server routing) for dynamic capability discovery. |
a2aServerAgentCardURLs | Array | No | Array of remote Agent Card URLs or JSON objects (for custom server routing) for A2A collaboration. |
a2aServerAgentCardJSONs | Array | No | Array of local pre-fetched Agent Card JSON objects (supports custom name aliases) to bypass HTTP card retrieval. |
subAgents | Array | No | Array of child LlmAgent instances for hierarchical delegation. |
skillFolderId | String | No | Google Drive Folder ID containing .md files for Agent Skills. |
codeExecutor | Object | No | Configuration object to enable Python execution Built-in capabilities. |
googleSearch | Object | No | Configuration object to enable the Built-in Google Search tool. |
maxReplans | Number | No | Maximum dynamic Re-Plan attempts on execution failure. Defaults to 2. |
timeoutMs | Number | No | Milliseconds before triggering a forced abort to evade the GAS 6-minute kill switch. Defaults to 280000 (280s). |
maxResultLength | Number | No | Maximum allowed string length per execution before truncation. Prevents payload crashes. Defaults to 20000. |
outputSchema | Object | No | Strict JSON Schema declaration. Forces the Synthesizer to format the output, disabling the direct Fast-Track bypass. |
logSpreadsheetId | String | No | Google Spreadsheet ID to activate multi-channel logging. Automatically propagates down to MCPApp and A2AApp. |
From v1.3.0, you can specify custom user-defined server names in mcpServers and a2aServerAgentCardURLs by using a JSON object instead of a simple string URL. This custom name is injected into the LLM context to ensure accurate routing when the user references specific servers by their aliases.
#### Format:
"https://example.com/mcp"{ "custom-server-alias": { "httpUrl": "https://example.com/mcp" } }#### Example:
const agent = new LlmAgent({
apiKey: API_KEY,
mcpServers: [
"https://basic.mcp.example.com", // Standard string URL
{
"server-trigger-test-project1": { // Custom server name
httpUrl: "https://script.google.com/macros/s/{deploymentID}/exec?accessKey=sample"
}
}
],
a2aServerAgentCardURLs: [
{
"my-custom-a2a-agent": { // Custom A2A server name
httpUrl: "https://script.google.com/macros/s/{deploymentID}/exec/.well-known/agent-card.json?accessKey=sample"
}
}
]
});From v1.3.1, you can pass pre-fetched Agent Card JSON objects directly to a2aServerAgentCardJSONs. This completely bypasses the HTTP card retrieval process, resolving the network latency overhead. When combined with directRouting, GASADK automatically bypasses the internal multi-step LLM planning proxy layers, executing direct JSON-RPC dispatch to the remote agent.
#### Example:
const agent = new LlmAgent({
apiKey: API_KEY,
a2aServerAgentCardJSONs: [
{
"local-cached-agent": { // Custom server name
name: "CachedAgent",
url: "https://script.google.com/macros/s/{deploymentID}/exec",
description: "Directly loaded JSON agent card.",
skills: [
{
id: "get_exchange_rate",
name: "Currency Exchange Rates Tool",
description: "Helps with exchange values",
inputModes: ["text/plain"],
outputModes: ["text/plain"]
}
]
}
}
]
});From v1.3.3, you can activate thread-safe, multi-channel logging simply by providing a logSpreadsheetId in the LlmAgent configuration. When configured, this property is automatically propagated down to downstream MCPApp and A2AApp instances.
The library validates and automatically creates the following isolated sheets inside the designated spreadsheet:
serializeEvent_) to prevent cyclical structure reference crashes.All sheet creation and row insertions are protected by thread-safe LockService structures, preventing data loss or layout errors during high-concurrency parallel executions.
#### Example:
const agent = new LlmAgent({
apiKey: API_KEY,
logSpreadsheetId: "YOUR_LOG_SPREADSHEET_ID_HERE", // Set the ID here
mcpServers: ["https://example.com/mcp-server"],
a2aServerAgentCardURLs: ["https://example.com/a2a-server/.well-known/agent-card.json"]
});setServices({ lock, properties })Mandatory for safe execution. Binds LockService.getScriptLock() and PropertiesService to the agent. Prevents catastrophic state corruption and race conditions when multiple webhooks execute concurrently.
run(prompt, logCallback)Executes the orchestrator. Takes the user prompt and an optional logCallback function to emit real-time telemetry on DAG planning and execution. Returns the final synthesized string, or a strictly formatted JSON object if outputSchema was defined.
setHistory(history)Sets the conversation history for the agent, enabling multi-turn context retention across runs. The history must be an array of objects compatible with GeminiWithFiles.
getHistory()Retrieves the current conversation history.
getAgentInf()An introspection tool that returns an array of all internally normalized capabilities (from Native tools, MCP, A2A, and Skills) bound to the agent.
When you try to test the following sample script, please set your API key for using Gemini API to the property GEMINI_API_KEY of Google Apps Script project.
A basic conversational initialization.
// Remove destructuring if you copied the scripts directly.
const { LlmAgent } = GASADK;
function test_quickstart() {
const properties = PropertiesService.getScriptProperties();
const API_KEY = properties.getProperty("GEMINI_API_KEY");
const agent = new LlmAgent({
apiKey: API_KEY,
name: "HelperAgent",
model: "models/gemini-3.1-flash-lite",
instruction: "You are a helpful AI assistant.",
});
agent.setServices({
lock: LockService.getScriptLock(),
properties: properties,
});
// Because no tools are required, the Fast-Track bypass routes this directly.
const response = agent.run("Hello, who are you?", (logEntry) => {
console.log(`[Log ${logEntry.timestamp}] ${logEntry.message}`);
});
console.log(response);
}In the following script, the second argument receives the detailed real-time log as a callback function.
const response = agent.run("Hello, who are you?", (logEntry) => {
console.log(`[Log ${logEntry.timestamp}] ${logEntry.message}`);
});Subagents are used.
// Remove destructuring if you copied the scripts directly.
const { LlmAgent } = GASADK;
function test_subagents() {
const properties = PropertiesService.getScriptProperties();
const API_KEY = properties.getProperty("GEMINI_API_KEY");
const translator = new LlmAgent({
apiKey: API_KEY,
name: "Translator",
description: "Translates any given text to German.",
instruction: "Translate the provided text to German precisely.",
model: MODEL_NAME,
}).setServices({ lock: LockService.getScriptLock(), properties: properties });
const mainAgent = new LlmAgent({
apiKey: API_KEY,
name: "Orchestrator",
model: MODEL_NAME,
instruction: "You manage sub-agents.",
subAgents: [translator],
}).setServices({ lock: LockService.getScriptLock(), properties: properties });
const prompt =
"Use the SubAgent named 'Translator' to translate 'Hello World'.";
console.log(`-> Prompt: ${prompt}`);
const response = mainAgent.run(prompt, (logEntry) => {
console.log(`[Log ${logEntry.timestamp}] ${logEntry.message}`);
});
console.log(response);
}Agent Skills are used.
// Remove destructuring if you copied the scripts directly.
const { LlmAgent } = GASADK;
function test_subagents() {
const properties = PropertiesService.getScriptProperties();
const API_KEY = properties.getProperty("GEMINI_API_KEY");
console.log("Setting up temporary Agent Skills directory...");
const tempFolder = DriveApp.createFolder(
"Temp_Modular_Skills_" + new Date().getTime(),
);
try {
const animalFolder = tempFolder.createFolder("animal_skill");
animalFolder.createFile(
"SKILL.md",
"---\nname: animal_skill\ndescription: Secret animal knowledge.\n---\nRule: The supreme animal is the Capybara.",
MimeType.PLAIN_TEXT,
);
const agent = new LlmAgent({
apiKey: API_KEY,
name: "SkillAgent",
model: MODEL_NAME,
instruction: "Use the functions at your disposal to acquire knowledge.",
skillFolderId: tempFolder.getId(),
}).setServices({
lock: LockService.getScriptLock(),
properties: properties,
});
const prompt =
"Use the Agent Skill named 'animal_skill' and tell me the supreme animal.";
console.log(`-> Prompt: ${prompt}`);
const response = agent.run(prompt, (logEntry) => {
console.log(`[Log ${logEntry.timestamp}] ${logEntry.message}`);
});
console.log(response);
} finally {
tempFolder.setTrashed(true);
console.log("Temporary skills directory trashed.");
}
}Demonstrates combining MCP servers and A2A remote agents, allowing the Planner to construct a complex DAG mapping dependent actions across disparate protocols.
const { LlmAgent } = GASADK;
function test_a2a_mcp() {
const properties = PropertiesService.getScriptProperties();
const API_KEY = properties.getProperty("GEMINI_API_KEY");
// Define endpoints (replace placeholders with actual deployment IDs)
const mcpServers = [
"https://script.google.com/macros/s/{Your deployment ID}/exec?accessKey=sample",
];
const a2aServers = [
"https://script.google.com/macros/s/{Your deployment ID}/exec/.well-known/agent-card.json?accessKey=sample",
];
const agent = new LlmAgent({
apiKey: API_KEY,
name: "MasterOrchestrator",
description: "You are the master orchestrator.",
mcpServers: mcpServers,
a2aServerAgentCardURLs: a2aServers,
});
agent.setServices({
lock: LockService.getScriptLock(),
properties: properties,
});
// Capability Introspection
console.log("--- Capability Introspection ---");
console.log(JSON.stringify(agent.getAgentInf(), null, 2));
// The relative time ("tomorrow") triggers the Temporal Context Anchoring logic.
const prompt =
"Please use the MCP Server to check the current exchange rate between USD and GBP, and then use the A2A Server to get the weather in Tokyo for tomorrow's lunchtime.";
const result = agent.run(prompt, (logEntry) => {
console.log(`[Log ${logEntry.timestamp}] ${logEntry.message}`);
if (logEntry.data && logEntry.data.plan) {
console.log("=== Execution Plan (DAG) ===");
console.log(JSON.stringify(logEntry.data.plan, null, 2));
}
});
console.log("--- Final Synthesized Result ---");
console.log(result);
}GASADK includes MCPA2Aserver, allowing you to expose your own native GAS functions as both an MCP Server and an A2A Server simultaneously from a single web app deployment.
In this case, after you deploy Web Apps, set `WEB_APPS_URL` by replacing with your Web Apps URL and deploy Web Apps to reflect the latest script.
const { MCPA2Aserver } = GASADK;
const API_KEY =
PropertiesService.getScriptProperties().getProperty("GEMINI_API_KEY");
const WEB_APPS_URL =
"https://script.google.com/macros/s/{Your deployment ID}/exec";
const object = {
apiKey: API_KEY,
model: "models/gemini-3.1-flash-lite",
accessKey: "sample",
};
const doGet = (e) => main(e);
const doPost = (e) => main(e);
function main(e) {
const lock = LockService.getScriptLock();
const context = createServerContext_(); // Defines functions and agentCard
const m = new MCPA2Aserver();
m.setServices({ lock: lock });
m.apiKey = object.apiKey;
m.model = object.model;
// Pre-configure server-side injected history (System Context/Persona)
m.setHistory([
{
role: "user",
parts: [{ text: "System Context Override: You are an elite financial API node named OMEGA-SERVER, located securely in Tokyo." }]
},
{
role: "model",
parts: [{ text: "Understood. My secret access code is OMEGA-99." }]
}
]);
// Force enablement of both server protocols
m.a2a = true;
m.mcp = true;
m.accessKey = object.accessKey;
const res = m.main(e, context, (log) => {
console.log(`[${log.level}] ${log.timestamp} - ${log.message}`);
});
return res;
}
function createServerContext_() {
const functions = {
params_: {
get_exchange_rate: {
description: "Get current exchange rate.",
parameters: {
type: "object",
properties: {
currency_from: { type: "string" },
currency_to: { type: "string" },
currency_date: { type: "string" },
},
required: ["currency_from", "currency_to", "currency_date"],
},
},
chat_and_identity: {
description: "Answer general conversation, identity, location, and secret code questions based on the chat history.",
parameters: {
type: "object",
properties: {
message: { type: "string", description: "The complete, detailed response message." }
},
required: ["message"]
}
}
},
get_exchange_rate: (args) => {
// Internal execution logic here...
const res = `Rate from ${args.currency_from} to ${args.currency_to} is 0.82.`;
const returnObj = {
mcp: {
jsonrpc: "2.0",
result: { content: [{ type: "text", text: res }], isError: false },
},
a2a: { result: res },
};
// [Fast-Track Halt Optimization]: Return immediately to bypass secondary LLM synthesis loops
return {
...returnObj,
_gemini_halt: true,
items: { functionResponse: returnObj }
};
},
chat_and_identity: (args) => {
const res = args.message || "I have processed your chat request.";
const returnObj = {
mcp: {
jsonrpc: "2.0",
result: { content: [{ type: "text", text: res }], isError: false },
},
a2a: { result: res },
};
return {
...returnObj,
_gemini_halt: true,
items: { functionResponse: returnObj }
};
}
};
const agentCard = {
name: "API Manager",
description: "Provide management for using various APIs and handle conversational queries.",
url: WEB_APPS_URL + `?accessKey=${object.accessKey}`,
skills: [
{ id: "get_exchange_rate", name: "Exchange Rate Tool" },
{ id: "chat_and_identity", name: "Chat and Identity" }
],
};
return { functions, agentCard };
}GASADK supports seamless multi-turn conversation tracking across the orchestrator, remote agents, and Consolidated servers.
const { LlmAgent } = GASADK;
function test_chat_history() {
const properties = PropertiesService.getScriptProperties();
const API_KEY = properties.getProperty("GEMINI_API_KEY");
const agent = new LlmAgent({
apiKey: API_KEY,
name: "ChattyAgent",
model: "models/gemini-3.1-flash-lite",
}).setServices({ lock: LockService.getScriptLock(), properties: properties });
// 1. Manually seed/restore conversation history
agent.setHistory([
{ role: "user", parts: [{ text: "Hello, my project code is XRAY-7." }] },
{ role: "model", parts: [{ text: "Acknowledged. I have recorded your project code as XRAY-7." }] }
]);
// 2. Run query referencing the context
const response = agent.run("What is my project code?");
console.log("Response:", response); // Output will resolve using the history
// 3. Inspect updated history
console.log("Updated History:", JSON.stringify(agent.getHistory(), null, 2));
}By combining GASADK with the GoogleApiApp library, you can build a stateless proxy that dynamically exposes Google Workspace services (Sheets, Drive, Docs, Calendar, Gmail, Slides, etc.) to any LLM-powered agent as local MCP tools.
For the full implementation script and a beginner-friendly setup guide, see: 👉 [Google API MCP Server Sample & Setup Guide](https://github.com/tanaikech/adk-gas/tree/master/samples/googleapiapp-mcp-server)
GASADK introduces a comprehensive Hooks system fully compliant with the Gemini CLI specifications. Developers can intercept the execution lifecycle of the agent, apply validations, inject context, mock responses, or enforce safety guardrails.
The system supports the following lifecycle events:
Below is a practical implementation of BeforeTool hooks to construct safety guardrails. It prevents an autonomous agent from executing dangerous operations (e.g., sending emails to external addresses or deleting critical folders) by validating model-generated arguments at the tool execution boundary.
#### 1. Defining the Guardrail Hook This GAS function intercepts Gmail transmission and Drive folder deletion:
function checkWorkspaceSafetyGuardrail(input) {
const toolInput = input.tool_input || {};
// Gmail Safety Check: Block external domains
if (input.tool_name === "gmail_send_email") {
const toAddress = toolInput.to || "";
if (!toAddress || !toAddress.endsWith("@mycompany.com")) {
return {
decision: "deny",
reason: "Blocked: Sending mail to external domains (" + toAddress + ") is restricted."
};
}
}
// Google Drive Safety Check: Prevent critical asset deletion
if (input.tool_name === "drive_delete_file") {
const fileId = toolInput.fileId || "";
if (fileId === "protected_root_id") {
return {
decision: "deny",
reason: "Blocked: Deletion of the protected root resource is strictly prohibited."
};
}
}
return { decision: "allow" };
}#### 2. Registering Hooks in the Agent Config Instantiate the LlmAgent with the safety hook registered under the BeforeTool event:
const agent = new LlmAgent({
apiKey: API_KEY,
name: "SecureWorkspaceAgent",
hooks: {
"BeforeTool": [
{
"matcher": "gmail_send_email|drive_delete_file", // RegExp tool matching
"type": "gas_function",
"functionName": "checkWorkspaceSafetyGuardrail"
}
]
},
tools: [
{
name: "gmail_send_email",
description: "Send emails to targets. Argument 'to' must contain the email address.",
parameters: {
type: "object",
properties: { to: { type: "string" }, body: { type: "string" } },
required: ["to", "body"]
},
function: (args) => {
// Actual Gmail transmission code
return "Email sent successfully to " + args.to;
}
},
{
name: "drive_delete_file",
description: "Delete files or folders. Argument 'fileId' must contain the ID.",
parameters: {
type: "object",
properties: { fileId: { type: "string" } },
required: ["fileId"]
},
function: (args) => {
// Actual Drive deletion code
return "File " + args.fileId + " deleted successfully.";
}
}
]
}).setServices({
lock: LockService.getScriptLock(),
properties: PropertiesService.getScriptProperties(),
globalContext: this // Exposes the global checkWorkspaceSafetyGuardrail function
});When the agent runs, any attempt to call gmail_send_email with an external domain, or drive_delete_file with the protected ID will be immediately intercepted and blocked.
GASADK v2.0.0 introduces Human-in-the-Loop (HITL) capability. When a hook function returns a decision of "suspend", the agent immediately saves its execution state (including prompt, plan, task results, history, and log traces) to script properties under HITL_STATE_[sessionId], then aborts the loop by throwing a "SUSPENDED" exception.
This allows developers to pause execution, solicit user confirmation (e.g., via Email, Slack, or a Web App form), and resume execution from the exact task that was suspended.
#### 1. Defining a Suspension Hook Return decision: "suspend" inside a BeforeTool hook to request human approval:
function checkGmailApproval(input) {
if (input.tool_name === "gmail_send_email") {
// Suspend execution for human review
return {
decision: "suspend",
reason: "Requires human review before sending email to " + (input.tool_input?.to || "recipient")
};
}
return { decision: "allow" };
}#### 2. Catching the Suspension and Resuming Run the agent, catch the suspension, and resume later using the resume() method:
const agent = new LlmAgent({
apiKey: API_KEY,
name: "ApprovalAgent",
hooks: {
"BeforeTool": [
{
"matcher": "gmail_send_email",
"type": "gas_function",
"functionName": "checkGmailApproval"
}
]
},
tools: [...]
}).setServices({
lock: LockService.getScriptLock(),
properties: PropertiesService.getScriptProperties(),
globalContext: this // Exposes checkGmailApproval
});
const sessionId = agent.sessionId;
try {
agent.run("Use gmail_send_email to email [email protected]");
} catch (err) {
if (err.message.includes("SUSPENDED")) {
console.log("Execution suspended. Session ID: " + sessionId);
// Send approval link/email to human
} else {
throw err;
}
}
// --- Later (e.g. from a Web App doGet/doPost approval trigger) ---
function approveSession(sessionId) {
const resumeAgent = new LlmAgent({
apiKey: API_KEY,
name: "ApprovalAgent",
tools: [...]
}).setServices({
lock: LockService.getScriptLock(),
properties: PropertiesService.getScriptProperties()
});
// Resume execution with 'allow' decision
const result = resumeAgent.resume(sessionId, "allow");
console.log("Resumed Final Result: " + result);
}To prevent runaway loops or budget overrun, GASADK automatically tracks token usage per session. You can enforce a strict token limit using the maxTokensPerSession option.
const agent = new LlmAgent({
apiKey: API_KEY,
maxTokensPerSession: 50000 // Limit session to 50k tokens
});.gitignoremcpServers and agentCardUrls.LlmAgent, A2AApp, and MCPA2Aserver.A2AApp (v2.6.0) to filter intermediate LLM reasoning steps from conversation history, preventing token bloat.MCPA2Aserver._gemini_halt) to bypass synthesis loops on explicit server functions.mcpServers and a2aServerAgentCardURLs.a2aServerAgentCardJSONs support to bypass HTTP fetching of Agent Cards, allowing direct JSON injection.directRouting optimization in A2AApp v2.7.0) to route requests directly to remote agents, slashing network and orchestration latency when specific target agents are known.GeminiWithFiles to v2.0.30.MCPApp and A2AApp via logSpreadsheetId.raw, MCP, A2A, and MCPA2Aserver_log) inside the specified log spreadsheet._getOrCreateSheet helper with LockService validation across MCPApp, A2AApp, and MCPA2Aserver to prevent concurrent sheet insertion conflicts.serializeEvent_) to bypass cyclical structure errors when writing to the raw sheet.[MCP Server Error], [A2A Client Error]).accessKey and webAppsUrl from the global namespace in agentCard_ToolsForMCPServer.js.MCPA2Aserver.js to dynamically inject the resolved Web App URL context at execution time, ensuring context safety.PreCompress and Notification).session_id, transcript_path, cwd, hook_event_name, timestamp) into hook payloads.DeployMcpServer.js) under samples/googleapiapp-mcp-server to expose the GoogleApiApp library.maxTokensPerSession configuration.hookManager and sessionId) and token constraints down to child sub-agents during hierarchical delegation.decision: "suspend" decisions in BeforeTool hooks. Seamlessly serializes and stores task state to PropertiesService (under HITL_STATE_[sessionId]) and throws a "SUSPENDED" exception. Added saveState(), loadState(sessionId), and resume(sessionId, decision, approvedArgs) methods to recover and run suspended agent execution loops.GeminiWithFiles.js, A2AApp.js, MCPApp.js, MCPA2Aserver.js, and FileSearchApp.js) to support the GasHookManager hooks system natively (allowing interceptors like BeforeModel, AfterModel, BeforeTool, and AfterTool).models/gemini-3.1-flash-lite across all workspace libraries, tests, and examples to provide modern, cost-effective defaults.GeminiWithFiles_GAS_lib, A2AApp_GAS_lib, MCPApp_GAS_lib, MCPA2Aserver_GAS_lib, FileSearchApp_GAS_lib) with next-version tags.~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.