Skip to content

Migrate Vision and AI-room-renovate for new motia version#140

Open
guibeira wants to merge 11 commits into
mainfrom
migrate-motia-py
Open

Migrate Vision and AI-room-renovate for new motia version#140
guibeira wants to merge 11 commits into
mainfrom
migrate-motia-py

Conversation

@guibeira

@guibeira guibeira commented Feb 20, 2026

Copy link
Copy Markdown

Summary by CodeRabbit

  • New Features

    • Added YAML-based configuration files for advanced examples with comprehensive module setup.
    • Added Python project configuration for AI examples.
    • Updated examples to use Motia 1.0.0-rc.14 with queue-based messaging architecture.
  • Chores

    • Removed TypeScript configuration files and UI override components from examples.
    • Updated dependencies across ai-room-renovate and vision-example projects.
    • Replaced deprecated vision-agent integration with Anthropic API in vision-example.

@guibeira guibeira changed the title Migrate Vision and AI-room-renovate. Migrate Vision and AI-room-renovate for new motia version Feb 20, 2026
@coderabbitai

coderabbitai Bot commented Feb 20, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This pull request upgrades two advanced examples (ai-room-renovate and vision-example) from beta Motia framework versions to release candidates (1.0.0-rc.14/rc.22). Changes include new YAML runtime configurations, Python dependency management via pyproject.toml, migrating all steps from event-based (subscribes/emits) to trigger-based (queue/api triggers with enqueues) architecture, updating handler signatures to the new Handlers pattern, and removing deprecated UI component overrides and old configuration files.

Changes

