Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion app/api/agents/[id]/tasks/drain/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ export async function POST(req: Request, context: RouteContext) {
type: "task.completed",
agentId: task.agentId,
taskId: task.id,
taskType: task.type,
result: {
summary: `Task ${task.type} processed`,
},
})
},
})
Expand Down
15 changes: 15 additions & 0 deletions app/api/internal/metrics/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { NextResponse } from 'next/server'
import { getMetricsSnapshot } from '@/lib/observability/metrics'
import { logger } from '@/lib/observability/logger'

export const dynamic = 'force-dynamic'

export async function GET(): Promise<NextResponse> {
const snapshot = getMetricsSnapshot()
await logger.info('metrics.snapshot.served', {
counters: snapshot.counters.length,
gauges: snapshot.gauges.length,
histograms: snapshot.histograms.length,
})
return NextResponse.json({ ok: true, ...snapshot })
}
23 changes: 23 additions & 0 deletions app/api/protocol/x402/settle/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { settleMockX402 } from '@/lib/mock/x402-mock'
import { publishSystemEvent } from '@/lib/events/system-events'
import { XP_AWARDS } from '@/lib/gamification/constants'
import { awardXP } from '@/lib/gamification/xp'
import { logger } from '@/lib/observability/logger'
import { recordX402Payment } from '@/lib/observability/metrics'

function ledgerFromBody(body: Record<string, unknown>): unknown {
return body.lastPaymentLedger ?? body.ledger ?? body.ledgerSequence
Expand Down Expand Up @@ -79,6 +81,8 @@ export async function POST(req: Request) {
receipt,
})
}
recordX402Payment({ service: paymentRef.split(':')[0] || 'mock', agentId: agentId || paidBy })
await logger.info('x402.settle.completed', { mode: 'mock', paymentRef, chain, txHash: receipt.txHash, agentId: agentId || paidBy, subscriptionId })
return await api.json({ ok: true, receipt, subscriptionProof }, undefined, { event: 'x402.settle.mock', paymentRef, subscriptionId })
}

Expand Down Expand Up @@ -114,6 +118,7 @@ export async function POST(req: Request) {
})

