
Building Your Team's First Shared Agent: A Practical Guide to Collaborative AI Workflows
A hands-on guide to architecting, building, and deploying a shared AI agent for your engineering team — from tool design to guardrails, observability, and workflow integration.
Every engineering team eventually hits the same wall: the AI assistant that works for you is locked in your IDE, the bot that helps the design team can't touch your repo, and the agent you hacked together last sprint is a black box that only you understand. The solution is to stop building AI tools for individuals and start building one shared agent for the whole team — a collaborative AI workflow that everyone can use, extend, and trust. This guide walks you through the architecture, code, and operational practices needed to ship your team's first shared agent, from defining its scope to deploying it as a production-grade service.
For more on agentic systems and workflow design, check out Tamiz's Insights.
- 1. What a Shared Agent Actually Is
- 2. Prerequisites
- 3. Step 1: Define Scope and Goals
- 4. Step 2: Choose the Model and Infrastructure
- 5. Step 3: Design the Tool Interface
- 6. Step 4: Build the Agent Loop
- 7. Step 5: Add Guardrails and Permissions
- 8. Step 6: Deploy as a Shared Service
- 9. Step 7: Integrate with Team Workflows
- 10. Step 8: Observability and Evaluation
- 11. Production Best Practices
- 12. Frequently Asked Questions
1. What a Shared Agent Actually Is
A shared agent is not just a chatbot in a shared Slack channel. It's a stateful, tool-calling AI service that any member of your team can invoke, with a consistent interface, shared memory, and centralized governance. Unlike a personal assistant that lives in your editor, a shared agent:
- Runs as a service (HTTP API, bot, or CLI) that the whole team can reach.
- Has a defined set of tools (functions) that map to real systems: your issue tracker, your CI, your code search, your docs.
- Enforces permissions and audit logging so you know who did what.
- Uses a shared context/memory so the team builds on the same knowledge.
Think of it as the difference between a personal laptop and a shared Kubernetes cluster. The cluster is harder to set up, but it's where real work happens.
2. Prerequisites
Before you start, you'll need:
- Node.js 18+ (or Python 3.10+) — we'll use TypeScript in this guide.
- An LLM API key — we'll assume an OpenAI-compatible endpoint (e.g. OpenAI, Anthropic via a gateway, or a local model via Ollama). The code uses the
openainpm package. - A tool registry — a place to define the functions your agent can call. We'll use a simple in-code registry, but in production you'd use MCP (Model Context Protocol) or a similar standard.
- A target system — for the walkthrough, we'll use a mock issue tracker and a mock CI status API.
3. Step 1: Define Scope and Goals
The most common mistake is building a "do everything" agent. Instead, define a narrow, high-value scope. For a first shared agent, pick one workflow that your team does repeatedly and that benefits from automation. Good candidates:
- On-call triage: Summarize alerts, correlate with recent deploys, suggest a rollback.
- Code review assistance: Fetch a PR, analyze diff, suggest tests.
- Incident postmortems: Draft a timeline from logs and chat history.
- Docs maintenance: Find stale docs and update them from code changes.
Write a one-paragraph scope statement. Example:
"Our shared agent,
team-bot, helps engineers triage CI failures. Given a failing pipeline run, it fetches the logs, identifies the failing test, checks recent commits for likely culprits, and posts a summary to the PR thread with a suggested fix."
This scope tells you exactly what tools you need (CI API, Git history, PR comments) and what you don't (no need to build a general-purpose web search tool yet).
4. Step 2: Choose the Model and Infrastructure
Model choice
For a first shared agent, you want a model that is good at tool calling and structured output. The model doesn't need to be the most powerful — it needs to be reliable and fast. A good default is a mid-tier model like gpt-4o-mini or claude-3-5-haiku. If you're running on a local GPU, llama3.1:8b via Ollama works well for internal tools.
Infrastructure
You have two main options:
| Option | Pros | Cons |
|---|---|---|
| Serverless (Vercel, Lambda) | Scales to zero, cheap for low volume, easy deploy | Cold starts, execution time limits (good for short agent runs) |
| Long-running service (Fly.io, ECS, K8s) | Supports long-running sessions, streaming, websockets | Always-on cost, more ops |
For a team agent that handles requests that may take 10–60 seconds, a long-running service is simpler. We'll use a simple Node.js + Express server in this guide.
5. Step 3: Design the Tool Interface
The agent's power comes from its tools. A tool is a function with a name, a description, and a JSON schema for its arguments. The LLM decides which tool to call based on the description and the user's request.
We'll define tools in TypeScript using a typed interface:
// tools/types.ts
export interface Tool {
name: string;
description: string;
parameters: {
type: "object";
properties: Record<string, unknown>;
required?: string[];
};
execute: (args: any) => Promise<string>;
}
Here's a concrete example — a tool that fetches CI logs:
// tools/ci.ts
import { Tool } from "./types";
export const getCILogs: Tool = {
name: "get_ci_logs",
description: "Fetch the logs for a given CI pipeline run.",
parameters: {
type: "object",
properties: {
runId: { type: "string", description: "The CI run ID" },
},
required: ["runId"],
},
async execute({ runId }) {
const res = await fetch(`https://ci.internal/api/runs/${runId}/logs`);
const logs = await res.text();
return logs.slice(0, 8000); // truncate to avoid context overflow
},
};
Notice we truncate the logs. Context window management is your #1 tool design concern. Always cap the size of data you return to the model.
Use MCP for a standard registry
In production, you should consider using the Model Context Protocol (MCP) to expose your tools. MCP is an open standard that lets any MCP-compatible client (Claude Desktop, Cursor, custom agents) discover and call your tools. If you build your tools as MCP servers, you can reuse them across many agents. For this guide, we'll keep it simple with an in-code registry, but the pattern is the same.
6. Step 4: Build the Agent Loop
The agent loop is the heart of the system. It's a simple cycle:
- Send the conversation history + tool definitions to the LLM.
- The LLM responds either with a final message or a tool call.
- If a tool call, execute it, append the result to the history, and loop back to step 1.
- If a final message, return it to the user.
Here's a complete TypeScript implementation using the openai SDK:
// agent.ts
import OpenAI from "openai";
import { Tool } from "./tools/types";
import { getCILogs } from "./tools/ci";
import { getRecentCommits } from "./tools/git";
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const tools: Tool[] = [getCILogs, getRecentCommits];
// Convert our Tool interface to OpenAI's function calling format
const openaiTools = tools.map((tool) => ({
type: "function" as const,
function: {
name: tool.name,
description: tool.description,
parameters: tool.parameters,
},
}));
const toolMap = new Map(tools.map((t) => [t.name, t]));
export async function runAgent(userMessage: string, history: any[] = []) {
const messages = [
{ role: "system", content: SYSTEM_PROMPT },
...history,
{ role: "user", content: userMessage },
];
// Loop with a max iterations guard to prevent infinite loops
for (let i = 0; i < 10; i++) {
const completion = await openai.chat.completions.create({
model: "gpt-4o-mini",
messages,
tools: openaiTools,
});
const message = completion.choices[0].message;
// If the model wants to call a tool
if (message.tool_calls && message.tool_calls.length > 0) {
messages.push(message as any);
for (const toolCall of message.tool_calls) {
const tool = toolMap.get(toolCall.function.name);
if (!tool) {
throw new Error(`Unknown tool: ${toolCall.function.name}`);
}
const args = JSON.parse(toolCall.function.arguments);
const result = await tool.execute(args);
messages.push({
role: "tool",
tool_call_id: toolCall.id,
content: result,
});
}
continue; // Loop back to ask the model again
}
// No tool calls — this is the final answer
return message.content;
}
throw new Error("Agent exceeded max iterations");
}
Key points:
- The
SYSTEM_PROMPTdefines the agent's persona and rules (we'll cover it in Step 5). - The loop is capped at 10 iterations to prevent runaway tool calls.
- We push the assistant message with tool calls, then push the tool results, then re-ask the model. This is the standard pattern.
7. Step 5: Add Guardrails and Permissions
A shared agent is a security boundary. You need to control what it can do and who can ask it to do it.
System prompt guardrails
The system prompt is your first line of defense. Write explicit rules:
const SYSTEM_PROMPT = `
You are team-bot, a collaborative AI agent for the platform engineering team.
Your job is to help triage CI failures and draft postmortems.
Rules:
- Only use the tools provided. Never invent data.
- If a tool call fails, explain the error to the user, don't retry more than once.
- Never expose secrets, API keys, or internal infrastructure details.
- If the user asks for something outside your scope, politely decline and suggest a human.
- Be concise. Use bullet points for summaries.
`;
Permission middleware
At the service layer, you need to authenticate users and check their permissions before they can invoke the agent. Here's a simple Express middleware:
// server.ts
import express from "express";
import { runAgent } from "./agent";
const app = express();
app.use(express.json());
// Simple auth middleware — in production, use OAuth/JWT
function requireTeamMember(req, res, next) {
const apiKey = req.headers["x-api-key"];
if (!apiKey || apiKey !== process.env.TEAM_API_KEY) {
return res.status(401).json({ error: "Unauthorized" });
}
next();
}
app.post("/api/agent", requireTeamMember, async (req, res) => {
const { message, history } = req.body;
try {
const result = await runAgent(message, history);
res.json({ result });
} catch (err) {
res.status(500).json({ error: "Agent failed", detail: (err as Error).message });
}
});
app.listen(3000, () => console.log("team-bot listening on :3000"));
Tool-level authorization
Some tools are more sensitive than others. For example, a create_rollback tool should require an admin role. You can extend the Tool interface with a requiredRole field and check it in the execute wrapper:
async function executeWithAuth(tool: Tool, args: any, user: User) {
if (tool.requiredRole && user.role !== tool.requiredRole) {
throw new Error(`User ${user.id} lacks role ${tool.requiredRole}`);
}
return tool.execute(args);
}
8. Step 6: Deploy as a Shared Service
Now we have a working agent. Let's deploy it. We'll use a simple Dockerfile and a docker-compose.yml for local dev, then discuss production options.
# Dockerfile
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
# docker-compose.yml
services:
agent:
build: .
ports:
- "3000:3000"
environment:
- OPENAI_API_KEY=${OPENAI_API_KEY}
- TEAM_API_KEY=${TEAM_API_KEY}
For production, you can deploy this to Fly.io, Railway, or a Kubernetes Deployment. The key is to make it a stateless service — the agent should not store conversation state in memory. Use Redis or a database for session history if you need persistence.
9. Step 7: Integrate with Team Workflows
A shared agent is only useful if it's where your team already works. The most common integrations are Slack and GitHub.
Slack integration
Here's a minimal Slack bot that listens to a mention and calls the agent:
// slack.ts
import { WebClient } from "@slack/web-api";
import { runAgent } from "./agent";
const token = process.env.SLACK_BOT_TOKEN;
const web = new WebClient(token);
export async function handleSlackEvent(event: any) {
if (event.type === "app_mention") {
const text = event.text.replace(/<@[^>]+>/g, "").trim(); // strip mention
const result = await runAgent(text);
await web.chat.postMessage({
channel: event.channel,
text: result,
});
}
}
GitHub Actions integration
You can also trigger the agent from a GitHub Action when a CI run fails:
# .github/workflows/agent-triage.yml
name: Triage CI Failure
on:
workflow_run:
workflows: ["CI"]
types: [completed]
branches: [main]
jobs:
triage:
if: ${{ github.event.workflow_run.conclusion == 'failure' }}
runs-on: ubuntu-latest
steps:
- run: |
curl -X POST $AGENT_URL/api/agent \
-H "x-api-key: $TEAM_API_KEY" \
-H "Content-Type: application/json" \
-d '{"message": "CI failed on main. Please triage the failure and post a summary to the PR."}'
10. Step 8: Observability and Evaluation
You can't manage what you can't measure. A shared agent needs three layers of observability:
1. Structured logs
Log every agent run with a correlation ID:
// logging.ts
export function logAgentRun(runId: string, event: string, data: any) {
console.log(JSON.stringify({ runId, event, timestamp: Date.now(), ...data }));
}
2. Tracing
Use OpenTelemetry to trace the agent loop. Each tool call should be a span:
import { trace } from "@opentelemetry/api";
const tracer = trace.getTracer("team-bot");
async function executeWithTracing(tool: Tool, args: any) {
return tracer.startActiveSpan(`tool:${tool.name}`, async (span) => {
span.setAttribute("args", JSON.stringify(args));
const result = await tool.execute(args);
span.setAttribute("result_length", result.length);
span.end();
return result;
});
}
3. Evaluation harness
Before you let the agent loose, build a small eval suite. Use a set of 10–20 representative prompts and check the outputs against expected behaviors. You can automate this with a script:
// eval.ts
const cases = [
{ input: "Why did build #1234 fail?", expectedIncludes: "test_foo" },
{ input: "Who might have caused this?", expectedIncludes: "alice" },
];
for (const c of cases) {
const result = await runAgent(c.input);
const pass = result.includes(c.expectedIncludes);
console.log(`${pass ? "PASS" : "FAIL"}: ${c.input}`);
}
11. Production Best Practices
- Rate limiting: A shared agent can burn through your API budget. Add rate limiting per user (e.g. 10 requests/hour).
- Cost controls: Set a max token budget per run. Use the
max_tokensparameter and truncate tool outputs aggressively. - Prompt injection resistance: The agent's tools may return content from external systems (e.g. logs, PR comments). Treat that content as untrusted. Never let it override your system prompt. Use a "tool output is data, not instructions" rule in your system prompt.
- Human-in-the-loop: For destructive actions (e.g. rollback, deploy), require a human confirmation step. The agent should propose the action, then wait for a user to approve it.
- Version the agent: Keep your system prompt and tool definitions in version control. Tag releases so you can roll back if a new prompt degrades performance.
For more on building robust AI systems, visit tamiz.pro.
12. Frequently Asked Questions
Q: What's the best model for a shared agent?
A: Start with a mid-tier model that supports tool calling well, like gpt-4o-mini or claude-3-5-haiku. It should be fast and cheap enough for team-wide use. You can always upgrade to a frontier model for complex tasks later.
Q: How do I prevent the agent from making unauthorized changes?
A: Three layers: (1) system prompt rules, (2) tool-level role checks, and (3) human-in-the-loop approval for destructive actions. Never let the agent directly call a production API without passing through all three.
Q: How do I share context across the team?
A: Use a shared memory store (e.g. Redis) keyed by a team namespace. Store conversation summaries, project facts, and tool results. But be careful: shared context can leak information between users. Always tag context with access control.