Cohort / File(s) Summary
Configuration & Setup Files
examples/advanced-use-cases/ai-room-renovate/config.yaml, examples/advanced-use-cases/vision-example/config.yaml
New YAML configurations define modular runtime setup with StreamModule, StateModule, RestApiModule, OtelModule, QueueModule, PubSubModule, CronModule, and ExecModules for TS/Python asset handling.
Python Project Configuration
examples/advanced-use-cases/ai-room-renovate/pyproject.toml, examples/advanced-use-cases/vision-example/pyproject.toml
Added project metadata, dependencies (motia[otel]==1.0.0-rc.22, iii-sdk, pydantic, google-adk/genai for room-renovate; anthropic for vision), and dev tooling (pytest, uv package flag).
Package Management
examples/advanced-use-cases/ai-room-renovate/package.json, examples/advanced-use-cases/vision-example/package.json
Updated motia dependency to 1.0.0-rc.14; replaced postinstall/generate-types/build/clean scripts with start/dev/dev:py; removed @motiadev/* beta plugin dependencies and jest-related devDependencies.
Removed Legacy Configuration
examples/advanced-use-cases/ai-room-renovate/motia.config.ts, motia-workbench.json, types.d.ts
Deleted plugin-wiring config, workbench layout data, and ambient type declarations; replaced with new YAML-driven setup and auto-generated types.
Removed Python Dependencies
examples/advanced-use-cases/ai-room-renovate/requirements.txt, steps/__init__.py
Removed deprecated pip dependencies (google-adk, google-genai, python-dotenv, pydantic) and sys.path mutation; transitioned to pyproject.toml and standard imports.
TypeScript Step Migrations (ai-room-renovate)
steps/renovation/design_planner.step.ts, edit_rendering_api.step.ts, get_rendering.step.ts, get_renovation_result.step.ts, info_handler.step.ts, project_coordinator.step.ts, start_renovation.step.ts, visual_assessor.step.ts
Migrated from EventConfig/ApiRouteConfig to StepConfig with queue/api triggers; replaced subscribes/emits with triggers/enqueues; updated handler type to Handlers; replaced emit with enqueue calls.
TypeScript Step Migrations (vision-example)
steps/api.step.ts, eval-agent/eval-agent.step.ts, eval-agent/eval-agent-results.api.step.ts, generate-image.step.ts
Migrated from EventConfig/ApiRouteConfig/StepHandler to StepConfig with queue/api triggers; updated handler signatures and replaced emit with enqueue; removed explicit Input type aliases.
Python Step Migrations (ai-room-renovate)
steps/renovation/edit-rendering_step.py, generate-rendering_step.py
Replaced context-based API with FlowContext; migrated from event-driven config to trigger/enqueue model; updated Gemini SDK usage (client.models.generate_content) and state access patterns via ctx.state.
Python Step Migrations (vision-example)
steps/enhance_image_prompt_step.py, evaluate_result_step.py
Refactored to FlowContext[Any] handler signature; migrated from AnthropicLMM to direct Anthropic client initialization; replaced event-driven config with queue triggers/enqueues; updated input handling and logging.
Removed UI Component Overrides
steps/renovation/design_planner.step.tsx, get_renovation_result.step.tsx, info_handler.step.tsx, project_coordinator.step.tsx, start_renovation.step.tsx, visual_assessor.step.tsx
Deleted React component Node exports; removed custom EventNode and ApiNode UI overrides for step visualization.
Minor Updates
examples/advanced-use-cases/vision-example/generate-dataset.ts, examples/advanced-use-cases/vision-example/README.md, examples/advanced-use-cases/vision-example/types.d.ts
Updated API endpoint from localhost:3000 to localhost:3111; revised setup instructions for Node.js/UV/pnpm and ANTHROPIC_API_KEY; added auto-generated Streams/Enqueues type augmentation.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 From beta bonds to stable code we hop,
With queue-based triggers that never stop,
Old configs fade, new YAML blooms bright,
FlowContext flows—our framework takes flight! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and clearly describes the primary change: migrating two example projects (Vision and AI-room-renovate) to a new motia version, which is the main objective across all modified files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch migrate-motia-py

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (8)
examples/advanced-use-cases/ai-room-renovate/steps/renovation/visual_assessor.step.ts (2)

99-102: ⚠️ Potential issue | 🔴 Critical

Bug: 'luxury' scope is unreachable — condition order is wrong.

avgCost > 600 is a strict subset of avgCost > 300, so the else if on Line 102 can never execute. Every value above 600 is already captured by the > 300 branch on Line 101.

Reverse the checks so the highest threshold is evaluated first:

🐛 Proposed fix
     let scope = 'moderate';
-    if (avgCost < 100) scope = 'cosmetic';
-    else if (avgCost > 300) scope = 'full';
-    else if (avgCost > 600) scope = 'luxury';
+    if (avgCost > 600) scope = 'luxury';
+    else if (avgCost > 300) scope = 'full';
+    else if (avgCost < 100) scope = 'cosmetic';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@examples/advanced-use-cases/ai-room-renovate/steps/renovation/visual_assessor.step.ts`
around lines 99 - 102, The conditional ordering in the block that sets scope
based on avgCost makes the 'luxury' branch unreachable; reorder the comparisons
in the conditional that assigns scope (the code using variable scope and
avgCost) so the largest threshold is checked first (e.g., if (avgCost > 600)
scope = 'luxury'; else if (avgCost > 300) scope = 'full'; else if (avgCost <
100) scope = 'cosmetic'; else scope = 'moderate';) to ensure each branch can be
reached.

97-98: ⚠️ Potential issue | 🟡 Minor

budget may not be a number — consider adding a numeric guard.

storedBudget is typed any (from state) and roomDetails.budget comes from string parsing. If either yields a string like "5000", the division on Line 98 still works in JS due to implicit coercion, but a non-numeric string (e.g. "$5,000") would produce NaN, silently breaking the cost estimate.

A simple Number(assessment.budget) coercion (with a NaN check) would make this more robust:

🛡️ Suggested guard
   if (assessment.budget) {
-    const avgCost = assessment.budget / assessment.squareFootage;
+    const budgetNum = Number(assessment.budget);
+    if (isNaN(budgetNum)) return;
+    const avgCost = budgetNum / assessment.squareFootage;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@examples/advanced-use-cases/ai-room-renovate/steps/renovation/visual_assessor.step.ts`
around lines 97 - 98, The calculation using assessment.budget can produce NaN if
budget is non-numeric; in the visual_assessor.step.ts code locate the block that
computes avgCost from assessment.budget and assessment.squareFootage and coerce
assessment.budget to a numeric value (e.g., via Number(...) or a sanitized
parse) then check isFinite/!isNaN before performing the division, and if the
value is invalid handle it (skip avgCost, use a fallback, or log/throw) so
avgCost = assessment.budget / assessment.squareFootage cannot silently become
NaN.
examples/advanced-use-cases/ai-room-renovate/steps/renovation/edit_rendering_api.step.ts (1)

71-77: ⚠️ Potential issue | 🟡 Minor

Over-broad catch conflates validation errors with infrastructure failures.

The try block covers state.get() and enqueue() in addition to bodySchema.parse(). Any infrastructure error (state storage unavailable, queue timeout) will surface to the caller as 400 "Invalid request data", which is semantically incorrect and prevents clients from distinguishing retriable failures from bad requests. The server-side log at Line 72 preserves observability, but the HTTP contract is broken for non-validation errors.

Per Motia docs, bodySchema is not validated automatically — manual .parse() is necessary — so the ZodError path is intentional, but it should be isolated.

🐛 Proposed fix — narrow the catch to ZodErrors
-  } catch (error) {
-    logger.error('Failed to process edit request', { error: String(error) });
-    return {
-      status: 400,
-      body: { error: 'Invalid request data' },
-    };
-  }
+  } catch (error) {
+    if (error instanceof Error && error.name === 'ZodError') {
+      logger.error('Invalid request body', { error: String(error) });
+      return {
+        status: 400,
+        body: { error: 'Invalid request data' },
+      };
+    }
+    logger.error('Failed to process edit request', { error: String(error) });
+    throw error;
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@examples/advanced-use-cases/ai-room-renovate/steps/renovation/edit_rendering_api.step.ts`
around lines 71 - 77, The current catch around bodySchema.parse(), state.get(),
and enqueue() treats all errors as validation failures; isolate validation by
calling bodySchema.parse() inside its own try/catch that only catches ZodError
(imported from "zod") and returns the 400 + "Invalid request data" response,
while letting other errors from state.get() or enqueue() bubble up or be handled
as server errors (log with logger.error in the outer catch and return 500).
Update the handler so bodySchema.parse() is validated first (or in a small try
that catches ZodError), and ensure logger.error calls (the existing
logger.error('Failed to process edit request', { error: String(error) })) remain
for non-validation failures.
examples/advanced-use-cases/ai-room-renovate/steps/renovation/design_planner.step.ts (1)

38-44: ⚠️ Potential issue | 🔴 Critical

Bug: 'luxury' scope is unreachable.

The avgCost > 600 check on Line 43 will never be reached because avgCost > 300 on Line 42 is evaluated first and catches all values above 300, including those above 600.

🐛 Proposed fix — reorder conditions from highest to lowest
   let scope = 'moderate';
   if (costEstimate) {
     const avgCost = costEstimate.totalLow / assessment.squareFootage;
     if (avgCost < 100) scope = 'cosmetic';
-    else if (avgCost > 300) scope = 'full';
-    else if (avgCost > 600) scope = 'luxury';
+    else if (avgCost > 600) scope = 'luxury';
+    else if (avgCost > 300) scope = 'full';
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@examples/advanced-use-cases/ai-room-renovate/steps/renovation/design_planner.step.ts`
around lines 38 - 44, The scope assignment logic makes 'luxury' unreachable
because the avgCost > 300 branch runs before avgCost > 600; update the
conditional order in the block that computes avgCost (which uses costEstimate
and assessment.squareFootage) to check the highest threshold first (avgCost >
600), then avgCost > 300, then avgCost < 100, or refactor to explicit range
checks so 'scope' can be set to 'luxury', 'full', 'moderate' or 'cosmetic'
correctly.
examples/advanced-use-cases/vision-example/steps/enhance_image_prompt_step.py (1)

32-33: ⚠️ Potential issue | 🔴 Critical

claude-3-sonnet-20240229 is retired — API calls to this model will fail at runtime.

Anthropic notified developers on January 21, 2025, that Claude Sonnet 3 models would be retired, and they were retired on November 6, 2024. Retired models are no longer available for use, and requests to them will fail. The companion step evaluate_result_step.py already uses claude-sonnet-4-20250514.

🐛 Proposed fix
-    model="claude-3-sonnet-20240229",
+    model="claude-sonnet-4-5-20251001",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@examples/advanced-use-cases/vision-example/steps/enhance_image_prompt_step.py`
around lines 32 - 33, The model string passed to client.messages.create
("claude-3-sonnet-20240229") is retired and will cause runtime failures; update
the model parameter in the client.messages.create call to a supported model
(e.g., the same "claude-sonnet-4-20250514" used in evaluate_result_step.py) so
requests succeed, ensuring any related code or tests that assume the old model
name are adjusted to the new model identifier.
examples/advanced-use-cases/vision-example/steps/generate-image.step.ts (1)

28-40: ⚠️ Potential issue | 🟠 Major

Unbounded recursion in getRequestStatus risks a stack overflow.

If the FAL API never returns COMPLETED (e.g., it errors, stays IN_PROGRESS, or returns an unexpected status), this function recurses indefinitely with only a 1-second delay between calls. Consider adding a max-retry limit or converting to an iterative loop with a timeout.

🛡️ Proposed iterative approach with a retry cap
-const getRequestStatus = async (requestId: string) => {
-  const status = await fal.queue.status(FAL_MODEL, {
-    requestId: requestId,
-    logs: true,
-  });
-
-  if (status.status !== 'COMPLETED') {
-    await new Promise(resolve => setTimeout(resolve, 1000));
-    return getRequestStatus(requestId);
-  }
-
-  return status.status;
-}
+const getRequestStatus = async (requestId: string, maxRetries = 120): Promise<string> => {
+  for (let attempt = 0; attempt < maxRetries; attempt++) {
+    const status = await fal.queue.status(FAL_MODEL, {
+      requestId: requestId,
+      logs: true,
+    });
+
+    if (status.status === 'COMPLETED') {
+      return status.status;
+    }
+
+    await new Promise(resolve => setTimeout(resolve, 1000));
+  }
+
+  throw new Error(`Request ${requestId} did not complete after ${maxRetries} retries`);
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/advanced-use-cases/vision-example/steps/generate-image.step.ts`
around lines 28 - 40, The getRequestStatus function currently recurses
indefinitely when fal.queue.status never returns 'COMPLETED' (risking stack
overflow); replace the recursive logic in getRequestStatus with an iterative
loop that polls fal.queue.status(FAL_MODEL, { requestId, logs:true }) with a
delay and enforces a max retry count or overall timeout, and if the limit is
reached either throw a descriptive error or return the last status object;
update any callers to handle the thrown error or timeout return.
examples/advanced-use-cases/vision-example/steps/eval-agent/eval-agent.step.ts (2)

133-139: ⚠️ Potential issue | 🟠 Major

Division by zero when datasetReport is empty.

If no files in tmp match the _report.txt pattern or none pass the reportData validation on Line 46, datasetReport.length will be 0, causing confidencePercentage to be Infinity. Guard against this before the enqueue call.

🛡️ Proposed fix
     await enqueue({
       topic: 'eval-image-generation-dataset-score',
       data: {
         reportPath,
-        confidencePercentage: (finalScore.success * 100) / datasetReport.length,
+        confidencePercentage: datasetReport.length > 0
+          ? (finalScore.success * 100) / datasetReport.length
+          : 0,
       },
     })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@examples/advanced-use-cases/vision-example/steps/eval-agent/eval-agent.step.ts`
around lines 133 - 139, Guard against division by zero when computing
confidencePercentage before calling enqueue: check datasetReport.length (or
compute a safe denominator variable) and if it is 0 set confidencePercentage to
0 (or an appropriate default) instead of doing (finalScore.success * 100) /
datasetReport.length; then call enqueue exactly as before with reportPath and
the safe confidencePercentage. Update the code around the enqueue call and any
helpers that compute finalScore to use the safe denominator to avoid Infinity.

96-96: ⚠️ Potential issue | 🟡 Minor

Misleading comment: actual sleep is 1 second, not 30.

The comment says "Sleep for 30 seconds" but sleep(1000) sleeps for 1 second.

📝 Proposed fix
-    await sleep(1000) // Sleep for 30 seconds
+    await sleep(1000) // Sleep for 1 second between API calls
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@examples/advanced-use-cases/vision-example/steps/eval-agent/eval-agent.step.ts`
at line 96, The inline comment next to the sleep call is incorrect; in the
eval-agent.step.ts code the statement await sleep(1000) sleeps for 1 second
(1000 ms) but the comment says 30 seconds—update the comment to accurately
reflect the actual delay (e.g., "Sleep for 1 second (1000 ms)") or, if the
intent was 30 seconds, change the argument to sleep(30000) instead; locate the
await sleep(...) call to apply the fix.
🧹 Nitpick comments (12)
examples/advanced-use-cases/vision-example/steps/eval-agent/eval-agent-results.api.step.ts (1)

22-31: Use async fs/promises instead of synchronous readdirSync, and handle missing tmp directory.

Two concerns here:

  1. fs.readdirSync blocks the event loop in an async handler. The sibling file eval-agent.step.ts uses fs/promises with await fs.readdir('tmp') — this file should be consistent.
  2. If the tmp directory doesn't exist, readdirSync throws ENOENT with no try-catch, resulting in an unhandled 500 instead of a meaningful error response.
Proposed fix

Update the import on line 3:

-import fs from 'fs'
+import fs from 'fs/promises'

Then wrap the directory read with error handling:

-    // Check for minimum number of report files
-    const reportFiles = fs.readdirSync('tmp')
-        .filter(file => file.endsWith('_report.txt'))
+    // Check for minimum number of report files
+    let reportFiles: string[] = []
+    try {
+        const files = await fs.readdir('tmp')
+        reportFiles = files.filter(file => file.endsWith('_report.txt'))
+    } catch {
+        return {
+            status: 400,
+            body: { message: 'No report files found. Please run the generate-image flow first.' },
+        }
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@examples/advanced-use-cases/vision-example/steps/eval-agent/eval-agent-results.api.step.ts`
around lines 22 - 31, Replace the blocking fs.readdirSync call with the async
fs/promises readdir and add error handling for a missing 'tmp' directory:
import/require the promise-based fs, await fs.readdir('tmp'), filter for files
ending with '_report.txt' (same reportFiles variable), and catch errors—if
err.code === 'ENOENT' return a 400 response with a clear message about the
missing 'tmp' directory; for other errors return a 500 or appropriate error
response. Ensure the logic still checks reportFiles.length < 10 and returns the
existing insufficient reports message.
examples/advanced-use-cases/ai-room-renovate/steps/renovation/visual_assessor.step.ts (1)

34-47: Consider extracting the state-unwrapping into a small helper.

The unwrap-if-wrapped pattern is duplicated three times. A tiny generic helper would reduce noise and make it easier to add more state reads later.

♻️ Suggested helper
+function unwrapState<T>(value: unknown): T | undefined {
+  if (value && typeof value === 'object' && 'data' in value) {
+    return (value as { data: T }).data;
+  }
+  return value as T | undefined;
+}
+

Then usage becomes:

const storedBudget = unwrapState<number>(await state.get<any>(sessionId, 'budget'));
const storedRoomType = unwrapState<string>(await state.get<any>(sessionId, 'roomType'));
const storedStyle = unwrapState<string>(await state.get<any>(sessionId, 'style'));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@examples/advanced-use-cases/ai-room-renovate/steps/renovation/visual_assessor.step.ts`
around lines 34 - 47, Extract the repeated "if object has 'data' unwrap it"
logic into a small generic helper function, e.g. unwrapState<T>(value: any): T |
undefined, that returns value.data when value is an object containing 'data' and
otherwise returns value; then replace the three manual blocks with calls like
const storedBudget = unwrapState<number>(await state.get<any>(sessionId,
'budget')), const storedRoomType = unwrapState<string>(await
state.get<any>(sessionId, 'roomType')), and const storedStyle =
unwrapState<string>(await state.get<any>(sessionId, 'style')) so the unwrapping
logic is centralized (refer to symbols unwrapState, storedBudget,
storedRoomType, storedStyle, and the state.get(...) calls).
examples/advanced-use-cases/ai-room-renovate/steps/renovation/edit_rendering_api.step.ts (1)

42-42: state.get is missing a type parameter.

Sibling steps (e.g. get_rendering.step.ts Line 47: state.get<any>(sessionId, 'rendering')) consistently pass a type argument. Without it, rendering is implicitly typed as unknown, which can cause unexpected narrowing downstream.

♻️ Proposed fix
-    const rendering = await state.get(sessionId, 'rendering');
+    const rendering = await state.get<Record<string, unknown>>(sessionId, 'rendering');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@examples/advanced-use-cases/ai-room-renovate/steps/renovation/edit_rendering_api.step.ts`
at line 42, state.get is called without a type argument causing rendering to be
inferred as unknown; update the call to state.get to include the appropriate
generic (e.g., state.get<any>(sessionId, 'rendering') or a specific interface)
so rendering is typed correctly; modify the invocation in
edit_rendering_api.step.ts (the state.get call that assigns rendering) to pass
the chosen type parameter.
examples/advanced-use-cases/ai-room-renovate/steps/renovation/project_coordinator.step.ts (1)

77-82: Note the naming convention mismatch between TS and Python steps.

The enqueue payload uses session_id (snake_case) while this step's own input schema uses sessionId (camelCase). This is intentional since the downstream consumer is generate-rendering_step.py which expects session_id, but it's worth being aware of this TS↔Python naming boundary. A shared schema or mapping convention would help prevent drift over time.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@examples/advanced-use-cases/ai-room-renovate/steps/renovation/project_coordinator.step.ts`
around lines 77 - 82, The enqueue call is sending session_id (snake_case) while
this step's input schema uses sessionId (camelCase); explicitly map the step
input sessionId to session_id in the enqueue payload (i.e., set data.session_id
= input.sessionId) and add a short comment noting the TS↔Python naming boundary,
or alternatively centralize the mapping in a shared helper to prevent drift;
update the call to enqueue (topic 'renovation.render') and ensure any references
to sessionId/session_id across this step and generate-rendering_step.py are
consistently mapped.
examples/advanced-use-cases/ai-room-renovate/steps/renovation/edit-rendering_step.py (1)

239-242: Same broad except Exception concern as in generate-rendering_step.py.

Consider adding the error type for better diagnostics, as suggested for the sibling file.

♻️ Suggested improvement
     except Exception as e:
         error_msg = str(e)
-        ctx.logger.error("Error editing rendering", {"session_id": session_id, "error": error_msg})
+        ctx.logger.error("Error editing rendering", {"session_id": session_id, "error": error_msg, "error_type": type(e).__name__})
         await ctx.state.set(session_id, "editError", error_msg)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@examples/advanced-use-cases/ai-room-renovate/steps/renovation/edit-rendering_step.py`
around lines 239 - 242, Replace the broad "except Exception as e" in the error
handling for edit-rendering_step with more specific exception types where
possible (e.g., ValueError, RuntimeError) or, if you cannot enumerate them,
augment the existing handler to include the exception type and stack trace in
logs/state; update the ctx.logger.error call to include type(e).__name__ (and
optionally traceback.format_exc()) and store a richer error value in await
ctx.state.set(session_id, "editError", ...) so both ctx.logger.error and
ctx.state.set include the error type (and stack) alongside the message.
examples/advanced-use-cases/ai-room-renovate/steps/renovation/generate-rendering_step.py (2)

230-236: Broad except Exception — consider preserving the traceback.

Per Ruff BLE001/TRY400, the bare Exception catch loses traceback context. While ctx.logger may not support .exception(), you can still capture and log the traceback explicitly.

♻️ Suggested improvement
-    except Exception as e:
-        error_msg = str(e)
-        ctx.logger.error("Error generating rendering", {
-            "session_id": session_id,
-            "error": error_msg
-        })
+    except Exception as e:
+        error_msg = str(e)
+        ctx.logger.error("Error generating rendering", {
+            "session_id": session_id,
+            "error": error_msg,
+            "error_type": type(e).__name__
+        })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@examples/advanced-use-cases/ai-room-renovate/steps/renovation/generate-rendering_step.py`
around lines 230 - 236, The except block currently swallows the traceback by
using "except Exception as e" and only logging str(e); import the traceback
module and capture the full traceback with traceback.format_exc(), then include
that traceback string (in addition to the error message) when calling
ctx.logger.error and when calling ctx.state.set to preserve diagnostic context;
update the error handling in the except block that surrounds ctx.logger.error /
await ctx.state.set (the block catching Exception e in
generate-rendering_step.py) to record both the error message and the formatted
traceback.

164-174: Synchronous streaming iteration inside an async handler blocks the event loop.

client.models.generate_content_stream(...) returns a synchronous iterator. Using a plain for loop in the async def handler will block the event loop for the entire duration of image generation. The same pattern exists in edit-rendering_step.py.

The google-genai SDK provides an async variant via Client().aio with async for, or you can wrap the sync call in asyncio.to_thread. This is pre-existing behavior, so deferring is fine, but worth tracking.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@examples/advanced-use-cases/ai-room-renovate/steps/renovation/generate-rendering_step.py`
around lines 164 - 174, The async handler is performing synchronous streaming
iteration (client.models.generate_content_stream) with a blocking for loop,
which will block the event loop; fix by using the SDK's async client variant
(client.aio.models.generate_content_stream) and iterate with "async for" inside
the async def handler, or alternatively call the sync generator inside
asyncio.to_thread to avoid blocking; update the loop in
generate-rendering_step.py (the async def handler that uses
client.models.generate_content_stream) and make the analogous change in
edit-rendering_step.py where the same pattern occurs.
examples/advanced-use-cases/ai-room-renovate/steps/renovation/start_renovation.step.ts (1)

46-46: substr is deprecated — use substring instead.

String.prototype.substr is deprecated in modern ECMAScript. Use substring for the same behavior.

♻️ Suggested fix
-    const sessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
+    const sessionId = `session_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@examples/advanced-use-cases/ai-room-renovate/steps/renovation/start_renovation.step.ts`
at line 46, Replace the deprecated String.prototype.substr call when building
sessionId: in the expression that generates the random suffix
(Math.random().toString(36).substr(2, 9)), switch to substring and use the
equivalent end index (Math.random().toString(36).substring(2, 11)) so the
produced sessionId in the const sessionId = `session_${Date.now()}_${...}`
retains the same length and behavior.
examples/advanced-use-cases/vision-example/steps/generate-image.step.ts (1)

63-109: Use logger.error instead of console.error for consistency.

The handler destructures logger (Line 63) and uses it for info logging (Line 64, 96), but error paths on Lines 76, 87, and 107 use console.error. This bypasses structured logging and any log routing the framework provides.

♻️ Proposed fix
     if (!requestId) {
-      console.error('failed to generate image')
+      logger.error('failed to generate image')
       return;
     }
     if (!result.data.images.length) {
-      console.error('no image generated')
+      logger.error('no image generated')
       return;
     }
   } catch (error) {
-    console.error('failed to generate image', error)
+    logger.error('failed to generate image', error)
     return;
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/advanced-use-cases/vision-example/steps/generate-image.step.ts`
around lines 63 - 109, The error paths in the exported handler use console.error
(three places) instead of the destructured logger; replace all console.error
calls inside handler with logger.error so errors go through the app's structured
logger. Specifically, update the error branches after fal.queue.submit (check
for missing requestId), after checking result.data.images.length (no image
generated), and in the catch block to call logger.error and include the
error/context (e.g., requestId, traceId, prompt) while keeping the existing
returns; locate these in the handler that calls fal.queue.submit,
getRequestStatus, fal.queue.result, and saveBase64Image.
examples/advanced-use-cases/vision-example/steps/eval-agent/eval-agent.step.ts (1)

20-20: Comment still references emit — update to reflect enqueue.

Line 131 says "we only emit the report path" but the code now uses enqueue. Minor terminology drift from the migration.

📝 Proposed fix
-    // For now we only emit the report path and the confidence percentage, for a future use case you can trigger
+    // For now we only enqueue the report path and the confidence percentage, for a future use case you can trigger

Also applies to: 131-132

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@examples/advanced-use-cases/vision-example/steps/eval-agent/eval-agent.step.ts`
at line 20, The review notes outdated terminology: update any inline comment or
log that says "emit" to use the new API name "enqueue" inside the handler
function (the exported handler: Handlers<typeof config>) where you describe
sending the report path (currently referenced as "we only emit the report
path"); locate the comment or string near the code that calls enqueue and
replace "emit" with "enqueue" (and adjust wording if it implies the old emit
behavior) so comments/logs match the actual enqueue(...) call.
examples/advanced-use-cases/ai-room-renovate/pyproject.toml (1)

10-11: Consider tightening google-adk and google-genai version constraints.

The google-adk package is currently at version 1.25.1, meaning >=0.1.0 will resolve to a version that has undergone many breaking changes from the originally tested 0.1.0 baseline. Using ~= (compatible release) or an explicit upper bound would make the example reproducible.

📌 Proposed fix using compatible-release operators
-  "google-adk>=0.1.0",
-  "google-genai>=0.2.0",
+  "google-adk~=1.0",
+  "google-genai~=1.0",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/advanced-use-cases/ai-room-renovate/pyproject.toml` around lines 10
- 11, The dependency constraints for "google-adk" and "google-genai" are too
loose (using >=0.1.0 and >=0.2.0); tighten them to ensure reproducible installs
by switching to compatible-release or adding an upper bound—for example use the
~= operator (e.g., "google-adk~=0.1" and "google-genai~=0.2") or explicit upper
bounds (e.g., "google-adk>=0.1.0,<1.0.0" and "google-genai>=0.2.0,<1.0.0");
update the entries for the "google-adk" and "google-genai" dependency lines in
pyproject.toml accordingly.
examples/advanced-use-cases/ai-room-renovate/config.yaml (1)

26-34: Wildcard CORS origin in a dev example is acceptable but worth noting for portability.

Since host is 127.0.0.1, external access is blocked at the network layer, so allowed_origins: ['*'] carries minimal risk here. If the host is ever changed to 0.0.0.0, the wildcard becomes a permissive CORS policy. Consider scoping it to http://localhost:* for a dev config to make the intent explicit.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/advanced-use-cases/ai-room-renovate/config.yaml` around lines 26 -
34, The CORS config uses a wildcard origin (cors.allowed_origins: ['*']), which
is too permissive if host is changed; update cors.allowed_origins in the YAML
(the cors section and allowed_origins key) to a scoped development value such as
explicit localhost patterns (e.g., http://localhost:3000 or http://localhost:*),
or list specific origins you expect, so the intent is explicit while keeping the
current host 127.0.0.1 behavior safe.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@examples/advanced-use-cases/ai-room-renovate/config.yaml`:
- Around line 39-41: The default SERVICE_NAMESPACE in config.yaml is set to
"production" which is misleading for an example; change the default placeholder
for service_namespace (the SERVICE_NAMESPACE env var) to a non-production value
like "example" or leave it empty/null so developers don't accidentally tag
traces as production; update the service_namespace:
${SERVICE_NAMESPACE:production} entry to use ${SERVICE_NAMESPACE:example} (or
remove the default) so service_namespace and SERVICE_NAME/service_version
defaults reflect an example/dev context.
- Around line 71-78: The ExecModule currently lists two exec commands under the
same module (exec: - npx motia dev - bun run --enable-source-maps
dist/index-dev.js) which run sequentially so the long-running `npx motia dev`
blocks the `bun run` command; fix it by splitting into two ExecModule entries:
one ExecModule with watch: ["steps/**/*.ts"] and exec: ["npx motia dev"] (for
the dev server), and a separate ExecModule that watches the built output (or
appropriate pattern) and runs exec: ["bun run --enable-source-maps
dist/index-dev.js"] so both processes can run concurrently (or alternatively run
both in a single shell command using `&` if you intend a single module).

