SHADOW AI

Until Chapter 10, ACAI could:
Understand
↓
Plan
↓
Retrieve
↓
Remember
↓
Select a model
↓
Generate
↓
Verify
↓
Evaluate
But an intelligent system often needs to do something outside the model.
For example:
Read a file
Search a database
Call an API
Calculate something
Create a document
Process an image
Run an approved computation
This chapter introduces controlled tool execution.
The core architecture becomes:
USER
↓
PLANNER
↓
TOOL SELECTION
↓
ARGUMENT VALIDATION
↓
PERMISSION CHECK
↓
TOOL EXECUTION
↓
OBSERVATION
↓
REASONING
↓
NEXT ACTION / FINAL ANSWER
A tool is a controlled function that ACAI can invoke.
Conceptually:
def calculator(
expression: str
):
...
The model does not directly execute arbitrary code.
Instead:
Model
↓
Request:
calculator("25 * 4")
↓
Tool Registry
↓
Calculator
↓
40?
↓
Observation
The tool produces the result.
A language model is primarily a reasoning/generation component.
External tools can provide:
Current information
Precise calculations
Structured data
File access
Database access
Specialized processing
External services
Therefore:
Model + Tools
can support a wider range of tasks than:
Model alone
But tool access introduces additional security and reliability concerns.
Create:
app/tools/base.py
from abc import ABC, abstractmethod
class Tool(ABC):
name: str
description: str
@abstractmethod
async def execute(
self,
arguments: dict,
):
raise NotImplementedError
Every tool follows the same basic interface.
A tool should describe itself.
Example:
class CalculatorTool(Tool):
name = "calculator"
description = (
"Performs approved "
"mathematical calculations."
)
The description helps the planner understand when the tool is appropriate.
Create:
app/tools/registry.py
class ToolRegistry:
def __init__(self):
self.tools = {}
def register(
self,
tool,
):
self.tools[
tool.name
] = tool
def get(
self,
name: str,
):
return self.tools.get(name)
def list_tools(self):
return list(
self.tools.values()
)
The registry becomes the controlled entry point for available tools.
A calculator is a good first tool because its behavior can be tightly constrained.
Example:
class CalculatorTool(Tool):
name = "calculator"
description = (
"Performs approved calculations."
)
async def execute(
self,
arguments: dict,
):
expression = arguments.get(
"expression"
)
if not expression:
raise ValueError(
"Expression is required."
)
# Use a dedicated safe
# expression parser here.
# Do not evaluate arbitrary
# Python code supplied by users.
return {
"expression": expression,
"status": "accepted",
}
The important security rule is:
Never use unrestricted
eval()on untrusted model output.
A real calculator should use a restricted parser or a dedicated mathematical library.
A tool request can look like:
{
"tool": "calculator",
"arguments": {
"expression": "25 * 4"
}
}
The tool system should validate this before execution.
Example:
from pydantic import BaseModel, Field
class CalculatorArguments(BaseModel):
expression: str = Field(
min_length=1,
max_length=1000,
)
Then:
validated = (
CalculatorArguments.model_validate(
arguments
)
)
Only validated data should reach the execution layer.
The complete flow becomes:
MODEL
│
▼
Tool Request
│
▼
Tool Registry
│
▼
Tool Exists?
┌─────┴─────┐
▼ ▼
YES NO
│ │
▼ ▼
Validate Args Reject
│
▼
Permission Check
│
▼
Execute Tool
│
▼
Result
│
▼
Observation
│
▼
Model
Not every tool should be available to every user or workflow.
Define permissions:
tool.read
tool.calculate
tool.search
tool.write
tool.external_api
Then:
User
↓
Permission Check
↓
Tool
Tools can also have risk classifications.
Example:
LOW
calculator
MEDIUM
read-only database query
HIGH
external write operation
CRITICAL
irreversible external action
The higher the risk, the stronger the approval and validation requirements should be.
This distinction is important.
Search
Read file
Read database
Fetch public information
Create record
Modify record
Send message
Delete resource
Publish content
Write operations should generally require stronger authorization than read operations.
For sensitive actions:
Model
↓
Tool Proposal
↓
Human Approval
↓
Execution
For example:
ACAI:
"I am ready to submit this action.
Approve?"
Only after approval:
Execute
This is preferable to giving an autonomous system unrestricted authority over consequential actions.
Tool results should be structured.
Example:
{
"success": true,
"tool": "calculator",
"result": "100"
}
On failure:
{
"success": false,
"tool": "calculator",
"error": {
"code": "TOOL_EXECUTION_ERROR",
"message": "Calculation failed."
}
}
Every external tool should have a timeout.
Example:
import asyncio
result = await asyncio.wait_for(
tool.execute(arguments),
timeout=30,
)
A tool that hangs indefinitely should not block the entire agent.
Retries should be selective.
Potentially retryable:
Temporary network failure
Temporary provider error
Timeout
Usually not automatically retryable:
Invalid arguments
Permission denied
Invalid resource
Permanent validation failure
A retry policy should therefore depend on error type.
Every tool invocation should be logged.
Record:
request_id
user/workflow identifier
tool
arguments
timestamp
result status
latency
error
approval status
Sensitive arguments should be redacted where necessary.
TOOL_EXECUTION
Request:
abc123
Tool:
calculator
Status:
success
Latency:
measured
Arguments:
redacted/validated
Timestamp:
recorded
This makes the system auditable.
An agent is essentially a controlled loop around:
Reason
↓
Act
↓
Observe
↓
Reason again
Architecture:
┌───────────────┐
│ MODEL │
└───────┬───────┘
│
▼
DECISION
│
┌──────┴──────┐
▼ ▼
TOOL FINAL
│ │
▼ ▼
EXECUTION ANSWER
│
▼
OBSERVATION
│
└───────────────► MODEL
The agent needs state.
Example:
class AgentState:
def __init__(self):
self.messages = []
self.tool_results = []
self.steps = 0
self.finished = False
self.final_answer = None
An agent loop must have a hard limit.
MAX_AGENT_STEPS = 10
Otherwise a bug could cause:
Tool
↓
Model
↓
Tool
↓
Model
↓
Tool
↓
...
forever.
A simplified controller:
class Agent:
def __init__(
self,
model,
tool_registry,
max_steps=10,
):
self.model = model
self.tools = tool_registry
self.max_steps = max_steps
async def run(
self,
request,
):
state = AgentState()
while (
state.steps
< self.max_steps
):
state.steps += 1
decision = (
await self.model.decide(
request,
state,
)
)
if decision["type"] == "final":
state.final_answer = (
decision["answer"]
)
state.finished = True
break
if decision["type"] == "tool":
result = await self.execute_tool(
decision,
state,
)
state.tool_results.append(
result
)
return state
This is an architectural skeleton, not a complete production agent.
The most important part is:
Model
↓
Proposal
↓
Validator
↓
Permission
↓
Executor
not:
Model
↓
Arbitrary system access
Some tools may need stronger isolation.
For example, if ACAI eventually supports user-supplied code execution, the code should not run directly inside the main application process.
Instead:
ACAI
↓
Sandbox
↓
Restricted Environment
↓
Execution
↓
Result
Possible controls include:
CPU limit
Memory limit
Time limit
Network restrictions
Filesystem restrictions
Process restrictions
Without isolation:
AI
↓
Code Execution
↓
Application Server
↓
Potentially sensitive resources
A safer design is:
AI
↓
Validated Request
↓
Sandbox
↓
Limited Resources
This is particularly important for any tool capable of executing code or interacting with system resources.
An API tool can use the same architecture:
Tool Interface
↓
Argument Validation
↓
Permission Check
↓
API Request
↓
Timeout
↓
Response Validation
↓
Observation
The model should not directly control arbitrary URLs, headers, or credentials.
For tools that access external services, consider:
Allowed domains
Allowed HTTP methods
Allowed endpoints
Allowed parameters
For example:
api.example.com
may be allowed while arbitrary domains are rejected.
This reduces the risk of unintended network access.
The model does not necessarily need every tool definition at all times.
A large system can use:
Tool Catalog
↓
Relevant Tool Search
↓
Small Tool Set
↓
Model
This reduces context size and makes tool selection easier.
A registry can categorize tools:
math
search
files
database
media
communication
productivity
developer
Example:
TOOLS_BY_CATEGORY = {
"math": [
"calculator",
],
"search": [
"search_tool",
],
"files": [
"read_file",
],
}
The planner can output:
{
"action": "use_tool",
"tool": "calculator",
"reason": "Precise arithmetic is required."
}
The execution system then validates the requested tool.
The reason field should be treated as explanatory metadata, not as a permission mechanism.
ACAI now separates:
Planner
↓
What should happen?
Agent Controller
↓
What action should happen next?
Tool Executor
↓
How is that action executed?
Architecture:
USER
↓
PLANNER
↓
PLAN
↓
AGENT CONTROLLER
↓
TOOL SELECTION
↓
VALIDATION
↓
EXECUTION
↓
OBSERVATION
↓
AGENT CONTROLLER
↓
FINAL ANSWER
Suppose:
Tool A
↓
Timeout
The agent should not necessarily terminate.
It may:
Retry
↓
Try fallback tool
↓
Modify request
↓
Ask user
↓
Return controlled failure
The correct response depends on the task and error.
Example:
Search Tool
↓
Timeout
↓
Retry once
↓
Still fails
↓
Alternative provider/tool
↓
Success
Maximum retries must remain bounded.
Some decisions cannot safely be inferred.
Example:
User:
"Send this to my team."
If multiple teams or recipients are possible:
ACAI:
"Which team should receive it?"
The agent should not invent a recipient.
When tool arguments are ambiguous:
Tool:
send_message
Required:
recipient
message
User:
"Send it."
The system should not guess the recipient.
Instead:
Missing required information
↓
Ask user
↓
Receive clarification
↓
Validate
↓
Execute
The architecture should enforce:
Model proposes
Tool system validates
Permission system authorizes
Executor performs
Audit system records
This separation is one of the most important design principles in agentic systems.
Each tool needs unit tests.
Example:
@pytest.mark.asyncio
async def test_calculator_validation():
tool = CalculatorTool()
result = await tool.execute(
{
"expression": "25 * 4"
}
)
assert result is not None
Additional tests:
Valid arguments
Missing arguments
Invalid arguments
Oversized arguments
Permission denied
Timeout
External failure
Malformed response
Test:
Model chooses final answer
Model chooses tool
Tool succeeds
Tool fails
Tool times out
Maximum steps reached
Invalid tool selected
Permission denied
The agent should never be allowed to exceed the configured step limit.
Suppose the user asks:
"What is 25 × 48?"
The flow:
User
↓
Planner
↓
Recognizes arithmetic
↓
Calculator Tool
↓
Argument validation
↓
Permission check
↓
Calculator
↓
Observation: 1200
↓
Verification
↓
Final Answer
The model does not need to invent a calculation when a deterministic calculator tool is available.
Suppose:
"Find information from an approved source and summarize it."
Potential flow:
User
↓
Planner
↓
Search Tool
↓
Search Result
↓
Observation
↓
Relevant Content
↓
Summarization
↓
Verification
↓
Final Answer
The exact search and source rules should be explicitly configured.
For a more complex task:
User
↓
Planner
↓
Step 1: retrieve data
↓
Step 2: analyze data
↓
Step 3: calculate result
↓
Step 4: verify
↓
Final answer
The workflow engine from earlier chapters can persist these steps.
Therefore:
Workflow Engine
+
Agent Controller
+
Tool Registry
becomes a powerful combination.
USER
│
▼
PLANNER
│
▼
WORKFLOW
│
▼
AGENT CONTROLLER
│
▼
TOOL SELECTION
│
▼
ARGUMENT VALIDATOR
│
▼
PERMISSION CHECK
│
┌────────────┼────────────┐
▼ ▼ ▼
Calculator Search Database
│ │ │
└────────────┼────────────┘
▼
RESULT
│
▼
OBSERVATION
│
▼
AGENT
│
┌─────────┴─────────┐
▼ ▼
Next Tool Final
│ │
└───────► ▼
VERIFY
│
▼
RESULT
Every action should be observable:
Request
↓
Plan
↓
Tool Proposal
↓
Permission
↓
Execution
↓
Result
↓
Verification
These events can be connected using the same:
request_id
workflow_id
introduced in earlier chapters.
As ACAI grows:
10 tools
↓
50 tools
↓
100 tools
↓
500 tools
the registry needs stronger organization.
Eventually:
Tool Catalog
↓
Semantic Tool Search
↓
Permission Filtering
↓
Capability Matching
↓
Final Tool Set
This prevents the model from receiving unnecessary tool definitions.
Instead of asking:
"Which exact tool name should I use?"
the planner can identify:
Capability:
calculate
Then the registry finds:
calculator_v1
or:
calculator_v2
This creates another layer of abstraction:
Task
↓
Capability
↓
Tool
Tools can evolve.
Example:
calculator:v1
calculator:v2
A workflow can record the exact version used.
This helps reproducibility.
Before allowing a tool into production:
[ ] Input validation
[ ] Output validation
[ ] Permission policy
[ ] Timeout
[ ] Retry policy
[ ] Resource limits
[ ] Audit logging
[ ] Error handling
[ ] Sensitive-data handling
[ ] Tests
[ ] Documentation
Chapter 11 is complete when:
[✓] Tool interface exists
[✓] Tool registry exists
[✓] Tool metadata exists
[✓] Argument validation exists
[✓] Permission system is defined
[✓] Risk classification exists
[✓] Tool timeout exists
[✓] Retry policy exists
[✓] Tool audit logging exists
[✓] Agent loop exists
[✓] Maximum agent steps exist
[✓] Failure recovery exists
[✓] Human approval path exists
[✓] Sandboxing architecture is defined
[✓] Tool tests are defined
[✓] Agent tests are defined
USER
│
▼
API GATEWAY
│
▼
ORCHESTRATOR
│
┌────────────────────────┼────────────────────────┐
▼ ▼ ▼
PLANNER MEMORY RETRIEVAL
│ │ │
│ ▼ │
│ DATABASE │
└────────────────────────┬────────────────────────┘
▼
WORKFLOW ENGINE
│
▼
AGENT CONTROLLER
│
▼
MODEL ROUTER
│
┌─────────────┼─────────────┐
▼ ▼ ▼
Provider A Provider B Local
│ │ │
└─────────────┼─────────────┘
▼
TOOL DECISION
│
▼
TOOL REGISTRY
│
┌────────┼────────┐
▼ ▼ ▼
Calculator Search Database
│ │ │
└────────┼────────┘
▼
TOOL RESULT
│
▼
OBSERVATION
│
▼
VERIFIER
│
┌─────────┴─────────┐
▼ ▼
PASS REVISE
│ │
▼ ▼
RESULT RETRY
│
┌─────────┼─────────┐
▼ ▼ ▼
CACHE LOGS METRICS
│
▼
EVALUATION
ACAI should not be designed as:
AI
↓
Unlimited system access
It should be:
AI
↓
Proposal
↓
Validation
↓
Authorization
↓
Controlled Execution
↓
Observation
↓
Verification
This architecture allows the system to become more capable while preserving explicit control boundaries.
The system now has:
Core API
Planning
Memory
Retrieval
Workflow
Verification
Persistence
Multi-provider routing
Fallback
Evaluation
Tool calling
Agent loops
Controlled external actions
The next major layer is to make the system capable of handling large amounts of information and long-running context without simply placing everything into one model prompt.
Therefore:
The next chapter will cover:
Short-term memory
Long-term memory
Episodic memory
Semantic memory
Working memory
Memory consolidation
Embeddings
Vector search
Hybrid retrieval
Knowledge graphs
Document chunking
Context compression
Long-context strategies
Memory relevance
Memory decay
Conflict resolution
Knowledge updates
The target architecture will become:
Experience
↓
Memory Extraction
↓
Memory Storage
↓
Retrieval
↓
Relevance Ranking
↓
Context Compression
↓
Model
↓
New Experience
End of Chapter 11
Comments
Post a Comment