diff --git a/data-pipeline/src/main.ts b/data-pipeline/src/main.ts index 08c5b50..d135b2f 100644 --- a/data-pipeline/src/main.ts +++ b/data-pipeline/src/main.ts @@ -1,5 +1,5 @@ import "dotenv/config"; -import { task } from "@renderinc/sdk/workflows"; +import { task, type TaskContext } from "@renderinc/sdk/workflows"; interface User { id: string; @@ -58,7 +58,7 @@ function simpleHash(str: string): number { const fetchUserData = task( { name: "fetchUserData", retry }, - async function fetchUserData(userIds: string[]) { + async function fetchUserData(_ctx: TaskContext, userIds: string[]) { console.log(`[SOURCE] Fetching user data for ${userIds.length} users`); const mockUsers: { [key: string]: User } = { @@ -79,7 +79,7 @@ const fetchUserData = task( const fetchTransactionData = task( { name: "fetchTransactionData", retry }, - async function fetchTransactionData(userIds: string[], days: number = 30) { + async function fetchTransactionData(_ctx: TaskContext, userIds: string[], days: number = 30) { console.log(`[SOURCE] Fetching transactions for ${userIds.length} users (${days} days)`); const transactions: Transaction[] = []; @@ -108,7 +108,7 @@ const fetchTransactionData = task( const fetchEngagementData = task( { name: "fetchEngagementData", retry }, - async function fetchEngagementData(userIds: string[]) { + async function fetchEngagementData(_ctx: TaskContext, userIds: string[]) { console.log(`[SOURCE] Fetching engagement data for ${userIds.length} users`); const engagement: Engagement[] = userIds.map((userId) => { @@ -135,7 +135,7 @@ const fetchEngagementData = task( const enrichWithGeoData = task( { name: "enrichWithGeoData", retry }, - async function enrichWithGeoData(userEmail: string) { + async function enrichWithGeoData(_ctx: TaskContext, userEmail: string) { console.log(`[ENRICH] Enriching geo data for ${userEmail}`); const idx = simpleHash(userEmail) % 4; return { @@ -149,6 +149,7 @@ const enrichWithGeoData = task( const calculateUserMetrics = task( { name: "calculateUserMetrics", retry }, async function calculateUserMetrics( + _ctx: TaskContext, user: User, transactions: Transaction[], engagement: Engagement, @@ -197,6 +198,7 @@ const calculateUserMetrics = task( const transformUserData = task( { name: "transformUserData", retry }, async function transformUserData( + ctx: TaskContext, userData: { data: User[] }, transactionData: { data: Transaction[] }, engagementData: { data: Engagement[] }, @@ -217,8 +219,8 @@ const transformUserData = task( users.map(async (user) => { const userEngagement = engagementMap.get(user.id) ?? ({} as Engagement); const [userMetrics, geoData] = await Promise.all([ - calculateUserMetrics(user, transactions, userEngagement), - enrichWithGeoData(user.email), + ctx.step(calculateUserMetrics, user, transactions, userEngagement), + ctx.step(enrichWithGeoData, user.email), ]); return { ...userMetrics, geo: geoData }; }), @@ -233,7 +235,7 @@ const transformUserData = task( const aggregateInsights = task( { name: "aggregateInsights", retry }, - function aggregateInsights(enrichedData: { data: EnrichedUser[] }) { + function aggregateInsights(_ctx: TaskContext, enrichedData: { data: EnrichedUser[] }) { console.log("[AGGREGATE] Generating insights from enriched data"); const users = enrichedData.data ?? []; @@ -284,7 +286,7 @@ const aggregateInsights = task( // Root task: full pipeline orchestrator task( { name: "runDataPipeline", retry, timeoutSeconds: 300 }, - async function runDataPipeline(userIds: string[]) { + async function runDataPipeline(ctx: TaskContext, userIds: string[]) { console.log("=".repeat(80)); console.log("[PIPELINE] Starting Data Pipeline"); console.log(`[PIPELINE] Processing ${userIds.length} users`); @@ -293,9 +295,9 @@ task( // Stage 1: Parallel extraction console.log("[PIPELINE] Stage 1/3: EXTRACT (parallel)"); const [userData, transactionData, engagementData] = await Promise.all([ - fetchUserData(userIds), - fetchTransactionData(userIds), - fetchEngagementData(userIds), + ctx.step(fetchUserData, userIds), + ctx.step(fetchTransactionData, userIds), + ctx.step(fetchEngagementData, userIds), ]); console.log( @@ -304,12 +306,17 @@ task( // Stage 2: Transform console.log("[PIPELINE] Stage 2/3: TRANSFORM"); - const enrichedData = await transformUserData(userData, transactionData, engagementData); + const enrichedData = await ctx.step( + transformUserData, + userData, + transactionData, + engagementData, + ); console.log(`[PIPELINE] Enriched ${enrichedData.count} user profiles`); // Stage 3: Aggregate console.log("[PIPELINE] Stage 3/3: AGGREGATE"); - const insights = await aggregateInsights(enrichedData as { data: EnrichedUser[] }); + const insights = await ctx.step(aggregateInsights, enrichedData); const pipelineResult = { status: "success", diff --git a/etl-job/src/main.ts b/etl-job/src/main.ts index 29073d8..84d84b0 100644 --- a/etl-job/src/main.ts +++ b/etl-job/src/main.ts @@ -1,5 +1,5 @@ import "dotenv/config"; -import { task } from "@renderinc/sdk/workflows"; +import { task, type TaskContext } from "@renderinc/sdk/workflows"; import { readFileSync, existsSync } from "node:fs"; import { resolve } from "node:path"; @@ -32,7 +32,7 @@ const retry = { // Subtask: extract rows from a CSV file const extractCsvData = task( { name: "extractCsvData", retry }, - function extractCsvData(filePath: string): Record[] { + function extractCsvData(_ctx: TaskContext, filePath: string): Record[] { console.log(`[EXTRACT] Reading CSV file: ${filePath}`); const fullPath = resolve(filePath); @@ -69,7 +69,7 @@ const extractCsvData = task( // Subtask: validate and clean a single record const validateRecord = task( { name: "validateRecord", retry }, - function validateRecord(record: Record): ValidatedRecord { + function validateRecord(_ctx: TaskContext, record: Record): ValidatedRecord { console.log(`[TRANSFORM] Validating record ID: ${record.id ?? "unknown"}`); const errors: string[] = []; @@ -112,7 +112,7 @@ const validateRecord = task( // Subtask: validate a batch of records by calling validateRecord for each const transformBatch = task( { name: "transformBatch", retry }, - async function transformBatch(records: Record[]) { + async function transformBatch(ctx: TaskContext, records: Record[]) { console.log(`[TRANSFORM] Starting batch transformation of ${records.length} records`); const validRecords: ValidatedRecord[] = []; @@ -120,7 +120,7 @@ const transformBatch = task( for (let i = 0; i < records.length; i++) { console.log(`[TRANSFORM] Processing record ${i + 1}/${records.length}`); - const validated = await validateRecord(records[i]); + const validated = await ctx.step(validateRecord, records[i]); if (validated.is_valid) { validRecords.push(validated); @@ -148,7 +148,7 @@ const transformBatch = task( // Subtask: compute statistics from validated records const computeStatistics = task( { name: "computeStatistics", retry }, - function computeStatistics(validRecords: ValidatedRecord[]) { + function computeStatistics(_ctx: TaskContext, validRecords: ValidatedRecord[]) { console.log(`[LOAD] Computing statistics for ${validRecords.length} records`); if (validRecords.length === 0) { @@ -191,24 +191,24 @@ const computeStatistics = task( // Root task: orchestrates the full ETL pipeline task( { name: "runEtlPipeline", retry, timeoutSeconds: 300 }, - async function runEtlPipeline(sourceFile: string) { + async function runEtlPipeline(ctx: TaskContext, sourceFile: string) { console.log("=".repeat(80)); console.log("[PIPELINE] Starting ETL Pipeline"); console.log(`[PIPELINE] Source: ${sourceFile}`); console.log("=".repeat(80)); console.log("[PIPELINE] Stage 1/3: EXTRACT"); - const rawRecords = await extractCsvData(sourceFile); + const rawRecords = await ctx.step(extractCsvData, sourceFile); console.log(`[PIPELINE] Extracted ${rawRecords.length} records`); console.log("[PIPELINE] Stage 2/3: TRANSFORM"); - const transformResult = await transformBatch(rawRecords); + const transformResult = await ctx.step(transformBatch, rawRecords); console.log( `[PIPELINE] Transformation complete: ${(transformResult.success_rate * 100).toFixed(1)}% success rate`, ); console.log("[PIPELINE] Stage 3/3: LOAD"); - const statistics = await computeStatistics(transformResult.valid_records); + const statistics = await ctx.step(computeStatistics, transformResult.valid_records); console.log("[PIPELINE] Statistics computed"); const pipelineResult = { diff --git a/file-analyzer/workflow-service/src/main.ts b/file-analyzer/workflow-service/src/main.ts index 5bfbd64..8b94c52 100644 --- a/file-analyzer/workflow-service/src/main.ts +++ b/file-analyzer/workflow-service/src/main.ts @@ -1,5 +1,5 @@ import "dotenv/config"; -import { task } from "@renderinc/sdk/workflows"; +import { task, type TaskContext } from "@renderinc/sdk/workflows"; interface ParsedData { success: boolean; @@ -19,7 +19,7 @@ const retry = { // Subtask: parse CSV content into structured data const parseCsvData = task( { name: "parseCsvData", retry }, - function parseCsvData(fileContent: string): ParsedData { + function parseCsvData(_ctx: TaskContext, fileContent: string): ParsedData { console.log("[PARSE] Starting CSV parsing"); try { @@ -56,7 +56,7 @@ const parseCsvData = task( // Subtask: calculate statistics from parsed data const calculateStatistics = task( { name: "calculateStatistics", retry }, - function calculateStatistics(data: ParsedData) { + function calculateStatistics(_ctx: TaskContext, data: ParsedData) { console.log("[STATS] Calculating statistics"); if (!data.success || data.rows.length === 0) { @@ -111,7 +111,7 @@ const calculateStatistics = task( // Subtask: identify trends and patterns const identifyTrends = task( { name: "identifyTrends", retry }, - function identifyTrends(data: ParsedData) { + function identifyTrends(_ctx: TaskContext, data: ParsedData) { console.log("[TRENDS] Identifying trends"); if (!data.success || data.rows.length === 0) { @@ -164,6 +164,7 @@ const identifyTrends = task( const generateInsights = task( { name: "generateInsights", retry }, async function generateInsights( + _ctx: TaskContext, stats: { success?: boolean; numeric_columns?: string[]; statistics?: { [col: string]: { avg: number; min: number; max: number; sum: number } } }, trends: { success?: boolean; categorical_columns?: string[]; categorical_analysis?: { [col: string]: { top_5: [string, number][]; distribution: { [key: string]: number } } } }, metadata: ParsedData, @@ -214,11 +215,11 @@ const generateInsights = task( // Root task: orchestrates the full analysis pipeline task( { name: "analyzeFile", retry, timeoutSeconds: 300 }, - async function analyzeFile(fileContent: string) { + async function analyzeFile(ctx: TaskContext, fileContent: string) { console.log("[ANALYZE_FILE] Starting file analysis pipeline"); console.log("[ANALYZE_FILE] Stage 1: Parsing CSV data"); - const parsedData = await parseCsvData(fileContent); + const parsedData = await ctx.step(parseCsvData, fileContent); if (!parsedData.success) { console.error("[ANALYZE_FILE] Failed to parse CSV data"); @@ -227,14 +228,14 @@ task( console.log(`[ANALYZE_FILE] Parsed ${parsedData.row_count} rows`); - console.log("[ANALYZE_FILE] Stage 2: Calculating statistics"); - const stats = await calculateStatistics(parsedData); + console.log("[ANALYZE_FILE] Stage 2: Calculating statistics and identifying trends"); + const [stats, trends] = await Promise.all([ + ctx.step(calculateStatistics, parsedData), + ctx.step(identifyTrends, parsedData), + ]); - console.log("[ANALYZE_FILE] Stage 3: Identifying trends"); - const trends = await identifyTrends(parsedData); - - console.log("[ANALYZE_FILE] Stage 4: Generating insights"); - const insights = await generateInsights(stats, trends, parsedData); + console.log("[ANALYZE_FILE] Stage 3: Generating insights"); + const insights = await ctx.step(generateInsights, stats, trends, parsedData); console.log("[ANALYZE_FILE] Analysis pipeline completed successfully"); diff --git a/file-processing/src/main.ts b/file-processing/src/main.ts index 3991d77..0ccbe8c 100644 --- a/file-processing/src/main.ts +++ b/file-processing/src/main.ts @@ -1,8 +1,53 @@ import "dotenv/config"; -import { task } from "@renderinc/sdk/workflows"; +import { task, type TaskContext } from "@renderinc/sdk/workflows"; import { readFileSync, existsSync } from "node:fs"; import { resolve, extname } from "node:path"; +interface ReadFailure { + success: false; + file_path: string; + error: string; +} + +interface CsvReadSuccess { + success: true; + file_path: string; + file_type: "csv"; + row_count: number; + data: { [key: string]: string }[]; + columns: string[]; +} + +interface JsonReadSuccess { + success: true; + file_path: string; + file_type: "json"; + data: unknown; + keys: string[] | null; +} + +interface TextReadSuccess { + success: true; + file_path: string; + file_type: "text"; + content: string; + line_count: number; + word_count: number; + char_count: number; +} + +type ReadResult = CsvReadSuccess | JsonReadSuccess | TextReadSuccess | ReadFailure; + +type ProcessedFile = + | { success: false; file_path: string; error: string } + | { + success: true; + file_path: string; + file_type: string; + read_result: ReadResult; + analysis: { [key: string]: unknown }; + }; + const retry = { maxRetries: 3, waitDurationMs: 1000, @@ -13,7 +58,7 @@ const retry = { const readCsvFile = task( { name: "readCsvFile", retry }, - function readCsvFile(filePath: string) { + function readCsvFile(_ctx: TaskContext, filePath: string): CsvReadSuccess | ReadFailure { console.log(`[CSV] Reading file: ${filePath}`); const fullPath = resolve(filePath); @@ -48,7 +93,7 @@ const readCsvFile = task( const readJsonFile = task( { name: "readJsonFile", retry }, - function readJsonFile(filePath: string) { + function readJsonFile(_ctx: TaskContext, filePath: string): JsonReadSuccess | ReadFailure { console.log(`[JSON] Reading file: ${filePath}`); const fullPath = resolve(filePath); @@ -66,7 +111,7 @@ const readJsonFile = task( file_path: filePath, file_type: "json", data, - keys: typeof data === "object" && !Array.isArray(data) ? Object.keys(data) : null, + keys: typeof data === "object" && data !== null && !Array.isArray(data) ? Object.keys(data) : null, }; } catch (e) { console.error(`[JSON] Error reading file: ${e}`); @@ -77,7 +122,7 @@ const readJsonFile = task( const readTextFile = task( { name: "readTextFile", retry }, - function readTextFile(filePath: string) { + function readTextFile(_ctx: TaskContext, filePath: string): TextReadSuccess | ReadFailure { console.log(`[TEXT] Reading file: ${filePath}`); const fullPath = resolve(filePath); @@ -112,12 +157,12 @@ const readTextFile = task( const analyzeCsvData = task( { name: "analyzeCsvData", retry }, - function analyzeCsvData(csvResult: { success?: boolean; data?: { [key: string]: string }[] }) { + function analyzeCsvData(_ctx: TaskContext, csvResult: CsvReadSuccess | ReadFailure) { console.log("[ANALYSIS] Analyzing CSV data"); if (!csvResult.success) return { success: false, error: "No data to analyze" }; - const rows = csvResult.data ?? []; + const rows = csvResult.data; if (rows.length === 0) return { success: false, error: "Empty dataset" }; let totalQuantity = 0; @@ -152,7 +197,7 @@ const analyzeCsvData = task( const analyzeJsonStructure = task( { name: "analyzeJsonStructure", retry }, - function analyzeJsonStructure(jsonResult: { success?: boolean; data?: unknown }) { + function analyzeJsonStructure(_ctx: TaskContext, jsonResult: JsonReadSuccess | ReadFailure) { console.log("[ANALYSIS] Analyzing JSON structure"); if (!jsonResult.success) return { success: false, error: "No data to analyze" }; @@ -184,15 +229,12 @@ const analyzeJsonStructure = task( const analyzeTextContent = task( { name: "analyzeTextContent", retry }, - function analyzeTextContent(textResult: { - success?: boolean; - content?: string; - }) { + function analyzeTextContent(_ctx: TaskContext, textResult: TextReadSuccess | ReadFailure) { console.log("[ANALYSIS] Analyzing text content"); if (!textResult.success) return { success: false, error: "No data to analyze" }; - const content = textResult.content ?? ""; + const content = textResult.content; const lines = content.split("\n"); const words = content.split(/\s+/).filter(Boolean); @@ -224,30 +266,34 @@ const analyzeTextContent = task( const processSingleFile = task( { name: "processSingleFile", retry }, - async function processSingleFile(filePath: string) { + async function processSingleFile(ctx: TaskContext, filePath: string): Promise { console.log(`[PROCESS] Processing file: ${filePath}`); const extension = extname(filePath).toLowerCase(); - let readResult: { success?: boolean; [key: string]: unknown }; + let readResult: ReadResult; let analysis: { [key: string]: unknown } = {}; if (extension === ".csv") { - readResult = await readCsvFile(filePath); - if (readResult.success) analysis = await analyzeCsvData(readResult as Parameters[0]); + readResult = await ctx.step(readCsvFile, filePath); + if (readResult.success) analysis = await ctx.step(analyzeCsvData, readResult); } else if (extension === ".json") { - readResult = await readJsonFile(filePath); - if (readResult.success) analysis = await analyzeJsonStructure(readResult as Parameters[0]); + readResult = await ctx.step(readJsonFile, filePath); + if (readResult.success) analysis = await ctx.step(analyzeJsonStructure, readResult); } else if (extension === ".txt") { - readResult = await readTextFile(filePath); - if (readResult.success) analysis = await analyzeTextContent(readResult as Parameters[0]); + readResult = await ctx.step(readTextFile, filePath); + if (readResult.success) analysis = await ctx.step(analyzeTextContent, readResult); } else { console.warn(`[PROCESS] Unsupported file type: ${extension}`); return { success: false, file_path: filePath, error: `Unsupported file type: ${extension}` }; } + if (!readResult.success) { + return { success: false, file_path: filePath, error: readResult.error }; + } + console.log(`[PROCESS] File processed: ${filePath}`); return { - success: readResult.success ?? false, + success: true, file_path: filePath, file_type: extension.slice(1), read_result: readResult, @@ -259,20 +305,20 @@ const processSingleFile = task( // Root task: processes multiple files in parallel task( { name: "processFileBatch", retry, timeoutSeconds: 300 }, - async function processFileBatch(...filePaths: string[]) { + async function processFileBatch(ctx: TaskContext, ...filePaths: string[]) { console.log("=".repeat(80)); console.log(`[BATCH] Starting batch processing of ${filePaths.length} files`); console.log("=".repeat(80)); - const results = await Promise.all(filePaths.map((fp) => processSingleFile(fp))); + const results = await Promise.all(filePaths.map((fp) => ctx.step(processSingleFile, fp))); const successful = results.filter((r) => r.success); const failed = results.filter((r) => !r.success); const fileTypes: { [key: string]: number } = {}; - for (const result of successful) { - const ft = (result.file_type as string) ?? "unknown"; - fileTypes[ft] = (fileTypes[ft] ?? 0) + 1; + for (const result of results) { + if (!result.success) continue; + fileTypes[result.file_type] = (fileTypes[result.file_type] ?? 0) + 1; } const batchResult = { @@ -297,14 +343,17 @@ task( // Root task: generate a consolidated report from batch results task( { name: "generateConsolidatedReport", retry }, - async function generateConsolidatedReport(batchResult: { - total_files?: number; - successful?: number; - failed?: number; - success_rate?: number; - file_types?: { [key: string]: number }; - results?: { success?: boolean; file_type?: string; analysis?: { total_records?: number; total_words?: number; total_keys?: number } }[]; - }) { + async function generateConsolidatedReport( + _ctx: TaskContext, + batchResult: { + total_files?: number; + successful?: number; + failed?: number; + success_rate?: number; + file_types?: { [key: string]: number }; + results?: { success?: boolean; file_type?: string; analysis?: { total_records?: number; total_words?: number; total_keys?: number } }[]; + }, + ) { console.log("[REPORT] Generating consolidated report"); const results = batchResult.results ?? []; diff --git a/hello-world/README.md b/hello-world/README.md index ed11810..9e9d914 100644 --- a/hello-world/README.md +++ b/hello-world/README.md @@ -9,16 +9,21 @@ This hello-world example demonstrates three foundational workflow patterns: ## What You'll Learn - How to define tasks with `task(...)` -- How to chain task runs using `await` and `Promise.all` +- How to chain task runs using `ctx.step` and `Promise.all` - How to customize retry behavior with `retry` ## Example Tasks -### `calculateSquare(a: number): number` +Every task takes a `TaskContext` as its first parameter, followed by its inputs. +The workflow system supplies the context, so the inputs you pass with +`--input` start after it. -The smallest possible task: takes one number and returns its square. +### `calculateSquare(ctx, a: number): number` -### `sumSquares(a: number, b: number): Promise` +The smallest possible task: takes one number and returns its square. It runs no +other tasks, so it never touches the context. + +### `sumSquares(ctx, a: number, b: number): Promise` Chains two runs of `calculateSquare` and sums the results. @@ -26,12 +31,12 @@ It uses `Promise.all(...)` to chain the two runs in parallel: ```ts const [result1, result2] = await Promise.all([ - calculateSquare(a), - calculateSquare(b), + ctx.step(calculateSquare, a), + ctx.step(calculateSquare, b), ]); ``` -### `flipCoin(): string` +### `flipCoin(ctx): string` Simulates a coin flip: @@ -89,10 +94,13 @@ Configure your Workflow service with: ### Task registration Any call to `task({ name: ... }, handler)` registers a runnable workflow task. +`task(...)` returns a task definition, which is not callable on its own — pass +it to `ctx.step` to run it. ### Chaining runs -Inside an async task, calling `await anotherTask(...)` chains a run of that task. +Inside an async task, `await ctx.step(anotherTask, ...inputs)` runs that task on +its own compute and resolves with its result. ### Retries diff --git a/hello-world/package.json b/hello-world/package.json index 048e8f5..3e870b4 100644 --- a/hello-world/package.json +++ b/hello-world/package.json @@ -7,7 +7,7 @@ "start": "tsx src/main.ts" }, "dependencies": { - "@renderinc/sdk": "^0.5.0" + "@renderinc/sdk": "latest" }, "devDependencies": { "tsx": "^4.20.2" diff --git a/hello-world/src/main.ts b/hello-world/src/main.ts index 6c00007..3f28905 100644 --- a/hello-world/src/main.ts +++ b/hello-world/src/main.ts @@ -1,18 +1,18 @@ -import { task } from "@renderinc/sdk/workflows"; +import { task, type TaskContext } from "@renderinc/sdk/workflows"; const calculateSquare = task( { name: "calculateSquare" }, - function calculateSquare(a: number): number { + function calculateSquare(_ctx: TaskContext, a: number): number { return a * a; }, ); const sumSquares = task( { name: "sumSquares" }, - async function sumSquares(a: number, b: number): Promise { + async function sumSquares(ctx: TaskContext, a: number, b: number): Promise { const [result1, result2] = await Promise.all([ - calculateSquare(a), - calculateSquare(b), + ctx.step(calculateSquare, a), + ctx.step(calculateSquare, b), ]); return result1 + result2; }, @@ -27,7 +27,7 @@ task( backoffScaling: 1.5, }, }, - function flipCoin(): string { + function flipCoin(_ctx: TaskContext): string { if (Math.random() < 0.5) { throw new Error("Flipped tails! Retrying."); } diff --git a/openai-agent/src/main.ts b/openai-agent/src/main.ts index 2a94582..6855895 100644 --- a/openai-agent/src/main.ts +++ b/openai-agent/src/main.ts @@ -1,5 +1,5 @@ import "dotenv/config"; -import { task } from "@renderinc/sdk/workflows"; +import { task, type TaskContext } from "@renderinc/sdk/workflows"; import OpenAI from "openai"; import type { ChatCompletionMessageParam, ChatCompletionTool } from "openai/resources/chat/completions"; @@ -24,7 +24,7 @@ function createOpenAIClient(): OpenAI { const getOrderStatus = task( { name: "getOrderStatus", retry }, - function getOrderStatus(orderId: string) { + function getOrderStatus(_ctx: TaskContext, orderId: string) { console.log(`[TOOL] Looking up order status for: ${orderId}`); const mockOrders: { [key: string]: { status: string; tracking: string | null; eta: string } } = { @@ -47,7 +47,7 @@ const getOrderStatus = task( // No retry: processing a refund is non-idempotent const processRefund = task( { name: "processRefund" }, - function processRefund(orderId: string, reason: string) { + function processRefund(_ctx: TaskContext, orderId: string, reason: string) { console.log(`[TOOL] Processing refund for order: ${orderId}`); console.log(`[TOOL] Refund reason: ${reason}`); @@ -69,7 +69,7 @@ const processRefund = task( const searchKnowledgeBase = task( { name: "searchKnowledgeBase", retry }, - function searchKnowledgeBase(query: string) { + function searchKnowledgeBase(_ctx: TaskContext, query: string) { console.log(`[TOOL] Searching knowledge base: ${query}`); const knowledge: { [key: string]: { title: string; content: string } } = { @@ -155,6 +155,7 @@ const tools: ChatCompletionTool[] = [ const callLlmWithTools = task( { name: "callLlmWithTools", retry }, async function callLlmWithTools( + _ctx: TaskContext, messages: ChatCompletionMessageParam[], toolDefs: ChatCompletionTool[], model: string = "gpt-4", @@ -191,17 +192,17 @@ const callLlmWithTools = task( const executeTool = task( { name: "executeTool", retry }, - async function executeTool(toolName: string, args: { [key: string]: string }) { + async function executeTool(ctx: TaskContext, toolName: string, args: { [key: string]: string }) { console.log(`[AGENT] Executing tool: ${toolName}`); try { switch (toolName) { case "get_order_status": - return await getOrderStatus(args.order_id); + return await ctx.step(getOrderStatus, args.order_id); case "process_refund": - return await processRefund(args.order_id, args.reason); + return await ctx.step(processRefund, args.order_id, args.reason); case "search_knowledge_base": - return await searchKnowledgeBase(args.query); + return await ctx.step(searchKnowledgeBase, args.query); default: console.error(`[AGENT] Unknown tool: ${toolName}`); return { error: `Unknown tool: ${toolName}` }; @@ -216,6 +217,7 @@ const executeTool = task( const agentTurn = task( { name: "agentTurn", retry }, async function agentTurn( + ctx: TaskContext, userMessage: string, conversationHistory: ChatCompletionMessageParam[] = [], ) { @@ -244,7 +246,7 @@ const agentTurn = task( { role: "user", content: userMessage }, ]; - const llmResponse = await callLlmWithTools(messages, tools); + const llmResponse = await ctx.step(callLlmWithTools, messages, tools); if (!llmResponse.tool_calls.length) { console.log("[AGENT TURN] No tool calls, returning response"); @@ -263,7 +265,8 @@ const agentTurn = task( const toolResults: { tool: string; result: unknown }[] = []; for (const toolCall of llmResponse.tool_calls) { - const result = await executeTool( + const result = await ctx.step( + executeTool, toolCall.function.name, JSON.parse(toolCall.function.arguments), ); @@ -290,7 +293,7 @@ const agentTurn = task( ...toolMessages, ]; - const finalResponse = await callLlmWithTools(finalMessages, tools); + const finalResponse = await ctx.step(callLlmWithTools, finalMessages, tools); console.log("[AGENT TURN] Agent turn complete"); @@ -309,7 +312,7 @@ const agentTurn = task( // Root task: multi-turn conversation task( { name: "multiTurnConversation", retry, timeoutSeconds: 300 }, - async function multiTurnConversation(...messages: string[]) { + async function multiTurnConversation(ctx: TaskContext, ...messages: string[]) { console.log("=".repeat(80)); console.log(`[CONVERSATION] Starting multi-turn conversation with ${messages.length} messages`); console.log("=".repeat(80)); @@ -320,7 +323,7 @@ task( for (let i = 0; i < messages.length; i++) { console.log(`[CONVERSATION] Turn ${i + 1}/${messages.length}`); - const turnResult = await agentTurn(messages[i], conversationHistory); + const turnResult = await ctx.step(agentTurn, messages[i], conversationHistory); responses.push({ turn: i + 1,