Chain-of-Agents: Market Intelligence System

Market intelligence has at least three dimensions on any given day: what the public story says, what the business signals suggest, and what the professional community seems to believe.

The Chain-of-Agents pattern exists because one agent doing everything has a blind spot. A single agent can sound complete while still looking at the problem from only one angle. A team of agents gives you something different. One specialist looks at recent news and public updates. One looks at financial performance and business signals. One looks at public and professional sentiment. Then a manager reads only the findings from that current run and produces one intelligence report.

That last phrase matters: current run. The biggest architecture lesson in this build was not how to make three agents talk. It was learning that an audit journal and a manager’s working notes are not the same thing.


What This Agent Does

The Chain-of-Agents Orchestrator runs a three-agent market intelligence pipeline. You supply a target, usually a company name or topic, and the system assigns three specialist agents to inspect it from different angles: a News Agent, a Financials Agent, and a Sentiment Agent.

Each specialist receives a narrow research role. The News Agent focuses only on recent news and public updates. The Financials Agent focuses only on financial performance and business signals. The Sentiment Agent focuses only on public and professional sentiment. None of them writes the final report. Each produces one focused paragraph of cautious analysis and open signals to investigate.

Once all three specialists finish, the Manager Agent receives those three current findings and synthesizes them into one report with short sections: Executive Summary, Key Signals, and Open Questions. The manager is explicitly told not to turn open questions or unsupported numbers into asserted facts. That instruction matters because market intelligence is full of uncertainty. A good report does not pretend every signal is confirmed.

After the manager writes the report, the full run gets appended to a JSONL journal. That journal records the timestamp, target, specialist findings, and manager report. It is the audit trail. It proves what happened. It is not the manager’s working memory.


Why I Built This

The single-agent pattern has a blind spot. When one agent produces one answer, you have no way of knowing whether a different framing of the same question would have produced a different answer. You are not getting intelligence. You are getting one perspective dressed up as a conclusion.

The multi-agent pattern changes that. Three agents looking at the same target from three angles will surface different signals. Sometimes those signals will point in the same direction. Sometimes they will create tension. That tension is useful. A company can have strong market sentiment and weak financial signals. A vendor can have good public messaging and unresolved operational questions. A founder can look strong in the press and still have unanswered questions around traction.

The first version of this build tried to make that tension explicit with a separate conflict scoring pass. That was interesting, but it made the teaching version heavier than it needed to be. The current repo makes a sharper architectural point: before you build fancy conflict scoring, you need to get the working-memory boundary right.

A manager should synthesize the findings from the current run. The journal should preserve history. Those are separate jobs. Mixing them creates contamination. It is the difference between a meeting agenda and the company archive. You want the archive to keep everything. You do not want today’s manager reading every old meeting note and accidentally blending last month’s target into today’s report.

That is the architecture lesson this build forced into focus.


What Actually Happened

What actually happened: the important issue was not the specialist prompts. It was memory scope. The agent had a journal that accumulated past runs, and the manager originally risked treating old journal entries like current working notes.

What broke: when historical journal records get treated as current context, the report can blend unrelated targets. If yesterday’s run was about one company and today’s run is about another, the manager can accidentally produce a report that mixes both. That is not intelligence. That is contamination.

The fix was structural. The manager now receives only the findings gathered in the current run. The journal still accumulates every run in the background, but it is appended after the report is produced. The report stopped depending on historical memory immediately.

What surprised me: the simpler architecture was stronger. Removing the conflict scoring pass made the core chain easier to understand. Three specialists produce findings. The manager synthesizes those findings. The journal records the run. Clean boundaries beat clever extra layers.

What I would change next: I would add structured conflict detection later, but only after preserving this current-run boundary. If conflict scoring returns, it should use a strict structured format, not a loose paragraph that the code has to scan for a number.

Why this matters: a system that reads its full history on every run will eventually contaminate its own outputs. Separating the audit trail from active working memory is not a cleanup detail. It is a production design principle.


The Architecture: How It Works

The system has four simple layers.

