$ -weight: 500;">npm i --save-exact @google/adk
-weight: 500;">npm i --save-dev --save-exact @google/adk-devtools
-weight: 500;">npm i --save-exact nodemailer
-weight: 500;">npm i --save-dev --save-exact @types/nodemailer rimraf
-weight: 500;">npm i --save-exact marked
-weight: 500;">npm i --save-exact zod
-weight: 500;">npm i --save-exact @google/adk
-weight: 500;">npm i --save-dev --save-exact @google/adk-devtools
-weight: 500;">npm i --save-exact nodemailer
-weight: 500;">npm i --save-dev --save-exact @types/nodemailer rimraf
-weight: 500;">npm i --save-exact marked
-weight: 500;">npm i --save-exact zod
-weight: 500;">npm i --save-exact @google/adk
-weight: 500;">npm i --save-dev --save-exact @google/adk-devtools
-weight: 500;">npm i --save-exact nodemailer
-weight: 500;">npm i --save-dev --save-exact @types/nodemailer rimraf
-weight: 500;">npm i --save-exact marked
-weight: 500;">npm i --save-exact zod
GEMINI_MODEL_NAME="gemini-3-flash-preview"
GOOGLE_CLOUD_PROJECT="<Google Cloud Project ID>"
GOOGLE_CLOUD_LOCATION="global" GOOGLE_GENAI_USE_VERTEXAI=TRUE # SMTP Settings (MailHog)
SMTP_HOST="localhost"
SMTP_PORT=1025
SMTP_USER=""
SMTP_PASS=""
SMTP_FROM="no-reply@test.local"
ADMIN_EMAIL="[email protected]"
GEMINI_MODEL_NAME="gemini-3-flash-preview"
GOOGLE_CLOUD_PROJECT="<Google Cloud Project ID>"
GOOGLE_CLOUD_LOCATION="global" GOOGLE_GENAI_USE_VERTEXAI=TRUE # SMTP Settings (MailHog)
SMTP_HOST="localhost"
SMTP_PORT=1025
SMTP_USER=""
SMTP_PASS=""
SMTP_FROM="no-reply@test.local"
ADMIN_EMAIL="[email protected]"
GEMINI_MODEL_NAME="gemini-3-flash-preview"
GOOGLE_CLOUD_PROJECT="<Google Cloud Project ID>"
GOOGLE_CLOUD_LOCATION="global" GOOGLE_GENAI_USE_VERTEXAI=TRUE # SMTP Settings (MailHog)
SMTP_HOST="localhost"
SMTP_PORT=1025
SMTP_USER=""
SMTP_PASS=""
SMTP_FROM="no-reply@test.local"
ADMIN_EMAIL="[email protected]"
services: mailhog: image: mailhog/mailhog container_name: mailhog ports: - "1025:1025" # SMTP port - "8025:8025" # HTTP (Web UI) port -weight: 500;">restart: always networks: - decision-tree-agent-network networks: decision-tree-agent-network:
services: mailhog: image: mailhog/mailhog container_name: mailhog ports: - "1025:1025" # SMTP port - "8025:8025" # HTTP (Web UI) port -weight: 500;">restart: always networks: - decision-tree-agent-network networks: decision-tree-agent-network:
services: mailhog: image: mailhog/mailhog container_name: mailhog ports: - "1025:1025" # SMTP port - "8025:8025" # HTTP (Web UI) port -weight: 500;">restart: always networks: - decision-tree-agent-network networks: decision-tree-agent-network:
-weight: 500;">docker compose up -d
-weight: 500;">docker compose up -d
-weight: 500;">docker compose up -d
process.loadEnvFile(); const model = process.env.GEMINI_MODEL_NAME || '';
if (!model) { throw new Error('GEMINI_MODEL_NAME is not set');
}
process.loadEnvFile(); const model = process.env.GEMINI_MODEL_NAME || '';
if (!model) { throw new Error('GEMINI_MODEL_NAME is not set');
}
process.loadEnvFile(); const model = process.env.GEMINI_MODEL_NAME || '';
if (!model) { throw new Error('GEMINI_MODEL_NAME is not set');
}
export const RECOMMENDATION_KEY = 'recommendation';
export const MERGED_RESULTS_KEY = 'mergedResults';
export const PROJECT_DESCRIPTION_KEY = 'project_description';
export const RECOMMENDATION_KEY = 'recommendation';
export const MERGED_RESULTS_KEY = 'mergedResults';
export const PROJECT_DESCRIPTION_KEY = 'project_description';
export const RECOMMENDATION_KEY = 'recommendation';
export const MERGED_RESULTS_KEY = 'mergedResults';
export const PROJECT_DESCRIPTION_KEY = 'project_description';
import { FunctionTool, LlmAgent, SequentialAgent } from '@google/adk';
import { z } from 'zod';
import { initWorkflowAgent } from './init.js';
import { MERGED_RESULTS_KEY, PROJECT_DESCRIPTION_KEY, VALIDATION_ATTEMPTS_KEY,
} from './sub-agents/output-keys.js'; const prepareEvaluationTool = new FunctionTool({ name: 'prepare_evaluation', parameters: z.object({ description: z.string() }), execute: async ({ description }, context) => { if (!context || !context.state) { return { -weight: 500;">status: 'ERROR', message: 'No session state found.' }; } const state = context.state; // Clear all previous evaluation data state.set(MERGED_RESULTS_KEY, null); state.set(VALIDATION_ATTEMPTS_KEY, 0); // Set the new description state.set(PROJECT_DESCRIPTION_KEY, description); return { -weight: 500;">status: 'SUCCESS', message: 'State reset and description updated.' }; },
}); export const SequentialEvaluationAgent = new SequentialAgent({ name: 'SequentialEvaluationAgent', subAgents: initWorkflowAgent(model),
});
import { FunctionTool, LlmAgent, SequentialAgent } from '@google/adk';
import { z } from 'zod';
import { initWorkflowAgent } from './init.js';
import { MERGED_RESULTS_KEY, PROJECT_DESCRIPTION_KEY, VALIDATION_ATTEMPTS_KEY,
} from './sub-agents/output-keys.js'; const prepareEvaluationTool = new FunctionTool({ name: 'prepare_evaluation', parameters: z.object({ description: z.string() }), execute: async ({ description }, context) => { if (!context || !context.state) { return { -weight: 500;">status: 'ERROR', message: 'No session state found.' }; } const state = context.state; // Clear all previous evaluation data state.set(MERGED_RESULTS_KEY, null); state.set(VALIDATION_ATTEMPTS_KEY, 0); // Set the new description state.set(PROJECT_DESCRIPTION_KEY, description); return { -weight: 500;">status: 'SUCCESS', message: 'State reset and description updated.' }; },
}); export const SequentialEvaluationAgent = new SequentialAgent({ name: 'SequentialEvaluationAgent', subAgents: initWorkflowAgent(model),
});
import { FunctionTool, LlmAgent, SequentialAgent } from '@google/adk';
import { z } from 'zod';
import { initWorkflowAgent } from './init.js';
import { MERGED_RESULTS_KEY, PROJECT_DESCRIPTION_KEY, VALIDATION_ATTEMPTS_KEY,
} from './sub-agents/output-keys.js'; const prepareEvaluationTool = new FunctionTool({ name: 'prepare_evaluation', parameters: z.object({ description: z.string() }), execute: async ({ description }, context) => { if (!context || !context.state) { return { -weight: 500;">status: 'ERROR', message: 'No session state found.' }; } const state = context.state; // Clear all previous evaluation data state.set(MERGED_RESULTS_KEY, null); state.set(VALIDATION_ATTEMPTS_KEY, 0); // Set the new description state.set(PROJECT_DESCRIPTION_KEY, description); return { -weight: 500;">status: 'SUCCESS', message: 'State reset and description updated.' }; },
}); export const SequentialEvaluationAgent = new SequentialAgent({ name: 'SequentialEvaluationAgent', subAgents: initWorkflowAgent(model),
});
export function initWorkflowAgent(model: string) { return [ createMergerAgent(model), createEmailAgent(), ];
}
export function initWorkflowAgent(model: string) { return [ createMergerAgent(model), createEmailAgent(), ];
}
export function initWorkflowAgent(model: string) { return [ createMergerAgent(model), createEmailAgent(), ];
}
export const rootAgent = new LlmAgent({ name: 'ProjectEvaluationAgent', model, instruction: `... instruction...`, tools: [prepareEvaluationTool], subAgents: [SequentialEvaluationAgent],
});
export const rootAgent = new LlmAgent({ name: 'ProjectEvaluationAgent', model, instruction: `... instruction...`, tools: [prepareEvaluationTool], subAgents: [SequentialEvaluationAgent],
});
export const rootAgent = new LlmAgent({ name: 'ProjectEvaluationAgent', model, instruction: `... instruction...`, tools: [prepareEvaluationTool], subAgents: [SequentialEvaluationAgent],
});
export type SmtpConfig = { host: string; port: number; user?: string; pass?: string; from: string; email: string;
};
export type SmtpConfig = { host: string; port: number; user?: string; pass?: string; from: string; email: string;
};
export type SmtpConfig = { host: string; port: number; user?: string; pass?: string; from: string; email: string;
};
export function createEmailAgent(): BaseAgent { const email = process.env.ADMIN_EMAIL || '[email protected]'; const host = process.env.SMTP_HOST || 'localhost'; const port = parseInt(process.env.SMTP_PORT || '1025'); const user = process.env.SMTP_USER || ''; const pass = process.env.SMTP_PASS || ''; const from = process.env.SMTP_FROM || 'no-reply@test.local'; const smtpConfig: SmtpConfig = { host, port, user, pass, from, email, }; return new EmailAgent(smtpConfig);
}
export function createEmailAgent(): BaseAgent { const email = process.env.ADMIN_EMAIL || '[email protected]'; const host = process.env.SMTP_HOST || 'localhost'; const port = parseInt(process.env.SMTP_PORT || '1025'); const user = process.env.SMTP_USER || ''; const pass = process.env.SMTP_PASS || ''; const from = process.env.SMTP_FROM || 'no-reply@test.local'; const smtpConfig: SmtpConfig = { host, port, user, pass, from, email, }; return new EmailAgent(smtpConfig);
}
export function createEmailAgent(): BaseAgent { const email = process.env.ADMIN_EMAIL || '[email protected]'; const host = process.env.SMTP_HOST || 'localhost'; const port = parseInt(process.env.SMTP_PORT || '1025'); const user = process.env.SMTP_USER || ''; const pass = process.env.SMTP_PASS || ''; const from = process.env.SMTP_FROM || 'no-reply@test.local'; const smtpConfig: SmtpConfig = { host, port, user, pass, from, email, }; return new EmailAgent(smtpConfig);
}
class EmailAgent extends BaseAgent { readonly smtpConfig: SmtpConfig; constructor(smtpConfig: SmtpConfig) { super({ name: 'EmailAgent', description: 'Send a recommendation and summary email to the administrator.', }); this.smtpConfig = smtpConfig; }
}
class EmailAgent extends BaseAgent { readonly smtpConfig: SmtpConfig; constructor(smtpConfig: SmtpConfig) { super({ name: 'EmailAgent', description: 'Send a recommendation and summary email to the administrator.', }); this.smtpConfig = smtpConfig; }
}
class EmailAgent extends BaseAgent { readonly smtpConfig: SmtpConfig; constructor(smtpConfig: SmtpConfig) { super({ name: 'EmailAgent', description: 'Send a recommendation and summary email to the administrator.', }); this.smtpConfig = smtpConfig; }
}
import { z } from 'zod'; export const recommendationSchema = z.object({ text: z.string(),
}); export type Recommendation = z.infer<typeof recommendationSchema>; export const mergerSchema = z.object({ summary: z.string(),
}); export type Merger = z.infer<typeof mergerSchema>;
import { z } from 'zod'; export const recommendationSchema = z.object({ text: z.string(),
}); export type Recommendation = z.infer<typeof recommendationSchema>; export const mergerSchema = z.object({ summary: z.string(),
}); export type Merger = z.infer<typeof mergerSchema>;
import { z } from 'zod'; export const recommendationSchema = z.object({ text: z.string(),
}); export type Recommendation = z.infer<typeof recommendationSchema>; export const mergerSchema = z.object({ summary: z.string(),
}); export type Merger = z.infer<typeof mergerSchema>;
export function getEvaluationContext(context: ReadonlyContext | undefined) { if (!context || !context.state) { return { recommendation: null, }; } const state = context.state; return { recommendation: state.get<Recommendation>(RECOMMENDATION_KEY) ?? null, };
} export function getMergerContext(context: ReadonlyContext | undefined) { if (!context || !context.state) { return { merger: null, }; } const state = context.state; return { merger: state.get<Merger>(MERGED_RESULTS_KEY) ?? null, };
}
export function getEvaluationContext(context: ReadonlyContext | undefined) { if (!context || !context.state) { return { recommendation: null, }; } const state = context.state; return { recommendation: state.get<Recommendation>(RECOMMENDATION_KEY) ?? null, };
} export function getMergerContext(context: ReadonlyContext | undefined) { if (!context || !context.state) { return { merger: null, }; } const state = context.state; return { merger: state.get<Merger>(MERGED_RESULTS_KEY) ?? null, };
}
export function getEvaluationContext(context: ReadonlyContext | undefined) { if (!context || !context.state) { return { recommendation: null, }; } const state = context.state; return { recommendation: state.get<Recommendation>(RECOMMENDATION_KEY) ?? null, };
} export function getMergerContext(context: ReadonlyContext | undefined) { if (!context || !context.state) { return { merger: null, }; } const state = context.state; return { merger: state.get<Merger>(MERGED_RESULTS_KEY) ?? null, };
}
protected async *runAsyncImpl(context: InvocationContext): AsyncGenerator<Event, void, void> { for await (const event of this.runLiveImpl(context)) { yield event; } }
protected async *runAsyncImpl(context: InvocationContext): AsyncGenerator<Event, void, void> { for await (const event of this.runLiveImpl(context)) { yield event; } }
protected async *runAsyncImpl(context: InvocationContext): AsyncGenerator<Event, void, void> { for await (const event of this.runLiveImpl(context)) { yield event; } }
protected async *runLiveImpl(context: InvocationContext): AsyncGenerator<Event, void, void> { const readonlyCtx = new ReadonlyContext(context); const { merger } = getMergerContext(readonlyCtx); const { recommendation } = getEvaluationContext(readonlyCtx); const recommendationText = recommendation?.text || 'No recommendation available.'; const emit = (-weight: 500;">status: 'success' | 'error', author: string) => createEmailStatusEvent({ author, context, -weight: 500;">status, recommendationText, }); if (!merger) { yield emit('error', this.name); return; } try { const emailContent = `${recommendation?.text || ''}\n\n## Summary\n\n${merger.summary}`; await sendEmail(this.smtpConfig, 'Project Evaluation Results', emailContent); yield emit('success', this.name); } catch (e) { console.error(e); yield emit('error', this.name); } }
protected async *runLiveImpl(context: InvocationContext): AsyncGenerator<Event, void, void> { const readonlyCtx = new ReadonlyContext(context); const { merger } = getMergerContext(readonlyCtx); const { recommendation } = getEvaluationContext(readonlyCtx); const recommendationText = recommendation?.text || 'No recommendation available.'; const emit = (-weight: 500;">status: 'success' | 'error', author: string) => createEmailStatusEvent({ author, context, -weight: 500;">status, recommendationText, }); if (!merger) { yield emit('error', this.name); return; } try { const emailContent = `${recommendation?.text || ''}\n\n## Summary\n\n${merger.summary}`; await sendEmail(this.smtpConfig, 'Project Evaluation Results', emailContent); yield emit('success', this.name); } catch (e) { console.error(e); yield emit('error', this.name); } }
protected async *runLiveImpl(context: InvocationContext): AsyncGenerator<Event, void, void> { const readonlyCtx = new ReadonlyContext(context); const { merger } = getMergerContext(readonlyCtx); const { recommendation } = getEvaluationContext(readonlyCtx); const recommendationText = recommendation?.text || 'No recommendation available.'; const emit = (-weight: 500;">status: 'success' | 'error', author: string) => createEmailStatusEvent({ author, context, -weight: 500;">status, recommendationText, }); if (!merger) { yield emit('error', this.name); return; } try { const emailContent = `${recommendation?.text || ''}\n\n## Summary\n\n${merger.summary}`; await sendEmail(this.smtpConfig, 'Project Evaluation Results', emailContent); yield emit('success', this.name); } catch (e) { console.error(e); yield emit('error', this.name); } }
import { marked } from 'marked';
import nodemailer from 'nodemailer'; export async function sendEmail(smtpConfig: SmtpConfig, subject: string, text: string) { const { host, port, user, pass, from, email: to } = smtpConfig; const transporter = nodemailer.createTransport({ host, port, auth: user && pass ? { user, pass } : undefined, secure: false, }); const html = await marked.parse(text); const mailOptions = { from, to, subject, text, html, }; return transporter.sendMail(mailOptions);
}
import { marked } from 'marked';
import nodemailer from 'nodemailer'; export async function sendEmail(smtpConfig: SmtpConfig, subject: string, text: string) { const { host, port, user, pass, from, email: to } = smtpConfig; const transporter = nodemailer.createTransport({ host, port, auth: user && pass ? { user, pass } : undefined, secure: false, }); const html = await marked.parse(text); const mailOptions = { from, to, subject, text, html, }; return transporter.sendMail(mailOptions);
}
import { marked } from 'marked';
import nodemailer from 'nodemailer'; export async function sendEmail(smtpConfig: SmtpConfig, subject: string, text: string) { const { host, port, user, pass, from, email: to } = smtpConfig; const transporter = nodemailer.createTransport({ host, port, auth: user && pass ? { user, pass } : undefined, secure: false, }); const html = await marked.parse(text); const mailOptions = { from, to, subject, text, html, }; return transporter.sendMail(mailOptions);
}
export type EmailStatusOptions = { author: string; context: InvocationContext; -weight: 500;">status: 'success' | 'error'; recommendationText: string;
}; export function createEmailStatusEvent(options: EmailStatusOptions): Event { return createEvent({ invocationId: options.context.invocationId, author: options.author, branch: options.context.branch || '', content: { role: 'model', parts: [ { text: JSON.stringify({ -weight: 500;">status: options.-weight: 500;">status, recommendationText: options.recommendationText, sessionId: options.context.session.id, invocationId: options.context.invocationId, }), }, ], }, });
}
export type EmailStatusOptions = { author: string; context: InvocationContext; -weight: 500;">status: 'success' | 'error'; recommendationText: string;
}; export function createEmailStatusEvent(options: EmailStatusOptions): Event { return createEvent({ invocationId: options.context.invocationId, author: options.author, branch: options.context.branch || '', content: { role: 'model', parts: [ { text: JSON.stringify({ -weight: 500;">status: options.-weight: 500;">status, recommendationText: options.recommendationText, sessionId: options.context.session.id, invocationId: options.context.invocationId, }), }, ], }, });
}
export type EmailStatusOptions = { author: string; context: InvocationContext; -weight: 500;">status: 'success' | 'error'; recommendationText: string;
}; export function createEmailStatusEvent(options: EmailStatusOptions): Event { return createEvent({ invocationId: options.context.invocationId, author: options.author, branch: options.context.branch || '', content: { role: 'model', parts: [ { text: JSON.stringify({ -weight: 500;">status: options.-weight: 500;">status, recommendationText: options.recommendationText, sessionId: options.context.session.id, invocationId: options.context.invocationId, }), }, ], }, });
}
"scripts": { "prebuild": "rimraf dist", "build": "npx tsc --project tsconfig.json", "web": "-weight: 500;">npm run build && npx @google/adk-devtools web --host 127.0.0.1 dist/agent.js" },
"scripts": { "prebuild": "rimraf dist", "build": "npx tsc --project tsconfig.json", "web": "-weight: 500;">npm run build && npx @google/adk-devtools web --host 127.0.0.1 dist/agent.js" },
"scripts": { "prebuild": "rimraf dist", "build": "npx tsc --project tsconfig.json", "web": "-weight: 500;">npm run build && npx @google/adk-devtools web --host 127.0.0.1 dist/agent.js" },
One of my favorite tech influencers just tweeted about a 'breakthrough in solid-state batteries.' Find which public company they might be referring to, check that company’s recent patent filings to see if it’s true, and then check their stock price to see if the market has already 'priced it in'.
One of my favorite tech influencers just tweeted about a 'breakthrough in solid-state batteries.' Find which public company they might be referring to, check that company’s recent patent filings to see if it’s true, and then check their stock price to see if the market has already 'priced it in'.
One of my favorite tech influencers just tweeted about a 'breakthrough in solid-state batteries.' Find which public company they might be referring to, check that company’s recent patent filings to see if it’s true, and then check their stock price to see if the market has already 'priced it in'. - TypeScript 5.9.3 or later
- Node.js 24.13.0 or later
- -weight: 500;">npm 11.8.0 or later
- Docker (For running MailHog locally)
- Google ADK (For building the custom agent)
- Gemini in Vertex AI (to call the model in LLM agents, although not required for the custom agent) - Open a terminal and type -weight: 500;">npm run web to -weight: 500;">start the API server.
- Open a new browser tab and type http://localhost:8000.
- Paste the following into the message box: - Ensure the root agent executes and halts when the email agent terminates. - Open another tab and navigate to http://localhost:8025 to open the MailHog web UI - The MailHog Web UI displays the recommendation and summary that summarizes the decision, any clear anti-patterns, and the URL of the cloud storage. - Google Development Kit in TypeScript
- Custom Agent in ADK TypeScript
- The standard for email delivery in Node.js.
- Convert Markdown to HTML format
- MailHog Docker Image
- Decision Tree Agent Repo