In `@examples/advanced-use-cases/ai-room-renovate/package.json`:
- Line 9: The "build" script currently runs "npx motia dev" which starts a dev
server rather than producing build artifacts; change the "build" script to
perform a one-shot build (for example "tsc -p tsconfig.json" or your project's
actual build command) and move "npx motia dev" to a clearly named dev script
such as "dev:motia" or "motia:dev" so "build" is safe for CI/deploy; update the
"build" and new dev script names in package.json accordingly, ensuring any CI
references use the new "build" script.

In
`@examples/advanced-use-cases/ai-room-renovate/steps/renovation/edit-rendering_step.py`:
- Around line 199-207: The edit history unwrap logic is in the wrong order: when
retrieving edit_history via ctx.state.get(session_id, "editHistory"), check for
a wrapped dict first (if isinstance(edit_history, dict) and "data" in
edit_history) and unwrap edit_history = edit_history["data"] before validating
type; then if not isinstance(edit_history, list) set edit_history = []; update
the code around the edit_history handling (the block using ctx.state.get, the
dict "data" check, and the list validation) so wrapped {"data": [...]} is
unwrapped prior to the list type check.

In `@examples/advanced-use-cases/vision-example/package.json`:
- Line 9: The package.json "build" script currently runs "npx motia dev" which
starts a long‑running watcher; change the script either by renaming it (e.g.,
"dev:ts" or "watch") to reflect dev mode or replace it with a one‑shot
build/compile command (e.g., a TypeScript compiler or bundler invocation) so
pnpm build is non-blocking; then update all references to this script in
config.yaml's TS ExecModule exec array to point to the new script name or
command (look for the "build" script entry in package.json and the TS ExecModule
"exec" entries in config.yaml to make the edits).

