Skip to content

feat(api-service): add thinking indicator toggle to agent behavior settings fixes NV-7366#10721

Merged
ChmaraX merged 1 commit intonextfrom
nv-7366-add-typing-indicator-toggle
Apr 15, 2026
Merged

feat(api-service): add thinking indicator toggle to agent behavior settings fixes NV-7366#10721
ChmaraX merged 1 commit intonextfrom
nv-7366-add-typing-indicator-toggle

Conversation

@ChmaraX
Copy link
Copy Markdown
Contributor

@ChmaraX ChmaraX commented Apr 15, 2026

Summary

  • Introduces an AgentBehavior subdocument on the Agent entity to hold per-agent runtime behavior settings, starting with thinkingIndicatorEnabled
  • This architecture supports future settings (reactions, interrupt mode, response timeout, etc.) under behavior without adding flat fields to the entity
  • Defaults to enabled — only an explicit false suppresses the indicator. Status text is "Thinking..." (not the generic platform "typing" label)

How it works

Data modelAgentEntity gets an optional behavior?: AgentBehavior subdocument (interface + inline Mongoose nested paths, following the same pattern as Organization.branding, Environment.widget, Integration.configurations):

interface AgentBehavior {
  thinkingIndicatorEnabled?: boolean;
  // future: inboundReaction, resolveReaction, interruptMode, etc.
}

APIPATCH /agents/:identifier accepts a nested behavior object. Uses dot-notation $set so individual fields can be updated without overwriting the whole subdocument:

{ "behavior": { "thinkingIndicatorEnabled": false } }

RuntimeAgentCredentialService.resolve() reads agent.behavior?.thinkingIndicatorEnabled and resolves it to a boolean (defaulting to true). AgentInboundHandler gates thread.startTyping('Thinking...') behind this resolved value.

Test plan

  • E2E test: PATCH with behavior persists and round-trips correctly
  • E2E test: re-enabling the indicator after disabling it works
  • Manual: agent with thinkingIndicatorEnabled: false does not trigger indicator on inbound messages
  • Manual: agent with no behavior set shows "Thinking..." indicator as before

Linear ticket

https://linear.app/novu/issue/NV-7366/add-typing-indicator-toggle-to-agent-behavior-settings

What changed

Added an optional behavior.typingIndicatorEnabled boolean to per-agent settings and persisted it in the database so agents can opt out of showing typing indicators. The API accepts and returns the nested behavior object on PATCH/GET /agents/:identifier and updates use dot-notation so individual behavior fields can be patched without overwriting the subdocument. At runtime AgentCredentialService resolves the flag (defaulting to enabled) and AgentInboundHandler only calls thread.startTyping() when the setting is not explicitly false.

Affected areas

  • api: Request/response DTOs, controller, update use case, response mapper, and AgentCredentialService were updated to accept, validate, persist, and resolve the new behavior.typingIndicatorEnabled flag; AgentInboundHandler now conditionally calls thread.startTyping().
  • dal: AgentEntity and Mongoose schema gained an optional behavior.typingIndicatorEnabled nested field to store the setting.
  • e2e tests (api): Added an end-to-end test that verifies behavior is initially unset, can be patched to false/true, persists, and is returned by GET.

Key technical decisions

  • Backwards-compatible default: undefined (or true) keeps typing indicators enabled; only an explicit false disables them (implementation: treat !== false as enabled).
  • Partial updates supported: PATCH uses dot-notation $set to update individual behavior fields without replacing the whole subdocument.
  • No new runtime dependencies or enterprise-specific changes introduced.

Testing

Added an E2E test covering PATCH/GET persistence and behavior toggling; runtime behavior verified by tests that inbound messages respect typingIndicatorEnabled=false and remain unchanged when true/unset.

@linear
Copy link
Copy Markdown

linear bot commented Apr 15, 2026

@netlify
Copy link
Copy Markdown

netlify bot commented Apr 15, 2026

Deploy Preview for dashboard-v2-novu-staging canceled.

Name Link
🔨 Latest commit 9c46139
🔍 Latest deploy log https://app.netlify.com/projects/dashboard-v2-novu-staging/deploys/69df5dad9d05cb0008e130f1

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 15, 2026

📝 Walkthrough

Walkthrough

Adds a nested behavior object with optional thinkingIndicatorEnabled across DTOs, entity/schema, mapper, controller, command/usecase, and services; inbound handler now conditionally starts typing only when thinkingIndicatorEnabled is truthy.

Changes

Cohort / File(s) Summary
DTOs
apps/api/src/app/agents/dtos/agent-behavior.dto.ts, apps/api/src/app/agents/dtos/update-agent-request.dto.ts, apps/api/src/app/agents/dtos/agent-response.dto.ts, apps/api/src/app/agents/dtos/index.ts
Added AgentBehaviorDto; request DTO accepts optional behavior (validated/transformed); response DTO exposes optional behavior; barrel export updated.
Controller & Mapper
apps/api/src/app/agents/agents.controller.ts, apps/api/src/app/agents/mappers/agent-response.mapper.ts
Controller forwards behavior into UpdateAgentCommand; mapper returns behavior from entity in response DTO.
Usecase & Command
apps/api/src/app/agents/usecases/update-agent/update-agent.command.ts, apps/api/src/app/agents/usecases/update-agent/update-agent.usecase.ts
Command accepts optional behavior; usecase requires at least one of name/description/behavior and conditionally sets behavior.thinkingIndicatorEnabled when provided.
Data Layer (entity & schema)
libs/dal/src/repositories/agent/agent.entity.ts, libs/dal/src/repositories/agent/agent.schema.ts
Added exported AgentBehavior interface and optional behavior field on AgentEntity; added behavior.thinkingIndicatorEnabled to Mongoose agent schema.
Services / Handler
apps/api/src/app/agents/services/agent-credential.service.ts, apps/api/src/app/agents/services/agent-inbound-handler.service.ts
Resolved platform config now includes thinkingIndicatorEnabled (computed from agent.behavior); inbound handler starts typing only when that flag is truthy.
Tests
apps/api/src/app/agents/e2e/agents.e2e.ts
New E2E test covers updating and persisting agent behavior.thinkingIndicatorEnabled.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant InboundHandler as AgentInboundHandler
  participant CredentialSvc as AgentCredentialService
  participant Thread
  participant DB
  participant Bridge

  Client->>InboundHandler: Send inbound message (agentId, message)
  InboundHandler->>CredentialSvc: resolve(agentId, integrationId)
  CredentialSvc-->>InboundHandler: ResolvedPlatformConfig (thinkingIndicatorEnabled)
  alt thinkingIndicatorEnabled == true
    InboundHandler->>Thread: startTyping('Thinking...')
    Thread-->>InboundHandler: started
  end
  InboundHandler->>DB: persist inbound message / thread state
  DB-->>InboundHandler: persisted
  InboundHandler->>Bridge: execute bridge with message/thread
  Bridge-->>InboundHandler: bridge result
  InboundHandler-->>Client: response / ack
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • scopsy
  • djabarovgeorge
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.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 title follows Conventional Commits format with a valid type (feat), valid scope (api-service), lowercase imperative description, and includes the Linear ticket reference (fixes NV-7366) at the end.

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


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.

…ttings fixes NV-7366

Introduce an `AgentBehavior` subdocument on the Agent entity to hold
per-agent runtime behavior settings (starting with `thinkingIndicatorEnabled`).
This architecture supports future settings (reactions, interrupt mode, etc.)
without adding flat fields to the entity.

Defaults to enabled — only an explicit `false` suppresses the indicator.
The status text is "Thinking..." (not the generic "typing" label).

Made-with: Cursor
@ChmaraX ChmaraX force-pushed the nv-7366-add-typing-indicator-toggle branch from 4b5056b to 9c46139 Compare April 15, 2026 09:43
@ChmaraX ChmaraX changed the title feat(api-service): add typing indicator toggle to agent settings fixes NV-7366 feat(api-service): add thinking indicator toggle to agent behavior settings fixes NV-7366 Apr 15, 2026
Copy link
Copy Markdown
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)
apps/api/src/app/agents/e2e/agents.e2e.ts (1)