if (!result.ok || !result.receipt) {
await logger.error('x402.settle.failed', { reason: result.error, paymentRef, chain, paidBy, agentId })
return await api.json(
{ ok: false, error: result.error || 'x402 settlement rejected' },
{ status: 400 },
Expand All @@ -136,6 +141,23 @@ export async function POST(req: Request) {
receipt: result.receipt,
})
awardXP(agentId || paidBy, XP_AWARDS.X402_PAYMENT_RECEIVED, 'payment.received')
recordX402Payment({
service: quote?.serviceId ?? paymentRef.split(':')[0] ?? 'unknown',
amountXlm: result.receipt.chain === 'stellar' ? Number(result.receipt.amountUnits ?? 0) / 10_000_000 : 0,
amountUsd: result.receipt.amountUsd,
agentId: agentId || paidBy,
})
await logger.info('x402.settle.completed', {
paymentRef,
chain,
paidBy,
agentId: agentId || paidBy,
txHash: result.receipt.txHash,
service: quote?.serviceId,
amountUsd: result.receipt.amountUsd,
amountUnits: result.receipt.amountUnits,
subscriptionId,
})

return await api.json({ ok: true, receipt: result.receipt, subscriptionProof }, undefined, {
event: 'x402.settle.completed',
Expand All @@ -146,6 +168,7 @@ export async function POST(req: Request) {
subscriptionId,
})
} catch (error) {
await logger.error('x402.settle.failed', { error })
return await api.report(
'error',
error,
Expand Down
2 changes: 1 addition & 1 deletion app/offline/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export default function OfflinePage() {
return (
<main className="min-h-screen bg-[#030712] flex flex-col items-center justify-center text-slate-100 gap-4">
<h1 className="font-mono text-2xl text-cyan-300">You're offline</h1>
<h1 className="font-mono text-2xl text-cyan-300">You&apos;re offline</h1>
<p className="text-slate-400 text-sm">Open Stellar will reconnect when your network is back.</p>
</main>
)
Expand Down
144 changes: 142 additions & 2 deletions components/admin/admin-console.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
"use client"

import { useEffect, useMemo, useState, type ReactNode } from "react"
import { Activity, AlertTriangle, Check, Cloud, Code2, Copy, Cpu, Download, ExternalLink, Fingerprint, ReceiptText, History, KeyRound, Layers3, ListChecks, RadioTower, Rocket, Server, Shield, Terminal, Wallet } from "lucide-react"
import { Activity, AlertTriangle, BarChart3, Check, Cloud, Code2, Copy, Cpu, Download, ExternalLink, Fingerprint, ReceiptText, History, KeyRound, Layers3, ListChecks, RadioTower, Rocket, Server, Shield, Terminal, Wallet } from "lucide-react"
import type { District, MoltbotAgent } from "@/lib/types"
import { PassportPanel } from "@/components/admin/passport-panel"

type AdminTab = "overview" | "queue" | "passport" | "private-deploy" | "receipts" | "cloud-agents"
type AdminTab = "overview" | "queue" | "observability" | "passport" | "private-deploy" | "receipts" | "cloud-agents"

type Plan = {
name: string
Expand Down Expand Up @@ -183,6 +183,9 @@
<TabButton active={tab === "queue"} onClick={() => setTab("queue")} icon={<ListChecks className="h-3.5 w-3.5" />}>
Task queue
</TabButton>
<TabButton active={tab === "observability"} onClick={() => setTab("observability")} icon={<BarChart3 className="h-3.5 w-3.5" />}>
Observability
</TabButton>
<TabButton active={tab === "passport"} onClick={() => setTab("passport")} icon={<Fingerprint className="h-3.5 w-3.5" />}>
Agent Passport (ZK)
</TabButton>
Expand All @@ -196,211 +199,213 @@

{tab === "queue" ? (
<TaskQueueTab />
) : tab === "observability" ? (
<ObservabilityTab />
): tab === "receipts" ? (
<ReceiptsTab />
) : tab === "passport" ? (
<section className="rounded-[28px] border border-cyan-500/20 bg-slate-950/60 p-5">
<div className="mb-5 max-w-3xl">
<div className="mb-3 inline-flex items-center gap-2 rounded-full border border-cyan-400/20 bg-cyan-400/10 px-3 py-1 text-[10px] uppercase tracking-[0.32em] text-cyan-200">
<Fingerprint className="h-3.5 w-3.5" />
Zero-knowledge trust layer
</div>
<h2 className="font-pixel text-xl uppercase leading-tight text-cyan-100">
Prove an agent is solvent &amp; authorized — without doxxing the owner
</h2>
<p className="mt-3 font-vt323 text-xl leading-7 text-slate-300">
Each agent mints a ZK passport (Groth16 / Soroban) proving it is backed by a verified human and is
solvent for its spend cap. The x402 settlement rail then releases a payment only when the agent holds a
valid passport and the amount stays within its proven, hidden cap.
</p>
</div>
<PassportPanel />
</section>
) : tab === "private-deploy" ? (
<PrivateDeployTab />
) : tab === "cloud-agents" ? (
<CloudAgentsTab />
) : (
<>
<section className="grid gap-5 xl:grid-cols-[0.8fr_1.2fr_0.8fr]">
<Panel
title="Infra posture"
eyebrow="Runtime telemetry"
bodyClassName="space-y-3"
>
<TelemetryRow icon={<Cpu className="h-4 w-4" />} label="Average CPU" value={`${avgCpu}%`} tone="text-cyan-300" />
<TelemetryRow icon={<Activity className="h-4 w-4" />} label="Average memory" value={`${avgMemory}%`} tone="text-violet-300" />
<TelemetryRow icon={<Layers3 className="h-4 w-4" />} label="District teams" value={String(districts.length)} tone="text-amber-300" />
<div className="rounded-2xl border border-slate-800 bg-slate-950/70 p-4">
<p className="text-[10px] uppercase tracking-[0.28em] text-slate-500">Business frame</p>
<p className="mt-3 font-vt323 text-lg leading-6 text-slate-300">
Monthly subscription buys access to orchestration control plus the x402 payment extension.
Request volume is the primary limiter, not seats.
</p>
</div>
</Panel>

<Panel
title="District squads"
eyebrow="Main page parity"
bodyClassName="grid gap-4 md:grid-cols-2"
>
{districtCards.map(({ district, squad, working, avgLoad }) => (
<div key={district.id} className="rounded-[22px] border border-slate-800 bg-slate-950/70 p-4">
<div className="flex items-start justify-between gap-3">
<div>
<p className="font-pixel text-sm uppercase text-slate-100">{district.name}</p>
<p className="mt-1 text-[10px] uppercase tracking-[0.28em] text-slate-500">
{squad.length} agents / {working} working now
</p>
</div>
<span className="mt-1 h-3 w-3 rounded-full" style={{ backgroundColor: district.color }} />
</div>

<div className="mt-4 h-2 overflow-hidden rounded-full bg-slate-900">
<div
className="h-full rounded-full transition-all"
style={{ width: `${avgLoad}%`, backgroundColor: district.color }}
/>
</div>

<div className="mt-4 flex flex-wrap gap-2">
{squad.slice(0, 4).map((agent) => (
<span
key={agent.id}
className="rounded-full border border-slate-700 bg-slate-900 px-2.5 py-1 font-mono text-[11px] text-slate-300"
>
{agent.name}
</span>
))}
</div>

<p className="mt-4 font-vt323 text-lg text-slate-300">
Suggested as an isolated customer squad with policy-based request caps and shared payment settlement.
</p>
</div>
))}
</Panel>

<Panel
title="Subscription lanes"
eyebrow="x402 recurring billing"
bodyClassName="space-y-3"
>
{plans.map((plan) => {
const isActive = plan.name === selectedPlan.name
return (
<button
key={plan.name}
type="button"
onClick={() => setSelectedPlan(plan)}
className={`w-full rounded-[22px] border p-4 text-left transition ${
isActive
? "border-cyan-400/40 bg-cyan-400/10"
: "border-slate-800 bg-slate-950/70 hover:border-slate-700"
}`}
>
<div className="flex items-start justify-between gap-4">
<div>
<p className={`font-pixel text-sm uppercase ${plan.accent}`}>{plan.name}</p>
<p className="mt-2 text-[10px] uppercase tracking-[0.28em] text-slate-500">{plan.requests}</p>
</div>
<p className="font-mono text-sm text-slate-100">{plan.price}</p>
</div>
<p className="mt-3 font-vt323 text-lg text-slate-300">{plan.teams}</p>
</button>
)
})}

<div className="rounded-[22px] border border-slate-800 bg-[#09101a] p-4">
<p className="text-[10px] uppercase tracking-[0.28em] text-slate-500">Selected lane</p>
<h3 className="mt-3 font-pixel text-base uppercase text-cyan-100">{selectedPlan.name}</h3>
<ul className="mt-4 space-y-2">
{selectedPlan.features.map((feature) => (
<li key={feature} className="flex items-center gap-2 font-vt323 text-lg text-slate-300">
<Check className="h-4 w-4 text-emerald-400" />
{feature}
</li>
))}
</ul>
</div>
</Panel>
</section>



<Panel title="Active x402 subscriptions" eyebrow="Admin billing console" bodyClassName="overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full min-w-[760px] text-left">
<thead>
<tr className="border-b border-slate-800 text-[10px] uppercase tracking-[0.24em] text-slate-500">
<th className="pb-3 font-normal">Subscriber</th>
<th className="pb-3 font-normal">Service</th>
<th className="pb-3 font-normal">Plan</th>
<th className="pb-3 font-normal">Calls</th>
<th className="pb-3 font-normal">Next renewal</th>
<th className="pb-3 font-normal">MRR</th>
<th className="pb-3 font-normal">Status</th>
</tr>
</thead>
<tbody>
{subscriptions.map((subscription) => (
<tr key={`${subscription.agent}-${subscription.service}`} className="border-b border-slate-900/80 font-mono text-xs text-slate-300">
<td className="py-3 text-cyan-200">{subscription.agent}</td>
<td className="py-3">{subscription.service}</td>
<td className="py-3">{subscription.plan}</td>
<td className="py-3">{subscription.used.toLocaleString()} / {subscription.limit.toLocaleString()}</td>
<td className="py-3">{subscription.renewsAt}</td>
<td className="py-3 text-emerald-300">{subscription.mrr}</td>
<td className="py-3">
<span className={`rounded-full border px-2 py-1 text-[10px] uppercase tracking-[0.18em] ${subscription.status === "active" ? "border-emerald-400/30 bg-emerald-400/10 text-emerald-300" : "border-amber-400/30 bg-amber-400/10 text-amber-300"}`}>
{subscription.status}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</Panel>

<section className="grid gap-5 xl:grid-cols-[1fr_0.9fr]">
<Panel title="Top operators" eyebrow="Main page agents" bodyClassName="grid gap-3 md:grid-cols-2 xl:grid-cols-1">
{topAgents.map((agent) => (
<div key={agent.id} className="rounded-[20px] border border-slate-800 bg-slate-950/70 p-4">
<div className="flex items-center justify-between gap-4">
<div>
<p className="font-mono text-sm" style={{ color: agent.color }}>{agent.name}</p>
<p className="mt-1 text-[10px] uppercase tracking-[0.24em] text-slate-500">
{agent.model} / {agent.status}
</p>
</div>
<div className="text-right">
<p className="font-mono text-sm text-cyan-200">{agent.tasksCompleted} tasks</p>
<p className="text-[10px] uppercase tracking-[0.24em] text-slate-500">{agent.district}</p>
</div>
</div>
</div>
))}
</Panel>

<Panel title="Offer framing" eyebrow="What this screen is selling" bodyClassName="grid gap-3 md:grid-cols-3">
<FeatureBlock
title="Agent teams"
text="Package districts as customer squads with isolated routing, policies, and operational telemetry."
/>
<FeatureBlock
title="x402 extension"
text="Use the payment rail as a monetized extension layer for request-triggered or workflow-triggered charges."
/>
<FeatureBlock
title="Request quotas"
text="Anchor the subscription on monthly volume. It is easier to explain, meter, and upsell than raw infrastructure seats."
/>
</Panel>
</section>
</>
)}

Check warning on line 408 in components/admin/admin-console.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this nested ternary operation into an independent statement.

See more on https://sonarcloud.io/project/issues?id=Bitcoindefi_Open-Stellar&issues=AZ8H-hBTIHStKnsF6nW5&open=AZ8H-hBTIHStKnsF6nW5&pullRequest=360
</div>
</main>
)
Expand Down Expand Up @@ -760,6 +765,109 @@
error?: string
}

type MetricsSnapshot = {
counters: Array<{ name: string; value: number; labels: Record<string, string> }>
gauges: Array<{ name: string; value: number; labels: Record<string, string> }>
histograms: Array<{ name: string; count: number; p50: number; p95: number; p99: number; labels: Record<string, string> }>
charts: {
requestRate24h: Array<{ hour: string; count: number }>
errorRate24h: Array<{ hour: string; count: number }>
x402Revenue24h: Array<{ hour: string; xlm: number }>
}
topFailingAgents: Array<{ agentId: string; failures: number }>
}

function ObservabilityTab() {
const [snapshot, setSnapshot] = useState<MetricsSnapshot | null>(null)
const [status, setStatus] = useState("Loading observability data…")

useEffect(() => {
let mounted = true
fetch("/api/internal/metrics", { cache: "no-store" })
.then((res) => res.json())
.then((data) => {
if (!mounted) return
if (!data.ok) {
setStatus(data.error || "Failed to load metrics")
return
}
setSnapshot(data)
setStatus("")
})
.catch(() => {
if (mounted) setStatus("Failed to load metrics")
})

return () => {
mounted = false
}
}, [])

const findCounter = (name: string) => snapshot?.counters.filter((sample) => sample.name === name).reduce((sum, sample) => sum + sample.value, 0) ?? 0
const findGauge = (name: string) => snapshot?.gauges.find((sample) => sample.name === name)?.value ?? 0
const taskHistogram = snapshot?.histograms.find((sample) => sample.name === "task.duration_ms")
const totalRequests = snapshot?.charts.requestRate24h.reduce((sum, bucket) => sum + bucket.count, 0) ?? 0
const totalErrors = snapshot?.charts.errorRate24h.reduce((sum, bucket) => sum + bucket.count, 0) ?? 0
const x402Revenue = snapshot?.charts.x402Revenue24h.reduce((sum, bucket) => sum + bucket.xlm, 0) ?? 0

if (status) {
return (
<section className="rounded-[28px] border border-cyan-500/20 bg-slate-950/60 p-5">
<p className="font-vt323 text-xl text-slate-400">{status}</p>
</section>
)
}

return (
<section className="grid gap-5 xl:grid-cols-[0.75fr_1.25fr]">
<Panel title="Live signals" eyebrow="Structured metrics" bodyClassName="space-y-3">
<TelemetryRow icon={<Activity className="h-4 w-4" />} label="Requests / 24h" value={String(totalRequests)} tone="text-cyan-300" />
<TelemetryRow icon={<AlertTriangle className="h-4 w-4" />} label="Errors / 24h" value={String(totalErrors)} tone="text-rose-300" />
<TelemetryRow icon={<RadioTower className="h-4 w-4" />} label="Agents online" value={String(findGauge("agents.online"))} tone="text-emerald-300" />
<TelemetryRow icon={<Wallet className="h-4 w-4" />} label="x402 revenue / 24h" value={`${x402Revenue.toFixed(4)} XLM`} tone="text-amber-300" />
<TelemetryRow icon={<Check className="h-4 w-4" />} label="Tasks completed" value={String(findCounter("tasks.completed"))} tone="text-violet-300" />
</Panel>

<Panel title="Request and error rate" eyebrow="Last 24 hours" bodyClassName="space-y-5">
<MiniBarChart
label="Request rate"
color="bg-cyan-400"
buckets={snapshot?.charts.requestRate24h.map((bucket) => ({ label: bucket.hour, value: bucket.count })) ?? []}
/>
<MiniBarChart
label="Error rate"
color="bg-rose-400"
buckets={snapshot?.charts.errorRate24h.map((bucket) => ({ label: bucket.hour, value: bucket.count })) ?? []}
/>
</Panel>

<Panel title="Task duration percentiles" eyebrow="Histogram" bodyClassName="grid gap-3 sm:grid-cols-3">
<SignalStat label="p50" value={`${Math.round(taskHistogram?.p50 ?? 0)}ms`} color="text-cyan-300" />
<SignalStat label="p95" value={`${Math.round(taskHistogram?.p95 ?? 0)}ms`} color="text-amber-300" />
<SignalStat label="p99" value={`${Math.round(taskHistogram?.p99 ?? 0)}ms`} color="text-rose-300" />
</Panel>

<Panel title="Top failing agents" eyebrow="Recovery focus" bodyClassName="space-y-3">
{snapshot?.topFailingAgents.length ? snapshot.topFailingAgents.map((agent) => (
<TelemetryRow key={agent.agentId} icon={<AlertTriangle className="h-4 w-4" />} label={agent.agentId} value={`${agent.failures} failures`} tone="text-rose-300" />
)) : (
<p className="rounded-2xl border border-emerald-500/20 bg-emerald-500/10 p-4 font-vt323 text-lg text-emerald-200">
No task failures recorded in this process.
</p>
)}
</Panel>

<Panel title="x402 revenue over time" eyebrow="Payment rail" bodyClassName="space-y-4 xl:col-span-2">
<MiniBarChart
label="XLM revenue"
color="bg-amber-300"
buckets={snapshot?.charts.x402Revenue24h.map((bucket) => ({ label: bucket.hour, value: bucket.xlm })) ?? []}
/>
</Panel>
</section>
)
}

function TaskQueueTab() {
const [tasks, setTasks] = useState<QueueTask[]>([])
const [loading, setLoading] = useState(true)
Expand Down Expand Up @@ -919,6 +1027,38 @@
)
}

function MiniBarChart({
label,
color,
buckets,
}: {
label: string
color: string
buckets: Array<{ label: string; value: number }>
}) {

Check warning on line 1038 in components/admin/admin-console.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Mark the props of the component as read-only.

See more on https://sonarcloud.io/project/issues?id=Bitcoindefi_Open-Stellar&issues=AZ8H-hBTIHStKnsF6nW6&open=AZ8H-hBTIHStKnsF6nW6&pullRequest=360
const max = Math.max(1, ...buckets.map((bucket) => bucket.value))

return (
<div className="rounded-[20px] border border-slate-800 bg-slate-950/70 p-4">
<div className="mb-3 flex items-center justify-between text-[10px] uppercase tracking-[0.28em] text-slate-500">
<span>{label}</span>
<span>{buckets.reduce((sum, bucket) => sum + bucket.value, 0).toFixed(2)}</span>
</div>
<div className="flex h-28 items-end gap-1">
{buckets.map((bucket) => (
<div key={bucket.label} className="flex flex-1 items-end">
<div
className={`w-full rounded-t ${color} opacity-80`}
title={`${new Date(bucket.label).toLocaleTimeString([], { hour: "2-digit" })}: ${bucket.value}`}
style={{ height: `${Math.max(4, (bucket.value / max) * 100)}%` }}
/>
</div>
))}
</div>
</div>
)
}

function FeatureBlock({ title, text }: { title: string; text: string }) {
return (
<div className="rounded-[20px] border border-slate-800 bg-slate-950/70 p-4">
Expand Down
11 changes: 11 additions & 0 deletions lib/agent-runtime/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { recordAgentHeartbeat } from "@/lib/agents/agent-health-store"
import { sendAgentMessage } from "@/lib/agent-runtime/messaging"
import type { AgentConfig, AgentMetrics, AgentRuntimeContext, AgentMessage, MessageHandler, Task, TaskHandler, TaskResult } from "@/lib/agent-runtime/types"
import { publishSystemEvent } from "@/lib/events/system-events"
import { logger } from "@/lib/observability/logger"
import { recordTaskCompleted, recordTaskFailed, setGauge } from "@/lib/observability/metrics"
import type { AgentStatus } from "@/lib/types"

const DEFAULT_HEARTBEAT_INTERVAL_MS = 15_000
Expand Down Expand Up @@ -85,6 +87,8 @@ export class Agent implements AgentRuntimeContext {
this.recordHeartbeat()
this.heartbeatTimer ??= setInterval(() => this.recordHeartbeat(), this.config.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS)
publishSystemEvent({ type: "agent.status", agentId: this.id, status: this.status })
setGauge("agents.online", {}, [...runtimeState.agents.values()].filter((agent) => agent.getStatus() !== "offline").length)
void logger.info("agent.started", { agentId: this.id, district: this.config.district })
}

async stop(): Promise<void> {
Expand All @@ -94,6 +98,8 @@ export class Agent implements AgentRuntimeContext {
this.status = "offline"
this.recordHeartbeat()
publishSystemEvent({ type: "agent.status", agentId: this.id, status: this.status })
setGauge("agents.online", {}, [...runtimeState.agents.values()].filter((agent) => agent.getStatus() !== "offline").length)
void logger.info("agent.stopped", { agentId: this.id, district: this.config.district })
}

async restart(): Promise<void> {
Expand All @@ -118,6 +124,7 @@ export class Agent implements AgentRuntimeContext {
this.recordHeartbeat(task.title)
writeTaskRecord(this.id, { task, result: null, status: "running", updatedAt: startedAt })
publishSystemEvent({ type: "task.started", agentId: this.id, task: { id: task.id, title: task.title, district: task.district } })
void logger.info("task.started", { agentId: this.id, taskId: task.id, district: task.district })

try {
const handler = this.taskHandlers.at(-1)
Expand All @@ -140,6 +147,8 @@ export class Agent implements AgentRuntimeContext {
this.recordHeartbeat()
writeTaskRecord(this.id, { task, result, status: "completed", updatedAt: completedAt })
publishSystemEvent({ type: "task.completed", agentId: this.id, taskId: task.id, result: { summary: result.summary, durationMs } })
recordTaskCompleted({ agentId: this.id, taskId: task.id, durationMs, district: task.district })
void logger.info("task.completed", { agentId: this.id, taskId: task.id, durationMs, district: task.district })
return result
} catch (error) {
const completedAt = isoNow()
Expand All @@ -159,6 +168,8 @@ export class Agent implements AgentRuntimeContext {
this.recordHeartbeat(task.title)
writeTaskRecord(this.id, { task, result, status: "failed", updatedAt: completedAt })
publishSystemEvent({ type: "task.completed", agentId: this.id, taskId: task.id, result: { summary: result.error ?? result.summary, durationMs } })
recordTaskFailed({ agentId: this.id, taskId: task.id, durationMs, district: task.district })
void logger.error("task.failed", { agentId: this.id, taskId: task.id, durationMs, district: task.district, error })
return result
}
}
Expand Down
Loading
Loading