In `@examples/advanced-use-cases/vision-example/README.md`:
- Around line 5-9: The README's Requirements list omits the bun runtime while
the project's start script ("bun run --enable-source-maps dist/index-dev.js")
depends on it; update the Requirements section to include bun as a required
runtime (mention minimum version if applicable) and add a brief note on how to
install or verify bun is installed to ensure the start script and development
workflow work as documented.

In `@examples/advanced-use-cases/vision-example/steps/evaluate_result_step.py`:
- Around line 28-29: The current try block in evaluate_result_step.py (around
the image reading, Anthropic API call, and response parsing inside the
function/method handling evaluation) only catches ValueError which leaves
FileNotFoundError, OSError and Anthropic API exceptions unhandled and can leave
raw_response unbound; update the error handling by broadening the excepts:
explicitly catch FileNotFoundError/OSError around the open(image, ...) call (or
add a small try/except there), catch the Anthropic client exceptions (e.g.,
APIError/AuthenticationError/rate limit errors from the Anthropic client) around
the API call, and add a final except Exception that logs the full exception
(including the exception object) and returns or raises a controlled error so
raw_response is never referenced if not set; also adjust the existing except
ValueError branch to only handle parsing/value errors after raw_response is
guaranteed assigned.
- Line 17: The step declares enqueues: ["eval-report"] but the step handler
never calls ctx.enqueue, so downstream step won't receive the report; either add
a ctx.enqueue("eval-report", payload) call in the handler after constructing the
evaluation result (the same object you write to file in the handler, around the
code that writes the local file and logs results), or remove "eval-report" from
the step config to avoid confusion—look for the handler function in
evaluate_result_step.py and insert ctx.enqueue("eval-report", { /* evaluation
payload */ }) immediately after the file write/logging (or remove the enqueues
entry if enqueueing is not intended).
- Around line 57-62: The payload hardcodes "media_type": "image/png" which
breaks for non-PNG inputs; determine the MIME type from the incoming image
path/filename in input_data (e.g., inspect input_data["image_path"] or the
source filename) and map extensions like .jpg/.jpeg -> "image/jpeg", .webp ->
"image/webp", .png -> "image/png" (fallback to "application/octet-stream" or use
Python's mimetypes.guess_type), assign it to a media_type variable and replace
the hardcoded string in the image source dict so the code uses media_type when
building the payload that includes image_data.
- Line 1: The async handler function (async def handler) is using the blocking
synchronous Anthropic client; replace the import and client with AsyncAnthropic,
instantiate the async client (e.g., AsyncAnthropic(...)) and change the blocking
call client.messages.create(...) to an awaited call (await
client.messages.create(...)) so the HTTP request does not block the event loop;
apply the same change in enhance_image_prompt_step.py by swapping Anthropic for
AsyncAnthropic, creating the async client, and awaiting its API calls, ensuring
all calls occur inside the async handler/coroutine context and any client
cleanup/close is handled appropriately.

In `@examples/advanced-use-cases/vision-example/types.d.ts`:
- Around line 5-10: The import of MotiaStream is unused in this ambient
declaration—remove the unused import statement for MotiaStream (or change the
generator to omit it) when no streams are declared; update the declarations for
the empty interfaces Streams and Enqueues in types.d.ts so they remain empty
without importing MotiaStream, ensuring there are no unused-import warnings from
the MotiaStream symbol.

---

Outside diff comments:
In
`@examples/advanced-use-cases/ai-room-renovate/steps/renovation/design_planner.step.ts`:
- Around line 38-44: The scope assignment logic makes 'luxury' unreachable
because the avgCost > 300 branch runs before avgCost > 600; update the
conditional order in the block that computes avgCost (which uses costEstimate
and assessment.squareFootage) to check the highest threshold first (avgCost >
600), then avgCost > 300, then avgCost < 100, or refactor to explicit range
checks so 'scope' can be set to 'luxury', 'full', 'moderate' or 'cosmetic'
correctly.

In
`@examples/advanced-use-cases/ai-room-renovate/steps/renovation/edit_rendering_api.step.ts`:
- Around line 71-77: The current catch around bodySchema.parse(), state.get(),
and enqueue() treats all errors as validation failures; isolate validation by
calling bodySchema.parse() inside its own try/catch that only catches ZodError
(imported from "zod") and returns the 400 + "Invalid request data" response,
while letting other errors from state.get() or enqueue() bubble up or be handled
as server errors (log with logger.error in the outer catch and return 500).
Update the handler so bodySchema.parse() is validated first (or in a small try
that catches ZodError), and ensure logger.error calls (the existing
logger.error('Failed to process edit request', { error: String(error) })) remain
for non-validation failures.

In
`@examples/advanced-use-cases/ai-room-renovate/steps/renovation/visual_assessor.step.ts`:
- Around line 99-102: The conditional ordering in the block that sets scope
based on avgCost makes the 'luxury' branch unreachable; reorder the comparisons
in the conditional that assigns scope (the code using variable scope and
avgCost) so the largest threshold is checked first (e.g., if (avgCost > 600)
scope = 'luxury'; else if (avgCost > 300) scope = 'full'; else if (avgCost <
100) scope = 'cosmetic'; else scope = 'moderate';) to ensure each branch can be
reached.
- Around line 97-98: The calculation using assessment.budget can produce NaN if
budget is non-numeric; in the visual_assessor.step.ts code locate the block that
computes avgCost from assessment.budget and assessment.squareFootage and coerce
assessment.budget to a numeric value (e.g., via Number(...) or a sanitized
parse) then check isFinite/!isNaN before performing the division, and if the
value is invalid handle it (skip avgCost, use a fallback, or log/throw) so
avgCost = assessment.budget / assessment.squareFootage cannot silently become
NaN.

In
`@examples/advanced-use-cases/vision-example/steps/enhance_image_prompt_step.py`:
- Around line 32-33: The model string passed to client.messages.create
("claude-3-sonnet-20240229") is retired and will cause runtime failures; update
the model parameter in the client.messages.create call to a supported model
(e.g., the same "claude-sonnet-4-20250514" used in evaluate_result_step.py) so
requests succeed, ensuring any related code or tests that assume the old model
name are adjusted to the new model identifier.

In
`@examples/advanced-use-cases/vision-example/steps/eval-agent/eval-agent.step.ts`:
- Around line 133-139: Guard against division by zero when computing
confidencePercentage before calling enqueue: check datasetReport.length (or
compute a safe denominator variable) and if it is 0 set confidencePercentage to
0 (or an appropriate default) instead of doing (finalScore.success * 100) /
datasetReport.length; then call enqueue exactly as before with reportPath and
the safe confidencePercentage. Update the code around the enqueue call and any
helpers that compute finalScore to use the safe denominator to avoid Infinity.
- Line 96: The inline comment next to the sleep call is incorrect; in the
eval-agent.step.ts code the statement await sleep(1000) sleeps for 1 second
(1000 ms) but the comment says 30 seconds—update the comment to accurately
reflect the actual delay (e.g., "Sleep for 1 second (1000 ms)") or, if the
intent was 30 seconds, change the argument to sleep(30000) instead; locate the
await sleep(...) call to apply the fix.

In `@examples/advanced-use-cases/vision-example/steps/generate-image.step.ts`:
- Around line 28-40: The getRequestStatus function currently recurses
indefinitely when fal.queue.status never returns 'COMPLETED' (risking stack
overflow); replace the recursive logic in getRequestStatus with an iterative
loop that polls fal.queue.status(FAL_MODEL, { requestId, logs:true }) with a
delay and enforces a max retry count or overall timeout, and if the limit is
reached either throw a descriptive error or return the last status object;
update any callers to handle the thrown error or timeout return.

---

Nitpick comments:
In `@examples/advanced-use-cases/ai-room-renovate/config.yaml`:
- Around line 26-34: The CORS config uses a wildcard origin
(cors.allowed_origins: ['*']), which is too permissive if host is changed;
update cors.allowed_origins in the YAML (the cors section and allowed_origins
key) to a scoped development value such as explicit localhost patterns (e.g.,
http://localhost:3000 or http://localhost:*), or list specific origins you
expect, so the intent is explicit while keeping the current host 127.0.0.1
behavior safe.

In `@examples/advanced-use-cases/ai-room-renovate/pyproject.toml`:
- Around line 10-11: The dependency constraints for "google-adk" and
"google-genai" are too loose (using >=0.1.0 and >=0.2.0); tighten them to ensure
reproducible installs by switching to compatible-release or adding an upper
bound—for example use the ~= operator (e.g., "google-adk~=0.1" and
"google-genai~=0.2") or explicit upper bounds (e.g., "google-adk>=0.1.0,<1.0.0"
and "google-genai>=0.2.0,<1.0.0"); update the entries for the "google-adk" and
"google-genai" dependency lines in pyproject.toml accordingly.

In
`@examples/advanced-use-cases/ai-room-renovate/steps/renovation/edit_rendering_api.step.ts`:
- Line 42: state.get is called without a type argument causing rendering to be
inferred as unknown; update the call to state.get to include the appropriate
generic (e.g., state.get<any>(sessionId, 'rendering') or a specific interface)
so rendering is typed correctly; modify the invocation in
edit_rendering_api.step.ts (the state.get call that assigns rendering) to pass
the chosen type parameter.

In
`@examples/advanced-use-cases/ai-room-renovate/steps/renovation/edit-rendering_step.py`:
- Around line 239-242: Replace the broad "except Exception as e" in the error
handling for edit-rendering_step with more specific exception types where
possible (e.g., ValueError, RuntimeError) or, if you cannot enumerate them,
augment the existing handler to include the exception type and stack trace in
logs/state; update the ctx.logger.error call to include type(e).__name__ (and
optionally traceback.format_exc()) and store a richer error value in await
ctx.state.set(session_id, "editError", ...) so both ctx.logger.error and
ctx.state.set include the error type (and stack) alongside the message.

In
`@examples/advanced-use-cases/ai-room-renovate/steps/renovation/generate-rendering_step.py`:
- Around line 230-236: The except block currently swallows the traceback by
using "except Exception as e" and only logging str(e); import the traceback
module and capture the full traceback with traceback.format_exc(), then include
that traceback string (in addition to the error message) when calling
ctx.logger.error and when calling ctx.state.set to preserve diagnostic context;
update the error handling in the except block that surrounds ctx.logger.error /
await ctx.state.set (the block catching Exception e in
generate-rendering_step.py) to record both the error message and the formatted
traceback.
- Around line 164-174: The async handler is performing synchronous streaming
iteration (client.models.generate_content_stream) with a blocking for loop,
which will block the event loop; fix by using the SDK's async client variant
(client.aio.models.generate_content_stream) and iterate with "async for" inside
the async def handler, or alternatively call the sync generator inside
asyncio.to_thread to avoid blocking; update the loop in
generate-rendering_step.py (the async def handler that uses
client.models.generate_content_stream) and make the analogous change in
edit-rendering_step.py where the same pattern occurs.

In
`@examples/advanced-use-cases/ai-room-renovate/steps/renovation/project_coordinator.step.ts`:
- Around line 77-82: The enqueue call is sending session_id (snake_case) while
this step's input schema uses sessionId (camelCase); explicitly map the step
input sessionId to session_id in the enqueue payload (i.e., set data.session_id
= input.sessionId) and add a short comment noting the TS↔Python naming boundary,
or alternatively centralize the mapping in a shared helper to prevent drift;
update the call to enqueue (topic 'renovation.render') and ensure any references
to sessionId/session_id across this step and generate-rendering_step.py are
consistently mapped.

In
`@examples/advanced-use-cases/ai-room-renovate/steps/renovation/start_renovation.step.ts`:
- Line 46: Replace the deprecated String.prototype.substr call when building
sessionId: in the expression that generates the random suffix
(Math.random().toString(36).substr(2, 9)), switch to substring and use the
equivalent end index (Math.random().toString(36).substring(2, 11)) so the
produced sessionId in the const sessionId = `session_${Date.now()}_${...}`
retains the same length and behavior.

In
`@examples/advanced-use-cases/ai-room-renovate/steps/renovation/visual_assessor.step.ts`:
- Around line 34-47: Extract the repeated "if object has 'data' unwrap it" logic
into a small generic helper function, e.g. unwrapState<T>(value: any): T |
undefined, that returns value.data when value is an object containing 'data' and
otherwise returns value; then replace the three manual blocks with calls like
const storedBudget = unwrapState<number>(await state.get<any>(sessionId,
'budget')), const storedRoomType = unwrapState<string>(await
state.get<any>(sessionId, 'roomType')), and const storedStyle =
unwrapState<string>(await state.get<any>(sessionId, 'style')) so the unwrapping
logic is centralized (refer to symbols unwrapState, storedBudget,
storedRoomType, storedStyle, and the state.get(...) calls).

In
`@examples/advanced-use-cases/vision-example/steps/eval-agent/eval-agent-results.api.step.ts`:
- Around line 22-31: Replace the blocking fs.readdirSync call with the async
fs/promises readdir and add error handling for a missing 'tmp' directory:
import/require the promise-based fs, await fs.readdir('tmp'), filter for files
ending with '_report.txt' (same reportFiles variable), and catch errors—if
err.code === 'ENOENT' return a 400 response with a clear message about the
missing 'tmp' directory; for other errors return a 500 or appropriate error
response. Ensure the logic still checks reportFiles.length < 10 and returns the
existing insufficient reports message.

In
`@examples/advanced-use-cases/vision-example/steps/eval-agent/eval-agent.step.ts`:
- Line 20: The review notes outdated terminology: update any inline comment or
log that says "emit" to use the new API name "enqueue" inside the handler
function (the exported handler: Handlers<typeof config>) where you describe
sending the report path (currently referenced as "we only emit the report
path"); locate the comment or string near the code that calls enqueue and
replace "emit" with "enqueue" (and adjust wording if it implies the old emit
behavior) so comments/logs match the actual enqueue(...) call.

In `@examples/advanced-use-cases/vision-example/steps/generate-image.step.ts`:
- Around line 63-109: The error paths in the exported handler use console.error
(three places) instead of the destructured logger; replace all console.error
calls inside handler with logger.error so errors go through the app's structured
logger. Specifically, update the error branches after fal.queue.submit (check
for missing requestId), after checking result.data.images.length (no image
generated), and in the catch block to call logger.error and include the
error/context (e.g., requestId, traceId, prompt) while keeping the existing
returns; locate these in the handler that calls fal.queue.submit,
getRequestStatus, fal.queue.result, and saveBase64Image.

Comment on lines +39 to +41
service_name: ${OTEL_SERVICE_NAME:iii-engine}
service_version: ${SERVICE_VERSION:0.2.0}
service_namespace: ${SERVICE_NAMESPACE:production}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Default SERVICE_NAMESPACE of "production" is misleading for a dev/example project.

If a developer runs this example with a real OTLP backend without setting SERVICE_NAMESPACE, all traces/metrics will be tagged as belonging to the production namespace.

✏️ Suggested fix
-      service_namespace: ${SERVICE_NAMESPACE:production}
+      service_namespace: ${SERVICE_NAMESPACE:development}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
service_name: ${OTEL_SERVICE_NAME:iii-engine}
service_version: ${SERVICE_VERSION:0.2.0}
service_namespace: ${SERVICE_NAMESPACE:production}
service_name: ${OTEL_SERVICE_NAME:iii-engine}
service_version: ${SERVICE_VERSION:0.2.0}
service_namespace: ${SERVICE_NAMESPACE:development}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/advanced-use-cases/ai-room-renovate/config.yaml` around lines 39 -
41, The default SERVICE_NAMESPACE in config.yaml is set to "production" which is
misleading for an example; change the default placeholder for service_namespace
(the SERVICE_NAMESPACE env var) to a non-production value like "example" or
leave it empty/null so developers don't accidentally tag traces as production;
update the service_namespace: ${SERVICE_NAMESPACE:production} entry to use
${SERVICE_NAMESPACE:example} (or remove the default) so service_namespace and
SERVICE_NAME/service_version defaults reflect an example/dev context.

Comment on lines +71 to +78
- class: modules::shell::ExecModule
config:
watch:
- steps/**/*.ts

exec:
- npx motia dev
- bun run --enable-source-maps dist/index-dev.js

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, locate and examine the config.yaml file
find . -name "config.yaml" -path "*/ai-room-renovate/*" | head -5

Repository: MotiaDev/motia-examples

Length of output: 125


🏁 Script executed:

# Check the repository structure to understand the project better
ls -la examples/advanced-use-cases/ai-room-renovate/ 2>/dev/null || echo "Directory not found"

Repository: MotiaDev/motia-examples

Length of output: 1051


🏁 Script executed:

# Search for Motia documentation or ExecModule references in the repo
find . -type f \( -name "*.md" -o -name "*.ts" -o -name "*.js" \) -exec grep -l "ExecModule\|motia" {} \; | head -20

Repository: MotiaDev/motia-examples

Length of output: 31238


🌐 Web query:

Motia ExecModule multiple exec entries sequential parallel execution behavior

💡 Result:

In Motia’s config.yaml, the iii Shell ExecModule treats config.exec as an ordered list: the commands are executed in order (sequentially), not in parallel. This is why the default dev setup runs a build command first (npx motia dev) and then starts the output (node dist/index-dev.js). When watched files change, the exec commands are re-run. [1]

If you need “parallel” behavior, you generally have to do it yourself inside a single exec entry using your shell (e.g., backgrounding with &, concurrently, etc.), because multiple exec: list items are intended to be ordered steps, not concurrent processes. [1]

Sources
[1] Motia Docs — “Configuration (config.yaml)” (Shell Exec module; “exec — commands to execute in order”) https://www.motia.dev/docs/development-guide/motia-config


🏁 Script executed:

# Read the config.yaml file to see the actual content at lines 71-78
cat -n examples/advanced-use-cases/ai-room-renovate/config.yaml | sed -n '60,85p'

Repository: MotiaDev/motia-examples

Length of output: 754


🏁 Script executed:

# Read the package.json to understand what these scripts do
cat examples/advanced-use-cases/ai-room-renovate/package.json

Repository: MotiaDev/motia-examples

Length of output: 705


🏁 Script executed:

# Check if there's any documentation in the repo about ExecModule or the intended behavior
cat examples/advanced-use-cases/ai-room-renovate/README.md | head -100

Repository: MotiaDev/motia-examples

Length of output: 3430


The two exec commands are executed sequentially; the second will never start.

Motia's ExecModule runs commands in the exec list in order, not in parallel. Since npx motia dev is a long-running dev/build server that doesn't exit, bun run --enable-source-maps dist/index-dev.js will never execute.

Looking at the rest of the config, there's already a separate ExecModule for Python files (lines 80+), suggesting TypeScript and Python should also be separate. Split these into two ExecModule entries:

  • One for npx motia dev watching steps/**/*.ts
  • One for bun run ... watching the built output (or handle both in a single shell command using &)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/advanced-use-cases/ai-room-renovate/config.yaml` around lines 71 -
78, The ExecModule currently lists two exec commands under the same module
(exec: - npx motia dev - bun run --enable-source-maps dist/index-dev.js) which
run sequentially so the long-running `npx motia dev` blocks the `bun run`
command; fix it by splitting into two ExecModule entries: one ExecModule with
watch: ["steps/**/*.ts"] and exec: ["npx motia dev"] (for the dev server), and a
separate ExecModule that watches the built output (or appropriate pattern) and
runs exec: ["bun run --enable-source-maps dist/index-dev.js"] so both processes
can run concurrently (or alternatively run both in a single shell command using
`&` if you intend a single module).

"start": "bun run --enable-source-maps dist/index-dev.js",
"dev": "iii",
"dev:py": "uv run motia dev --dir steps --watch",
"build": "npx motia dev"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

"build": "npx motia dev" is semantically wrong — this starts a dev server, not a build.

Running npm run build in CI or during deployment will spin up a long-running dev server instead of producing artifacts. If the intent is to compile TypeScript to dist/, this command should be the TypeScript compilation step. If npx motia dev is intentional (i.e., it performs a one-shot code generation step), rename the script to something unambiguous like "dev:ts".

✏️ Suggested fix
-    "build": "npx motia dev"
+    "dev:ts": "npx motia dev"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"build": "npx motia dev"
"dev:ts": "npx motia dev"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/advanced-use-cases/ai-room-renovate/package.json` at line 9, The
"build" script currently runs "npx motia dev" which starts a dev server rather
than producing build artifacts; change the "build" script to perform a one-shot
build (for example "tsc -p tsconfig.json" or your project's actual build
command) and move "npx motia dev" to a clearly named dev script such as
"dev:motia" or "motia:dev" so "build" is safe for CI/deploy; update the "build"
and new dev script names in package.json accordingly, ensuring any CI references
use the new "build" script.

Comment on lines +199 to 207
edit_history = await ctx.state.get(session_id, "editHistory")
if not edit_history or not isinstance(edit_history, list):
edit_history = []

# If it's wrapped in data, unwrap it
if isinstance(edit_history, dict) and "data" in edit_history:
edit_history = edit_history["data"]
if not isinstance(edit_history, list):
edit_history = []

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Bug: Edit history unwrapping is in the wrong order — wrapped state data will be silently lost.

Line 200 checks not isinstance(edit_history, list) and resets to [] before line 204 attempts to unwrap a dict with a "data" key. If Motia state wraps the list in {"data": [...]}, the dict fails the isinstance(list) check and gets replaced with [], making the unwrapping on Line 204 unreachable.

🐛 Proposed fix — unwrap before validating
                 # Store edit history
                 edit_history = await ctx.state.get(session_id, "editHistory")
+
+                # If it's wrapped in data, unwrap it first
+                if isinstance(edit_history, dict) and "data" in edit_history:
+                    edit_history = edit_history["data"]
+
                 if not edit_history or not isinstance(edit_history, list):
                     edit_history = []
-
-                # If it's wrapped in data, unwrap it
-                if isinstance(edit_history, dict) and "data" in edit_history:
-                    edit_history = edit_history["data"]
-                    if not isinstance(edit_history, list):
-                        edit_history = []
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@examples/advanced-use-cases/ai-room-renovate/steps/renovation/edit-rendering_step.py`
around lines 199 - 207, The edit history unwrap logic is in the wrong order:
when retrieving edit_history via ctx.state.get(session_id, "editHistory"), check
for a wrapped dict first (if isinstance(edit_history, dict) and "data" in
edit_history) and unwrap edit_history = edit_history["data"] before validating
type; then if not isinstance(edit_history, list) set edit_history = []; update
the code around the edit_history handling (the block using ctx.state.get, the
dict "data" check, and the list validation) so wrapped {"data": [...]} is
unwrapped prior to the list type check.

"start": "bun run --enable-source-maps dist/index-dev.js",
"dev": "iii",
"dev:py": "uv run motia dev --dir steps --watch",
"build": "npx motia dev"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

build script runs npx motia dev — semantically misleading.

motia dev starts a dev watch process rather than producing a one-shot build artifact. Users or CI pipelines invoking pnpm build expecting an offline compile will get a long-running watcher. Consider renaming this script (e.g., compile or dev:ts) or replacing it with a proper build command, and update the references in config.yaml's TS ExecModule exec array accordingly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/advanced-use-cases/vision-example/package.json` at line 9, The
package.json "build" script currently runs "npx motia dev" which starts a
long‑running watcher; change the script either by renaming it (e.g., "dev:ts" or
"watch") to reflect dev mode or replace it with a one‑shot build/compile command
(e.g., a TypeScript compiler or bundler invocation) so pnpm build is
non-blocking; then update all references to this script in config.yaml's TS
ExecModule exec array to point to the new script name or command (look for the
"build" script entry in package.json and the TS ExecModule "exec" entries in
config.yaml to make the edits).

@@ -1,28 +1,38 @@
import ultraimport
from vision_agent.lmm import AnthropicLMM
from anthropic import Anthropic

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

cat examples/advanced-use-cases/vision-example/steps/evaluate_result_step.py

Repository: MotiaDev/motia-examples

Length of output: 3579


🏁 Script executed:

fd "enhance_image_prompt_step.py" examples/advanced-use-cases/vision-example --exec cat {}

Repository: MotiaDev/motia-examples

Length of output: 1434


🏁 Script executed:

rg "AsyncAnthropic|async def.*handler" examples/advanced-use-cases/vision-example --context 3

Repository: MotiaDev/motia-examples

Length of output: 1566


Synchronous Anthropic client blocks the async event loop.

The handler is async def (line 21) but uses the synchronous Anthropic client (line 9). The client.messages.create(...) call on line 50 performs a blocking HTTP request, which will block the event loop and prevent concurrent task processing.

Use AsyncAnthropic and await the API call instead. The same issue exists in enhance_image_prompt_step.py (lines 6, 16, 24).

Suggested fix
-from anthropic import Anthropic
+from anthropic import AsyncAnthropic
-client = Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
+client = AsyncAnthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
-        response = client.messages.create(
+        response = await client.messages.create(
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
from anthropic import Anthropic
from anthropic import AsyncAnthropic
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/advanced-use-cases/vision-example/steps/evaluate_result_step.py` at
line 1, The async handler function (async def handler) is using the blocking
synchronous Anthropic client; replace the import and client with AsyncAnthropic,
instantiate the async client (e.g., AsyncAnthropic(...)) and change the blocking
call client.messages.create(...) to an awaited call (await
client.messages.create(...)) so the HTTP request does not block the event loop;
apply the same change in enhance_image_prompt_step.py by swapping Anthropic for
AsyncAnthropic, creating the async client, and awaiting its API calls, ensuring
all calls occur inside the async handler/coroutine context and any client
cleanup/close is handled appropriately.

"triggers": [
queue("eval-image-result")
],
"enqueues": ["eval-report"],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

enqueues: ["eval-report"] is declared but never invoked in the handler.

The config declares this step enqueues to "eval-report", but the handler never calls ctx.enqueue(...). The evaluation result is only written to a local file (line 80-88) and logged (lines 90-93), meaning no downstream step will receive the report via the flow.

If downstream processing is intended, an enqueue call is missing. If it's not needed, remove "eval-report" from the config to avoid confusion.

Example fix if enqueue is intended
         if score > 90:
             ctx.logger.info('image is a good representation, do something with it', score)
         else:
             ctx.logger.info('image is not a good representation, try again or use a different prompt', score)
+
+        await ctx.enqueue("eval-report", report)

Also applies to: 80-93

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/advanced-use-cases/vision-example/steps/evaluate_result_step.py` at
line 17, The step declares enqueues: ["eval-report"] but the step handler never
calls ctx.enqueue, so downstream step won't receive the report; either add a
ctx.enqueue("eval-report", payload) call in the handler after constructing the
evaluation result (the same object you write to file in the handler, around the
code that writes the local file and logs results), or remove "eval-report" from
the step config to avoid confusion—look for the handler function in
evaluate_result_step.py and insert ctx.enqueue("eval-report", { /* evaluation
payload */ }) immediately after the file write/logging (or remove the enqueues
entry if enqueueing is not intended).

Comment on lines 28 to +29
try:
lmm = AnthropicLMM()
prompt = """Evaluate if the image is a good representation of the following prompt:
# Read and base64 encode the image

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Exception handling is too narrow — only ValueError is caught.

The try block includes file I/O (line 30), an external API call (line 50), and response parsing (line 72-74). Only ValueError is caught, so:

  1. FileNotFoundError/OSError from open(image, ...) will propagate unhandled if the image path is invalid.
  2. Anthropic API errors (APIError, AuthenticationError, rate limits, etc.) will propagate unhandled.
  3. In the except ValueError block (line 96), raw_response could be UnboundLocalError if the ValueError is raised before line 72 assigns it (unlikely with current code but fragile).

Consider broadening the error handling or adding an outer except Exception with proper logging.

Suggested fix
     except ValueError:
-        ctx.logger.error('Invalid response from vision agent', raw_response)
+        ctx.logger.exception('Invalid response from vision agent: %s', raw_response)
+    except FileNotFoundError:
+        ctx.logger.exception('Image file not found: %s', image)
+    except Exception:
+        ctx.logger.exception('Unexpected error evaluating image')

Also applies to: 95-96

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/advanced-use-cases/vision-example/steps/evaluate_result_step.py`
around lines 28 - 29, The current try block in evaluate_result_step.py (around
the image reading, Anthropic API call, and response parsing inside the
function/method handling evaluation) only catches ValueError which leaves
FileNotFoundError, OSError and Anthropic API exceptions unhandled and can leave
raw_response unbound; update the error handling by broadening the excepts:
explicitly catch FileNotFoundError/OSError around the open(image, ...) call (or
add a small try/except there), catch the Anthropic client exceptions (e.g.,
APIError/AuthenticationError/rate limit errors from the Anthropic client) around
the API call, and add a final except Exception that logs the full exception
(including the exception object) and returns or raises a controlled error so
raw_response is never referenced if not set; also adjust the existing except
ValueError branch to only handle parsing/value errors after raw_response is
guaranteed assigned.

Comment on lines +57 to +62
"type": "image",
"source": {
"type": "base64",
"media_type": "image/png",
"data": image_data,
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Hardcoded media_type: "image/png" may be incorrect for non-PNG images.

The image path comes from input_data and could reference a JPEG, WebP, or other format. Sending the wrong media type to the Anthropic API may cause errors or misinterpretation.

Suggested fix: detect media type from the file extension
+    import mimetypes
+
     # Read and base64 encode the image
+    media_type = mimetypes.guess_type(image)[0] or "image/png"
     with open(image, "rb") as f:
         image_data = base64.standard_b64encode(f.read()).decode("utf-8")

Then use media_type variable on line 60:

-                        "media_type": "image/png",
+                        "media_type": media_type,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/advanced-use-cases/vision-example/steps/evaluate_result_step.py`
around lines 57 - 62, The payload hardcodes "media_type": "image/png" which
breaks for non-PNG inputs; determine the MIME type from the incoming image
path/filename in input_data (e.g., inspect input_data["image_path"] or the
source filename) and map extensions like .jpg/.jpeg -> "image/jpeg", .webp ->
"image/webp", .png -> "image/png" (fallback to "application/octet-stream" or use
Python's mimetypes.guess_type), assign it to a media_type variable and replace
the hardcoded string in the image source dict so the code uses media_type when
building the payload that includes image_data.

Comment on lines +5 to +10
import { MotiaStream } from 'motia'

declare module 'motia' {
interface Streams {}

interface Enqueues {}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

MotiaStream is imported but never referenced.

The Streams and Enqueues interfaces are empty, so the MotiaStream import is unused. While this is auto-generated, a stale import in ambient declaration files can produce TypeScript warnings depending on verbatimModuleSyntax settings. If the generator is under your control, suppress this import when no streams are declared.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/advanced-use-cases/vision-example/types.d.ts` around lines 5 - 10,
The import of MotiaStream is unused in this ambient declaration—remove the
unused import statement for MotiaStream (or change the generator to omit it)
when no streams are declared; update the declarations for the empty interfaces
Streams and Enqueues in types.d.ts so they remain empty without importing
MotiaStream, ensuring there are no unused-import warnings from the MotiaStream
symbol.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant