Skip to content

feat(tegg): [3/3] add @AgentController decorator with plugin integration#5827

Open
jerryliang64 wants to merge 13 commits intonextfrom
feat/pr3-agent-controller
Open

feat(tegg): [3/3] add @AgentController decorator with plugin integration#5827
jerryliang64 wants to merge 13 commits intonextfrom
feat/pr3-agent-controller

Conversation

@jerryliang64
Copy link
Contributor

@jerryliang64 jerryliang64 commented Mar 9, 2026

Summary

  • Add @AgentController() decorator that auto-registers 7 OpenAI-compatible HTTP routes under /api/v1
  • Add AgentHandler interface, AgentInfoUtil metadata utilities, and AgentControllerProto/AgentControllerObject for full IoC lifecycle integration
  • Add NodeSSEWriter implementation with lazy header writing and lowercase HTTP headers
  • Enforce createStore() method in @AgentController (no default store — users must provide their own AgentStore)
  • Use AgentRuntime.create() with AgentExecutor interface (replaces removed createAgentRuntime + AgentControllerHost)
  • Add AGENT_CONTROLLER_PROTO_IMPL_TYPE and metadata symbols in @eggjs/tegg-types
  • Register agent controller factory in controller plugin boot hook (app.ts)
  • Add @eggjs/tegg/agent public API entry point for re-exports

Stacked PRs

This is part of a 3-PR series (review in order):

  1. feat(tegg): [1/3] add agent-runtime package with OSS-backed store #5823 — Store layer (AgentStore + OSSAgentStore) ✅ merged
  2. feat(tegg): [2/3] add AgentRuntime with sync/async/stream execution modes #5824 — Runtime (AgentRuntime + MessageConverter + RunBuilder) ✅ merged
  3. This PR — Decorator (@AgentController + Plugin integration)

Changes from previous PR (#5820)

  • Fixed broken imports: AgentHandler.ts and AgentFooController.ts now import types from @eggjs/tegg-types/agent-runtime instead of non-existent model/AgentControllerTypes.ts
  • Added NodeSSEWriter: New implementation in @eggjs/agent-runtime with lazy header writing, lowercase HTTP headers (content-type), and single res.write() per event
  • Enforced createStore(): Removed FileAgentStore fallback — @AgentController now requires users to implement createStore()
  • Switched to AgentRuntime.create(): Replaced createAgentRuntime({ host }) with AgentRuntime.create({ executor }) to match PR2's API
  • Fixed agent.ts exports: Removed FileAgentStore, added NodeSSEWriter, types now re-exported from @eggjs/agent-runtime
  • Updated snapshot: Removed agent-related enums from @eggjs/controller-decorator exports (they live in @eggjs/tegg-types/agent-runtime)
  • Squashed into single commit for clean history

Test plan

  • NodeSSEWriter unit tests pass (8 tests: lazy headers, event format, closed state, onClose callbacks, idempotent end)
  • Controller-decorator snapshot test updated and passing
  • tsc --noEmit type checking passes for all affected packages
  • Pre-commit hooks (oxfmt + oxlint) pass

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Agent controller support with lifecycle-integrated controller objects and a streaming AgentHandler.
    • Server-Sent Events support for Node.js to stream runtime events to clients.
    • New unified agent module exposing controller/handler types, runtime types, and agent errors.
  • Bug Fixes

    • Improved cancellation handling and clearer distinction between cancelled vs failed streaming runs.
    • Completed message outputs now include richer, consistent message structure.
  • Tests

    • Added comprehensive unit tests for decorators, controller lifecycle, SSE streaming, and fixtures.
  • Chores

    • Package/export updates to publish the new agent surface.

- Add @AgentController() decorator that auto-registers 7 OpenAI-compatible
  HTTP routes under /api/v1
- Add AgentHandler interface, AgentInfoUtil metadata utilities, and
  AgentControllerProto/AgentControllerObject for full IoC lifecycle integration
- Add NodeSSEWriter implementation with lazy header writing and lowercase
  HTTP headers
- Enforce createStore() method in @AgentController (no default store)
- Use AgentRuntime.create() with AgentExecutor interface
- Add @eggjs/tegg/agent public API entry point for re-exports
- Add AGENT_CONTROLLER_PROTO_IMPL_TYPE and metadata symbols in @eggjs/tegg-types
- Register agent controller factory in controller plugin boot hook (app.ts)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 9, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds agent controller and agent-runtime features: Node SSE writer, AgentController decorator and AgentHandler interface, metadata utilities, AgentControllerProto/Object with AgentRuntime wiring and streamRun → NodeSSEWriter integration, package exports, tests, and fixtures.

Changes

Cohort / File(s) Summary
SSE Writer Infrastructure
tegg/core/agent-runtime/src/NodeSSEWriter.ts, tegg/core/agent-runtime/test/NodeSSEWriter.test.ts
New NodeSSEWriter implementing SSEWriter for Node ServerResponse with lazy header emission, SSE event formatting, onClose callbacks, closed-state tracking and idempotent end; tests exercise header timing, formatting, disconnect handling and callbacks.
Agent Controller Decorator & Interface
tegg/core/controller-decorator/src/decorator/agent/AgentController.ts, tegg/core/controller-decorator/src/decorator/agent/AgentHandler.ts, tegg/core/controller-decorator/src/decorator/agent/index.ts, tegg/core/controller-decorator/src/decorator/index.ts
Adds @AgentController decorator that registers fixed HTTP routes and injects not-implemented stubs; introduces AgentHandler interface (execRun async-generator + optional lifecycle/run methods); re-exports agent decorator API.
Agent Metadata & Utilities
tegg/core/controller-decorator/src/util/AgentInfoUtil.ts, tegg/core/controller-decorator/src/util/index.ts, tegg/core/types/src/controller-decorator/MetadataKey.ts
New AgentInfoUtil for marking/querying agent controller/not-implemented/enhanced metadata; exports new metadata symbols and AGENT_CONTROLLER_PROTO_IMPL_TYPE.
Agent Controller Runtime Objects
tegg/plugin/controller/src/lib/AgentControllerProto.ts, tegg/plugin/controller/src/lib/AgentControllerObject.ts, tegg/plugin/controller/src/app.ts
Adds AgentControllerProto wrapper and AgentControllerObject implementing EggObject lifecycle with AgentRuntime installation, store initialization, method delegation (asyncRun/streamRun/syncRun/etc.), streamRun wrapped to use NodeSSEWriter, destruction sequencing and plugin registration.
Agent Runtime & Types Changes
tegg/core/agent-runtime/src/AgentRuntime.ts, tegg/core/agent-runtime/src/index.ts, tegg/core/agent-runtime/test/AgentRuntime.test.ts, tegg/core/types/src/agent-runtime/AgentRuntime.ts, tegg/core/agent-runtime/src/MessageConverter.ts
Adjusts stream-run completion to emit completed message objects (non-mutative), adds MessageConverter.completeMessage, distinct cancellation handling on abort, re-exports NodeSSEWriter, updates tests and type imports.
Package Exports & Aggregation
tegg/core/tegg/src/agent.ts, tegg/core/tegg/package.json, tegg/plugin/controller/package.json
Adds tegg agent aggregator module re-exporting controller and runtime types/errors/NodeSSEWriter; adds ./agent export mapping and workspace dependency on @eggjs/agent-runtime.
Tests & Fixtures
tegg/core/controller-decorator/test/AgentController.test.ts, tegg/core/controller-decorator/test/fixtures/AgentFooController.ts
New tests validate decorator metadata, routes, parameter metadata, default stub behavior and HTTPControllerMetaBuilder integration; fixture controller implements execRun and yields AgentStreamMessage.

Sequence Diagram(s)

sequenceDiagram
    participant Lifecycle as EggObject Lifecycle
    participant ACO as AgentControllerObject
    participant User as Controller (user object)
    participant Store as AgentStore
    participant Runtime as AgentRuntime
    participant Client as HTTP Client/Response

    Lifecycle->>ACO: init(ctx)
    ACO->>User: call createStore()? (if implemented)
    User-->>Store: returns AgentStore
    ACO->>Store: store.init()? (if present)
    ACO->>Runtime: AgentRuntime.create(executor=User, store, logger)
    ACO->>ACO: bind runtime methods (asyncRun/syncRun/streamRun...)
    ACO->>User: call user.init()
    User-->>ACO: init complete

    Client->>ACO: streamRun HTTP request
    ACO->>ACO: disable auto-response, create NodeSSEWriter(res)
    ACO->>Runtime: runtime.streamRun(input)
    Runtime-->>ACO: yield AgentStreamMessage
    ACO->>Client: NodeSSEWriter.writeEvent(event, data)
    Client-->>ACO: may close connection -> NodeSSEWriter.onClose triggers
    ACO->>Runtime: stop/cleanup on client disconnect

    Lifecycle->>ACO: destroy(ctx)
    ACO->>Runtime: runtime.destroy()
    Runtime-->>ACO: cleanup complete
    ACO->>User: run preDestroy/destroy hooks
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • gxkl
  • akitaSummer

Poem

🐰 I hop where agents wake and stream,

Threads hum softly, headers gleam,
Controllers bind and runtimes play,
SSE whispers carry the day,
Rabbit cheers — code leaps in beam!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% 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 PR title clearly summarizes the main change: introducing the @AgentController decorator with plugin integration, covering the core feature across multiple modules and services.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/pr3-agent-controller

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.

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request, the third in a series, introduces the @AgentController decorator to the Tegg framework. This decorator simplifies the creation of OpenAI-compatible HTTP endpoints for agent management by automatically registering standard routes. It also integrates a new NodeSSEWriter for efficient Server-Sent Events, refactors the AgentRuntime creation process, and establishes a robust Inversion of Control (IoC) lifecycle for agent controllers, requiring users to explicitly provide an AgentStore implementation.

Highlights

  • AgentController Decorator: Added the @AgentController() decorator, which automatically registers 7 OpenAI-compatible HTTP routes under /api/v1 for agent management.
  • IoC Integration: Introduced AgentHandler interface, AgentInfoUtil for metadata, and AgentControllerProto/AgentControllerObject for seamless Inversion of Control (IoC) lifecycle integration.
  • NodeSSEWriter: Implemented NodeSSEWriter for Server-Sent Events, featuring lazy header writing and ensuring lowercase HTTP headers for consistency.
  • Explicit AgentStore Requirement: Enforced the createStore() method within @AgentController, requiring users to explicitly provide their own AgentStore implementation instead of relying on a default.
  • AgentRuntime Refactoring: Switched to AgentRuntime.create() with an AgentExecutor interface, replacing the older createAgentRuntime and AgentControllerHost for improved modularity.
  • Metadata and Prototype Types: Added AGENT_CONTROLLER_PROTO_IMPL_TYPE and new metadata symbols within @eggjs/tegg-types to support agent controller identification and enhancement.
  • Plugin Boot Hook Registration: Registered the agent controller factory within the controller plugin's boot hook (app.ts) to ensure proper initialization and lifecycle management.
  • Public API Entry Point: Created a new public API entry point at @eggjs/tegg/agent for convenient re-exports of agent-related functionalities.
Changelog
  • pnpm-lock.yaml
    • Updated dependencies to include @eggjs/agent-runtime for tegg/core/tegg and tegg/plugin/controller.
  • tegg/core/agent-runtime/src/NodeSSEWriter.ts
    • Implemented NodeSSEWriter for Server-Sent Events, featuring lazy header writing and lowercase HTTP headers.
  • tegg/core/agent-runtime/src/index.ts
    • Exported the new NodeSSEWriter.
  • tegg/core/agent-runtime/test/NodeSSEWriter.test.ts
    • Added comprehensive unit tests for NodeSSEWriter functionality.
  • tegg/core/controller-decorator/src/decorator/agent/AgentController.ts
    • Created the @AgentController decorator, which automatically configures 7 OpenAI-compatible HTTP routes and injects default error-throwing stubs for unimplemented methods.
  • tegg/core/controller-decorator/src/decorator/agent/AgentHandler.ts
    • Defined the AgentHandler interface, outlining methods expected for agent controllers.
  • tegg/core/controller-decorator/src/decorator/index.ts
    • Exported the new AgentController decorator and AgentHandler interface.
  • tegg/core/controller-decorator/src/util/AgentInfoUtil.ts
    • Introduced AgentInfoUtil to manage metadata specific to agent controllers.
  • tegg/core/controller-decorator/src/util/index.ts
    • Exported AgentInfoUtil.
  • tegg/core/controller-decorator/test/AgentController.test.ts
    • Added unit tests to verify the behavior and metadata setup of the @AgentController decorator.
  • tegg/core/controller-decorator/test/snapshots/index.test.ts.snap
    • Updated the snapshot to reflect new exports related to agent controllers.
  • tegg/core/controller-decorator/test/fixtures/AgentFooController.ts
    • Added a test fixture AgentFooController to demonstrate the usage of @AgentController.
  • tegg/core/tegg/package.json
    • Updated package.json to include a new public API entry point for agent and added @eggjs/agent-runtime as a dependency.
  • tegg/core/tegg/src/agent.ts
    • Created a new entry point for re-exporting agent-related decorators, types, and classes.
  • tegg/core/types/src/controller-decorator/MetadataKey.ts
    • Defined new metadata keys (CONTROLLER_AGENT_CONTROLLER, CONTROLLER_AGENT_NOT_IMPLEMENTED, CONTROLLER_AGENT_ENHANCED) and a new prototype implementation type (AGENT_CONTROLLER_PROTO_IMPL_TYPE).
  • tegg/plugin/controller/package.json
    • Added @eggjs/agent-runtime as a dependency.
  • tegg/plugin/controller/src/app.ts
    • Modified the application boot hook to register AgentControllerProto and AgentControllerObject for proper IoC integration.
  • tegg/plugin/controller/src/lib/AgentControllerObject.ts
    • Implemented AgentControllerObject to manage the lifecycle of @AgentController instances, including the dynamic installation of AgentRuntime delegates.
  • tegg/plugin/controller/src/lib/AgentControllerProto.ts
    • Created AgentControllerProto to act as a wrapper, enabling custom object creation for agent controllers within the Tegg framework.
Activity
  • This pull request is the third in a series of three stacked PRs, indicating a structured and incremental development approach.
  • The author provided a detailed summary of changes from a previous PR (feat(tegg): [3/3] add @AgentController decorator with plugin integration #5820), demonstrating iterative refinement and addressing prior feedback.
  • A comprehensive test plan was outlined, confirming that NodeSSEWriter unit tests (8 tests) passed, controller-decorator snapshot tests were updated and passed, tsc --noEmit type checking passed for all affected packages, and pre-commit hooks (oxfmt + oxlint) passed.
  • The PR was generated with Claude Code, suggesting the use of AI assistance in its creation.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Mar 9, 2026

Deploying egg with  Cloudflare Pages  Cloudflare Pages

Latest commit: 2af6c6f
Status: ✅  Deploy successful!
Preview URL: https://918411d4.egg-cci.pages.dev
Branch Preview URL: https://feat-pr3-agent-controller.egg-cci.pages.dev

View logs

@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Mar 9, 2026

Deploying egg-v3 with  Cloudflare Pages  Cloudflare Pages

Latest commit: 2af6c6f
Status: ✅  Deploy successful!
Preview URL: https://dce1b716.egg-v3.pages.dev
Branch Preview URL: https://feat-pr3-agent-controller.egg-v3.pages.dev

View logs

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces the @AgentController decorator, a significant feature for creating OpenAI-compatible API endpoints, integrating new decorators, interfaces (AgentHandler), and a NodeSSEWriter with the tegg IoC container. A medium-severity SSE injection vulnerability has been identified in NodeSSEWriter.ts due to unsanitized event parameters. Additionally, there's a potential maintainability issue in AgentControllerObject.ts due to code duplication. Addressing the SSE injection is critical for security, and resolving the code duplication will improve maintainability.

writeEvent(event: string, data: unknown): void {
if (this._closed) return;
this.ensureHeaders();
this.res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
Copy link
Contributor

Choose a reason for hiding this comment

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

security-medium medium

The writeEvent method directly concatenates the event parameter into the SSE response stream without sanitization. If an attacker can control the event string (e.g., through a malicious agent implementation or manipulated LLM output that influences the event type), they can inject arbitrary SSE fields (like id, retry) or even split the event by injecting newline characters (\n or \r). This can lead to protocol manipulation and potentially mislead the client-side SSE handler.

To remediate this, sanitize the event string by removing or escaping newline characters before using it in the response.

    const sanitizedEvent = event.replace(/[\r\n]/g, '');
    this.res.write(`event: ${sanitizedEvent}\ndata: ${JSON.stringify(data)}\n\n`);

Comment on lines +79 to +157
async init(ctx: EggObjectLifeCycleContext): Promise<void> {
try {
// 1. Construct object
this._obj = this.proto.constructEggObject();
const objLifecycleHook = this._obj as EggObjectLifecycle;

// 2. Global preCreate hook
await EggObjectLifecycleUtil.objectPreCreate(ctx, this);

// 3. Self postConstruct hook
const postConstructMethod =
EggObjectLifecycleUtil.getLifecycleHook('postConstruct', this.proto) ?? 'postConstruct';
if (objLifecycleHook[postConstructMethod]) {
await objLifecycleHook[postConstructMethod](ctx, this);
}

// 4. Self preInject hook
const preInjectMethod = EggObjectLifecycleUtil.getLifecycleHook('preInject', this.proto) ?? 'preInject';
if (objLifecycleHook[preInjectMethod]) {
await objLifecycleHook[preInjectMethod](ctx, this);
}

// 5. Inject dependencies
await Promise.all(
this.proto.injectObjects.map(async (injectObject) => {
const proto = injectObject.proto;
const loadUnit = LoadUnitFactory.getLoadUnitById(proto.loadUnitId);
if (!loadUnit) {
throw new Error(`can not find load unit: ${proto.loadUnitId}`);
}
if (
this.proto.initType !== ObjectInitType.CONTEXT &&
injectObject.proto.initType === ObjectInitType.CONTEXT
) {
this.injectProperty(
injectObject.refName,
EggObjectUtil.contextEggObjectGetProperty(proto, injectObject.objName),
);
} else {
const injectObj = await EggContainerFactory.getOrCreateEggObject(proto, injectObject.objName);
this.injectProperty(injectObject.refName, EggObjectUtil.eggObjectGetProperty(injectObj));
}
}),
);

// 6. Global postCreate hook
await EggObjectLifecycleUtil.objectPostCreate(ctx, this);

// 7. Self postInject hook
const postInjectMethod = EggObjectLifecycleUtil.getLifecycleHook('postInject', this.proto) ?? 'postInject';
if (objLifecycleHook[postInjectMethod]) {
await objLifecycleHook[postInjectMethod](ctx, this);
}

// === AgentRuntime installation (before user init) ===
await this.installAgentRuntime();

// 8. Self init hook (user's init())
const initMethod = EggObjectLifecycleUtil.getLifecycleHook('init', this.proto) ?? 'init';
if (objLifecycleHook[initMethod]) {
await objLifecycleHook[initMethod](ctx, this);
}

// 9. Ready
this.status = EggObjectStatus.READY;
} catch (e) {
// Clean up runtime if it was created but init failed
if (this.runtime) {
try {
await this.runtime.destroy();
} catch {
// Swallow cleanup errors to preserve the original exception
}
this.runtime = undefined;
}
this.status = EggObjectStatus.ERROR;
throw e;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The init method in AgentControllerObject appears to be a complete reimplementation of the object initialization and dependency injection lifecycle, likely copied from EggObjectImpl.initWithInjectProperty. While this allows for the crucial insertion of installAgentRuntime() at the correct point in the lifecycle (after postInject and before the user's init), it introduces a significant maintainability risk.

Any future changes to the core object lifecycle in EggObjectImpl will not be automatically reflected here, potentially leading to subtle bugs. This code duplication could become a source of technical debt.

Consider exploring alternatives that could reduce this duplication. For example:

  • Could EggObjectImpl be made extensible, allowing AgentControllerObject to inherit from it and override a specific lifecycle hook?
  • Could a new, more granular lifecycle hook be introduced into the core framework specifically for this type of enhancement?

If this is the only viable approach given the current framework design, please add a prominent comment linking to the source of the copied logic (e.g., EggObjectImpl.initWithInjectProperty) to aid future maintenance.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (4)
tegg/core/agent-runtime/test/NodeSSEWriter.test.ts (1)

38-40: Drop the repeated as any casts in these tests.

The fixture only needs a small ServerResponse surface, but the current casts and lint suppressions bypass the repo’s TS rules and make API drift easy to miss. A typed helper or an unknown bridge would keep the mock checked without eight separate suppressions.

As per coding guidelines, "Avoid any type; use unknown when type is truly unknown in TypeScript".

Also applies to: 54-56, 65-67, 74-76, 89-90, 102-103, 118-119, 135-136

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

In `@tegg/core/agent-runtime/test/NodeSSEWriter.test.ts` around lines 38 - 40, The
tests use repeated "as any" casts for the fixture (res) which bypass type
checks; replace those casts by either (a) creating a small typed mock helper
like createResMock(): Partial<ServerResponse> that implements only the methods
used by NodeSSEWriter and use it as res as ServerResponse, or (b) use an unknown
bridge (res as unknown as ServerResponse) instead of "as any", and remove the
eslint-disable comments; update each instantiation of NodeSSEWriter (and other
places flagged) to use the typed mock or unknown cast so TypeScript validates
the ServerResponse surface while keeping the test behavior the same.
tegg/core/controller-decorator/test/fixtures/AgentFooController.ts (1)

3-4: Consider using .js extension for ESM imports.

The imports use .ts extensions, but the coding guidelines specify using .js extensions for ESM files. There's also inconsistency within this PR - AgentController.test.ts line 16 imports this fixture with .js extension ('./fixtures/AgentFooController.js').

🔧 Suggested change for consistency
-import { AgentController } from '../../src/decorator/agent/AgentController.ts';
-import type { AgentHandler } from '../../src/decorator/agent/AgentHandler.ts';
+import { AgentController } from '../../src/decorator/agent/AgentController.js';
+import type { AgentHandler } from '../../src/decorator/agent/AgentHandler.js';

As per coding guidelines: "All imports should use .js extensions for ESM files".

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

In `@tegg/core/controller-decorator/test/fixtures/AgentFooController.ts` around
lines 3 - 4, Update the ESM imports in the AgentFooController fixture to use .js
extensions to match project conventions and the test import; specifically change
the import specifiers that reference
'../../src/decorator/agent/AgentController.ts' and
'../../src/decorator/agent/AgentHandler.ts' to use
'../../src/decorator/agent/AgentController.js' and
'../../src/decorator/agent/AgentHandler.js' so the fixture
(AgentFooController.ts) and AgentController.test.ts use consistent .js imports.
tegg/plugin/controller/src/lib/AgentControllerObject.ts (1)

220-227: Consider validating logger availability before AgentRuntime creation.

If AgentControllerObject.setLogger() is not called before createObject(), the logger passed to AgentRuntime.create() will be undefined. While the wiring in app.ts ensures setLogger is called in the constructor, this implicit dependency could cause issues if the order changes or in test scenarios.

🛡️ Suggested defensive check
     // Create runtime with AgentRuntime.create factory
+    if (!AgentControllerObject.logger) {
+      throw new Error('AgentControllerObject.setLogger() must be called before creating agent controllers');
+    }
     const runtime = AgentRuntime.create({
       executor: this._obj as AgentExecutor,
       store,
       logger: AgentControllerObject.logger,
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tegg/plugin/controller/src/lib/AgentControllerObject.ts` around lines 220 -
227, The code passes AgentControllerObject.logger into AgentRuntime.create
without ensuring it is set; update createObject (the block that calls
AgentRuntime.create) to validate that AgentControllerObject.logger is available
(e.g., check this.logger or AgentControllerObject.logger) before creating the
runtime and either throw a clear error or provide a sensible default logger;
ensure the check occurs right before calling AgentRuntime.create so
instance[AGENT_RUNTIME], this.runtime, and the executor wiring only proceed when
a valid logger is present.
tegg/plugin/controller/src/lib/AgentControllerProto.ts (1)

92-94: Avoid any type; use unknown for type-safe delegation.

The ...args: any parameter violates the coding guideline to avoid any type. Additionally, public methods should have explicit return type annotations per isolatedDeclarations requirements.

🔧 Proposed fix
-  constructEggObject(...args: any): object {
+  constructEggObject(...args: unknown[]): object {
     return this.delegate.constructEggObject(...args);
   }

As per coding guidelines: "Avoid any type; use unknown when type is truly unknown in TypeScript".

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

In `@tegg/plugin/controller/src/lib/AgentControllerProto.ts` around lines 92 - 94,
Change the constructEggObject signature to avoid `any` by declaring the rest
parameter as `...args: unknown[]` and add an explicit return type annotation
(e.g., `: object`) to satisfy isolatedDeclarations; forward the arguments to the
delegate call via `this.delegate.constructEggObject(...args)` (if the delegate
API requires a narrower type, narrow or assert the args at the call site inside
the method) so the public method stays type-safe and declaration-compliant.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tegg/core/agent-runtime/src/NodeSSEWriter.ts`:
- Around line 30-33: writeEvent currently ignores res.write()'s return value and
can overflow memory; update NodeSSEWriter.writeEvent (and ensureHeaders if
needed) to handle backpressure by either: (a) implementing the drain
pattern—check the boolean result of this.res.write(...), and when it returns
false stop emitting/writing further events until this.res.once('drain', ...)
resumes writing; or (b) refactor the producer to a Readable stream and pipe it
to this.res so Node handles backpressure automatically; ensure _closed is still
respected and that pending events are queued/paused during the drain period to
avoid dropping data.

In `@tegg/core/controller-decorator/src/decorator/agent/AgentHandler.ts`:
- Line 14: The AgentHandler.createStore signature uses Promise<unknown> and is
optional; update it to return the concrete store contract and make it required
so incompatible implementations can't compile. Replace the declaration
createStore?(): Promise<unknown>; with createStore(): Promise<AgentStore>; (or
the actual store interface name used in your codebase), import that store
interface into the file, and update all concrete classes implementing
AgentHandler to return the correct AgentStore type from createStore.

---

Nitpick comments:
In `@tegg/core/agent-runtime/test/NodeSSEWriter.test.ts`:
- Around line 38-40: The tests use repeated "as any" casts for the fixture (res)
which bypass type checks; replace those casts by either (a) creating a small
typed mock helper like createResMock(): Partial<ServerResponse> that implements
only the methods used by NodeSSEWriter and use it as res as ServerResponse, or
(b) use an unknown bridge (res as unknown as ServerResponse) instead of "as
any", and remove the eslint-disable comments; update each instantiation of
NodeSSEWriter (and other places flagged) to use the typed mock or unknown cast
so TypeScript validates the ServerResponse surface while keeping the test
behavior the same.

In `@tegg/core/controller-decorator/test/fixtures/AgentFooController.ts`:
- Around line 3-4: Update the ESM imports in the AgentFooController fixture to
use .js extensions to match project conventions and the test import;
specifically change the import specifiers that reference
'../../src/decorator/agent/AgentController.ts' and
'../../src/decorator/agent/AgentHandler.ts' to use
'../../src/decorator/agent/AgentController.js' and
'../../src/decorator/agent/AgentHandler.js' so the fixture
(AgentFooController.ts) and AgentController.test.ts use consistent .js imports.

In `@tegg/plugin/controller/src/lib/AgentControllerObject.ts`:
- Around line 220-227: The code passes AgentControllerObject.logger into
AgentRuntime.create without ensuring it is set; update createObject (the block
that calls AgentRuntime.create) to validate that AgentControllerObject.logger is
available (e.g., check this.logger or AgentControllerObject.logger) before
creating the runtime and either throw a clear error or provide a sensible
default logger; ensure the check occurs right before calling AgentRuntime.create
so instance[AGENT_RUNTIME], this.runtime, and the executor wiring only proceed
when a valid logger is present.

In `@tegg/plugin/controller/src/lib/AgentControllerProto.ts`:
- Around line 92-94: Change the constructEggObject signature to avoid `any` by
declaring the rest parameter as `...args: unknown[]` and add an explicit return
type annotation (e.g., `: object`) to satisfy isolatedDeclarations; forward the
arguments to the delegate call via `this.delegate.constructEggObject(...args)`
(if the delegate API requires a narrower type, narrow or assert the args at the
call site inside the method) so the public method stays type-safe and
declaration-compliant.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 050a68fd-27d8-4089-baae-a041e2b72d19

📥 Commits

Reviewing files that changed from the base of the PR and between 1a4fb20 and a4f7658.

⛔ Files ignored due to path filters (2)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • tegg/core/controller-decorator/test/__snapshots__/index.test.ts.snap is excluded by !**/*.snap
📒 Files selected for processing (17)
  • tegg/core/agent-runtime/src/NodeSSEWriter.ts
  • tegg/core/agent-runtime/src/index.ts
  • tegg/core/agent-runtime/test/NodeSSEWriter.test.ts
  • tegg/core/controller-decorator/src/decorator/agent/AgentController.ts
  • tegg/core/controller-decorator/src/decorator/agent/AgentHandler.ts
  • tegg/core/controller-decorator/src/decorator/index.ts
  • tegg/core/controller-decorator/src/util/AgentInfoUtil.ts
  • tegg/core/controller-decorator/src/util/index.ts
  • tegg/core/controller-decorator/test/AgentController.test.ts
  • tegg/core/controller-decorator/test/fixtures/AgentFooController.ts
  • tegg/core/tegg/package.json
  • tegg/core/tegg/src/agent.ts
  • tegg/core/types/src/controller-decorator/MetadataKey.ts
  • tegg/plugin/controller/package.json
  • tegg/plugin/controller/src/app.ts
  • tegg/plugin/controller/src/lib/AgentControllerObject.ts
  • tegg/plugin/controller/src/lib/AgentControllerProto.ts

Comment on lines +30 to +33
writeEvent(event: string, data: unknown): void {
if (this._closed) return;
this.ensureHeaders();
this.res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
Copy link
Contributor

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

🌐 Web query:

In current Node.js, what does http.ServerResponse.write()returningfalse mean, and what is the recommended way to handle backpressure for streaming/SSE responses?

💡 Result:

What http.ServerResponse.write() returning false means

In current Node.js, response.write(chunk) returns:

  • true: the chunk was flushed to the OS (kernel) buffer.
  • false: some/all of the chunk had to be queued in user-space memory because the writable buffer is “full” (i.e., at/over highWaterMark). When capacity is available again, Node will emit 'drain'. (nodejs.org)

So false is the backpressure signal: stop producing/writing more data until 'drain' fires, otherwise Node will keep buffering and you can run up memory (and in extreme cases create a security issue if the peer never reads). (nodejs.org)


Recommended backpressure handling for streaming/SSE responses

1) If you can model output as a stream: use pipe() / pipeline()

Node’s guidance is: if data can be generated on demand, encapsulate it as a Readable and connect it to the response (a Writable) via pipe() (or stream.pipeline() for better cleanup/error propagation). This automatically applies backpressure. (nodejs.org)

2) If you’re doing manual res.write() (common in SSE): wait for 'drain'

Pattern:

import { once } from 'node:events';

function writeSSE(res, msg) {
  const ok = res.write(`data: ${msg}\n\n`);
  return ok ? Promise.resolve() : once(res, 'drain');
}

// producer loop
for await (const msg of source) {
  await writeSSE(res, msg); // this is the backpressure gate
}

This is exactly the contract: when write() returns false, pause your producer and resume only after 'drain'. (nodejs.org)

Citations:


Handle ServerResponse.write() backpressure for reliable SSE streaming.

On slow clients, res.write() returns false when the internal buffer reaches capacity (highWaterMark). The current implementation ignores this signal and continues writing, causing Node to buffer unsent chunks in user-space memory. For long-lived agent streaming responses that can produce high volumes of events, this creates a memory leak and reliability risk.

Implement one of the two Node.js-recommended patterns:

  1. Drain pattern (if using manual res.write()): Check the return value and pause the event source until the 'drain' event fires before resuming writes.
  2. Stream API (preferred): Model the event source as a Readable and use pipe() or stream.pipeline() to connect it to the response; this applies backpressure automatically.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tegg/core/agent-runtime/src/NodeSSEWriter.ts` around lines 30 - 33,
writeEvent currently ignores res.write()'s return value and can overflow memory;
update NodeSSEWriter.writeEvent (and ensureHeaders if needed) to handle
backpressure by either: (a) implementing the drain pattern—check the boolean
result of this.res.write(...), and when it returns false stop emitting/writing
further events until this.res.once('drain', ...) resumes writing; or (b)
refactor the producer to a Readable stream and pipe it to this.res so Node
handles backpressure automatically; ensure _closed is still respected and that
pending events are queued/paused during the drain period to avoid dropping data.

// SSE streaming, async execution, and cancellation via smart defaults.
export interface AgentHandler {
execRun(input: CreateRunInput, signal?: AbortSignal): AsyncGenerator<AgentStreamMessage>;
createStore?(): Promise<unknown>;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Type createStore() to the actual store contract.

Promise<unknown> erases the new store requirement and still allows incompatible implementations to compile. Since agent controllers are now expected to provide a store, this should return the concrete store interface and ideally not stay optional.

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

In `@tegg/core/controller-decorator/src/decorator/agent/AgentHandler.ts` at line
14, The AgentHandler.createStore signature uses Promise<unknown> and is
optional; update it to return the concrete store contract and make it required
so incompatible implementations can't compile. Replace the declaration
createStore?(): Promise<unknown>; with createStore(): Promise<AgentStore>; (or
the actual store interface name used in your codebase), import that store
interface into the file, and update all concrete classes implementing
AgentHandler to return the correct AgentStore type from createStore.

@codecov
Copy link

codecov bot commented Mar 9, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 85.74%. Comparing base (1a4fb20) to head (2af6c6f).

Additional details and impacted files
@@            Coverage Diff             @@
##             next    #5827      +/-   ##
==========================================
+ Coverage   85.39%   85.74%   +0.34%     
==========================================
  Files         659      665       +6     
  Lines       12813    12963     +150     
  Branches     1474     1486      +12     
==========================================
+ Hits        10942    11115     +173     
+ Misses       1751     1728      -23     
  Partials      120      120              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Save close handler reference and remove it in end() to prevent
memory leaks. Clear closeCallbacks after execution to release refs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
tegg/core/agent-runtime/src/NodeSSEWriter.ts (1)

33-36: ⚠️ Potential issue | 🟠 Major

writeEvent() still ignores write backpressure.

Line 36 drops the boolean from res.write(). On long-lived SSE streams, a slow reader will make that return false; continuing to emit after that lets Node buffer unsent chunks in memory. Please stop producing or queue events until 'drain' fires.

In current Node.js, what does http.ServerResponse.write() returning false mean, and what is the recommended backpressure handling pattern for manual SSE responses?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tegg/core/agent-runtime/src/NodeSSEWriter.ts` around lines 33 - 36, The
writeEvent method in NodeSSEWriter currently ignores write backpressure by
discarding the boolean return of this.res.write; update NodeSSEWriter.writeEvent
to observe the boolean result from this.res.write(...) and, when it returns
false, stop writing further events and queue the event payloads (or set a
_paused flag) and attach a one-time 'drain' listener on this.res to flush the
queue and clear the paused state; reuse ensureHeaders() as before, respect
this._closed, and make sure the drain handler is cleaned up to avoid leaks.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tegg/core/agent-runtime/src/NodeSSEWriter.ts`:
- Around line 43-48: The end() implementation clears the close listener and
closeCallbacks before calling this.res.end(), which prevents onClose() callbacks
from running on normal completion; change end() to set this._closed = true
early, then call this.res.end(), and only after the stream has finished (or
immediately after res.end() while guarding against duplicate invocation) remove
the 'close' listener (onResClose) and clear/execute closeCallbacks so registered
onClose() cleanup always runs; reference functions/fields: end(), onResClose,
onClose(), closeCallbacks, res.end(), and _closed to locate and adjust the
logic.
- Line 3: Update the ES module import specifier in NodeSSEWriter by replacing
the TypeScript file extension with .js: change the import of SSEWriter from
'./SSEWriter.ts' to './SSEWriter.js' (the import statement that declares "import
type { SSEWriter } from './SSEWriter.ts';"); ensure the rest of the file still
compiles with the type-only import style.

---

Duplicate comments:
In `@tegg/core/agent-runtime/src/NodeSSEWriter.ts`:
- Around line 33-36: The writeEvent method in NodeSSEWriter currently ignores
write backpressure by discarding the boolean return of this.res.write; update
NodeSSEWriter.writeEvent to observe the boolean result from this.res.write(...)
and, when it returns false, stop writing further events and queue the event
payloads (or set a _paused flag) and attach a one-time 'drain' listener on
this.res to flush the queue and clear the paused state; reuse ensureHeaders() as
before, respect this._closed, and make sure the drain handler is cleaned up to
avoid leaks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ec09778b-34b9-424d-b3b1-707c3af0bb83

📥 Commits

Reviewing files that changed from the base of the PR and between a4f7658 and 5b2a400.

📒 Files selected for processing (1)
  • tegg/core/agent-runtime/src/NodeSSEWriter.ts

@@ -0,0 +1,55 @@
import type { ServerResponse } from 'node:http';

import type { SSEWriter } from './SSEWriter.ts';
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether this package is explicitly configured to allow `.ts` import specifiers.
fd '^tsconfig.*\.json$' -x sh -c 'echo "== {} =="; sed -n "1,220p" "{}"'
rg -n '"allowImportingTsExtensions"|"noEmit"|"moduleResolution"|"module"' -g 'tsconfig*.json'

Repository: eggjs/egg

Length of output: 24635


🏁 Script executed:

# Check import patterns in tegg directory to confirm the .js extension guideline is consistently applied
rg "import\s+(type\s+)?\{.*\}\s+from\s+['\"]\..*\.(ts|js)" tegg/ --no-heading -A 0 | head -30

Repository: eggjs/egg

Length of output: 4047


Use a .js specifier here.

Line 3 imports ./SSEWriter.ts, which violates the repository's ESM convention for the tegg/ directory. Please change this to ./SSEWriter.js.

Suggested change
-import type { SSEWriter } from './SSEWriter.ts';
+import type { SSEWriter } from './SSEWriter.js';
📝 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
import type { SSEWriter } from './SSEWriter.ts';
import type { SSEWriter } from './SSEWriter.js';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tegg/core/agent-runtime/src/NodeSSEWriter.ts` at line 3, Update the ES module
import specifier in NodeSSEWriter by replacing the TypeScript file extension
with .js: change the import of SSEWriter from './SSEWriter.ts' to
'./SSEWriter.js' (the import statement that declares "import type { SSEWriter }
from './SSEWriter.ts';"); ensure the rest of the file still compiles with the
type-only import style.

Comment on lines +43 to +48
end(): void {
if (!this._closed) {
this._closed = true;
this.res.off('close', this.onResClose);
this.closeCallbacks.length = 0;
this.res.end();
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

end() drops registered onClose() cleanup.

Lines 46-47 remove the close listener and clear closeCallbacks before res.end() closes the response, so callbacks registered via onClose() never run on the normal completion path. That makes cleanup behavior depend on how the stream ends.

Suggested fix
+  private runCloseCallbacks(): void {
+    if (this._closed) return;
+    this._closed = true;
+    this.res.off('close', this.onResClose);
+    const callbacks = this.closeCallbacks.splice(0);
+    for (const cb of callbacks) cb();
+  }
+
   constructor(res: ServerResponse) {
     this.res = res;
     this.onResClose = () => {
-      this._closed = true;
-      for (const cb of this.closeCallbacks) cb();
-      this.closeCallbacks.length = 0;
+      this.runCloseCallbacks();
     };
     res.on('close', this.onResClose);
   }
@@
   end(): void {
-    if (!this._closed) {
-      this._closed = true;
-      this.res.off('close', this.onResClose);
-      this.closeCallbacks.length = 0;
-      this.res.end();
-    }
+    if (this._closed) return;
+    this.runCloseCallbacks();
+    this.res.end();
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tegg/core/agent-runtime/src/NodeSSEWriter.ts` around lines 43 - 48, The end()
implementation clears the close listener and closeCallbacks before calling
this.res.end(), which prevents onClose() callbacks from running on normal
completion; change end() to set this._closed = true early, then call
this.res.end(), and only after the stream has finished (or immediately after
res.end() while guarding against duplicate invocation) remove the 'close'
listener (onResClose) and clear/execute closeCallbacks so registered onClose()
cleanup always runs; reference functions/fields: end(), onResClose, onClose(),
closeCallbacks, res.end(), and _closed to locate and adjust the logic.

…tions

- Add agent/index.ts barrel export to match http/index.ts and mcp/index.ts
- Rename setIsAgentController to setAgentController for set/get consistency

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
tegg/core/controller-decorator/src/util/AgentInfoUtil.ts (1)

18-24: Consider unifying metadata access through MetadataUtil for consistency.

These methods use raw Reflect.defineMetadata/Reflect.getMetadata while other methods in this class use MetadataUtil. The divergence is understandable since MetadataUtil is typed for EggProtoImplClass, not Function.

For long-term maintainability, consider either:

  1. Extending MetadataUtil with function-level metadata helpers (e.g., defineFunctionMetaData, getFunctionBooleanMetaData)
  2. Or add a brief comment explaining why raw Reflect is used here

This is a minor consistency nit and can be deferred.

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

In `@tegg/core/controller-decorator/src/util/AgentInfoUtil.ts` around lines 18 -
24, The setNotImplemented and isNotImplemented functions use raw Reflect
metadata while the rest of the class uses MetadataUtil; to unify, add
function-level helpers to MetadataUtil (e.g., defineFunctionMetaData(key:
string, target: Function, value: any) and getFunctionBooleanMetaData(key:
string, target: Function): boolean) and then replace calls in
AgentInfoUtil.setNotImplemented and AgentInfoUtil.isNotImplemented to use
MetadataUtil.defineFunctionMetaData(CONTROLLER_AGENT_NOT_IMPLEMENTED, fn, true)
and MetadataUtil.getFunctionBooleanMetaData(CONTROLLER_AGENT_NOT_IMPLEMENTED,
fn) respectively; alternatively, if you prefer not to change MetadataUtil now,
add a concise comment above these two methods explaining why raw Reflect is used
and referencing the typing limitation of MetadataUtil.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@tegg/core/controller-decorator/src/util/AgentInfoUtil.ts`:
- Around line 18-24: The setNotImplemented and isNotImplemented functions use
raw Reflect metadata while the rest of the class uses MetadataUtil; to unify,
add function-level helpers to MetadataUtil (e.g., defineFunctionMetaData(key:
string, target: Function, value: any) and getFunctionBooleanMetaData(key:
string, target: Function): boolean) and then replace calls in
AgentInfoUtil.setNotImplemented and AgentInfoUtil.isNotImplemented to use
MetadataUtil.defineFunctionMetaData(CONTROLLER_AGENT_NOT_IMPLEMENTED, fn, true)
and MetadataUtil.getFunctionBooleanMetaData(CONTROLLER_AGENT_NOT_IMPLEMENTED,
fn) respectively; alternatively, if you prefer not to change MetadataUtil now,
add a concise comment above these two methods explaining why raw Reflect is used
and referencing the typing limitation of MetadataUtil.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b569f124-ab75-4c17-85e5-a825df264bca

📥 Commits

Reviewing files that changed from the base of the PR and between 5b2a400 and f90b7e8.

📒 Files selected for processing (4)
  • tegg/core/controller-decorator/src/decorator/agent/AgentController.ts
  • tegg/core/controller-decorator/src/decorator/agent/index.ts
  • tegg/core/controller-decorator/src/decorator/index.ts
  • tegg/core/controller-decorator/src/util/AgentInfoUtil.ts
✅ Files skipped from review due to trivial changes (1)
  • tegg/core/controller-decorator/src/decorator/agent/index.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • tegg/core/controller-decorator/src/decorator/agent/AgentController.ts

jerryliang64 and others added 2 commits March 10, 2026 01:32
…Obj mutation

- Unify CreateRunInput.input.messages to use InputMessage[] from AgentStore,
  eliminating duplicate type definitions
- Add abortController.signal.aborted check in streamRun catch block to mark
  aborted runs as cancelled instead of failed, consistent with syncRun/asyncRun
- Replace msgObj mutation with new object spread to avoid aliasing issues
- Replace inline import() types with top-level imports in AgentRuntime types
- Add streamRun thread messages persistence test for parity with syncRun/asyncRun

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Change EggLogger import from 'egg-logger' to 'egg' in
  AgentControllerObject to fix oxlint TS2307 module resolution error
- Update @eggjs/tegg exports snapshot to include new AgentController
  exports from controller-decorator

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tegg/core/types/src/agent-runtime/AgentRuntime.ts`:
- Line 1: Update the ESM import specifier in AgentRuntime.ts to use a .js
extension: change the type import line that imports AgentRunConfig,
InputMessage, MessageObject, RunStatus from './AgentStore.ts' to import from
'./AgentStore.js' (keep it as an `import type` and only modify the module
specifier).

In `@tegg/plugin/controller/src/lib/AgentControllerObject.ts`:
- Line 19: Update the local ESM import specifier to use a .js extension: replace
the import of AgentControllerProto in AgentControllerObject (the line importing
AgentControllerProto) so it references './AgentControllerProto.js' instead of
'./AgentControllerProto.ts' to follow the repo's ESM import convention.
- Around line 205-225: The store created by createStoreFn and initialized via
store.init may leak if AgentRuntime.create throws before this.runtime is
assigned; wrap the store initialization and AgentRuntime.create call in a
try/catch/finally such that on any error prior to assigning this.runtime you
call store.destroy() (or await store.destroy() if async) to clean up resources,
rethrow the error, and only set this.runtime = runtime after successful
creation; reference createStoreFn, store.init, AgentRuntime.create, this.runtime
and store.destroy in your changes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7ccc6c33-3efd-43ea-915a-6587144c0eb4

📥 Commits

Reviewing files that changed from the base of the PR and between f90b7e8 and c4819e4.

⛔ Files ignored due to path filters (1)
  • tegg/core/tegg/test/__snapshots__/exports.test.ts.snap is excluded by !**/*.snap
📒 Files selected for processing (4)
  • tegg/core/agent-runtime/src/AgentRuntime.ts
  • tegg/core/agent-runtime/test/AgentRuntime.test.ts
  • tegg/core/types/src/agent-runtime/AgentRuntime.ts
  • tegg/plugin/controller/src/lib/AgentControllerObject.ts

@@ -1,3 +1,5 @@
import type { AgentRunConfig, InputMessage, MessageObject, RunStatus } from './AgentStore.ts';
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Switch the local ESM import to .js.

This new import still uses a .ts specifier, which can leak TypeScript-only paths into emitted ESM.

♻️ Suggested fix
-import type { AgentRunConfig, InputMessage, MessageObject, RunStatus } from './AgentStore.ts';
+import type { AgentRunConfig, InputMessage, MessageObject, RunStatus } from './AgentStore.js';

As per coding guidelines, "All imports should use .js extensions for ESM files".

📝 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
import type { AgentRunConfig, InputMessage, MessageObject, RunStatus } from './AgentStore.ts';
import type { AgentRunConfig, InputMessage, MessageObject, RunStatus } from './AgentStore.js';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tegg/core/types/src/agent-runtime/AgentRuntime.ts` at line 1, Update the ESM
import specifier in AgentRuntime.ts to use a .js extension: change the type
import line that imports AgentRunConfig, InputMessage, MessageObject, RunStatus
from './AgentStore.ts' to import from './AgentStore.js' (keep it as an `import
type` and only modify the module specifier).

import type { AgentStore, CreateRunInput } from '@eggjs/tegg-types/agent-runtime';
import type { EggLogger } from 'egg';

import { AgentControllerProto } from './AgentControllerProto.ts';
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Use a .js specifier for the local ESM import.

./AgentControllerProto.ts breaks the repo's ESM import convention for tegg/* sources.

♻️ Suggested fix
-import { AgentControllerProto } from './AgentControllerProto.ts';
+import { AgentControllerProto } from './AgentControllerProto.js';

As per coding guidelines, "All imports should use .js extensions for ESM files".

📝 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
import { AgentControllerProto } from './AgentControllerProto.ts';
import { AgentControllerProto } from './AgentControllerProto.js';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tegg/plugin/controller/src/lib/AgentControllerObject.ts` at line 19, Update
the local ESM import specifier to use a .js extension: replace the import of
AgentControllerProto in AgentControllerObject (the line importing
AgentControllerProto) so it references './AgentControllerProto.js' instead of
'./AgentControllerProto.ts' to follow the repo's ESM import convention.

Comment on lines +205 to +225
// Create store — user must implement createStore()
let store: AgentStore;
const createStoreFn = instance['createStore'];
if (typeof createStoreFn === 'function') {
store = (await Reflect.apply(createStoreFn, this._obj, [])) as AgentStore;
} else {
throw new Error(
'@AgentController requires a createStore() method. ' +
'Implement createStore() in your controller to provide an AgentStore instance.',
);
}
if (store.init) {
await store.init();
}

// Create runtime with AgentRuntime.create factory
const runtime = AgentRuntime.create({
executor: this._obj as AgentExecutor,
store,
logger: AgentControllerObject.logger,
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Destroy the store if runtime installation fails before ownership transfers.

createStore() / store.init() can allocate handles, but the only init-failure cleanup path goes through this.runtime.destroy(). If store.init() or AgentRuntime.create() throws before this.runtime is assigned, the store never gets destroy() and its resources leak.

🛠️ Suggested fix
-    let store: AgentStore;
+    let store: AgentStore;
     const createStoreFn = instance['createStore'];
     if (typeof createStoreFn === 'function') {
       store = (await Reflect.apply(createStoreFn, this._obj, [])) as AgentStore;
     } else {
       throw new Error(
@@
-    if (store.init) {
-      await store.init();
-    }
-
-    // Create runtime with AgentRuntime.create factory
-    const runtime = AgentRuntime.create({
-      executor: this._obj as AgentExecutor,
-      store,
-      logger: AgentControllerObject.logger,
-    });
+    try {
+      await store.init?.();
+    } catch (err) {
+      await store.destroy?.();
+      throw err;
+    }
+
+    let runtime: AgentRuntime;
+    try {
+      runtime = AgentRuntime.create({
+        executor: this._obj as AgentExecutor,
+        store,
+        logger: AgentControllerObject.logger,
+      });
+    } catch (err) {
+      await store.destroy?.();
+      throw err;
+    }
     this.runtime = runtime;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tegg/plugin/controller/src/lib/AgentControllerObject.ts` around lines 205 -
225, The store created by createStoreFn and initialized via store.init may leak
if AgentRuntime.create throws before this.runtime is assigned; wrap the store
initialization and AgentRuntime.create call in a try/catch/finally such that on
any error prior to assigning this.runtime you call store.destroy() (or await
store.destroy() if async) to clean up resources, rethrow the error, and only set
this.runtime = runtime after successful creation; reference createStoreFn,
store.init, AgentRuntime.create, this.runtime and store.destroy in your changes.

jerryliang64 and others added 3 commits March 10, 2026 10:22
…ymbols

Add missing AGENT_CONTROLLER_PROTO_IMPL_TYPE and CONTROLLER_AGENT_*
symbols to the exports stability snapshot, fixing CI shard 3/3 failures.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…mutation

Replace loose index signature with explicit fields (role, status, content,
runId, threadId) for type safety. Add MessageConverter.completeMessage()
factory method to avoid mutating message objects in AgentRuntime streaming,
aligning with the immutable pattern already used by RunBuilder.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add unit tests to cover previously untested code paths:
- AgentControllerProto: constructor delegation, symbol property copying,
  all getter/method delegations, and createProto static factory
- AgentInfoUtil: setEnhanced/isEnhanced methods

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

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)
tegg/core/agent-runtime/test/AgentRuntime.test.ts (1)

322-358: Assert that thread.message.created stays in_progress.

This test only checks event names. The new completeMessage() path exists to prevent the ThreadMessageCreated payload from being rewritten to completed by reference, and MockSSEWriter is exactly the harness that can catch that regression. Please assert that the created event still has status === in_progress and content === [] after the completed event is emitted.

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

In `@tegg/core/agent-runtime/test/AgentRuntime.test.ts` around lines 322 - 358,
The test is missing assertions that the ThreadMessageCreated event's payload
remains status === ThreadMessageStatus.InProgress and content === [] after the
ThreadMessageCompleted event; using MockSSEWriter, find the ThreadMessageCreated
event in writer.events (event === AgentSSEEvent.ThreadMessageCreated), capture
its data before/after the completion sequence and assert its status is
InProgress and its content is an empty array (use the same event object from
writer.events to catch reference mutations introduced by completeMessage()).
Ensure you reference AgentSSEEvent.ThreadMessageCreated,
AgentSSEEvent.ThreadMessageCompleted, MockSSEWriter, and the completeMessage()
path when adding these assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tegg/core/agent-runtime/src/AgentRuntime.ts`:
- Around line 303-324: The code currently mutates the terminal builder rb to
completed before an awaited update and then reuses rb in the error path
(rb.cancelling() / rb.fail()), which can cause InvalidRunStateTransitionError
and emit a completed snapshot; instead, after any failed await to
this.store.updateRun(run.id, rb.complete()/...), rehydrate the authoritative run
state from the store (e.g., fetch the persisted run record for run.id) and
construct a new RunBuilder from that persisted record before calling
cancelling() or fail(); then use that fresh builder to call
this.store.updateRun(...) and
writer.writeEvent(AgentSSEEvent.ThreadRunCancelled/ThreadRunFailed, ...) so you
never call cancelling()/fail() on a builder already transitioned to completed.

---

Nitpick comments:
In `@tegg/core/agent-runtime/test/AgentRuntime.test.ts`:
- Around line 322-358: The test is missing assertions that the
ThreadMessageCreated event's payload remains status ===
ThreadMessageStatus.InProgress and content === [] after the
ThreadMessageCompleted event; using MockSSEWriter, find the ThreadMessageCreated
event in writer.events (event === AgentSSEEvent.ThreadMessageCreated), capture
its data before/after the completion sequence and assert its status is
InProgress and its content is an empty array (use the same event object from
writer.events to catch reference mutations introduced by completeMessage()).
Ensure you reference AgentSSEEvent.ThreadMessageCreated,
AgentSSEEvent.ThreadMessageCompleted, MockSSEWriter, and the completeMessage()
path when adding these assertions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e136a122-362c-4338-bd21-b2b265a981cb

📥 Commits

Reviewing files that changed from the base of the PR and between c4819e4 and c6b4c97.

⛔ Files ignored due to path filters (1)
  • tegg/core/types/test/__snapshots__/index.test.ts.snap is excluded by !**/*.snap
📒 Files selected for processing (6)
  • tegg/core/agent-runtime/src/AgentRuntime.ts
  • tegg/core/agent-runtime/src/MessageConverter.ts
  • tegg/core/agent-runtime/test/AgentRuntime.test.ts
  • tegg/core/agent-runtime/test/MessageConverter.test.ts
  • tegg/core/agent-runtime/test/RunBuilder.test.ts
  • tegg/core/types/src/agent-runtime/AgentStore.ts

Comment on lines +303 to +324
if (abortController.signal.aborted) {
// Client disconnected or cancelRun fired — mark as cancelled, not failed
rb.cancelling();
try {
await this.store.updateRun(run.id, rb.cancel());
} catch (storeErr) {
this.logger.error('[AgentRuntime] failed to write cancelled status during stream error:', storeErr);
}
if (!writer.closed) {
writer.writeEvent(AgentSSEEvent.ThreadRunCancelled, rb.snapshot());
}
} else {
try {
await this.store.updateRun(run.id, rb.fail(err as Error));
} catch (storeErr) {
this.logger.error('[AgentRuntime] failed to update run status after error:', storeErr);
}

// event: thread.run.failed
if (!writer.closed) {
writer.writeEvent(AgentSSEEvent.ThreadRunFailed, rb.snapshot());
// event: thread.run.failed
if (!writer.closed) {
writer.writeEvent(AgentSSEEvent.ThreadRunFailed, rb.snapshot());
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't reuse a post-complete() builder in the error path.

Line 298 mutates rb to completed before the awaited store write. If that write or Line 301 throws, this catch block reuses a terminal builder, so rb.cancelling()/rb.fail() can throw InvalidRunStateTransitionError, and ThreadRunFailed may be emitted with a completed snapshot. Rehydrate from the persisted run record before deciding whether this should become cancelled or failed.

Suggested direction
     } catch (err: unknown) {
+      const latest = await this.store.getRun(run.id).catch(() => undefined);
+      const errorRb = latest ? RunBuilder.fromRecord(latest) : rb;
+
       if (abortController.signal.aborted) {
         // Client disconnected or cancelRun fired — mark as cancelled, not failed
-        rb.cancelling();
+        if (AgentRuntime.TERMINAL_RUN_STATUSES.has(errorRb.snapshot().status)) {
+          return;
+        }
+        errorRb.cancelling();
         try {
-          await this.store.updateRun(run.id, rb.cancel());
+          await this.store.updateRun(run.id, errorRb.cancel());
         } catch (storeErr) {
           this.logger.error('[AgentRuntime] failed to write cancelled status during stream error:', storeErr);
         }
         if (!writer.closed) {
-          writer.writeEvent(AgentSSEEvent.ThreadRunCancelled, rb.snapshot());
+          writer.writeEvent(AgentSSEEvent.ThreadRunCancelled, errorRb.snapshot());
         }
       } else {
         try {
-          await this.store.updateRun(run.id, rb.fail(err as Error));
+          if (!AgentRuntime.TERMINAL_RUN_STATUSES.has(errorRb.snapshot().status)) {
+            await this.store.updateRun(run.id, errorRb.fail(err as Error));
+          }
         } catch (storeErr) {
           this.logger.error('[AgentRuntime] failed to update run status after error:', storeErr);
         }

         // event: thread.run.failed
         if (!writer.closed) {
-          writer.writeEvent(AgentSSEEvent.ThreadRunFailed, rb.snapshot());
+          writer.writeEvent(AgentSSEEvent.ThreadRunFailed, errorRb.snapshot());
         }
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tegg/core/agent-runtime/src/AgentRuntime.ts` around lines 303 - 324, The code
currently mutates the terminal builder rb to completed before an awaited update
and then reuses rb in the error path (rb.cancelling() / rb.fail()), which can
cause InvalidRunStateTransitionError and emit a completed snapshot; instead,
after any failed await to this.store.updateRun(run.id, rb.complete()/...),
rehydrate the authoritative run state from the store (e.g., fetch the persisted
run record for run.id) and construct a new RunBuilder from that persisted record
before calling cancelling() or fail(); then use that fresh builder to call
this.store.updateRun(...) and
writer.writeEvent(AgentSSEEvent.ThreadRunCancelled/ThreadRunFailed, ...) so you
never call cancelling()/fail() on a builder already transitioned to completed.

jerryliang64 and others added 5 commits March 10, 2026 11:06
"Node" prefix is redundant in a Node.js framework — the class actually
adapts the HTTP ServerResponse for SSE, so "Http" is more precise.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move ContentBlockType, InputContentPart, TextContentBlock, MessageContentBlock,
InputMessage, and MessageObject into a dedicated AgentMessage.ts file to eliminate
the circular dependency between AgentStore.ts and AgentRuntime.ts (previously
worked around with an inline import() type).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The runtime already throws if createStore is missing. Align the
interface with the actual behavior so implementors get a compile-time
error instead of a runtime surprise.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The previous commit made createStore required in AgentHandler interface
but missed updating this test fixture, causing typecheck CI failure.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@akitaSummer akitaSummer left a comment

Choose a reason for hiding this comment

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

1

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.

2 participants