Send Subagent telemetry to Langfuse
Overview¶
This example extends the existing agent telemetry pattern with delegated work and shows the Langfuse connection inline. The parent agent stays visible in the console, but the same event stream is also projected to Langfuse so you can inspect the full parent and subagent trace tree.
Key concepts:
- explicit LangfuseConfig / LangfuseHttpTransport / LangfuseExporter setup
- UseSubagents: lets the parent delegate work through spawn_subagent
- AgentDefinitionRegistry: defines the available delegated workers
- RuntimeEventBridge: projects the full parent and child event stream into telemetry
- AgentsTelemetryProjector: maps agent execution, tool, and subagent lifecycle events
- UseBash: gives the delegated subagent a real tool boundary to emit nested telemetry
Example¶
<?php
require 'examples/boot.php';
use Cognesy\Agents\Builder\AgentBuilder;
use Cognesy\Agents\Capability\Bash\UseBash;
use Cognesy\Agents\Capability\Core\UseContextConfig;
use Cognesy\Agents\Capability\Core\UseGuards;
use Cognesy\Agents\Capability\Core\UseLLMConfig;
use Cognesy\Agents\Capability\Subagent\UseSubagents;
use Cognesy\Agents\Collections\NameList;
use Cognesy\Agents\Data\AgentState;
use Cognesy\Agents\Enums\ExecutionStatus;
use Cognesy\Agents\Events\SubagentCompleted;
use Cognesy\Agents\Events\SubagentSpawning;
use Cognesy\Agents\Events\Support\AgentEventConsoleObserver;
use Cognesy\Agents\Telemetry\AgentsTelemetryProjector;
use Cognesy\Agents\Template\AgentDefinitionRegistry;
use Cognesy\Agents\Template\Data\AgentDefinition;
use Cognesy\Config\Env;
use Cognesy\Events\Dispatchers\EventDispatcher;
use Cognesy\Http\Telemetry\HttpClientTelemetryProjector;
use Cognesy\Messages\Messages;
use Cognesy\Polyglot\Inference\LLMProvider;
use Cognesy\Polyglot\Telemetry\PolyglotTelemetryProjector;
use Cognesy\Telemetry\Adapters\Langfuse\LangfuseConfig;
use Cognesy\Telemetry\Adapters\Langfuse\LangfuseExporter;
use Cognesy\Telemetry\Adapters\Langfuse\LangfuseHttpTransport;
use Cognesy\Telemetry\Application\Registry\TraceRegistry;
use Cognesy\Telemetry\Application\Telemetry;
use Cognesy\Telemetry\Application\Projector\CompositeTelemetryProjector;
use Cognesy\Telemetry\Application\Projector\RuntimeEventBridge;
$serviceName = 'examples.d05.subagent-telemetry-langfuse';
$baseUrl = (string) Env::get('LANGFUSE_BASE_URL', '');
if ($baseUrl === '') {
throw new RuntimeException('Set LANGFUSE_BASE_URL in .env to run this example.');
}
$publicKey = (string) Env::get('LANGFUSE_PUBLIC_KEY', '');
if ($publicKey === '') {
throw new RuntimeException('Set LANGFUSE_PUBLIC_KEY in .env to run this example.');
}
$secretKey = (string) Env::get('LANGFUSE_SECRET_KEY', '');
if ($secretKey === '') {
throw new RuntimeException('Set LANGFUSE_SECRET_KEY in .env to run this example.');
}
$events = new EventDispatcher($serviceName);
$hub = new Telemetry(
registry: new TraceRegistry(),
exporter: new LangfuseExporter(
transport: new LangfuseHttpTransport(new LangfuseConfig(
baseUrl: $baseUrl,
publicKey: $publicKey,
secretKey: $secretKey,
)),
),
);
(new RuntimeEventBridge(new CompositeTelemetryProjector([
new AgentsTelemetryProjector($hub),
new PolyglotTelemetryProjector($hub),
new HttpClientTelemetryProjector($hub),
])))->attachTo($events);
$logger = new AgentEventConsoleObserver(
useColors: true,
showTimestamps: true,
showContinuation: true,
showToolArgs: true,
);
$workDir = dirname(__DIR__, 3);
$registry = new AgentDefinitionRegistry();
$registry->register(new AgentDefinition(
name: 'repo_inspector',
description: 'Inspects repository paths with bash and reports concise evidence',
systemPrompt: <<<'SYSTEM'
You inspect repositories with the bash tool.
Every factual claim must come from bash output.
Use bash at least 3 separate times.
Never combine commands with && or ;.
SYSTEM,
tools: NameList::fromArray(['bash']),
));
$subagentSpawns = [];
$subagentCompletions = [];
$agent = AgentBuilder::base($events)
->withCapability(new UseContextConfig(systemPrompt: <<<'SYSTEM'
You are an orchestration agent.
If `repo_inspector` is available, you must delegate repository inspection to it.
Your first tool call must be `spawn_subagent`.
Do not use bash directly when `repo_inspector` can do the work.
If you skip delegation, the task is incomplete.
Summarize delegated findings clearly and briefly.
SYSTEM))
->withCapability(new UseLLMConfig(llm: LLMProvider::using('openai')))
->withCapability(new UseBash(baseDir: $workDir))
->withCapability(new UseSubagents(provider: $registry))
->withCapability(new UseGuards(maxSteps: 8, maxTokens: 12288, maxExecutionTime: 60))
->build()
->wiretap($logger->wiretap())
->wiretap(static function (object $event) use (&$subagentSpawns, &$subagentCompletions): void {
if ($event instanceof SubagentSpawning) {
$subagentSpawns[] = $event;
}
if ($event instanceof SubagentCompleted) {
$subagentCompletions[] = $event;
}
});
$task = <<<'TASK'
You must call `spawn_subagent` as your first tool call.
Use subagent `repo_inspector`.
Do not call bash yourself.
Pass this task to the subagent:
Inspect this repository with bash.
Requirements:
- Use bash at least 3 separate times.
- Do not combine commands with && or ;.
- Run these as separate bash calls:
1. pwd
2. ls examples/D05_AgentTroubleshooting
3. rg -n "UseSubagents|SpawnSubagentTool|SubagentSpawning" packages/agents/src/Capability/Subagent/*.php packages/agents/src/Events/Subagent*.php
Return exactly 3 short bullets:
1. What kind of repository this is
2. Where the agent telemetry examples live
3. What the subagent telemetry path records
After the subagent returns, provide only its 3 bullets as the final answer.
TASK;
$state = AgentState::empty()->withMessages(Messages::fromString($task));
echo "=== Agent Execution Log ===\n\n";
$finalState = $agent->execute($state);
$hub->flush();
$collectToolNames = function (AgentState $state) use (&$collectToolNames): array {
return array_reduce(
$state->stepExecutions()->all(),
static function (array $names, $stepExecution) use (&$collectToolNames): array {
$stepNames = [];
foreach ($stepExecution->step()->toolExecutions()->all() as $toolExecution) {
$stepNames[] = $toolExecution->name();
$value = $toolExecution->value();
if ($value instanceof AgentState) {
$stepNames = [...$stepNames, ...$collectToolNames($value)];
}
}
return [...$names, ...$stepNames];
},
[],
);
};
$toolNames = $collectToolNames($finalState);
$toolCallCount = count($toolNames);
$bashCallCount = count(array_filter($toolNames, static fn(string $name): bool => $name === 'bash'));
$spawnSubagentCount = count(array_filter($toolNames, static fn(string $name): bool => $name === 'spawn_subagent'));
echo "\n=== Result ===\n";
$response = $finalState->finalResponse()->toString() ?: 'No response';
echo "Answer: {$response}\n";
echo "Status: {$finalState->status()->value}\n";
echo "Steps: {$finalState->stepCount()}\n";
echo "Tools used: " . implode(' > ', $toolNames) . "\n";
echo "Total tool calls: {$toolCallCount}\n";
echo "Bash calls across parent/child runs: {$bashCallCount}\n";
echo "spawn_subagent calls: {$spawnSubagentCount}\n";
echo "Subagents spawned: " . count($subagentSpawns) . "\n";
echo "Subagents completed: " . count($subagentCompletions) . "\n";
echo "Telemetry: flushed to Langfuse\n";
assert($finalState->status() === ExecutionStatus::Completed);
assert($response !== '');
assert($spawnSubagentCount >= 1);
assert(count($subagentSpawns) >= 1);
assert(count($subagentCompletions) >= 1);
assert($bashCallCount >= 3);
?>