The specialist layer runs three focused agents. Each agent receives the same target but a different lens. News. Financials. Sentiment. Think of this like sending three analysts into the same briefing room, but each analyst is told to pay attention to a different part of the business.

The manager layer receives only the three current findings. It does not read the journal. It does not search old runs. It does not pull yesterday’s report into today’s analysis. Its job is to synthesize what the current specialists found.

The journal layer appends the finished run to a JSONL file. JSONL means each line is one complete JSON record. It is useful for audit history because the file can grow over time without rewriting old entries.

The model layer routes every agent call through a LiteLLM-compatible HTTP endpoint. The Python code does not import the OpenAI SDK. It does not import the LiteLLM Python package. It sends a normal HTTP request to the configured endpoint using the Python standard library.

This is the whole system in one sentence: ask three specialists, give their answers to the manager, write the completed run to the journal.


The Model-Agnostic Layer

The model is controlled through .env, not hardcoded into the agent. The important values are LITELLM_BASE_URL, MODEL_NAME, LITELLM_API_KEY, DEFAULT_TARGET, and MEMORY_LOG_PATH.

That means the chain architecture does not care which supported provider sits behind the LiteLLM-compatible endpoint. The specialist prompts, manager prompt, and journal logic stay the same. The model behind the endpoint can change without rewriting the orchestration code.

This matters because the architecture should not belong to one provider. The chain is the product. The model is the engine plugged into it.


The Build: Step by Step

Step 1: Define the specialist team.

SPECIALISTS = [ ("News Agent", "recent news and public updates"), ("Financials Agent", "financial performance and business signals"), ("Sentiment Agent", "public and professional sentiment"), ]

What This Code Is Actually Doing

This is the analyst roster. Instead of creating three separate files or three complicated classes, the agent stores each specialist as a name and a focus area.

Think of it like writing three assignments on a whiteboard. One analyst gets news. One gets financials. One gets sentiment. The loop later walks down this list and runs each assignment against the same target.

The important part is role clarity. Each agent has a lane. That keeps the findings cleaner because the News Agent is not trying to write the full market report, and the Sentiment Agent is not pretending to be the manager.

Step 2: Call the model through the configured endpoint.

def call_llm(prompt: str, temperature: float = 0.3) -> str:
    payload = json.dumps(
        {
            "model": os.getenv("MODEL_NAME"),
            "messages": [{"role": "user", "content": prompt}],
            "temperature": temperature,
        }
    ).encode("utf-8")

    request = Request(
        os.getenv("LITELLM_BASE_URL").rstrip("/") + "/chat/completions",
        data=payload,
        headers={
            "Authorization": "Bearer " + os.getenv("LITELLM_API_KEY"),
            "Content-Type": "application/json",
        },
    )
    with urlopen(request, timeout=90) as response:
        return json.loads(response.read().decode("utf-8"))["choices"][0]["message"]["content"].strip()

What This Code Is Actually Doing

This function is the phone line to the model. Every specialist and the manager use this same function when they need an AI response.

The payload is the message being sent. It includes the model name, the prompt, and the temperature. Temperature controls how much variation the model uses when writing. A lower number keeps the answer more focused. That matters here because market intelligence should sound like analysis, not creative writing.

The function sends the request to the LiteLLM-compatible endpoint from .env, waits for the response, and returns the text. No provider-specific SDK is imported. The model call is centralized in one place.

Step 3: Run one specialist.

def run_specialist(name: str, focus: str, target: str) -> dict[str, str]:
    prompt = (
        f"You are {name}, one specialist in a chain-of-agents workflow. "
        f"Target: {target}\n"
        f"Focus only on: {focus}.\n"
        "You do not have live browsing or private company data. "
        "Do not invent specific funding rounds, partnerships, metrics, clients, dates, or news. "
        "Use no numbers unless they are included in the target text. "
        "Return one concise paragraph of cautious analysis and open signals to investigate. "
        "Do not write the final report."
    )
    return {"agent": name, "focus": focus, "finding": call_llm(prompt)}

What This Code Is Actually Doing

This function hires one specialist for one task. It tells the model the agent’s name, the target, and the exact focus area.

The strongest part of this prompt is the constraint. The agent is told that it does not have live browsing or private company data. It is also told not to invent specific metrics, clients, dates, partnerships, or news. After the NVIDIA test, I also tightened the prompt so it avoids numbers unless they are included in the target text. That does not guarantee perfect behavior, but it gives the specialist a narrow, honest job.

This is like telling an analyst: “Give me your cautious read, and tell me what signals I should investigate. Do not pretend you already verified what you have not verified.”

That tone is important. This agent is not a financial data source. It is a market intelligence summarizer that produces structured starting points for human review.

Step 4: Run the manager.

def run_manager(target: str, findings: list[dict[str, str]]) -> str:
    findings_text = "\n\n".join(f"{item['agent']} ({item['focus']}):\n{item['finding']}" for item in findings)
    prompt = (
        "You are the manager agent in a chain-of-agents workflow. "
        "Synthesize the specialist findings into one clear intelligence report.\n\n"
        f"Target: {target}\n\n"
        f"Specialist findings:\n{findings_text}\n\n"
        "Use short sections: Executive Summary, Key Signals, Open Questions. "
        "Do not turn open questions or unsupported numbers into asserted facts."
    )
    return call_llm(prompt, temperature=0.4)

What This Code Is Actually Doing

This is the manager’s desk. The function takes the three specialist findings from the current run and formats them into one briefing packet.

The manager receives the target, the specialist findings, and the instruction to produce three sections: Executive Summary, Key Signals, and Open Questions. That last section matters. Open questions are not failures. They are the places where a human analyst should look next.

The key detail is what the function does not receive. It does not receive the historical journal. It does not receive old targets. It does not receive every past finding ever written. It only receives the current run’s findings.

That boundary keeps the report clean.

Step 5: Write the journal after the report.

def output_path() -> Path:
    path = Path(os.getenv("MEMORY_LOG_PATH", "outputs/memory_log.jsonl"))
    return path if path.is_absolute() else PROJECT_DIR / path


def write_journal(target: str, findings: list[dict[str, str]], report: str) -> Path:
    path = output_path()
    path.parent.mkdir(parents=True, exist_ok=True)
    entry = {"timestamp": datetime.now(timezone.utc).isoformat(), "target": target, "findings": findings, "report": report}
    with path.open("a", encoding="utf-8") as file:
        file.write(json.dumps(entry) + "\n")
    return path

What This Code Is Actually Doing

This function writes the completed run to the audit journal. It records when the run happened, what target was analyzed, what the specialists found, and what the manager reported.

Think of this as putting the finished briefing into the company archive. The archive is valuable. It lets you go back later and see what happened. But it is not the same thing as the manager’s working notes.

That is the architectural distinction. The journal is for audit history. The current findings are for synthesis.

Step 6: Run the full chain.

def run_chain(target: str) -> None:
    print(f"[TARGET] {target}\n")
    findings = []
    for name, focus in SPECIALISTS:
        print(f"[RUNNING] {name}")
        finding = run_specialist(name, focus, target)
        findings.append(finding)
        print(finding["finding"] + "\n")

    print("=" * 60)
    print("FINAL MANAGER REPORT")
    print("=" * 60)
    report = run_manager(target, findings)
    print(report)
    path = write_journal(target, findings, report)
    print(f"\n[JOURNAL] Appended audit record to {path}")

What This Code Is Actually Doing

This is the whole operation. It starts with an empty findings list. Then it runs each specialist, adds that specialist’s finding to the list, and prints the finding so the operator can see the work as it happens.

After all three specialists finish, the manager receives the findings list and writes the final report. Only after that report exists does the agent append the run to the journal.

The order matters. Current findings first. Manager report second. Journal append third. That order is what prevents old history from contaminating the current report.

Why the Journal Decision Matters

The word “memory” sounds powerful in AI systems, but memory is dangerous if the system does not know what kind of memory it is using.

A journal is long-term memory. It is the permanent record. You want it to keep everything.

Working memory is short-term memory. It is what the manager needs right now to complete this run. You want it to be small, current, and relevant.

