Skip to content

[Follow-up #3523][NOT scope creep] Ship external LiveContext providers (Slack) — distinct from ContextComposer #3557

Description

@Dhivya-Bharathy

Follow-up #3523 — Runtime External Context Providers (Slack / GDrive / Wiki / MCP Live Data)

Title

[Follow-up #3523][NOT scope creep] Ship external ContextProvider backends for live Slack, Google Workspace, wiki, and MCP data — distinct from token-window ContextComposer


Executive Summary

Parent issue #3523 was auto-closed as COMPLETED by praisonai-triage-agent on 2026-07-30 with verdict: “reject as scope creep — duplicates existing praisonaiagents/context/ module.”

That closure is incorrect. No code was merged. No Slack/GDrive/wiki provider shipped. The existing context/ package solves a different problem (token budget assembly of in-process segments) than the Agno-parity gap ( live external data fetch at runtime).

This follow-up narrows scope, documents the distinction with repository evidence, and provides an @claude-ready rebuttal so triage can produce a PR instead of re-closing.

Business Impact

Stakeholder Impact if ignored
Enterprise evaluators “Connect Slack + docs out of the box” — Agno wins
Developers ~200 LOC custom OAuth tool per integration
Maintainers Repeated ad-hoc Slack tools in cookbooks

ROI: One provider backend → many agents; matches competitive gap filed after PraisonAI vs Agno analysis (2026-07-30).


Parent Issue Status

Field Value
Original #3523
Closed 2026-07-30T09:34:57Z
State reason COMPLETED (incorrect — no implementation)
Claude job https://github.com/MervinPraison/PraisonAI/actions/runs/30531125287
Verdict “scope creep + mis-routed — no PR”

Compare #3522 (same batch): correctly implemented via PR #3525 (Helm chart merged). #3523 was closed in the same triage pass without an equivalent PR.


Why Claude Called This “Scope Creep” (and Why That Is Wrong)

Claude’s argument (verbatim summary)

From triage comment on #3523:

The proposed praisonaiagents/context/ module (protocols, ContextComposer, ContextMode, budget/dedupe/truncate) already existsAgent(context=...) is already a first-class param … reject as scope creep.

Rebuttal — two different meanings of “context”

flowchart TD
    subgraph exists [EXISTS TODAY — NOT the gap]
        CC[ContextComposer]
        CC --> SP[system_prompt]
        CC --> H[history messages]
        CC --> M[memory string]
        CC --> T[tool schemas]
        CC --> B[token budget / trim]
    end

    subgraph missing [MISSING — the gap #3523]
        CP[External ContextProvider]
        CP --> SL[Slack live channels]
        CP --> GD[Google Drive files]
        CP --> WK[Wiki pages]
        CP --> MC[MCP as context source]
        CP --> FETCH[async fetch + OAuth refresh]
    end

    exists -.->|Claude conflated| missing
Loading
Dimension Existing praisonaiagents/context/ Requested external providers
Purpose Assemble LLM prompt from already-known segments Fetch live data from external systems
Key class ContextComposer (composer.py) ContextProvider (Agno pattern — absent)
Inputs system_prompt, rules, skills, memory, history Slack API, GDrive API, wiki URL
OAuth None Required per backend
YAML N/A context: [{type: slack, channels: [...]}]
Agno equivalent N/A libs/agno/agno/context/slack/, gdrive/, wiki/

Repository evidence — what ContextComposer actually does

From src/praisonai-agents/praisonaiagents/context/composer.py:

class ContextComposer:
    """
    Composes context from segments within budget constraints.
    Assembles system prompt, rules, skills, memory, tools, and history
    while respecting token budgets and applying trimming as needed.
    """

Verified: No subdirectory context/slack/, context/gdrive/, context/wiki/, or ContextProvider fetch protocol in praisonaiagents/context/ (34 files — indexer, budgeter, ledger, fast context for codebase symbols, not Slack).

Agent(context=...) param (if present) refers to in-agent context config, not Agno-style live provider list — triage conflated the parameter name with the feature.


Competitor Analysis (Agno — Verified 2026-07-30)

Agno path Role
libs/agno/agno/context/provider.py ContextProvider, ContextBackend, ContextMode
context/slack/ Live Slack data
context/gdrive/, gmail/ Google workspace
context/wiki/ Wiki sources
context/mcp/ MCP servers as context
cookbook/12_context/ Runnable examples

Agno agents: Agent(context=[SlackProvider(...)])fetch then inject.

PraisonAI today: developer writes custom tool → LLM must choose to call tool → no standard provider lifecycle.


Current PraisonAI Behaviour

Workflow today

sequenceDiagram
    participant Dev as Developer
    participant Agent as Agent
    participant Tool as custom_slack_tool
    participant API as Slack API

    Dev->>Tool: Implement OAuth + fetch (200+ LOC)
    Dev->>Agent: tools=[custom_slack_tool]
    Agent->>Tool: Only if LLM invokes tool
    Tool->>API: HTTP
    API-->>Agent: String in tool result
Loading

Pain points

  1. No standard OAuth refresh / error taxonomy across integrations
  2. No praisonai context doctor for provider health
  3. No YAML context: block (triple parity gap vs Python/CLI)
  4. Enterprise security review treats each custom tool as one-off

What exists that is adjacent (not duplicate)

Module Role Why not a duplicate
context/fast/ Codebase symbol index Local repo only
praisonai_platform/.../workspace_context.py Platform workspace layer Not agent-runtime provider
MCP client Agent calls external tools Pull model; not context injection
RAG/knowledge Indexed documents Batch index, not live Slack

Gap Analysis

Technical reasoning

External providers need:

  1. Fetch protocol — async fetch() -> ContextChunk[]
  2. Auth backend — OAuth token store + refresh
  3. Mode — LIVE vs CACHED (Agno ContextMode)
  4. Composer hook — merge fetched chunks before ContextComposer.compose() budget pass

Adding Slack backend extends the architecture; it does not duplicate ContextComposer.

Architectural reasoning

Per ARCHITECTURE.md Tier 1 protocol-driven design, the correct placement is:

praisonaiagents/providers/context/   # NEW — external fetch backends
praisonaiagents/context/composer.py  # EXISTING — budget assembly (consume provider output)

Naming collision caused triage failure — recommend ExternalContextProvider or LiveContextProvider to disambiguate from ContextComposer.


This Is NOT Scope Creep — AGENTS.md Compliance

AGENTS.md says: “stay lightweight and powerful… reject scope creep… prefer existing capabilities.”

Scope creep (reject) This follow-up (accept)
Re-implement ContextComposer Do not touch composer
Add 400-line new subsystem with no consumer Phase 1: Slack provider only (~150 LOC + tests)
Duplicate MCP client Compose with MCP — provider wraps client
New API when tools suffice Tools require LLM invocation; providers inject before LLM

Precedent: #3522 was accepted as non-creep — minimal Helm chart, new directory, no core rewrite. Same pattern here: new providers/context/slack.py, wire into agent start hook.


User Experience Analysis

Developer UX — before vs after

Task Before After (Phase 1)
Slack summarizer Custom tool + OAuth LiveContextProvider.from_slack(channels=["general"])
CLI None praisonai run --live-context slack:general "summarize"
YAML None live_context: [{type: slack, channels: [general]}]

Learning curve

Agno cookbook 12_context/ — runnable in minutes. PraisonAI — read tools docs + write integration.


Tester Experience

Test Approach
Unit Mock Slack API; provider returns ContextChunk
Contract Provider must not mutate ContextComposer budget logic
Integration VCR recorded Slack fixture
Regression Agent with zero providers — zero overhead (lazy import)

Maintainer Experience

  • Ownership: praisonaiagents/providers/context/ — Tier 1, publishable with agents package
  • Phased: Slack only in v1; GDrive/wiki follow
  • No breaking changes: tools= and existing context/ unchanged

Enterprise Impact

Area Detail
Security Read-only OAuth scopes default
Audit Emit bus event CONTEXT_FETCH with provider id
Compliance Centralized token handling vs N custom tools

Technical Design (Minimal Phase 1)

Component interaction

sequenceDiagram
    participant A as Agent.start
    participant L as LiveContextRegistry
    participant S as SlackLiveProvider
    participant C as ContextComposer
    participant LLM as LLM

    A->>L: resolve configured providers
    L->>S: fetch(live)
    S-->>L: ContextChunk[]
    L->>C: merge as memory segment
    C->>C: budget + trim
    C-->>A: composed messages
    A->>LLM: chat
Loading

Proposed files (Phase 1 only)

File Purpose
praisonaiagents/providers/context/protocols.py LiveContextProviderProtocol
praisonaiagents/providers/context/slack.py Slack backend
praisonaiagents/providers/context/models.py ContextChunk, FetchMode
praisonaiagents/agent/context_mixin.py Hook before compose (lazy)
tests/unit/providers/context/test_slack_provider.py Unit tests

Explicit non-goals for Phase 1: GDrive, wiki, gateway sync, YAML compiler — separate issues.


Before vs After

Aspect Before (#3523 closed) After Phase 1
Slack agent Custom tool Standard provider
Triage status Closed, no code PR with tests
Agno parity Gap Partial (Slack)
ContextComposer Unchanged Consumes provider output

Proposed Solution

Preferred approach

  1. Add LiveContextProviderProtocolnew name avoids composer collision
  2. Implement SlackLiveProvider with env SLACK_BOT_TOKEN or OAuth
  3. Wire at agent start: fetched text → memory segment passed to ContextComposer
  4. CLI: --live-context slack:channel on praisonai run
  5. Tests + cookbook example

Alternatives considered

Option Verdict
Document “use MCP for Slack” Insufficient — LLM must invoke; not injection
Extend ContextComposer Wrong layer — composer doesn't fetch
PraisonAI-Tools repo Agent-callable tools ≠ pre-run context

Technical Implementation Guide (Step-by-Step Code Plan)

This section is the implementation blueprint for a single PR. Follow file-by-file; do not re-implement ContextComposer or touch Agent(context=...).

Step 0 — Naming rule (avoids #3523 triage mistake)

Name Meaning Touch?
Agent(context=True) ContextManager — token budget / compaction NO
ContextComposer Assembles segments in context/composer.py NO (consume only)
Agent(live_context=[...]) NEW — external fetch providers YES

Step 1 — New package layout

Create under Tier 1 (src/praisonai-agents/praisonaiagents/):

providers/
└── live_context/
    ├── __init__.py          # export SlackLiveProvider, LiveContextChunk
    ├── protocols.py         # LiveContextProviderProtocol
    ├── models.py            # LiveContextChunk, FetchMode
    ├── registry.py          # parse "slack:general" CLI strings
    ├── composer_bridge.py   # merge chunks → str for injection
    └── slack.py             # SlackLiveProvider (Phase 1 only)

Why providers/live_context/ not context/slack/: Prevents triage from grep-matching context/ and claiming duplicate.


Step 2 — Protocol + models (protocols.py, models.py)

# providers/live_context/models.py
from dataclasses import dataclass
from enum import Enum
from typing import Any

class FetchMode(str, Enum):
    LIVE = "live"       # always fetch
    CACHED = "cached"   # TTL cache (Phase 2)

@dataclass(frozen=True)
class LiveContextChunk:
    provider: str           # "slack"
    source: str             # "#general"
    content: str            # normalized text for LLM
    metadata: dict[str, Any]

# providers/live_context/protocols.py
from typing import Protocol, runtime_checkable

@runtime_checkable
class LiveContextProviderProtocol(Protocol):
    provider_id: str

    async def fetch(self) -> list[LiveContextChunk]:
        """Fetch live data. Must not call LLM. Read-only HTTP/API."""
        ...

Step 3 — Slack provider (slack.py) — ~120 LOC

# providers/live_context/slack.py
import os
import httpx
from .models import LiveContextChunk, FetchMode

class SlackLiveProvider:
    provider_id = "slack"

    def __init__(
        self,
        channels: list[str],
        *,
        token: str | None = None,
        mode: FetchMode = FetchMode.LIVE,
        max_messages: int = 50,
        timeout_s: float = 5.0,
    ):
        self.channels = channels
        self.token = token or os.environ.get("SLACK_BOT_TOKEN", "")
        self.mode = mode
        self.max_messages = max_messages
        self.timeout_s = timeout_s

    async def fetch(self) -> list[LiveContextChunk]:
        if not self.token:
            raise ValueError("SLACK_BOT_TOKEN required for SlackLiveProvider")

        chunks: list[LiveContextChunk] = []
        async with httpx.AsyncClient(timeout=self.timeout_s) as client:
            for channel in self.channels:
                resp = await client.get(
                    "https://slack.com/api/conversations.history",
                    headers={"Authorization": f"Bearer {self.token}"},
                    params={"channel": channel, "limit": self.max_messages},
                )
                resp.raise_for_status()
                data = resp.json()
                if not data.get("ok"):
                    raise RuntimeError(data.get("error", "slack_api_error"))

                lines = [
                    f"[{m.get('user', '?')}] {m.get('text', '')}"
                    for m in data.get("messages", [])
                ]
                chunks.append(LiveContextChunk(
                    provider="slack",
                    source=channel,
                    content="\n".join(lines) or "(no messages)",
                    metadata={"message_count": len(lines)},
                ))
        return chunks

Phase 1 auth: bot token env only. OAuth refresh → Phase 2 issue.


Step 4 — Bridge into existing context pipeline (composer_bridge.py)

Do not modify ContextComposer.compose() signature. Inject fetched text as a synthetic memory segment before the existing manager runs:

# providers/live_context/composer_bridge.py
from .models import LiveContextChunk

def merge_chunks(chunks: list[LiveContextChunk], *, max_chars: int = 8000) -> str:
    parts = []
    for c in chunks:
        header = f"## Live context from {c.provider}:{c.source}\n"
        parts.append(header + c.content)
    merged = "\n\n".join(parts)
    return merged[:max_chars]

async def fetch_all(providers: list) -> str:
    import asyncio
    if not providers:
        return ""
    results = await asyncio.gather(
        *[p.fetch() for p in providers],
        return_exceptions=True,
    )
    chunks: list[LiveContextChunk] = []
    for r in results:
        if isinstance(r, Exception):
            continue  # log warning; degrade gracefully
        chunks.extend(r)
    return merge_chunks(chunks)

Step 5 — Agent integration (agent/agent.py + new mixin)

Add one new constructor param (lazy, zero overhead when empty):

# agent/agent.py — __init__ signature addition (after context= param)
live_context: Optional[List[Any]] = None,  # LiveContextProvider instances

# agent/live_context_mixin.py — NEW small mixin
class LiveContextMixin:
    _live_context_providers: list = []

    async def _inject_live_context(self, messages: list[dict]) -> list[dict]:
        if not self._live_context_providers:
            return messages
        from ..providers.live_context.composer_bridge import fetch_all
        block = await fetch_all(self._live_context_providers)
        if not block:
            return messages
        # Prepend to system or inject user block — match existing message format
        injected = {
            "role": "user",
            "content": f"<live_context>\n{block}\n</live_context>",
        }
        # Insert after system message if present, else at start
        if messages and messages[0].get("role") == "system":
            return [messages[0], injected, *messages[1:]]
        return [injected, *messages]

Hook point: In execution_mixin.py (or equivalent path that builds messages before first LLM call), call:

messages = await self._inject_live_context(messages)

Search anchor: where context_manager compacts history — run _inject_live_context once per run start, before first model call, not every tool loop iteration.

sequenceDiagram
    participant E as ExecutionMixin
    participant L as LiveContextMixin
    participant S as SlackLiveProvider
    participant CM as ContextManager (existing)
    participant LLM as LLM

    E->>L: _inject_live_context(messages)
    L->>S: fetch()
    S-->>L: LiveContextChunk[]
    L->>L: merge_chunks → injected user block
    E->>CM: existing compact/trim (unchanged)
    E->>LLM: chat(messages)
Loading

Step 6 — CLI wiring (praisonai-code)

# praisonai_code/cli/app.py — add option to run/chat commands
live_context: Optional[list[str]] = typer.Option(
    None, "--live-context", help="Repeatable: slack:general, slack:#support"
),

# praisonai_code/cli/live_context_factory.py — NEW
from praisonaiagents.providers.live_context.registry import build_providers

def build_providers(specs: list[str]):
    providers = []
    for spec in specs:
        kind, _, target = spec.partition(":")
        if kind == "slack":
            from praisonaiagents.providers.live_context.slack import SlackLiveProvider
            providers.append(SlackLiveProvider(channels=[target or "general"]))
        else:
            raise typer.BadParameter(f"Unknown live context provider: {kind}")
    return providers

# When constructing Agent for CLI run:
if live_context:
    agent_kwargs["live_context"] = build_providers(live_context)

Registry parser (registry.py):

def parse_spec(spec: str) -> tuple[str, str]:
    # "slack:general" → ("slack", "general")
    ...

Step 7 — YAML (optional Phase 1.5 — same PR if small)

Extend agents YAML schema validator (wrapper) — new key, not context::

agents:
  - name: support-bot
    instructions: "Summarize Slack activity"
    live_context:
      - type: slack
        channels: [general, support]

Compiler maps to live_context=[SlackLiveProvider(...)] when building Agent.


Step 8 — Tests (exact files)

File Tests
tests/unit/providers/live_context/test_slack_provider.py mock httpx; ok/error/token missing
tests/unit/providers/live_context/test_composer_bridge.py merge truncation
tests/unit/agent/test_live_context_mixin.py inject block; empty providers = no-op
tests/integration/test_agent_slack_live_context.py optional VCR; gated by env

Example unit test skeleton:

@pytest.mark.asyncio
async def test_slack_provider_parses_messages(httpx_mock):
    httpx_mock.get("https://slack.com/api/conversations.history").respond(
        json={"ok": True, "messages": [{"user": "U1", "text": "hello"}]}
    )
    p = SlackLiveProvider(channels=["C1"], token="x")
    chunks = await p.fetch()
    assert "hello" in chunks[0].content

Step 9 — PR checklist (what triage should produce)

  • ~6 new files under providers/live_context/
  • LiveContextMixin + one hook in execution path
  • Agent(live_context=[]) default None — no import overhead (lazy)
  • CLI --live-context slack:general
  • Zero edits to context/composer.py logic
  • Zero changes to Agent(context=...) behavior
  • Cookbook: examples/providers/slack_live_context.py

Suggested Code Changes (summary)

agent = Agent(
    instructions="Summarize today's Slack",
    live_context=[SlackLiveProvider(channels=["general"])],  # NEW param
    context=True,  # UNCHANGED — still ContextManager for token budget
)

Testing Strategy

  • Unit: Slack provider mock, chunk merge, empty provider list
  • Integration: agent start with provider → system/memory contains Slack summary
  • Performance: fetch parallelized; timeout 5s default
  • Security: token not logged

Acceptance Criteria

  • SlackLiveProvider ships in praisonaiagents
  • Agent(live_context=[...]) injects fetched data before first LLM call
  • ContextComposer tests unchanged (no regression)
  • CLI --live-context slack:general works
  • Cookbook example under examples/context/slack_live/
  • Triage can implement in one PR without “scope creep” rejection

For @claude — Please Do Not Close as Scope Creep

Copy this block when filing or triaging:

@claude This follow-up replaces #3523 which was auto-closed WITHOUT implementation.

NOT scope creep because:
1. Existing praisonaiagents/context/ is ContextComposer — token budget assembly of in-process segments (composer.py docstring). It does NOT fetch Slack/GDrive/wiki.
2. No context/slack/, context/gdrive/, or LiveContextProvider exists (grep verified 2026-07-30).
3. Agent(context=...) param name ≠ Agno ContextProvider feature — different semantics.
4. Phase 1 scope is MINIMAL: SlackLiveProvider only (~150 LOC), new package path providers/context/ — same pattern as #3522 Helm (new dir, no core rewrite).
5. #3522 was accepted in the same triage batch; this is the same class of competitive gap.

Implement Phase 1 only. Do NOT re-implement ContextComposer. Do NOT close as duplicate of context/ module.

Acceptance: SlackLiveProvider + Agent(live_context=[...]) + unit tests.

Risks

Risk Mitigation
Re-closed as creep This document + narrow Phase 1
Naming collision Use live_context / LiveContextProvider
OAuth complexity Bot token path first; OAuth Phase 2

Estimated Complexity

Medium — Phase 1 Slack only (not full Agno 10-provider parity).


Priority

High — Enterprise competitive gap; parent incorrectly closed.


References

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingclaudeAuto-trigger Claude analysisdocumentationImprovements or additions to documentationperformancesecurity

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions