Skip to content

feat: add invoke() to DederProjectInternals for dynamic task invocati…#106

Merged
sake92 merged 4 commits into
mainfrom
plugin-task-invocation-api
Jun 20, 2026
Merged

feat: add invoke() to DederProjectInternals for dynamic task invocati…#106
sake92 merged 4 commits into
mainfrom
plugin-task-invocation-api

Conversation

@sake92

@sake92 sake92 commented Jun 20, 2026

Copy link
Copy Markdown
Owner

…on by plugins

Add TaskInvokeOutcome case class, CallerType.Plugin enum variant, and invoke(taskName, moduleIds, args) method to DederProjectInternals trait. Implementation in DederProjectInternalsImpl delegates to DederProjectState through a direct reference (wired in ServerMain), using the same execution pipeline as CLI requests (planning, locking, caching, cancellation).

Motivation: plugins (primarily web dashboard) can trigger builds dynamically

  • just pass task name, module IDs, and args like CLI does.

Summary by CodeRabbit

  • New Features
    • Added support for dynamic task execution at runtime via an invocation API
    • Task results now include per-module outcomes with success status, error details, and cache information
    • Cross-module task execution summaries are now available for aggregated results

sake92 added 2 commits June 20, 2026 00:08
…on by plugins

Add TaskInvokeOutcome case class, CallerType.Plugin enum variant, and
invoke(taskName, moduleIds, args) method to DederProjectInternals trait.
Implementation in DederProjectInternalsImpl delegates to DederProjectState
through a direct reference (wired in ServerMain), using the same execution
pipeline as CLI requests (planning, locking, caching, cancellation).

Motivation: plugins (primarily web dashboard) can trigger builds dynamically
- just pass task name, module IDs, and args like CLI does.
…ult summary

- New TaskInvokerApi trait in plugin-api: invoke(taskName, moduleIds, args, onNotification)
  returns TaskInvokeResult with per-module outcomes + rendered plaintext summary
- TaskInvokeResult wraps Seq[TaskInvokeOutcome] + renderedSummary: Option[String]
- Passed to plugins via PluginInitParams.taskInvoker
- DederProjectInternalsImpl implements TaskInvokerApi
- Refactored invoke into shared invokeInternal that handles both the simple
  DederProjectInternals.invoke and the richer TaskInvokerApi.invoke
- Cross-module summary renders using Summarizable + PlainTextWritable typeclass
  (same output as CLI)
@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@sake92, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 41 minutes and 58 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 753e5de1-b9f6-48bb-affa-223e368bc114

📥 Commits

Reviewing files that changed from the base of the PR and between ecd5b53 and 0e8b507.

📒 Files selected for processing (2)
  • deder-common/src/ba/sake/deder/DederProjectInternals.scala
  • server/src/ba/sake/deder/DederProjectInternalsImpl.scala
📝 Walkthrough

Walkthrough

Adds a dynamic task invocation API for plugins. A new TaskInvokerApi trait and TaskInvokeOutcome/TaskInvokeResult result types are defined in common and plugin-api modules. DederProjectInternalsImpl implements this trait via two invoke overloads backed by invokeInternal, which resolves modules, executes tasks, and optionally renders a cross-module summary. ServerMain and PluginLoader are updated to wire the implementation.

Changes

Plugin Task Invocation API

Layer / File(s) Summary
Invocation contracts and result types
deder-common/src/ba/sake/deder/DederProjectInternals.scala, plugin-api/src/ba/sake/deder/DederPluginApi.scala
CallerType gains Plugin. DederProjectInternals gains an invoke method. TaskInvokeOutcome and TaskInvokeResult case classes (deriving JsonRW) are added. In plugin-api, TaskInvokerApi trait is defined with the full invoke signature (including notification callback), and PluginInitParams is extended with a taskInvoker: TaskInvokerApi field.
Server-side invoke implementation
server/src/ba/sake/deder/DederProjectInternalsImpl.scala
DederProjectInternalsImpl extends TaskInvokerApi, gains a @volatile projectState: Option[DederProjectState] field, and implements invokeInternal: resolves module wildcards, executes tasks via state.executeTasks, maps TaskExecResult to TaskInvokeOutcome, and optionally renders a cross-module plaintext summary via summarizeValueUnsafe and OutputFormat.render.
Server startup and plugin loader wiring
server/src/ba/sake/deder/ServerMain.scala, server/src/ba/sake/deder/plugin/PluginLoader.scala
ServerMain wires projectState into internals after construction. PluginLoader passes internals.asInstanceOf[TaskInvokerApi] as the new taskInvoker argument to PluginInitParams.

Sequence Diagram

sequenceDiagram
  participant Plugin
  participant DederProjectInternalsImpl
  participant DederProjectState
  participant TaskSummarizer

  Plugin->>DederProjectInternalsImpl: invoke(taskName, moduleIds, args, onNotification)
  DederProjectInternalsImpl->>DederProjectInternalsImpl: validate projectState set
  DederProjectInternalsImpl->>DederProjectInternalsImpl: resolve moduleIds (WildcardUtils / all)
  DederProjectInternalsImpl->>DederProjectState: executeTasks(requestId, taskName, modules, args, notificationWrapper)
  DederProjectState-->>DederProjectInternalsImpl: Seq[TaskExecResult]
  DederProjectInternalsImpl->>DederProjectInternalsImpl: map TaskExecResult → TaskInvokeOutcome
  DederProjectInternalsImpl->>TaskSummarizer: summarizeValueUnsafe + OutputFormat.render
  TaskSummarizer-->>DederProjectInternalsImpl: Option[String] renderedSummary
  DederProjectInternalsImpl-->>Plugin: TaskInvokeResult(outcomes, renderedSummary)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • sake92/deder#51: Introduced the PluginInitParams/init(params) plugin lifecycle, which this PR directly extends by adding taskInvoker: TaskInvokerApi to that same case class.
  • sake92/deder#104: Extended the task execution path with interrupt-based cancellation via DederGlobals.runningTaskThreads, which is the same state.executeTasks path that the new invokeInternal flows through.

Poem

🐇 A plugin once asked, "Can I run tasks too?"
The server said "Yes!" and the API grew.
invoke was born with outcomes galore,
Per-module, per-summary, cached to the core.
Now plugins can trigger a task with great ease—
The rabbit hops on and does just as it please! 🎉

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add invoke() to DederProjectInternals for dynamic task invocation' accurately summarizes the main change: adding a dynamic task invocation capability via a new invoke() method to DederProjectInternals.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch plugin-task-invocation-api

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: 1

🧹 Nitpick comments (1)
server/src/ba/sake/deder/plugin/PluginLoader.scala (1)

150-150: ⚖️ Poor tradeoff

Consider passing TaskInvokerApi as an explicit constructor parameter.

The asInstanceOf[TaskInvokerApi] cast works because DederProjectInternalsImpl implements both traits, but it couples PluginLoader to that implementation detail. Passing TaskInvokerApi as a separate constructor parameter would make the dependency explicit and type-safe.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/ba/sake/deder/plugin/PluginLoader.scala` at line 150, In the
PluginInitParams constructor call within PluginLoader, remove the
asInstanceOf[TaskInvokerApi] cast on internals and instead pass TaskInvokerApi
as an explicit separate constructor parameter to PluginInitParams. This requires
updating the PluginInitParams case class to accept TaskInvokerApi as its own
parameter rather than deriving it from internals through a cast, making the
dependency explicit and removing the coupling to DederProjectInternalsImpl's
implementation details.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@server/src/ba/sake/deder/DederProjectInternalsImpl.scala`:
- Around line 140-150: The module resolution logic in the wildcard matching
section silently converts both readState and
WildcardUtils.getMatchesOrRecommendations errors into empty sequences, hiding
failures from callers. Instead of returning Seq.empty for both the Left case in
readState and the Left case in WildcardUtils.getMatchesOrRecommendations,
propagate these errors through the function's return type (likely
TaskInvokeResult) so that plugins can receive distinguished failure outcomes
indicating why module resolution failed. This requires changing the return type
from a simple resolved Ids sequence to a Result type that can communicate both
success and failure states to the caller.

---

Nitpick comments:
In `@server/src/ba/sake/deder/plugin/PluginLoader.scala`:
- Line 150: In the PluginInitParams constructor call within PluginLoader, remove
the asInstanceOf[TaskInvokerApi] cast on internals and instead pass
TaskInvokerApi as an explicit separate constructor parameter to
PluginInitParams. This requires updating the PluginInitParams case class to
accept TaskInvokerApi as its own parameter rather than deriving it from
internals through a cast, making the dependency explicit and removing the
coupling to DederProjectInternalsImpl's implementation details.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 128478c3-cd30-4faf-a3fb-c302219dc575

📥 Commits

Reviewing files that changed from the base of the PR and between 2413468 and ecd5b53.

📒 Files selected for processing (5)
  • deder-common/src/ba/sake/deder/DederProjectInternals.scala
  • plugin-api/src/ba/sake/deder/DederPluginApi.scala
  • server/src/ba/sake/deder/DederProjectInternalsImpl.scala
  • server/src/ba/sake/deder/ServerMain.scala
  • server/src/ba/sake/deder/plugin/PluginLoader.scala

Comment on lines +140 to +150
// Resolve wildcards: empty moduleIds → all modules
val resolvedIds = state.readState(useLastGood = false) match {
case Left(_) => Seq.empty
case Right(s) =>
val allIds = s.tasksResolver.allModules.map(_.id)
if moduleIds.isEmpty then allIds
else WildcardUtils.getMatchesOrRecommendations(allIds, moduleIds) match {
case Left(_) => Seq.empty
case Right(ids) => ids
}
}

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 | ⚡ Quick win

Silent failure when module resolution fails.

When readState returns Left (parse/load error) or WildcardUtils.getMatchesOrRecommendations returns Left (no matches), resolvedIds becomes Seq.empty. This silently produces an empty TaskInvokeResult with no indication to the plugin caller that module resolution failed.

Consider propagating the error or returning a distinguished failure outcome so plugins can diagnose misconfigurations.

Possible approach
     val resolvedIds = state.readState(useLastGood = false) match {
-      case Left(_) => Seq.empty
+      case Left(err) =>
+        return TaskInvokeResult(Seq.empty, Some(s"Failed to read project state: $err"))
       case Right(s) =>
         val allIds = s.tasksResolver.allModules.map(_.id)
         if moduleIds.isEmpty then allIds
         else WildcardUtils.getMatchesOrRecommendations(allIds, moduleIds) match {
-          case Left(_)    => Seq.empty
+          case Left(suggestions) =>
+            val hint = if suggestions.nonEmpty then s" Did you mean: ${suggestions.mkString(", ")}?" else ""
+            return TaskInvokeResult(Seq.empty, Some(s"No modules matched: ${moduleIds.mkString(", ")}.$hint"))
           case Right(ids) => ids
         }
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/ba/sake/deder/DederProjectInternalsImpl.scala` around lines 140 -
150, The module resolution logic in the wildcard matching section silently
converts both readState and WildcardUtils.getMatchesOrRecommendations errors
into empty sequences, hiding failures from callers. Instead of returning
Seq.empty for both the Left case in readState and the Left case in
WildcardUtils.getMatchesOrRecommendations, propagate these errors through the
function's return type (likely TaskInvokeResult) so that plugins can receive
distinguished failure outcomes indicating why module resolution failed. This
requires changing the return type from a simple resolved Ids sequence to a
Result type that can communicate both success and failure states to the caller.

@sake92 sake92 merged commit 41f4ca3 into main Jun 20, 2026
2 of 4 checks passed
@sake92 sake92 deleted the plugin-task-invocation-api branch June 20, 2026 14:02
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