If those two get mixed together, the agent starts dragging old context into new work. That is how a report about one company can accidentally inherit claims from a previous run about another company.

In human terms, it is like asking a consultant to brief you on today’s client while forcing them to read every note from every previous client first. More information does not always mean better judgment. Sometimes it just means polluted context.


Errors I Hit During This Build

Memory log contamination across runs. After testing with one target and then switching to another, the report risk was clear: if the manager reads the accumulated journal, unrelated targets can blend together. The fix was to stop using the journal as manager input. The manager now works from the current run’s findings list only, and the journal remains as an audit trail.

Overbuilt conflict detection. The older version included an LLM-based contradiction score and regex parsing. That was interesting, but it created another fragile layer before the core chain pattern was fully clean. The simplified repo removed that step so the main lesson is easier to see: specialists produce current findings, the manager synthesizes current findings, and the journal records the run afterward.

Dependency weight. The previous direction used heavier client and retry layers than the teaching version needed. The current build removes third-party Python dependencies entirely and uses the standard library to call the LiteLLM-compatible endpoint directly.

Unsupported numbers in the NVIDIA test. The agent ran successfully, but one run produced numeric-style financial claims that were not provided by the target text. That was too confident for a no-browsing demo. The fix was to tighten the specialist prompt: no numbers unless they are included in the target text. The manager prompt was also updated so unsupported numbers do not become asserted facts.

Windows console encoding. One NVIDIA run failed because the model returned a non-ASCII hyphen and the Windows console could not print it with the default encoding. The fix was to configure stdout and stderr for UTF-8 at startup.


What This Means

Every organization running AI agents for research, competitive intelligence, vendor review, or market monitoring eventually needs to answer the same question: how do you know whether the AI is giving you a coherent picture or simply polishing one incomplete angle?

The Chain-of-Agents pattern answers that structurally. You do not rely on one agent to be complete. You run multiple specialists with different mandates, then have a manager synthesize the current findings into a report that keeps open questions visible.

The simplified repo does not claim to verify live market data. It does not browse privately. It does not produce guaranteed financial truth. It produces a structured intelligence starting point from multiple perspectives, with the right boundary between current working context and audit history.

For a founder making a competitive decision, a compliance team reviewing a vendor, or an analyst briefing a board, that boundary matters. A polished single answer can hide uncertainty. A multi-perspective report with open questions gives the human decision-maker a better place to start.


Where This Gets Interesting

These are hypothetical scenarios, patterns I can see this architecture fitting based on how the agent works. Nothing here is a deployment guarantee or a promise about what AI can deliver in a specific environment.

Law firms doing vendor due diligence. Before signing a major contract, a firm runs a vendor through the pipeline. One specialist looks at public updates. One looks at business signals. One looks at professional sentiment. The manager produces a briefing and keeps open questions visible. The lawyer still makes the call. The agent makes sure the first pass was not one-dimensional.

Logistics companies monitoring suppliers. A company with 50 active suppliers cannot manually track every public signal every week. This pipeline could help create first-pass supplier briefs, then flag which ones deserve human review. The agent handles the volume. The operator handles the judgment.

Founders doing competitive research. Before a pitch, pricing decision, or product positioning call, a founder runs competitors through the chain. Not because the report is the final answer, but because it gives three angles quickly. That can reveal which assumptions need to be checked before walking into the room.

HR teams doing executive background research. A company considering a senior hire could use the pattern to separate public updates, business track record signals, and professional sentiment into different first-pass views. The hiring manager still decides. The agent reduces the chance that one data point dominates the whole picture.

Investment clubs doing initial screening. Before spending hours on deeper research, a group can use the chain as a first filter. If the findings are interesting but full of open questions, that is not a problem. It tells the group where to focus the next hour of human analysis.


Tools Used


(c) 2026 NosisTech LLC. Licensed under CC BY 4.0. Use freely, just credit us.

WEEKLY BUILD NOTES

One documented agent build in your inbox, every week

Real systems, real code, the errors left in. No spam, unsubscribe anytime.