68-99: Prefer try/finally cleanup to avoid leaking test data on assertion failures.

If any assertion throws before Line 98, the created agent is never deleted.

♻️ Suggested refactor
   it('should update and return agent behavior settings', async () => {
     const identifier = `e2e-behavior-${Date.now()}`;
-
-    const createRes = await session.testAgent.post('/v1/agents').send({
-      name: 'Behavior Agent',
-      identifier,
-    });
-
-    expect(createRes.status).to.equal(201);
-    expect(createRes.body.data.behavior).to.equal(undefined);
-
-    const patchRes = await session.testAgent.patch(`/v1/agents/${encodeURIComponent(identifier)}`).send({
-      behavior: { typingIndicatorEnabled: false },
-    });
-
-    expect(patchRes.status).to.equal(200);
-    expect(patchRes.body.data.behavior).to.deep.equal({ typingIndicatorEnabled: false });
-
-    const getRes = await session.testAgent.get(`/v1/agents/${encodeURIComponent(identifier)}`);
-
-    expect(getRes.status).to.equal(200);
-    expect(getRes.body.data.behavior.typingIndicatorEnabled).to.equal(false);
-
-    const reEnableRes = await session.testAgent.patch(`/v1/agents/${encodeURIComponent(identifier)}`).send({
-      behavior: { typingIndicatorEnabled: true },
-    });
-
-    expect(reEnableRes.status).to.equal(200);
-    expect(reEnableRes.body.data.behavior.typingIndicatorEnabled).to.equal(true);
-
-    await session.testAgent.delete(`/v1/agents/${encodeURIComponent(identifier)}`);
+    try {
+      const createRes = await session.testAgent.post('/v1/agents').send({
+        name: 'Behavior Agent',
+        identifier,
+      });
+
+      expect(createRes.status).to.equal(201);
+      expect(createRes.body.data.behavior).to.equal(undefined);
+
+      const patchRes = await session.testAgent.patch(`/v1/agents/${encodeURIComponent(identifier)}`).send({
+        behavior: { typingIndicatorEnabled: false },
+      });
+
+      expect(patchRes.status).to.equal(200);
+      expect(patchRes.body.data.behavior).to.deep.equal({ typingIndicatorEnabled: false });
+
+      const getRes = await session.testAgent.get(`/v1/agents/${encodeURIComponent(identifier)}`);
+
+      expect(getRes.status).to.equal(200);
+      expect(getRes.body.data.behavior.typingIndicatorEnabled).to.equal(false);
+
+      const reEnableRes = await session.testAgent.patch(`/v1/agents/${encodeURIComponent(identifier)}`).send({
+        behavior: { typingIndicatorEnabled: true },
+      });
+
+      expect(reEnableRes.status).to.equal(200);
+      expect(reEnableRes.body.data.behavior.typingIndicatorEnabled).to.equal(true);
+    } finally {
+      await session.testAgent.delete(`/v1/agents/${encodeURIComponent(identifier)}`);
+    }
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/app/agents/e2e/agents.e2e.ts` around lines 68 - 99, The test
"should update and return agent behavior settings" can leak the created agent if
an assertion fails; wrap the agent lifecycle in a try/finally: create the agent
with session.testAgent.post(...) as before, then put all subsequent assertions
and patch/get calls inside a try block, and perform the cleanup in a finally
block that calls await
session.testAgent.delete(`/v1/agents/${encodeURIComponent(identifier)}`)
(guarding for a successful create or non-empty identifier and optionally
catching/ignoring delete errors so cleanup doesn't fail the test).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/api/src/app/agents/e2e/agents.e2e.ts`:
- Around line 79-96: The E2E test uses the wrong field name: replace all
occurrences of thinkingIndicatorEnabled with typingIndicatorEnabled in the
requests and assertions for the agent update flow (update calls made via
session.testAgent.patch(`/v1/agents/${encodeURIComponent(identifier)}`), the
subsequent GET via
session.testAgent.get(`/v1/agents/${encodeURIComponent(identifier)}`), and the
response checks on patchRes.body.data.behavior, getRes.body.data.behavior, and
reEnableRes.body.data.behavior) so the PATCH payloads and expect(...) checks
validate typingIndicatorEnabled instead of thinkingIndicatorEnabled.

---

Nitpick comments:
In `@apps/api/src/app/agents/e2e/agents.e2e.ts`:
- Around line 68-99: The test "should update and return agent behavior settings"
can leak the created agent if an assertion fails; wrap the agent lifecycle in a
try/finally: create the agent with session.testAgent.post(...) as before, then
put all subsequent assertions and patch/get calls inside a try block, and
perform the cleanup in a finally block that calls await
session.testAgent.delete(`/v1/agents/${encodeURIComponent(identifier)}`)
(guarding for a successful create or non-empty identifier and optionally
catching/ignoring delete errors so cleanup doesn't fail the test).
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 41ac2c61-3121-4786-9085-8e6505b769cf

📥 Commits

Reviewing files that changed from the base of the PR and between 4b5056b and 9c46139.

📒 Files selected for processing (13)
  • apps/api/src/app/agents/agents.controller.ts
  • apps/api/src/app/agents/dtos/agent-behavior.dto.ts
  • apps/api/src/app/agents/dtos/agent-response.dto.ts
  • apps/api/src/app/agents/dtos/index.ts
  • apps/api/src/app/agents/dtos/update-agent-request.dto.ts
  • apps/api/src/app/agents/e2e/agents.e2e.ts
  • apps/api/src/app/agents/mappers/agent-response.mapper.ts
  • apps/api/src/app/agents/services/agent-credential.service.ts
  • apps/api/src/app/agents/services/agent-inbound-handler.service.ts
  • apps/api/src/app/agents/usecases/update-agent/update-agent.command.ts
  • apps/api/src/app/agents/usecases/update-agent/update-agent.usecase.ts
  • libs/dal/src/repositories/agent/agent.entity.ts
  • libs/dal/src/repositories/agent/agent.schema.ts
✅ Files skipped from review due to trivial changes (4)
  • apps/api/src/app/agents/dtos/index.ts
  • apps/api/src/app/agents/services/agent-inbound-handler.service.ts
  • libs/dal/src/repositories/agent/agent.schema.ts
  • apps/api/src/app/agents/dtos/agent-behavior.dto.ts
🚧 Files skipped from review as they are similar to previous changes (7)
  • apps/api/src/app/agents/mappers/agent-response.mapper.ts
  • apps/api/src/app/agents/dtos/update-agent-request.dto.ts
  • apps/api/src/app/agents/dtos/agent-response.dto.ts
  • libs/dal/src/repositories/agent/agent.entity.ts
  • apps/api/src/app/agents/usecases/update-agent/update-agent.usecase.ts
  • apps/api/src/app/agents/usecases/update-agent/update-agent.command.ts
  • apps/api/src/app/agents/services/agent-credential.service.ts

Comment on lines +79 to +96
const patchRes = await session.testAgent.patch(`/v1/agents/${encodeURIComponent(identifier)}`).send({
behavior: { thinkingIndicatorEnabled: false },
});

expect(patchRes.status).to.equal(200);
expect(patchRes.body.data.behavior).to.deep.equal({ thinkingIndicatorEnabled: false });

const getRes = await session.testAgent.get(`/v1/agents/${encodeURIComponent(identifier)}`);

expect(getRes.status).to.equal(200);
expect(getRes.body.data.behavior.thinkingIndicatorEnabled).to.equal(false);

const reEnableRes = await session.testAgent.patch(`/v1/agents/${encodeURIComponent(identifier)}`).send({
behavior: { thinkingIndicatorEnabled: true },
});

expect(reEnableRes.status).to.equal(200);
expect(reEnableRes.body.data.behavior.thinkingIndicatorEnabled).to.equal(true);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Use typingIndicatorEnabled instead of thinkingIndicatorEnabled in this E2E flow.

The test currently validates a different field name than the feature contract in the PR objective, which can mask a real API mismatch.

✅ Suggested fix
-    const patchRes = await session.testAgent.patch(`/v1/agents/${encodeURIComponent(identifier)}`).send({
-      behavior: { thinkingIndicatorEnabled: false },
-    });
+    const patchRes = await session.testAgent.patch(`/v1/agents/${encodeURIComponent(identifier)}`).send({
+      behavior: { typingIndicatorEnabled: false },
+    });

     expect(patchRes.status).to.equal(200);
-    expect(patchRes.body.data.behavior).to.deep.equal({ thinkingIndicatorEnabled: false });
+    expect(patchRes.body.data.behavior).to.deep.equal({ typingIndicatorEnabled: false });

     const getRes = await session.testAgent.get(`/v1/agents/${encodeURIComponent(identifier)}`);

     expect(getRes.status).to.equal(200);
-    expect(getRes.body.data.behavior.thinkingIndicatorEnabled).to.equal(false);
+    expect(getRes.body.data.behavior.typingIndicatorEnabled).to.equal(false);

     const reEnableRes = await session.testAgent.patch(`/v1/agents/${encodeURIComponent(identifier)}`).send({
-      behavior: { thinkingIndicatorEnabled: true },
+      behavior: { typingIndicatorEnabled: true },
     });

     expect(reEnableRes.status).to.equal(200);
-    expect(reEnableRes.body.data.behavior.thinkingIndicatorEnabled).to.equal(true);
+    expect(reEnableRes.body.data.behavior.typingIndicatorEnabled).to.equal(true);
📝 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
const patchRes = await session.testAgent.patch(`/v1/agents/${encodeURIComponent(identifier)}`).send({
behavior: { thinkingIndicatorEnabled: false },
});
expect(patchRes.status).to.equal(200);
expect(patchRes.body.data.behavior).to.deep.equal({ thinkingIndicatorEnabled: false });
const getRes = await session.testAgent.get(`/v1/agents/${encodeURIComponent(identifier)}`);
expect(getRes.status).to.equal(200);
expect(getRes.body.data.behavior.thinkingIndicatorEnabled).to.equal(false);
const reEnableRes = await session.testAgent.patch(`/v1/agents/${encodeURIComponent(identifier)}`).send({
behavior: { thinkingIndicatorEnabled: true },
});
expect(reEnableRes.status).to.equal(200);
expect(reEnableRes.body.data.behavior.thinkingIndicatorEnabled).to.equal(true);
const patchRes = await session.testAgent.patch(`/v1/agents/${encodeURIComponent(identifier)}`).send({
behavior: { typingIndicatorEnabled: false },
});
expect(patchRes.status).to.equal(200);
expect(patchRes.body.data.behavior).to.deep.equal({ typingIndicatorEnabled: false });
const getRes = await session.testAgent.get(`/v1/agents/${encodeURIComponent(identifier)}`);
expect(getRes.status).to.equal(200);
expect(getRes.body.data.behavior.typingIndicatorEnabled).to.equal(false);
const reEnableRes = await session.testAgent.patch(`/v1/agents/${encodeURIComponent(identifier)}`).send({
behavior: { typingIndicatorEnabled: true },
});
expect(reEnableRes.status).to.equal(200);
expect(reEnableRes.body.data.behavior.typingIndicatorEnabled).to.equal(true);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/app/agents/e2e/agents.e2e.ts` around lines 79 - 96, The E2E test
uses the wrong field name: replace all occurrences of thinkingIndicatorEnabled
with typingIndicatorEnabled in the requests and assertions for the agent update
flow (update calls made via
session.testAgent.patch(`/v1/agents/${encodeURIComponent(identifier)}`), the
subsequent GET via
session.testAgent.get(`/v1/agents/${encodeURIComponent(identifier)}`), and the
response checks on patchRes.body.data.behavior, getRes.body.data.behavior, and
reEnableRes.body.data.behavior) so the PATCH payloads and expect(...) checks
validate typingIndicatorEnabled instead of thinkingIndicatorEnabled.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant