Merge pull request #184 from rohitg00/feat/typescript-coverage-pass-3

feat: TypeScript coverage pass 3 (protocols + agent engineering)
This commit is contained in:
Rohit Ghumare
2026-05-25 23:56:38 +01:00
committed by GitHub
7 changed files with 1583 additions and 6 deletions
+12 -6
View File
@@ -7246,7 +7246,8 @@
"has_quiz": false,
"has_notebook": true,
"code_files": [
"main.py"
"main.py",
"main.ts"
],
"outputs": [
{
@@ -7416,7 +7417,8 @@
"has_quiz": false,
"has_notebook": true,
"code_files": [
"main.py"
"main.py",
"main.ts"
],
"outputs": [
{
@@ -7765,7 +7767,8 @@
"has_quiz": false,
"has_notebook": true,
"code_files": [
"main.py"
"main.py",
"main.ts"
],
"outputs": [
{
@@ -7917,7 +7920,8 @@
"has_quiz": true,
"has_notebook": true,
"code_files": [
"main.py"
"main.py",
"main.ts"
],
"outputs": [
{
@@ -8062,7 +8066,8 @@
"has_quiz": true,
"has_notebook": true,
"code_files": [
"main.py"
"main.py",
"main.ts"
],
"outputs": [
{
@@ -8268,7 +8273,8 @@
"has_quiz": true,
"has_notebook": true,
"code_files": [
"main.py"
"main.py",
"main.ts"
],
"outputs": [
{
@@ -0,0 +1,285 @@
// Phase 13 Lesson 01 — the tool interface, in TypeScript.
//
// Mirrors code/main.py: describe -> decide -> execute -> observe.
// The "decide" step is faked with a keyword router so the loop runs offline;
// replace with any real provider client and the shape stays the same.
//
// Spec references:
// OpenAI tool calling https://platform.openai.com/docs/guides/function-calling
// Anthropic tool use https://docs.anthropic.com/en/docs/build-with-claude/tool-use
// MCP tool primitive https://modelcontextprotocol.io/specification/2025-11-25
//
// Run: npx tsx code/main.ts
import { randomUUID } from "node:crypto";
const MAX_TURNS = 5;
type JsonSchema = {
type?: "object" | "string" | "number" | "integer" | "boolean" | "array";
properties?: Record<string, JsonSchema>;
required?: string[];
enum?: unknown[];
};
type ToolArgs = Record<string, unknown>;
type ToolResult = Record<string, unknown>;
type Tool = {
name: string;
description: string;
inputSchema: JsonSchema;
executor: (args: ToolArgs) => ToolResult;
consequential?: boolean;
};
type HistoryEntry =
| { role: "user"; content: string }
| { role: "tool"; id: string; name: string; content: string };
type ToolCall = {
id: string;
name: string;
arguments: ToolArgs;
};
type Decision = { content: string } | { toolCalls: ToolCall[] };
function toolAdd(args: ToolArgs): ToolResult {
const a = args.a as number;
const b = args.b as number;
return { sum: a + b };
}
function toolGetTime(args: ToolArgs): ToolResult {
const timezone = (args.timezone as string | undefined) ?? "UTC";
const now = new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
return { now, timezone };
}
function toolGetWeather(args: ToolArgs): ToolResult {
const fake: Record<string, number> = {
Bengaluru: 28,
Tokyo: 12,
Zurich: 4,
Lagos: 31,
};
const city = args.city as string;
const units = (args.units as string | undefined) ?? "celsius";
const temp = fake[city] ?? 20;
return { city, temp, units };
}
const REGISTRY: Tool[] = [
{
name: "add",
description:
"Use when the user asks for the sum of two numbers. " +
"Do not use for subtraction, product, or symbolic algebra.",
inputSchema: {
type: "object",
properties: {
a: { type: "number" },
b: { type: "number" },
},
required: ["a", "b"],
},
executor: toolAdd,
},
{
name: "get_time",
description:
"Use when the user asks what time it is. " +
"Do not use for historical dates or future scheduling.",
inputSchema: {
type: "object",
properties: {
timezone: { type: "string" },
},
required: [],
},
executor: toolGetTime,
},
{
name: "get_weather",
description:
"Use when the user asks about current conditions in a named city. " +
"Do not use for forecasts or historical weather data.",
inputSchema: {
type: "object",
properties: {
city: { type: "string" },
units: { type: "string", enum: ["celsius", "fahrenheit"] },
},
required: ["city"],
},
executor: toolGetWeather,
},
];
function validate(schema: JsonSchema, value: unknown): string[] {
const errors: string[] = [];
const t = schema.type;
if (t === "object") {
if (typeof value !== "object" || value === null || Array.isArray(value)) {
return [`expected object, got ${describeType(value)}`];
}
const obj = value as Record<string, unknown>;
for (const field of schema.required ?? []) {
if (!(field in obj)) errors.push(`missing required field '${field}'`);
}
for (const [key, sub] of Object.entries(schema.properties ?? {})) {
if (key in obj) errors.push(...validate(sub, obj[key]));
}
return errors;
}
if (t === "number" && typeof value !== "number") {
errors.push(`expected number, got ${describeType(value)}`);
}
if (t === "string" && typeof value !== "string") {
errors.push(`expected string, got ${describeType(value)}`);
}
if (schema.enum && !schema.enum.includes(value as never)) {
errors.push(`value ${JSON.stringify(value)} not in enum ${JSON.stringify(schema.enum)}`);
}
return errors;
}
function describeType(value: unknown): string {
if (value === null) return "null";
if (Array.isArray(value)) return "array";
return typeof value;
}
function newCallId(): string {
return `call_${randomUUID().replace(/-/g, "").slice(0, 8)}`;
}
// Stand-in for the model. Routes by keyword so the loop runs offline.
// Production substitute: replace with a provider call returning the same shape.
function fakeDecide(userMsg: string, history: HistoryEntry[]): Decision {
const last = history[history.length - 1];
if (last && last.role === "tool") {
return { content: `Final answer built from tool output: ${last.content}` };
}
const msg = userMsg.toLowerCase();
if (/\b(add|sum|plus)\b/.test(msg)) {
const nums = (msg.match(/-?\d+\.?\d*/g) ?? []).map((n) => Number(n));
if (nums.length >= 2) {
return {
toolCalls: [
{ id: newCallId(), name: "add", arguments: { a: nums[0], b: nums[1] } },
],
};
}
}
if (msg.includes("time")) {
return {
toolCalls: [
{ id: newCallId(), name: "get_time", arguments: { timezone: "UTC" } },
],
};
}
const weatherMatch = msg.match(/weather in (\w+)/);
if (weatherMatch) {
const city = weatherMatch[1][0].toUpperCase() + weatherMatch[1].slice(1);
return {
toolCalls: [
{
id: newCallId(),
name: "get_weather",
arguments: { city, units: "celsius" },
},
],
};
}
return { content: "I cannot route that query to any registered tool." };
}
function runLoop(userMsg: string): void {
console.log("=".repeat(72));
console.log(`USER : ${userMsg}`);
console.log("-".repeat(72));
const toolsByName = new Map(REGISTRY.map((t) => [t.name, t]));
const history: HistoryEntry[] = [{ role: "user", content: userMsg }];
for (let turn = 1; turn <= MAX_TURNS; turn++) {
const decision = fakeDecide(userMsg, history);
if ("content" in decision) {
console.log(`TURN ${turn} DECIDE : final answer`);
console.log(`MODEL : ${decision.content}`);
return;
}
for (const call of decision.toolCalls) {
const tool = toolsByName.get(call.name);
console.log(`TURN ${turn} DECIDE : call ${call.name} id=${call.id}`);
console.log(` args = ${JSON.stringify(call.arguments)}`);
if (!tool) {
console.log(` ERROR : unknown tool ${call.name}`);
return;
}
const errs = validate(tool.inputSchema, call.arguments);
if (errs.length > 0) {
console.log(` VALIDATION ERRORS : ${JSON.stringify(errs)}`);
return;
}
if (tool.consequential) {
console.log(" GATE : tool is consequential, would confirm");
}
const start = performance.now();
const result = tool.executor(call.arguments);
const ms = performance.now() - start;
console.log(
`TURN ${turn} EXECUTE: ${tool.name} -> ${JSON.stringify(result)} [${ms.toFixed(2)} ms]`,
);
history.push({
role: "tool",
id: call.id,
name: tool.name,
content: JSON.stringify(result),
});
}
console.log(`TURN ${turn} OBSERVE: history length = ${history.length}`);
}
console.log("LOOP TERMINATED : hit MAX_TURNS circuit breaker");
}
function describeRegistry(): void {
console.log("TOOL REGISTRY");
console.log("-".repeat(72));
for (const t of REGISTRY) {
const kind = t.consequential ? "consequential" : "pure";
console.log(` ${t.name.padEnd(14)} [${kind}] - ${t.description}`);
}
console.log();
}
function main(): void {
console.log("=".repeat(72));
console.log("PHASE 13 LESSON 01 - THE TOOL INTERFACE (TypeScript port)");
console.log("=".repeat(72));
describeRegistry();
const queries = [
"please add 7 and 35",
"what time is it?",
"tell me the weather in Bengaluru",
"write me a haiku about tea",
];
for (const q of queries) {
runLoop(q);
console.log();
}
}
main();
@@ -0,0 +1,356 @@
// Phase 13 Lesson 07 — toy MCP server, in TypeScript, stdlib only.
//
// Implements the 2025-11-25 spec's core flow:
// initialize, tools/list, tools/call, resources/list, resources/read,
// prompts/list, prompts/get, plus notifications/initialized.
//
// Spec references:
// MCP 2025-11-25 https://modelcontextprotocol.io/specification/2025-11-25
// JSON-RPC 2.0 https://www.jsonrpc.org/specification
//
// Not a production server: no auth, no Streamable HTTP transport (Lesson 09),
// no subscriptions. But the wire shape is spec-shaped; any MCP client can
// handshake and call the three notes tools.
//
// Run demo: npx tsx code/main.ts --demo
// Pipe JSON-RPC: echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' | npx tsx code/main.ts
import { randomUUID } from "node:crypto";
import { createInterface } from "node:readline";
const PROTOCOL_VERSION = "2025-11-25";
const SERVER_INFO = { name: "notes-lesson-07", version: "1.0.0" };
type Note = { title: string; body: string; tag: string };
const NOTES: Record<string, Note> = {
"note-1": { title: "MCP overview", body: "Primitives, lifecycle, JSON-RPC.", tag: "mcp" },
"note-2": { title: "Function calling", body: "Provider shapes diff by envelope.", tag: "api" },
"note-3": { title: "Tool schemas", body: "Atomic beats monolithic.", tag: "design" },
};
type JsonSchema = {
type?: string;
properties?: Record<string, JsonSchema>;
required?: string[];
minimum?: number;
maximum?: number;
};
type ToolDescriptor = {
name: string;
description: string;
inputSchema: JsonSchema;
annotations?: { readOnlyHint?: boolean; idempotentHint?: boolean; destructiveHint?: boolean };
};
const TOOLS: ToolDescriptor[] = [
{
name: "notes_list",
description:
"Use when the user wants all notes or a filtered list by tag. Do not use to read a note body.",
inputSchema: {
type: "object",
properties: { tag: { type: "string" } },
required: [],
},
annotations: { readOnlyHint: true, idempotentHint: true },
},
{
name: "notes_search",
description:
"Use when the user searches notes by content keywords. Do not use for tag filters.",
inputSchema: {
type: "object",
properties: {
query: { type: "string" },
limit: { type: "integer", minimum: 1, maximum: 50 },
},
required: ["query"],
},
annotations: { readOnlyHint: true },
},
{
name: "notes_create",
description: "Use when the user writes a new note. Do not use to edit existing ones.",
inputSchema: {
type: "object",
properties: {
title: { type: "string" },
body: { type: "string" },
tag: { type: "string" },
},
required: ["title", "body"],
},
annotations: { destructiveHint: false, idempotentHint: false },
},
];
const PROMPTS = [
{
name: "review_note",
description: "Produce a critique of a note with concrete improvements.",
arguments: [
{ name: "note_id", description: "The id of the note to review", required: true },
],
},
];
type ContentBlock =
| { type: "text"; text: string }
| { type: "resource"; resource: { uri: string; text: string } };
type ToolArgs = Record<string, unknown>;
function execNotesList(args: ToolArgs): ContentBlock[] {
const tag = args.tag as string | undefined;
const items: Array<{ id: string; title: string; tag: string }> = [];
for (const [id, note] of Object.entries(NOTES)) {
if (tag && note.tag !== tag) continue;
items.push({ id, title: note.title, tag: note.tag });
}
return [{ type: "text", text: JSON.stringify(items) }];
}
function execNotesSearch(args: ToolArgs): ContentBlock[] {
const q = String(args.query).toLowerCase();
const limit = (args.limit as number | undefined) ?? 10;
const hits: Array<{ id: string; title: string }> = [];
for (const [id, n] of Object.entries(NOTES)) {
if (n.title.toLowerCase().includes(q) || n.body.toLowerCase().includes(q)) {
hits.push({ id, title: n.title });
}
}
return [{ type: "text", text: JSON.stringify(hits.slice(0, limit)) }];
}
function execNotesCreate(args: ToolArgs): ContentBlock[] {
const id = `note-${randomUUID().replace(/-/g, "").slice(0, 6)}`;
const body = String(args.body);
NOTES[id] = {
title: String(args.title),
body,
tag: (args.tag as string | undefined) ?? "",
};
return [
{ type: "text", text: `Created ${id}` },
{ type: "resource", resource: { uri: `notes://${id}`, text: body } },
];
}
const TOOL_EXECUTORS: Record<string, (args: ToolArgs) => ContentBlock[]> = {
notes_list: execNotesList,
notes_search: execNotesSearch,
notes_create: execNotesCreate,
};
type JsonRpcRequest = {
jsonrpc: "2.0";
id?: number | string | null;
method: string;
params?: Record<string, unknown>;
};
type JsonRpcResponse = {
jsonrpc: "2.0";
id: number | string | null;
result?: unknown;
error?: { code: number; message: string; data?: unknown };
};
function handleInitialize(): unknown {
return {
protocolVersion: PROTOCOL_VERSION,
capabilities: {
tools: { listChanged: false },
resources: { listChanged: false, subscribe: false },
prompts: { listChanged: false },
},
serverInfo: SERVER_INFO,
};
}
function handleToolsList(): unknown {
return { tools: TOOLS };
}
function handleToolsCall(params: Record<string, unknown>): unknown {
const name = params.name as string;
const args = (params.arguments as ToolArgs | undefined) ?? {};
const exec = TOOL_EXECUTORS[name];
if (!exec) {
return { content: [{ type: "text", text: `unknown tool ${name}` }], isError: true };
}
try {
return { content: exec(args), isError: false };
} catch (err) {
return { content: [{ type: "text", text: String(err) }], isError: true };
}
}
function handleResourcesList(): unknown {
const items = Object.entries(NOTES).map(([id, n]) => ({
uri: `notes://${id}`,
name: n.title,
mimeType: "text/markdown",
}));
return { resources: items };
}
function handleResourcesRead(params: Record<string, unknown>): unknown {
const uri = String(params.uri);
const id = uri.replace("notes://", "");
const n = NOTES[id];
if (!n) throw new Error(`not found: ${uri}`);
return {
contents: [
{
uri,
mimeType: "text/markdown",
text: `# ${n.title}\n\n${n.body}\n\ntag: ${n.tag}`,
},
],
};
}
function handlePromptsList(): unknown {
return { prompts: PROMPTS };
}
function handlePromptsGet(params: Record<string, unknown>): unknown {
if (params.name !== "review_note") throw new Error("unknown prompt");
const args = (params.arguments as Record<string, unknown> | undefined) ?? {};
const id = String(args.note_id ?? "");
const body = NOTES[id]?.body ?? "(not found)";
return {
description: "Review the note and propose concrete improvements.",
messages: [
{
role: "user",
content: {
type: "text",
text: `Review this note and propose improvements:\n\n${body}`,
},
},
],
};
}
const HANDLERS: Record<string, (params: Record<string, unknown>) => unknown> = {
initialize: handleInitialize,
"tools/list": handleToolsList,
"tools/call": handleToolsCall,
"resources/list": handleResourcesList,
"resources/read": handleResourcesRead,
"prompts/list": handlePromptsList,
"prompts/get": handlePromptsGet,
};
function dispatch(msg: JsonRpcRequest): JsonRpcResponse | null {
const method = msg.method;
if (msg.id === undefined) return null;
const id = msg.id;
const handler = HANDLERS[method];
if (!handler) {
return {
jsonrpc: "2.0",
id,
error: { code: -32601, message: `Method not found: ${method}` },
};
}
try {
const result = handler(msg.params ?? {});
return { jsonrpc: "2.0", id, result };
} catch (err) {
return {
jsonrpc: "2.0",
id,
error: { code: -32603, message: String(err) },
};
}
}
function serveStdio(): void {
const rl = createInterface({ input: process.stdin, terminal: false });
rl.on("line", (line) => {
const trimmed = line.trim();
if (!trimmed) return;
let msg: JsonRpcRequest;
try {
msg = JSON.parse(trimmed) as JsonRpcRequest;
} catch (err) {
process.stderr.write(`parse error: ${String(err)}\n`);
process.stdout.write(
JSON.stringify({
jsonrpc: "2.0",
id: null,
error: { code: -32700, message: "Parse error", data: String(err) },
}) + "\n",
);
return;
}
const resp = dispatch(msg);
if (resp) process.stdout.write(JSON.stringify(resp) + "\n");
});
}
function demo(): void {
console.log("=".repeat(72));
console.log("PHASE 13 LESSON 07 - MCP SERVER DEMO (TypeScript port, no transport)");
console.log("=".repeat(72));
const scenarios: JsonRpcRequest[] = [
{ jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: PROTOCOL_VERSION } },
{ jsonrpc: "2.0", id: 2, method: "tools/list" },
{
jsonrpc: "2.0",
id: 3,
method: "tools/call",
params: { name: "notes_search", arguments: { query: "MCP" } },
},
{ jsonrpc: "2.0", id: 4, method: "resources/list" },
{
jsonrpc: "2.0",
id: 5,
method: "resources/read",
params: { uri: "notes://note-1" },
},
{
jsonrpc: "2.0",
id: 6,
method: "tools/call",
params: {
name: "notes_create",
arguments: { title: "Session notes", body: "Built it.", tag: "mcp" },
},
},
{
jsonrpc: "2.0",
id: 7,
method: "prompts/get",
params: { name: "review_note", arguments: { note_id: "note-1" } },
},
{
jsonrpc: "2.0",
id: 8,
method: "tools/call",
params: { name: "no_such_tool", arguments: {} },
},
];
for (const msg of scenarios) {
console.log("\n>>>", msg.method);
const resp = dispatch(msg);
console.log(JSON.stringify(resp, null, 2).slice(0, 400));
}
}
function main(): void {
if (process.argv.includes("--demo")) {
demo();
} else {
serveStdio();
}
}
main();
@@ -0,0 +1,210 @@
// Phase 13 Lesson 19 — A2A agent-to-agent protocol, in TypeScript.
//
// Research agent calls writer agent via A2A:
// 1. Research agent fetches writer's Agent Card
// 2. Submits a Task with text + file + data parts
// 3. Writer transitions working -> input_required -> working -> completed
// 4. Research agent receives an Artifact
//
// Stdlib only; in-process transport stands in for JSON-RPC over HTTP.
//
// Spec references:
// A2A protocol https://a2aproject.github.io/A2A/specification
// Agent Card schema https://a2aproject.github.io/A2A/specification/#agent-card
//
// Run: npx tsx code/main.ts
import { randomUUID } from "node:crypto";
type Capabilities = { streaming: boolean; pushNotifications: boolean };
type Skill = {
id: string;
name: string;
description: string;
inputModes: string[];
outputModes: string[];
};
type AgentCard = {
schemaVersion: string;
name: string;
description: string;
url: string;
version: string;
skills: Skill[];
capabilities: Capabilities;
};
const WRITER_AGENT_CARD: AgentCard = {
schemaVersion: "1.0",
name: "writer-agent",
description: "Drafts technical summaries and reports from source material.",
url: "https://writer.example.com/a2a",
version: "1.0.0",
skills: [
{
id: "draft_report",
name: "Draft report",
description: "Given source material and a target length, produce a report.",
inputModes: ["text", "file", "data"],
outputModes: ["text", "artifact"],
},
],
capabilities: { streaming: true, pushNotifications: false },
};
type TextPart = { kind: "text"; payload: { text: string } };
type FilePart = {
kind: "file";
payload: { file: { name: string; mimeType: string; bytes: string } };
};
type DataPart = { kind: "data"; payload: Record<string, unknown> };
type Part = TextPart | FilePart | DataPart;
type Message = { role: "user" | "agent"; parts: Part[] };
type Artifact = { name: string; mimeType: string; parts: Part[] };
type TaskState =
| "submitted"
| "working"
| "input_required"
| "completed"
| "failed"
| "canceled";
type Task = {
id: string;
state: TaskState;
messages: Message[];
artifact: Artifact | null;
};
const TASK_STORE = new Map<string, Task>();
function newTask(): Task {
const id = `task_${randomUUID().replace(/-/g, "").slice(0, 10)}`;
const task: Task = { id, state: "submitted", messages: [], artifact: null };
TASK_STORE.set(id, task);
return task;
}
function findDataPart(message: Message): DataPart | undefined {
return message.parts.find((p): p is DataPart => p.kind === "data");
}
function finish(task: Task, length: string): void {
const text =
`[writer agent] ${length} summary of provided source: ` +
`topic identified, key points extracted, conclusion drafted.`;
task.artifact = {
name: "summary",
mimeType: "text/markdown",
parts: [{ kind: "text", payload: { text } }],
};
task.state = "completed";
console.log(` WRITER : completed task ${task.id}`);
}
function writerTasksSend(skillId: string, message: Message): Task {
const task = newTask();
task.state = "working";
task.messages.push(message);
console.log(` WRITER : started task ${task.id} skill=${skillId}`);
const data = findDataPart(message);
if (!data || !("targetLength" in data.payload)) {
task.state = "input_required";
task.messages.push({
role: "agent",
parts: [
{
kind: "text",
payload: { text: "Please specify targetLength as a data part." },
},
],
});
console.log(` WRITER : paused input_required`);
} else {
finish(task, String(data.payload.targetLength));
}
return task;
}
function writerTasksReply(taskId: string, message: Message): Task {
const task = TASK_STORE.get(taskId);
if (!task) throw new Error(`unknown task ${taskId}`);
task.messages.push(message);
const data = findDataPart(message);
if (task.state === "input_required" && data) {
task.state = "working";
finish(task, String(data.payload.targetLength ?? "short"));
}
return task;
}
function researchAgentFlow(): void {
console.log("=".repeat(72));
console.log("PHASE 13 LESSON 19 - A2A CALL FROM RESEARCH TO WRITER (TypeScript port)");
console.log("=".repeat(72));
console.log("\n--- research agent fetches writer Agent Card ---");
console.log(
JSON.stringify(
{
name: WRITER_AGENT_CARD.name,
url: WRITER_AGENT_CARD.url,
skills: WRITER_AGENT_CARD.skills,
},
null,
2,
),
);
const skill = WRITER_AGENT_CARD.skills[0];
const skillId = skill.id;
console.log(`\n research agent will invoke skill: ${skillId}`);
const fakePdfBytes = Buffer.from("fake-pdf").toString("base64");
const initialMessage: Message = {
role: "user",
parts: [
{ kind: "text", payload: { text: "Summarize the attached paper." } },
{
kind: "file",
payload: {
file: { name: "paper.pdf", mimeType: "application/pdf", bytes: fakePdfBytes },
},
},
],
};
let task = writerTasksSend(skillId, initialMessage);
console.log(` research : task state = ${task.state}`);
if (task.state === "input_required") {
console.log("\n--- research agent supplies the missing data ---");
const followup: Message = {
role: "user",
parts: [{ kind: "data", payload: { targetLength: "3 paragraphs" } }],
};
task = writerTasksReply(task.id, followup);
console.log(` research : task state = ${task.state}`);
}
console.log("\n--- research agent reads artifact ---");
if (task.artifact) {
const firstPart = task.artifact.parts[0];
console.log(` name : ${task.artifact.name}`);
console.log(` mimeType : ${task.artifact.mimeType}`);
if (firstPart.kind === "text") {
console.log(` content : ${firstPart.payload.text}`);
}
}
console.log("\n--- lifecycle observation ---");
console.log(` final state : ${task.state}`);
console.log(` messages : ${task.messages.length}`);
}
researchAgentFlow();
@@ -0,0 +1,210 @@
// Phase 14 Lesson 01 — toy ReAct agent loop, in TypeScript.
//
// Mirrors code/main.py: message buffer, tool registry, stop condition,
// turn budget, observation formatter. The model is a scripted ToyLLM so the
// loop runs offline and deterministic; swap for a real provider client and
// the control flow is identical.
//
// References:
// ReAct paper https://arxiv.org/abs/2210.03629
// Anthropic agents https://www.anthropic.com/engineering/building-effective-agents
//
// Run: npx tsx code/main.ts
type ToolFn = (args: Record<string, string>) => string;
type ToolCall = {
name: string;
args: Record<string, string>;
};
type Turn = {
kind: "user" | "thought" | "action" | "final";
content: string;
toolCall?: ToolCall;
observation?: string;
};
class ToolRegistry {
private tools = new Map<string, ToolFn>();
register(name: string, fn: ToolFn): void {
this.tools.set(name, fn);
}
names(): string[] {
return [...this.tools.keys()].sort();
}
dispatch(call: ToolCall): string {
const fn = this.tools.get(call.name);
if (!fn) return `error: unknown tool ${JSON.stringify(call.name)}`;
try {
return fn(call.args);
} catch (err) {
const e = err as Error;
return `error: ${e.name}: ${e.message}`;
}
}
}
function calculator(args: Record<string, string>): string {
const expr = args.expr;
if (typeof expr !== "string") return "error: missing expr";
if (!/^[0-9+\-*/(). ]+$/.test(expr)) {
return "error: illegal character in expr";
}
try {
const fn = new Function(`"use strict"; return (${expr});`);
const value = fn();
if (typeof value !== "number" || !Number.isFinite(value)) {
return `error: non-finite result for ${expr}`;
}
return String(value);
} catch (err) {
const e = err as Error;
return `error: ${e.name}: ${e.message}`;
}
}
class KVStore {
private store = new Map<string, string>();
get = (args: Record<string, string>): string => {
const key = args.key;
if (!this.store.has(key)) return `missing:${key}`;
return this.store.get(key) as string;
};
set = (args: Record<string, string>): string => {
this.store.set(args.key, args.value);
return `stored ${args.key}`;
};
}
type ScriptEntry =
| { kind: "action"; thought: string; action: string; args: Record<string, string> }
| { kind: "finish"; content: string };
// Scripted ReAct policy. Returns one assistant turn per call.
// Replace with a provider client and the loop is identical.
class ToyLLM {
private cursor = 0;
constructor(private script: ScriptEntry[]) {}
respond(_history: Turn[]): ScriptEntry {
if (this.cursor >= this.script.length) {
return { kind: "finish", content: "no more actions" };
}
return this.script[this.cursor++];
}
}
class AgentLoop {
history: Turn[] = [];
constructor(
private llm: ToyLLM,
private tools: ToolRegistry,
private maxTurns = 12,
) {}
run(userMessage: string): string {
this.history.push({ kind: "user", content: userMessage });
for (let step = 0; step < this.maxTurns; step++) {
const reply = this.llm.respond(this.history);
if (reply.kind === "finish") {
this.history.push({ kind: "final", content: reply.content });
return reply.content;
}
this.history.push({ kind: "thought", content: reply.thought });
const call: ToolCall = { name: reply.action, args: reply.args };
const observation = this.tools.dispatch(call);
this.history.push({
kind: "action",
content: call.name,
toolCall: call,
observation,
});
}
this.history.push({ kind: "final", content: "budget exhausted" });
return "budget exhausted";
}
toolNames(): string[] {
return this.tools.names();
}
}
function prettyTrace(history: Turn[]): void {
history.forEach((turn, i) => {
const tag = `[${String(i).padStart(2, "0")} ${turn.kind.padStart(7)}]`;
if (turn.kind === "user" || turn.kind === "thought" || turn.kind === "final") {
console.log(`${tag} ${turn.content}`);
} else if (turn.kind === "action" && turn.toolCall) {
const argText = JSON.stringify(turn.toolCall.args);
console.log(`${tag} ${turn.toolCall.name}(${argText}) -> ${turn.observation}`);
}
});
}
function buildDemoAgent(): AgentLoop {
const tools = new ToolRegistry();
tools.register("calculator", calculator);
const kv = new KVStore();
tools.register("kv_get", kv.get);
tools.register("kv_set", kv.set);
const script: ScriptEntry[] = [
{
kind: "action",
thought: "store the base price",
action: "kv_set",
args: { key: "base", value: "120" },
},
{
kind: "action",
thought: "compute 15% tax",
action: "calculator",
args: { expr: "120 * 0.15" },
},
{
kind: "action",
thought: "store the tax",
action: "kv_set",
args: { key: "tax", value: "18.0" },
},
{
kind: "action",
thought: "compute total",
action: "calculator",
args: { expr: "120 + 18.0" },
},
{
kind: "action",
thought: "confirm stored values",
action: "kv_get",
args: { key: "base" },
},
{ kind: "finish", content: "the total including 15% tax is 138.0" },
];
return new AgentLoop(new ToyLLM(script), tools, 10);
}
function main(): void {
console.log("=".repeat(70));
console.log("TOY REACT LOOP — Phase 14, Lesson 01 (TypeScript port)");
console.log("=".repeat(70));
const agent = buildDemoAgent();
const final = agent.run("What is 120 plus 15% tax, stored in kv?");
console.log();
prettyTrace(agent.history);
console.log();
console.log(`final answer: ${final}`);
const actions = agent.history.filter((t) => t.kind === "action").length;
console.log(`turns used: ${actions}`);
console.log(`tools used: ${JSON.stringify(agent.toolNames())}`);
}
main();
@@ -0,0 +1,266 @@
// Phase 14 Lesson 06 — tool use and function calling, in TypeScript.
//
// Stdlib tool registry with JSON Schema subset validation and parallel dispatch.
// Subset: required fields, string/integer/number/boolean/array/object,
// enum, minimum/maximum. Every validation failure becomes a structured
// observation so an agent can retry.
//
// References:
// OpenAI function-calling https://platform.openai.com/docs/guides/function-calling
// Anthropic tool-use https://docs.anthropic.com/en/docs/build-with-claude/tool-use
// JSON Schema 2020-12 https://json-schema.org/draft/2020-12
//
// Run: npx tsx code/main.ts
type Primitive = "integer" | "number" | "boolean" | "string" | "array" | "object";
type PropSchema = {
type: Primitive;
enum?: unknown[];
minimum?: number;
maximum?: number;
};
type ToolInputSchema = {
type: "object";
properties: Record<string, PropSchema>;
required?: string[];
};
type ToolArgs = Record<string, unknown>;
type ToolDef = {
name: string;
description: string;
inputSchema: ToolInputSchema;
executor: (args: ToolArgs) => string;
timeoutMs?: number;
};
type ToolCall = {
toolUseId: string;
name: string;
args: ToolArgs;
};
type ToolResult = {
toolUseId: string;
ok: boolean;
content: string;
};
function describeType(value: unknown): string {
if (value === null) return "null";
if (Array.isArray(value)) return "array";
if (typeof value === "number" && Number.isInteger(value)) return "integer";
return typeof value;
}
function coerce(value: unknown, schema: PropSchema): { value: unknown; error: string | null } {
const t = schema.type;
if (t === "integer") {
if (typeof value === "number" && Number.isInteger(value)) return { value, error: null };
if (typeof value === "string") {
const parsed = Number(value);
if (Number.isInteger(parsed)) return { value: parsed, error: null };
return { value, error: `cannot coerce string ${JSON.stringify(value)} to integer` };
}
return { value, error: `expected integer, got ${describeType(value)}` };
}
if (t === "number") {
if (typeof value === "number") return { value, error: null };
if (typeof value === "string") {
const parsed = Number(value);
if (Number.isFinite(parsed)) return { value: parsed, error: null };
return { value, error: `cannot coerce string ${JSON.stringify(value)} to number` };
}
return { value, error: `expected number, got ${describeType(value)}` };
}
if (t === "boolean") {
if (typeof value === "boolean") return { value, error: null };
return { value, error: `expected boolean, got ${describeType(value)}` };
}
if (t === "string") {
if (typeof value === "string") return { value, error: null };
return { value, error: `expected string, got ${describeType(value)}` };
}
if (t === "array") {
if (Array.isArray(value)) return { value, error: null };
return { value, error: `expected array, got ${describeType(value)}` };
}
if (t === "object") {
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
return { value, error: null };
}
return { value, error: `expected object, got ${describeType(value)}` };
}
return { value, error: null };
}
function validate(args: ToolArgs, schema: ToolInputSchema): { out: ToolArgs; errors: string[] } {
const errors: string[] = [];
const props = schema.properties;
const required = schema.required ?? [];
const out: ToolArgs = {};
for (const name of required) {
if (!(name in args)) errors.push(`missing required: ${name}`);
}
for (const [name, value] of Object.entries(args)) {
const prop = props[name];
if (!prop) {
errors.push(`unknown field: ${name}`);
continue;
}
const { value: coerced, error } = coerce(value, prop);
if (error) {
errors.push(`${name}: ${error}`);
continue;
}
if (prop.enum && !prop.enum.includes(coerced as never)) {
errors.push(`${name}: ${JSON.stringify(coerced)} not in ${JSON.stringify(prop.enum)}`);
continue;
}
if (prop.type === "number" || prop.type === "integer") {
const numVal = coerced as number;
if (prop.minimum !== undefined && numVal < prop.minimum) {
errors.push(`${name}: ${numVal} < minimum ${prop.minimum}`);
continue;
}
if (prop.maximum !== undefined && numVal > prop.maximum) {
errors.push(`${name}: ${numVal} > maximum ${prop.maximum}`);
continue;
}
}
out[name] = coerced;
}
return { out, errors };
}
class ToolRegistry {
private tools = new Map<string, ToolDef>();
register(tool: ToolDef): void {
this.tools.set(tool.name, tool);
}
catalog(): Array<Pick<ToolDef, "name" | "description" | "inputSchema">> {
return [...this.tools.values()].map((t) => ({
name: t.name,
description: t.description,
inputSchema: t.inputSchema,
}));
}
dispatch(call: ToolCall): ToolResult {
const tool = this.tools.get(call.name);
if (!tool) {
return { toolUseId: call.toolUseId, ok: false, content: `error: unknown tool ${JSON.stringify(call.name)}` };
}
const { out, errors } = validate(call.args, tool.inputSchema);
if (errors.length > 0) {
return {
toolUseId: call.toolUseId,
ok: false,
content: `validation error: ${errors.join("; ")}`,
};
}
try {
return { toolUseId: call.toolUseId, ok: true, content: tool.executor(out) };
} catch (err) {
const e = err as Error;
return {
toolUseId: call.toolUseId,
ok: false,
content: `execution error: ${e.name}: ${e.message}`,
};
}
}
dispatchMany(calls: ToolCall[]): ToolResult[] {
return calls.map((c) => this.dispatch(c));
}
}
function add(args: ToolArgs): string {
const a = args.a as number;
const b = args.b as number;
return String(a + b);
}
function multiply(args: ToolArgs): string {
const a = args.a as number;
const b = args.b as number;
return String(a * b);
}
function classify(args: ToolArgs): string {
return `classified as ${args.status as string}`;
}
function main(): void {
console.log("=".repeat(70));
console.log("TOOL USE and FUNCTION CALLING — Phase 14, Lesson 06 (TypeScript port)");
console.log("=".repeat(70));
const reg = new ToolRegistry();
reg.register({
name: "add",
description: "Add two integers a and b. Use for any integer addition.",
inputSchema: {
type: "object",
properties: { a: { type: "integer" }, b: { type: "integer" } },
required: ["a", "b"],
},
executor: add,
});
reg.register({
name: "multiply",
description: "Multiply two integers a and b. Prefer multiplication over looped addition.",
inputSchema: {
type: "object",
properties: { a: { type: "integer" }, b: { type: "integer" } },
required: ["a", "b"],
},
executor: multiply,
});
reg.register({
name: "classify",
description: "Classify a status as one of the allowed labels.",
inputSchema: {
type: "object",
properties: {
status: { type: "string", enum: ["open", "closed", "pending"] },
},
required: ["status"],
},
executor: classify,
});
console.log("\ncatalog (as presented to the model)");
for (const entry of reg.catalog()) {
console.log(` - ${entry.name}: ${entry.description}`);
}
const calls: ToolCall[] = [
{ toolUseId: "u01", name: "add", args: { a: 2, b: 3 } },
{ toolUseId: "u02", name: "multiply", args: { a: "4", b: 5 } },
{ toolUseId: "u03", name: "classify", args: { status: "in_progress" } },
{ toolUseId: "u04", name: "classify", args: { status: "open" } },
{ toolUseId: "u05", name: "subtract", args: { a: 1, b: 2 } },
];
console.log("\nparallel dispatch (5 calls in one turn)");
for (const result of reg.dispatchMany(calls)) {
const tag = result.ok ? "OK " : "ERR";
console.log(` ${result.toolUseId} ${tag}: ${result.content}`);
}
console.log();
console.log("observation shape: every validation failure is a structured error");
console.log("string the agent can read and retry against. never raise to the loop.");
}
main();
@@ -0,0 +1,244 @@
// Phase 14 Lesson 13 — LangGraph-shaped stateful graph, in TypeScript.
//
// Mirrors code/main.py: State is a plain object, nodes return Update objects,
// the runtime serializes state after every node so resume picks up exactly
// where it left off. Human gate pauses; an external approval lets resume()
// continue from the next node.
//
// References:
// LangGraph (TS) https://langchain-ai.github.io/langgraphjs/
// StateGraph reference https://langchain-ai.github.io/langgraphjs/reference/classes/langgraph.StateGraph.html
//
// Run: npx tsx code/main.ts
type State = Record<string, unknown>;
type Update = Record<string, unknown>;
type NodeFn = (state: State) => Update;
type Router = (state: State) => string;
type Predicate = (state: State) => boolean;
const END = "__end__";
type Edge = {
src: string;
dst: string;
predicate: Predicate | null;
};
class StateGraph {
nodes = new Map<string, NodeFn>();
edges = new Map<string, Edge[]>();
entry: string | null = null;
addNode(name: string, fn: NodeFn): void {
this.nodes.set(name, fn);
}
setEntry(name: string): void {
this.entry = name;
}
addEdge(src: string, dst: string): void {
const list = this.edges.get(src) ?? [];
list.push({ src, dst, predicate: null });
this.edges.set(src, list);
}
addConditionalEdges(
src: string,
router: Router,
targets: Record<string, string>,
): void {
for (const [value, dst] of Object.entries(targets)) {
const predicate: Predicate = (state) => router(state) === value;
const list = this.edges.get(src) ?? [];
list.push({ src, dst, predicate });
this.edges.set(src, list);
}
}
next(current: string, state: State): string | null {
for (const edge of this.edges.get(current) ?? []) {
if (edge.predicate === null || edge.predicate(state)) return edge.dst;
}
return null;
}
}
class InMemoryCheckpointer {
private store = new Map<string, Array<[string, State]>>();
save(sessionId: string, stepName: string, state: State): void {
const list = this.store.get(sessionId) ?? [];
list.push([stepName, structuredClone(state)]);
this.store.set(sessionId, list);
}
loadLatest(sessionId: string): [string, State] | null {
const list = this.store.get(sessionId);
if (!list || list.length === 0) return null;
return list[list.length - 1];
}
history(sessionId: string): Array<[string, State]> {
return [...(this.store.get(sessionId) ?? [])];
}
}
class PausedAtNode extends Error {
constructor(public node: string, public state: State) {
super(node);
this.name = "PausedAtNode";
}
}
type RunOptions = {
sessionId: string;
initialState: State;
resumeFrom?: string;
stateOverride?: State;
};
class Runner {
constructor(public graph: StateGraph, public checkpointer: InMemoryCheckpointer) {}
run(opts: RunOptions): State {
const { sessionId, initialState, resumeFrom, stateOverride } = opts;
let state: State = structuredClone(stateOverride ?? initialState);
let current = resumeFrom ?? this.graph.entry;
if (!current) throw new Error("no entry node set");
while (current && current !== END) {
const fn = this.graph.nodes.get(current);
if (!fn) throw new Error(`unknown node ${JSON.stringify(current)}`);
const update = fn(state) ?? {};
state = { ...state, ...update };
this.checkpointer.save(sessionId, current, state);
if (state._pause_reason) {
const reason = state._pause_reason;
delete state._pause_reason;
void reason;
throw new PausedAtNode(current, state);
}
const nxt = this.graph.next(current, state);
current = nxt;
}
return state;
}
}
function classify(state: State): Update {
const text = String(state.input).toLowerCase();
let route: string;
if (text.includes("refund") || text.includes("money back")) route = "refund";
else if (text.includes("crash") || text.includes("bug") || text.includes("error")) route = "bug";
else if (text.includes("pricing") || text.includes("quote")) route = "sales";
else route = "sales";
return { route, step: (state.step as number ?? 0) + 1 };
}
function refund(state: State): Update {
return { ticket: `REF-${String(state.input ?? "").slice(0, 12)}`, step: (state.step as number ?? 0) + 1 };
}
function bug(state: State): Update {
return { ticket: `BUG-${String(state.input ?? "").slice(0, 12)}`, step: (state.step as number ?? 0) + 1 };
}
function sales(state: State): Update {
return { ticket: `SAL-${String(state.input ?? "").slice(0, 12)}`, step: (state.step as number ?? 0) + 1 };
}
function humanGate(state: State): Update {
if (!state.human_approval) {
return { _pause_reason: "awaiting human approval", step: (state.step as number ?? 0) + 1 };
}
return { step: (state.step as number ?? 0) + 1 };
}
function send(state: State): Update {
return { output: `sent ${state.ticket as string | undefined}`, step: (state.step as number ?? 0) + 1 };
}
function buildGraph(): StateGraph {
const g = new StateGraph();
g.addNode("classify", classify);
g.addNode("refund", refund);
g.addNode("bug", bug);
g.addNode("sales", sales);
g.addNode("human_gate", humanGate);
g.addNode("send", send);
g.setEntry("classify");
g.addConditionalEdges(
"classify",
(s) => String(s.route),
{ refund: "refund", bug: "bug", sales: "sales" },
);
g.addEdge("refund", "human_gate");
g.addEdge("bug", "human_gate");
g.addEdge("sales", "human_gate");
g.addEdge("human_gate", "send");
g.addEdge("send", END);
return g;
}
function main(): void {
console.log("=".repeat(70));
console.log("LANGGRAPH STATE MACHINE — Phase 14, Lesson 13 (TypeScript port)");
console.log("=".repeat(70));
const graph = buildGraph();
const ckpt = new InMemoryCheckpointer();
const runner = new Runner(graph, ckpt);
const session = "s001";
const initial: State = {
input: "the CLI crashes on ctrl-c, please fix",
step: 0,
human_approval: false,
};
console.log("\nfirst run (will pause at human_gate)");
try {
const final = runner.run({ sessionId: session, initialState: initial });
console.log(` final: ${JSON.stringify(final)}`);
} catch (err) {
if (err instanceof PausedAtNode) {
console.log(` PAUSED at ${err.node}`);
console.log(` state at pause: ${JSON.stringify(err.state)}`);
} else {
throw err;
}
}
console.log("\ncheckpoint history");
for (const [node, snap] of ckpt.history(session)) {
console.log(
` ${node} route=${snap.route as string | undefined} ` +
`ticket=${snap.ticket as string | undefined} step=${snap.step as number | undefined}`,
);
}
console.log("\nhuman approves; resume from next node after human_gate");
const latest = ckpt.loadLatest(session);
if (!latest) throw new Error("no checkpoint");
const [lastNode, lastState] = latest;
const approved: State = { ...lastState, human_approval: true };
delete approved._pause_reason;
ckpt.save(session, `${lastNode}_reviewed`, approved);
const final = runner.run({
sessionId: session,
initialState: initial,
resumeFrom: "send",
stateOverride: approved,
});
console.log(` final: ${JSON.stringify(final)}`);
console.log();
console.log("property: state serializes after every node; resume is exact.");
console.log("no fresh re-runs after step 38 fails; pick up at step 39.");
}
main();