@agent-live/cli 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin.mjs +41 -0
- package/console-dist/assets/components-0p8SghrX.js +1 -0
- package/console-dist/assets/index-49jCgePm.js +1 -0
- package/console-dist/assets/index-BnLKN4xZ.css +1 -0
- package/console-dist/assets/index-jfHKGDWM.js +76 -0
- package/console-dist/assets/page-BRDi_757.js +1 -0
- package/console-dist/assets/page-BpIZP_o-.js +1 -0
- package/console-dist/assets/page-Byx3HD88.js +1 -0
- package/console-dist/assets/page-CEUp9Rfn.js +1 -0
- package/console-dist/assets/page-CmST3snz.js +1 -0
- package/console-dist/assets/page-DMYOtzYN.js +1 -0
- package/console-dist/index.html +4 -0
- package/package.json +19 -0
- package/server/demo/data.mjs +524 -0
- package/server/http.mjs +102 -0
- package/server/real/data.mjs +170 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{a as n,u as l,m as o,j as e,f as d}from"./index-jfHKGDWM.js";import{P as c,D as p,a as h,c as m,b as u,E as j}from"./index-49jCgePm.js";import{A as x}from"./components-0p8SghrX.js";function A(){const{agentId:r,search:i}=n(),a=l("approvals",r),t=a.data?.approvals.filter(s=>o(i,s.label,s.agentName,s.customerAlias));return e.jsxs(e.Fragment,{children:[e.jsx(c,{title:"Approvals",description:"Observed approval requests and decisions. Decisions are made in the originating application.",children:e.jsx(x,{})}),e.jsx(p,{pending:a.isPending,error:a.error,retry:a.refetch,children:t&&e.jsx(h,{children:t.length?e.jsx(m,{label:"Approval events",headers:["Request","Agent","Customer","When","Status"],children:t.map(s=>e.jsxs("tr",{children:[e.jsxs("td",{children:[s.label,e.jsx("div",{className:"row-sub",children:s.channel})]}),e.jsx("td",{children:s.agentName}),e.jsx("td",{children:s.customerAlias}),e.jsx("td",{children:d(s.at)}),e.jsxs("td",{children:[e.jsx(u,{status:s.status}),s.open&&e.jsx("div",{className:"row-sub",children:"Needs a decision"})]})]},s.id))}):e.jsx(j,{children:"No approvals match this view. Adjust filters or wait for an approval request."})})})]})}export{A as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{a as n,u as l,m as c,j as e,f as o}from"./index-jfHKGDWM.js";import{P as m,D as d,a as h,c as u,b as j,E as x}from"./index-49jCgePm.js";import{A as v}from"./components-0p8SghrX.js";function y(){const{agentId:a,search:i}=n(),r=l("deliveries",a),t=r.data?.deliveries.filter(s=>c(i,s.summary,s.purpose,s.agentName,s.customerAlias));return e.jsxs(e.Fragment,{children:[e.jsx(m,{title:"Deliveries",description:"Delivery events and their recorded outcomes.",children:e.jsx(v,{})}),e.jsx(d,{pending:r.isPending,error:r.error,retry:r.refetch,children:t&&e.jsx(h,{children:t.length?e.jsx(u,{label:"Delivery events",headers:["Delivery","Agent","Customer","When","Outcome"],children:t.map(s=>e.jsxs("tr",{children:[e.jsxs("td",{children:[s.purpose,e.jsx("div",{className:"row-sub",children:s.summary})]}),e.jsx("td",{children:s.agentName}),e.jsx("td",{children:s.customerAlias}),e.jsx("td",{children:o(s.at)}),e.jsx("td",{children:e.jsx(j,{status:s.outcome})})]},s.id))}):e.jsx(x,{children:"No deliveries match this view. Adjust filters or wait for a delivery event."})})})]})}export{y as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{a as i,u as o,m as c,j as e}from"./index-jfHKGDWM.js";import{P as u,D as m,a as d}from"./index-49jCgePm.js";import{A as l,R as g}from"./components-0p8SghrX.js";function f(){const{agentId:t,search:n}=i(),a=o("runs",t),r=a.data?.runs.filter(s=>c(n,s.summary,s.agentName,s.customerAlias,s.channel,s.id));return e.jsxs(e.Fragment,{children:[e.jsx(u,{title:"Activity",description:"Runs and outcomes across your agents.",children:e.jsx(l,{})}),e.jsx(m,{pending:a.isPending,error:a.error,retry:a.refetch,children:r&&e.jsx(d,{children:e.jsx(g,{runs:r})})})]})}export{f as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{u as t,j as e,f as n}from"./index-jfHKGDWM.js";import{P as i,D as c,a as r,E as l}from"./index-49jCgePm.js";import{C as o}from"./components-0p8SghrX.js";function p(){const s=t("touchpoints");return e.jsxs(e.Fragment,{children:[e.jsx(i,{title:"Touchpoints",description:"Where customers interact with your agents."}),e.jsx(c,{pending:s.isPending,error:s.error,retry:s.refetch,children:s.data&&e.jsxs("div",{className:"panels",children:[e.jsx(r,{title:"Channel share",children:e.jsx(o,{items:s.data.breakdown})}),e.jsx(r,{title:"Recent touches",children:e.jsx("div",{className:"card-b",children:s.data.recent.length?s.data.recent.map(a=>e.jsxs("div",{className:"row",children:[e.jsxs("div",{className:"row-title",children:[a.action,e.jsxs("div",{className:"row-sub",children:[a.customerAlias," · ",a.agentName," · ",n(a.at)]})]}),e.jsx("span",{className:"pill pill-info",children:a.channel})]},a.id)):e.jsx(l,{children:"Customer interactions will appear here when received."})})})]})})]})}export{p as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{u as a,j as e,L as n,f as r}from"./index-jfHKGDWM.js";import{P as i,D as d,b as c,E as l}from"./index-49jCgePm.js";function g(){const t=a("agents");return e.jsxs(e.Fragment,{children:[e.jsx(i,{title:"Agents",description:"Fleet health. Open an agent to see its activity."}),e.jsx(d,{pending:t.isPending,error:t.error,retry:t.refetch,children:t.data&&(t.data.agents.length?e.jsx("div",{className:"agent-grid",children:t.data.agents.map(s=>e.jsxs(n,{className:"agent-card",to:`/activity?${new URLSearchParams({agent:s.id})}`,children:[e.jsxs("div",{className:"agent-title",children:[e.jsx("h2",{children:s.name}),e.jsx(c,{status:s.status})]}),e.jsx("p",{children:s.description}),e.jsxs("div",{className:"agent-meta",children:[e.jsxs("span",{children:[s.runsToday," runs today"]}),e.jsxs("span",{children:[Math.round(s.failureRate*100),"% failed"]}),e.jsx("span",{children:s.channels.join(", ")}),e.jsxs("span",{children:["Last ",r(s.lastActivityAt)]})]})]},s.id))}):e.jsx(l,{children:"Connected agents will appear here when the server receives activity."}))})]})}export{g as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{u as l,j as e,f as r}from"./index-jfHKGDWM.js";import{P as d,D as o,S as i,a as n,b as c,E as u}from"./index-49jCgePm.js";import{C as v,R as x}from"./components-0p8SghrX.js";function m(){const t=l("overview"),a=t.data;return e.jsxs(e.Fragment,{children:[e.jsx(d,{title:"Overview",description:"Customer touchpoints, agent activity, and what needs attention."}),e.jsx(o,{pending:t.isPending,error:t.error,retry:t.refetch,children:a&&e.jsxs("div",{className:"stack",children:[e.jsxs("div",{className:"grid-stats",children:[e.jsx(i,{label:"Active agents",value:a.stats.activeAgents,detail:`${a.stats.agentCount} in fleet`}),e.jsx(i,{label:"Open approvals",value:a.stats.openApprovals,detail:"Waiting on humans"}),e.jsx(i,{label:"Failed deliveries",value:a.stats.failedDeliveries,detail:"Need follow-up"}),e.jsx(i,{label:"Touches today",value:a.stats.touchesToday,detail:`${a.stats.runningRuns} runs live`})]}),e.jsxs("div",{className:"panels",children:[e.jsx(n,{title:"Needs attention",children:e.jsx("div",{className:"card-b",children:a.attention.length?a.attention.map(s=>e.jsxs("div",{className:"row",children:[e.jsxs("div",{className:"row-title",children:[s.title,e.jsxs("div",{className:"row-sub",children:[s.agentName," · ",s.customerAlias," · ",r(s.at)]})]}),e.jsx(c,{status:s.kind==="approval"?"waiting":"failed"})]},`${s.kind}:${s.runId}`)):e.jsx(u,{children:"No outstanding approvals or failed deliveries."})})}),e.jsx(n,{title:"Touch mix",children:e.jsx(v,{items:a.channelBreakdown})})]}),e.jsx(n,{title:"Recent activity",children:e.jsx(x,{runs:a.recentRuns})})]})})]})}export{m as default};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en"><head><meta charset="UTF-8"/><meta name="viewport" content="width=device-width, initial-scale=1.0"/><meta name="description" content="Agent Live — agent activity, approvals, and delivery outcomes."/><title>Agent Live</title> <script type="module" crossorigin src="/assets/index-jfHKGDWM.js"></script>
|
|
3
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BnLKN4xZ.css">
|
|
4
|
+
</head><body><div id="root"></div></body></html>
|
package/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@agent-live/cli",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Run a local dashboard for agents instrumented with @agent-live/sdk — one command, no build tooling required.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"keywords": ["ai-agents", "observability", "telemetry", "dashboard", "cli"],
|
|
7
|
+
"type": "module",
|
|
8
|
+
"bin": {
|
|
9
|
+
"agent-live": "./bin.mjs"
|
|
10
|
+
},
|
|
11
|
+
"files": ["bin.mjs", "console-dist", "server"],
|
|
12
|
+
"publishConfig": { "access": "public" },
|
|
13
|
+
"scripts": {
|
|
14
|
+
"prepack": "node ./scripts/build-assets.mjs"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@agent-live/sdk": "^0.0.2"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
export const ORG = {
|
|
2
|
+
id: 'org_northline',
|
|
3
|
+
name: 'Northline Ops',
|
|
4
|
+
mode: 'demo',
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function ago(ms) {
|
|
8
|
+
return new Date(Date.now() - ms).toISOString()
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function seed() {
|
|
12
|
+
const agents = [
|
|
13
|
+
{
|
|
14
|
+
id: 'agt_invoice',
|
|
15
|
+
name: 'Invoice Concierge',
|
|
16
|
+
description: 'Collects, reviews, and delivers invoice packs over WhatsApp',
|
|
17
|
+
status: 'busy',
|
|
18
|
+
channels: ['whatsapp'],
|
|
19
|
+
runsToday: 48,
|
|
20
|
+
failureRate: 0.04,
|
|
21
|
+
lastActivityAt: ago(40_000),
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: 'agt_support',
|
|
25
|
+
name: 'Support Desk',
|
|
26
|
+
description: 'Triages inbound email and drafts grounded replies',
|
|
27
|
+
status: 'busy',
|
|
28
|
+
channels: ['email', 'web'],
|
|
29
|
+
runsToday: 112,
|
|
30
|
+
failureRate: 0.07,
|
|
31
|
+
lastActivityAt: ago(18_000),
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: 'agt_ops',
|
|
35
|
+
name: 'Ops Runner',
|
|
36
|
+
description: 'Schedules field jobs and confirms with customers',
|
|
37
|
+
status: 'idle',
|
|
38
|
+
channels: ['whatsapp', 'api'],
|
|
39
|
+
runsToday: 31,
|
|
40
|
+
failureRate: 0.02,
|
|
41
|
+
lastActivityAt: ago(14 * 60_000),
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: 'agt_billing',
|
|
45
|
+
name: 'Billing Follow-up',
|
|
46
|
+
description: 'Sends payment reminders and escalates disputes',
|
|
47
|
+
status: 'degraded',
|
|
48
|
+
channels: ['email', 'whatsapp'],
|
|
49
|
+
runsToday: 67,
|
|
50
|
+
failureRate: 0.14,
|
|
51
|
+
lastActivityAt: ago(3 * 60_000),
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: 'agt_onboard',
|
|
55
|
+
name: 'Onboarding Guide',
|
|
56
|
+
description: 'Walks new accounts through setup checklists',
|
|
57
|
+
status: 'idle',
|
|
58
|
+
channels: ['web', 'email'],
|
|
59
|
+
runsToday: 19,
|
|
60
|
+
failureRate: 0.01,
|
|
61
|
+
lastActivityAt: ago(42 * 60_000),
|
|
62
|
+
},
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
const customers = [
|
|
66
|
+
{ id: 'cus_mira', alias: 'Mira Chen', hint: 'WhatsApp · +1 ···· 1212' },
|
|
67
|
+
{ id: 'cus_jonas', alias: 'Jonas Reed', hint: 'Email · jonas@…' },
|
|
68
|
+
{ id: 'cus_nova', alias: 'Nova Freight', hint: 'API · tenant nova' },
|
|
69
|
+
{ id: 'cus_hala', alias: 'Hala Ortiz', hint: 'WhatsApp · +1 ···· 9981' },
|
|
70
|
+
{ id: 'cus_orbit', alias: 'Orbit Labs', hint: 'Web · portal' },
|
|
71
|
+
{ id: 'cus_eli', alias: 'Eli Park', hint: 'Email · eli@…' },
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
const runs = []
|
|
75
|
+
const eventsByRun = new Map()
|
|
76
|
+
|
|
77
|
+
function addRun(partial, events) {
|
|
78
|
+
const run = {
|
|
79
|
+
model: null,
|
|
80
|
+
inputTokens: null,
|
|
81
|
+
outputTokens: null,
|
|
82
|
+
errorCode: null,
|
|
83
|
+
externalTraceId: null,
|
|
84
|
+
inputPreview: null,
|
|
85
|
+
outputPreview: null,
|
|
86
|
+
...partial,
|
|
87
|
+
}
|
|
88
|
+
runs.push(run)
|
|
89
|
+
eventsByRun.set(
|
|
90
|
+
run.id,
|
|
91
|
+
events.map((e, i) => ({
|
|
92
|
+
id: `${run.id}:${i + 1}`,
|
|
93
|
+
runId: run.id,
|
|
94
|
+
agentId: run.agentId,
|
|
95
|
+
tenantId: run.customerId,
|
|
96
|
+
sequence: i + 1,
|
|
97
|
+
status: null,
|
|
98
|
+
completedAt: null,
|
|
99
|
+
durationMs: null,
|
|
100
|
+
...e,
|
|
101
|
+
})),
|
|
102
|
+
)
|
|
103
|
+
return run
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
addRun(
|
|
107
|
+
{
|
|
108
|
+
id: 'run_live_invoice',
|
|
109
|
+
agentId: 'agt_invoice',
|
|
110
|
+
customerId: 'cus_mira',
|
|
111
|
+
channel: 'whatsapp',
|
|
112
|
+
status: 'running',
|
|
113
|
+
startedAt: ago(2 * 60_000),
|
|
114
|
+
completedAt: null,
|
|
115
|
+
durationMs: null,
|
|
116
|
+
summary: 'Reviewing invoice pack #4421 — waiting on approval',
|
|
117
|
+
},
|
|
118
|
+
[
|
|
119
|
+
{ type: 'run.received', label: 'Inbound WhatsApp', startedAt: ago(120_000) },
|
|
120
|
+
{ type: 'context.loaded', label: 'Customer context ready', startedAt: ago(110_000) },
|
|
121
|
+
{ type: 'tool.completed', label: 'lookup_invoice', startedAt: ago(90_000) },
|
|
122
|
+
{
|
|
123
|
+
type: 'approval.waiting',
|
|
124
|
+
label: 'Send invoice summary to customer',
|
|
125
|
+
startedAt: ago(45_000),
|
|
126
|
+
metadata: { action: 'Send invoice-summary.pdf to Mira Chen' },
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
addRun(
|
|
132
|
+
{
|
|
133
|
+
id: 'run_done_invoice',
|
|
134
|
+
agentId: 'agt_invoice',
|
|
135
|
+
customerId: 'cus_hala',
|
|
136
|
+
channel: 'whatsapp',
|
|
137
|
+
status: 'completed',
|
|
138
|
+
startedAt: ago(28 * 60_000),
|
|
139
|
+
completedAt: ago(24 * 60_000),
|
|
140
|
+
durationMs: 210_000,
|
|
141
|
+
summary: 'Delivered weekly invoice summary',
|
|
142
|
+
},
|
|
143
|
+
[
|
|
144
|
+
{ type: 'run.received', label: 'Inbound WhatsApp', startedAt: ago(28 * 60_000) },
|
|
145
|
+
{ type: 'approval.approved', label: 'Customer approved send', startedAt: ago(26 * 60_000) },
|
|
146
|
+
{
|
|
147
|
+
type: 'output.published',
|
|
148
|
+
label: 'File ready',
|
|
149
|
+
startedAt: ago(25.5 * 60_000),
|
|
150
|
+
metadata: { fileName: 'invoice-summary.pdf' },
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
type: 'receipt.sent',
|
|
154
|
+
label: 'Work receipt',
|
|
155
|
+
startedAt: ago(25 * 60_000),
|
|
156
|
+
metadata: {
|
|
157
|
+
checked: ['Invoice #4418'],
|
|
158
|
+
created: ['PDF summary'],
|
|
159
|
+
approval: 'approved',
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
type: 'delivery.delivered',
|
|
164
|
+
label: 'Delivered on WhatsApp',
|
|
165
|
+
startedAt: ago(24.2 * 60_000),
|
|
166
|
+
metadata: { purpose: 'send_result' },
|
|
167
|
+
},
|
|
168
|
+
{ type: 'run.completed', label: 'Completed', startedAt: ago(24 * 60_000) },
|
|
169
|
+
],
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
addRun(
|
|
173
|
+
{
|
|
174
|
+
id: 'run_fail_billing',
|
|
175
|
+
agentId: 'agt_billing',
|
|
176
|
+
customerId: 'cus_eli',
|
|
177
|
+
channel: 'email',
|
|
178
|
+
status: 'failed',
|
|
179
|
+
startedAt: ago(55 * 60_000),
|
|
180
|
+
completedAt: ago(52 * 60_000),
|
|
181
|
+
durationMs: 180_000,
|
|
182
|
+
summary: 'Reminder prepared but delivery failed',
|
|
183
|
+
},
|
|
184
|
+
[
|
|
185
|
+
{ type: 'run.received', label: 'Scheduled reminder', startedAt: ago(55 * 60_000) },
|
|
186
|
+
{
|
|
187
|
+
type: 'output.published',
|
|
188
|
+
label: 'Reminder draft',
|
|
189
|
+
startedAt: ago(53.5 * 60_000),
|
|
190
|
+
metadata: { fileName: 'payment-reminder.eml' },
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
type: 'delivery.started',
|
|
194
|
+
label: 'Sending email',
|
|
195
|
+
startedAt: ago(53 * 60_000),
|
|
196
|
+
metadata: { purpose: 'payment_reminder' },
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
type: 'delivery.failed',
|
|
200
|
+
label: 'Mailbox rejected',
|
|
201
|
+
startedAt: ago(52.4 * 60_000),
|
|
202
|
+
metadata: { purpose: 'payment_reminder' },
|
|
203
|
+
},
|
|
204
|
+
{ type: 'run.failed', label: 'Stopped', startedAt: ago(52 * 60_000) },
|
|
205
|
+
],
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
addRun(
|
|
209
|
+
{
|
|
210
|
+
id: 'run_support_live',
|
|
211
|
+
agentId: 'agt_support',
|
|
212
|
+
customerId: 'cus_jonas',
|
|
213
|
+
channel: 'email',
|
|
214
|
+
status: 'running',
|
|
215
|
+
startedAt: ago(90_000),
|
|
216
|
+
completedAt: null,
|
|
217
|
+
durationMs: null,
|
|
218
|
+
summary: 'Drafting reply for shipping delay ticket',
|
|
219
|
+
},
|
|
220
|
+
[
|
|
221
|
+
{ type: 'run.received', label: 'Inbound email', startedAt: ago(90_000) },
|
|
222
|
+
{ type: 'context.loading', label: 'Loading ticket thread', startedAt: ago(80_000) },
|
|
223
|
+
{ type: 'tool.started', label: 'search_orders', startedAt: ago(40_000) },
|
|
224
|
+
],
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
addRun(
|
|
228
|
+
{
|
|
229
|
+
id: 'run_ops_done',
|
|
230
|
+
agentId: 'agt_ops',
|
|
231
|
+
customerId: 'cus_nova',
|
|
232
|
+
channel: 'api',
|
|
233
|
+
status: 'completed',
|
|
234
|
+
startedAt: ago(3 * 60 * 60_000),
|
|
235
|
+
completedAt: ago(3 * 60 * 60_000 - 90_000),
|
|
236
|
+
durationMs: 90_000,
|
|
237
|
+
summary: 'Confirmed two field jobs via API',
|
|
238
|
+
},
|
|
239
|
+
[
|
|
240
|
+
{ type: 'run.received', label: 'API job.confirm', startedAt: ago(3 * 60 * 60_000) },
|
|
241
|
+
{ type: 'tool.completed', label: 'schedule_jobs', startedAt: ago(3 * 60 * 60_000 - 40_000) },
|
|
242
|
+
{ type: 'run.completed', label: 'Completed', startedAt: ago(3 * 60 * 60_000 - 90_000) },
|
|
243
|
+
],
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
addRun(
|
|
247
|
+
{
|
|
248
|
+
id: 'run_onboard_web',
|
|
249
|
+
agentId: 'agt_onboard',
|
|
250
|
+
customerId: 'cus_orbit',
|
|
251
|
+
channel: 'web',
|
|
252
|
+
status: 'completed',
|
|
253
|
+
startedAt: ago(5 * 60 * 60_000),
|
|
254
|
+
completedAt: ago(5 * 60 * 60_000 - 4 * 60_000),
|
|
255
|
+
durationMs: 240_000,
|
|
256
|
+
summary: 'Finished onboarding checklist',
|
|
257
|
+
},
|
|
258
|
+
[
|
|
259
|
+
{ type: 'run.received', label: 'Portal session', startedAt: ago(5 * 60 * 60_000) },
|
|
260
|
+
{ type: 'progress.sent', label: 'Step 3 of 5', startedAt: ago(5 * 60 * 60_000 - 90_000) },
|
|
261
|
+
{ type: 'run.completed', label: 'Completed', startedAt: ago(5 * 60 * 60_000 - 4 * 60_000) },
|
|
262
|
+
],
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
const touchpoints = [
|
|
266
|
+
{ id: 'tp1', channel: 'whatsapp', customerId: 'cus_mira', agentId: 'agt_invoice', at: ago(120_000), action: 'Message received' },
|
|
267
|
+
{ id: 'tp2', channel: 'email', customerId: 'cus_jonas', agentId: 'agt_support', at: ago(90_000), action: 'Ticket opened' },
|
|
268
|
+
{ id: 'tp3', channel: 'whatsapp', customerId: 'cus_hala', agentId: 'agt_invoice', at: ago(24 * 60_000), action: 'Delivery read' },
|
|
269
|
+
{ id: 'tp4', channel: 'email', customerId: 'cus_eli', agentId: 'agt_billing', at: ago(52 * 60_000), action: 'Bounce received' },
|
|
270
|
+
{ id: 'tp5', channel: 'api', customerId: 'cus_nova', agentId: 'agt_ops', at: ago(3 * 60 * 60_000), action: 'job.confirm' },
|
|
271
|
+
{ id: 'tp6', channel: 'web', customerId: 'cus_orbit', agentId: 'agt_onboard', at: ago(5 * 60 * 60_000), action: 'Checklist started' },
|
|
272
|
+
{ id: 'tp7', channel: 'whatsapp', customerId: 'cus_mira', agentId: 'agt_billing', at: ago(6 * 60 * 60_000), action: 'Reminder opened' },
|
|
273
|
+
{ id: 'tp8', channel: 'email', customerId: 'cus_jonas', agentId: 'agt_support', at: ago(7 * 60 * 60_000), action: 'Follow-up reply' },
|
|
274
|
+
{ id: 'tp9', channel: 'web', customerId: 'cus_orbit', agentId: 'agt_support', at: ago(8 * 60 * 60_000), action: 'Help center chat' },
|
|
275
|
+
{ id: 'tp10', channel: 'api', customerId: 'cus_nova', agentId: 'agt_ops', at: ago(9 * 60 * 60_000), action: 'status.poll' },
|
|
276
|
+
{ id: 'tp11', channel: 'whatsapp', customerId: 'cus_hala', agentId: 'agt_invoice', at: ago(10 * 60 * 60_000), action: 'Approval granted' },
|
|
277
|
+
{ id: 'tp12', channel: 'email', customerId: 'cus_eli', agentId: 'agt_billing', at: ago(11 * 60 * 60_000), action: 'Reminder queued' },
|
|
278
|
+
]
|
|
279
|
+
|
|
280
|
+
return { agents, customers, runs, eventsByRun, touchpoints }
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export const db = seed()
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
export function agentById(id) {
|
|
287
|
+
return db.agents.find((a) => a.id === id)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export function customerById(id) {
|
|
291
|
+
return db.customers.find((c) => c.id === id)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export function enrichRun(run) {
|
|
295
|
+
const agent = agentById(run.agentId)
|
|
296
|
+
const customer = customerById(run.customerId)
|
|
297
|
+
return {
|
|
298
|
+
...run,
|
|
299
|
+
agentName: agent?.name ?? run.agentId,
|
|
300
|
+
customerAlias: customer?.alias ?? run.customerId,
|
|
301
|
+
customerHint: customer?.hint ?? '',
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function channelBreakdown() {
|
|
306
|
+
const counts = {}
|
|
307
|
+
for (const t of db.touchpoints) {
|
|
308
|
+
counts[t.channel] = (counts[t.channel] || 0) + 1
|
|
309
|
+
}
|
|
310
|
+
const total = db.touchpoints.length || 1
|
|
311
|
+
return Object.entries(counts)
|
|
312
|
+
.map(([channel, count]) => ({
|
|
313
|
+
channel,
|
|
314
|
+
count,
|
|
315
|
+
pct: Math.round((count / total) * 100),
|
|
316
|
+
}))
|
|
317
|
+
.sort((a, b) => b.count - a.count)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function runHasOpenApproval(run) {
|
|
321
|
+
if (run.status !== 'running') return false
|
|
322
|
+
const events = db.eventsByRun.get(run.id) || []
|
|
323
|
+
const waiting = events.filter((e) => e.type === 'approval.waiting')
|
|
324
|
+
if (!waiting.length) return false
|
|
325
|
+
const resolved = events.some((e) => e.type === 'approval.approved' || e.type === 'approval.cancelled')
|
|
326
|
+
return !resolved
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function openApprovalEvent(run) {
|
|
330
|
+
if (!runHasOpenApproval(run)) return null
|
|
331
|
+
const events = db.eventsByRun.get(run.id) || []
|
|
332
|
+
return events.findLast?.((e) => e.type === 'approval.waiting') || events.filter((e) => e.type === 'approval.waiting').at(-1)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export function buildOverview() {
|
|
336
|
+
const openApprovals = db.runs.filter((r) => runHasOpenApproval(r)).length
|
|
337
|
+
const failedDeliveries = db.runs.filter((r) =>
|
|
338
|
+
(db.eventsByRun.get(r.id) || []).some((e) => e.type === 'delivery.failed'),
|
|
339
|
+
).length
|
|
340
|
+
const activeAgents = db.agents.filter((a) => a.status === 'busy' || a.status === 'degraded').length
|
|
341
|
+
const touchesToday = db.touchpoints.length
|
|
342
|
+
const running = db.runs.filter((r) => r.status === 'running').length
|
|
343
|
+
|
|
344
|
+
return {
|
|
345
|
+
org: ORG,
|
|
346
|
+
stats: {
|
|
347
|
+
activeAgents,
|
|
348
|
+
agentCount: db.agents.length,
|
|
349
|
+
openApprovals,
|
|
350
|
+
failedDeliveries,
|
|
351
|
+
touchesToday,
|
|
352
|
+
runningRuns: running,
|
|
353
|
+
},
|
|
354
|
+
recentRuns: db.runs
|
|
355
|
+
.slice()
|
|
356
|
+
.sort((a, b) => Date.parse(b.startedAt) - Date.parse(a.startedAt))
|
|
357
|
+
.slice(0, 6)
|
|
358
|
+
.map(enrichRun),
|
|
359
|
+
channelBreakdown: channelBreakdown(),
|
|
360
|
+
attention: [
|
|
361
|
+
...db.runs.flatMap((r) => {
|
|
362
|
+
const waiting = openApprovalEvent(r)
|
|
363
|
+
if (!waiting) return []
|
|
364
|
+
return [
|
|
365
|
+
{
|
|
366
|
+
kind: 'approval',
|
|
367
|
+
runId: r.id,
|
|
368
|
+
title: waiting.metadata?.action || waiting.label,
|
|
369
|
+
agentName: agentById(r.agentId)?.name,
|
|
370
|
+
customerAlias: customerById(r.customerId)?.alias,
|
|
371
|
+
at: waiting.startedAt,
|
|
372
|
+
},
|
|
373
|
+
]
|
|
374
|
+
}),
|
|
375
|
+
...db.runs
|
|
376
|
+
.filter((r) => (db.eventsByRun.get(r.id) || []).some((e) => e.type === 'delivery.failed'))
|
|
377
|
+
.map((r) => ({
|
|
378
|
+
kind: 'delivery_failed',
|
|
379
|
+
runId: r.id,
|
|
380
|
+
title: r.summary,
|
|
381
|
+
agentName: agentById(r.agentId)?.name,
|
|
382
|
+
customerAlias: customerById(r.customerId)?.alias,
|
|
383
|
+
at: r.completedAt || r.startedAt,
|
|
384
|
+
})),
|
|
385
|
+
],
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export function listApprovals(agentId) {
|
|
390
|
+
const items = []
|
|
391
|
+
for (const run of db.runs) {
|
|
392
|
+
if (agentId && run.agentId !== agentId) continue
|
|
393
|
+
for (const e of db.eventsByRun.get(run.id) || []) {
|
|
394
|
+
if (!e.type.startsWith('approval.')) continue
|
|
395
|
+
items.push({
|
|
396
|
+
id: e.id,
|
|
397
|
+
runId: run.id,
|
|
398
|
+
type: e.type,
|
|
399
|
+
label: e.metadata?.action || e.label,
|
|
400
|
+
status: e.type === 'approval.waiting' ? 'waiting' : e.type === 'approval.approved' ? 'approved' : 'cancelled',
|
|
401
|
+
at: e.startedAt,
|
|
402
|
+
agentId: run.agentId,
|
|
403
|
+
agentName: agentById(run.agentId)?.name,
|
|
404
|
+
customerAlias: customerById(run.customerId)?.alias,
|
|
405
|
+
customerHint: customerById(run.customerId)?.hint,
|
|
406
|
+
channel: run.channel,
|
|
407
|
+
open: e.type === 'approval.waiting' && runHasOpenApproval(run),
|
|
408
|
+
})
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
return items.sort((a, b) => Date.parse(b.at) - Date.parse(a.at))
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
export function listDeliveries(agentId) {
|
|
415
|
+
const items = []
|
|
416
|
+
for (const run of db.runs) {
|
|
417
|
+
if (agentId && run.agentId !== agentId) continue
|
|
418
|
+
for (const e of db.eventsByRun.get(run.id) || []) {
|
|
419
|
+
if (!e.type.startsWith('delivery.')) continue
|
|
420
|
+
let outcome = 'in_progress'
|
|
421
|
+
if (e.type === 'delivery.delivered' || e.type === 'delivery.completed' || e.type === 'delivery.read') {
|
|
422
|
+
outcome = 'delivered'
|
|
423
|
+
} else if (e.type === 'delivery.failed') {
|
|
424
|
+
outcome = 'failed'
|
|
425
|
+
} else if (e.type === 'delivery.accepted' || e.type === 'delivery.sent') {
|
|
426
|
+
outcome = 'in_progress'
|
|
427
|
+
}
|
|
428
|
+
items.push({
|
|
429
|
+
id: e.id,
|
|
430
|
+
runId: run.id,
|
|
431
|
+
type: e.type,
|
|
432
|
+
outcome,
|
|
433
|
+
purpose: e.metadata?.purpose || e.label,
|
|
434
|
+
at: e.startedAt,
|
|
435
|
+
agentId: run.agentId,
|
|
436
|
+
agentName: agentById(run.agentId)?.name,
|
|
437
|
+
customerAlias: customerById(run.customerId)?.alias,
|
|
438
|
+
channel: run.channel,
|
|
439
|
+
summary: run.summary,
|
|
440
|
+
})
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
return items.sort((a, b) => Date.parse(b.at) - Date.parse(a.at))
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
export function startSimulation(broadcast) {
|
|
448
|
+
// Simulate live fleet activity
|
|
449
|
+
let tick = 0
|
|
450
|
+
return setInterval(() => {
|
|
451
|
+
tick += 1
|
|
452
|
+
const support = db.runs.find((r) => r.id === 'run_support_live')
|
|
453
|
+
if (support && tick === 1) {
|
|
454
|
+
const list = db.eventsByRun.get(support.id) || []
|
|
455
|
+
const next = {
|
|
456
|
+
id: `${support.id}:${list.length + 1}`,
|
|
457
|
+
runId: support.id,
|
|
458
|
+
agentId: support.agentId,
|
|
459
|
+
tenantId: support.customerId,
|
|
460
|
+
sequence: list.length + 1,
|
|
461
|
+
type: 'tool.completed',
|
|
462
|
+
status: null,
|
|
463
|
+
label: 'search_orders finished',
|
|
464
|
+
metadata: null,
|
|
465
|
+
startedAt: new Date().toISOString(),
|
|
466
|
+
completedAt: null,
|
|
467
|
+
durationMs: null,
|
|
468
|
+
}
|
|
469
|
+
list.push(next)
|
|
470
|
+
support.lastEventAt = next.startedAt
|
|
471
|
+
const agent = agentById(support.agentId)
|
|
472
|
+
if (agent) agent.lastActivityAt = next.startedAt
|
|
473
|
+
broadcast('run-event', {
|
|
474
|
+
...next,
|
|
475
|
+
agentName: agent?.name,
|
|
476
|
+
customerAlias: customerById(support.customerId)?.alias,
|
|
477
|
+
})
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (tick === 2) {
|
|
481
|
+
const touch = {
|
|
482
|
+
id: `tp_live_${Date.now()}`,
|
|
483
|
+
channel: 'whatsapp',
|
|
484
|
+
customerId: 'cus_mira',
|
|
485
|
+
agentId: 'agt_invoice',
|
|
486
|
+
at: new Date().toISOString(),
|
|
487
|
+
action: 'Customer opened approval prompt',
|
|
488
|
+
agentName: agentById('agt_invoice')?.name,
|
|
489
|
+
customerAlias: customerById('cus_mira')?.alias,
|
|
490
|
+
}
|
|
491
|
+
db.touchpoints.unshift(touch)
|
|
492
|
+
broadcast('touch', touch)
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (tick === 3) {
|
|
496
|
+
const support = db.runs.find((r) => r.id === 'run_support_live')
|
|
497
|
+
if (!support) return
|
|
498
|
+
const list = db.eventsByRun.get(support.id) || []
|
|
499
|
+
const next = {
|
|
500
|
+
id: `${support.id}:${list.length + 1}`,
|
|
501
|
+
runId: support.id,
|
|
502
|
+
agentId: support.agentId,
|
|
503
|
+
tenantId: support.customerId,
|
|
504
|
+
sequence: list.length + 1,
|
|
505
|
+
type: 'model.started',
|
|
506
|
+
status: null,
|
|
507
|
+
label: 'Drafting grounded reply',
|
|
508
|
+
metadata: null,
|
|
509
|
+
startedAt: new Date().toISOString(),
|
|
510
|
+
completedAt: null,
|
|
511
|
+
durationMs: null,
|
|
512
|
+
}
|
|
513
|
+
list.push(next)
|
|
514
|
+
const agent = agentById(support.agentId)
|
|
515
|
+
if (agent) agent.lastActivityAt = next.startedAt
|
|
516
|
+
broadcast('run-event', {
|
|
517
|
+
...next,
|
|
518
|
+
agentName: agent?.name,
|
|
519
|
+
customerAlias: customerById(support.customerId)?.alias,
|
|
520
|
+
})
|
|
521
|
+
}
|
|
522
|
+
}, 5000)
|
|
523
|
+
|
|
524
|
+
}
|
package/server/http.mjs
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { createServer as createHttpServer } from 'node:http'
|
|
2
|
+
import { readFile } from 'node:fs/promises'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import { resolve, sep, extname } from 'node:path'
|
|
5
|
+
import { db, buildOverview as buildDemoOverview, channelBreakdown as demoChannelBreakdown, enrichRun, listApprovals as listDemoApprovals, listDeliveries as listDemoDeliveries, agentById, customerById, startSimulation } from './demo/data.mjs'
|
|
6
|
+
import { createRealData } from './real/data.mjs'
|
|
7
|
+
|
|
8
|
+
const defaultAssets = fileURLToPath(new URL('../../console/dist/', import.meta.url))
|
|
9
|
+
const mimeTypes = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript; charset=utf-8', '.css': 'text/css; charset=utf-8', '.svg': 'image/svg+xml', '.woff2': 'font/woff2', '.png': 'image/png', '.ico': 'image/x-icon' }
|
|
10
|
+
export function sendJson(res, status, body) {
|
|
11
|
+
res.writeHead(status, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' }).end(JSON.stringify(body))
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function demoData() {
|
|
15
|
+
return {
|
|
16
|
+
buildOverview: buildDemoOverview,
|
|
17
|
+
listAgents: () => ({ agents: db.agents }),
|
|
18
|
+
listTouchpoints: () => ({
|
|
19
|
+
breakdown: demoChannelBreakdown(),
|
|
20
|
+
recent: db.touchpoints.slice().sort((a, b) => Date.parse(b.at) - Date.parse(a.at)).map(t => ({ ...t, agentName: agentById(t.agentId)?.name, customerAlias: customerById(t.customerId)?.alias })),
|
|
21
|
+
}),
|
|
22
|
+
listRuns: (agentId) => ({ runs: db.runs.filter(r => !agentId || r.agentId === agentId).map(enrichRun).sort((a, b) => Date.parse(b.startedAt) - Date.parse(a.startedAt)) }),
|
|
23
|
+
listApprovals: (agentId) => ({ approvals: listDemoApprovals(agentId) }),
|
|
24
|
+
listDeliveries: (agentId) => ({ deliveries: listDemoDeliveries(agentId) }),
|
|
25
|
+
subscribe: (broadcast) => { const sim = startSimulation(broadcast); return () => clearInterval(sim) },
|
|
26
|
+
dispose() {},
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** `mode: 'demo'` serves fabricated sample data; `mode: 'real'` reads the SDK's SQLite store at `dbPath`. */
|
|
31
|
+
export function createServer({ mode = 'demo', dbPath, assetsDir = defaultAssets, simulate = true } = {}) {
|
|
32
|
+
const data = mode === 'real' ? createRealData({ dbPath }) : demoData()
|
|
33
|
+
const clients = new Set()
|
|
34
|
+
const heartbeats = new Set()
|
|
35
|
+
function broadcast(event, payload) {
|
|
36
|
+
for (const client of clients) client.write(`event: ${event}\ndata: ${JSON.stringify(payload)}\n\n`)
|
|
37
|
+
}
|
|
38
|
+
const unsubscribe = (mode === 'demo' ? simulate : true) ? data.subscribe(broadcast) : null
|
|
39
|
+
const server = createHttpServer((req, res) => {
|
|
40
|
+
handle(req, res).catch(() => {
|
|
41
|
+
if (!res.headersSent) sendJson(res, 500, { error: 'Request failed' })
|
|
42
|
+
else res.end()
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
async function handle(req, res) {
|
|
46
|
+
const url = new URL(req.url || '/', 'http://localhost')
|
|
47
|
+
const agentId = url.searchParams.get('agentId') || undefined
|
|
48
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
49
|
+
res.setHeader('Allow', 'GET, HEAD')
|
|
50
|
+
return sendJson(res, 405, { error: 'Method not allowed' })
|
|
51
|
+
}
|
|
52
|
+
if (url.pathname === '/api/overview') return sendJson(res, 200, data.buildOverview())
|
|
53
|
+
if (url.pathname === '/api/agents') return sendJson(res, 200, data.listAgents())
|
|
54
|
+
if (url.pathname === '/api/touchpoints') return sendJson(res, 200, data.listTouchpoints())
|
|
55
|
+
if (url.pathname === '/api/runs') return sendJson(res, 200, data.listRuns(agentId))
|
|
56
|
+
if (url.pathname === '/api/approvals') return sendJson(res, 200, data.listApprovals(agentId))
|
|
57
|
+
if (url.pathname === '/api/deliveries') return sendJson(res, 200, data.listDeliveries(agentId))
|
|
58
|
+
if (url.pathname === '/api/stream') {
|
|
59
|
+
res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'X-Accel-Buffering': 'no' })
|
|
60
|
+
if (req.method === 'HEAD') return res.end()
|
|
61
|
+
res.write(': connected\n\n')
|
|
62
|
+
clients.add(res)
|
|
63
|
+
const heartbeat = setInterval(() => res.write(': ping\n\n'), 15000)
|
|
64
|
+
heartbeats.add(heartbeat)
|
|
65
|
+
res.on('close', () => { clearInterval(heartbeat); heartbeats.delete(heartbeat); clients.delete(res) })
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
if (url.pathname.startsWith('/api/') || url.pathname === '/api') return sendJson(res, 404, { error: 'Not found' })
|
|
69
|
+
let pathname
|
|
70
|
+
try { pathname = decodeURIComponent(url.pathname) } catch { return sendJson(res, 400, { error: 'Invalid path' }) }
|
|
71
|
+
const root = resolve(assetsDir)
|
|
72
|
+
const target = resolve(root, '.' + pathname)
|
|
73
|
+
if (target !== root && !target.startsWith(root + sep)) return sendJson(res, 403, { error: 'Forbidden' })
|
|
74
|
+
const isAsset = pathname.startsWith('/assets/') || extname(pathname) !== ''
|
|
75
|
+
const file = isAsset ? target : resolve(root, 'index.html')
|
|
76
|
+
try {
|
|
77
|
+
const body = await readFile(file)
|
|
78
|
+
res.writeHead(200, {
|
|
79
|
+
'Content-Type': mimeTypes[extname(file)] || 'application/octet-stream',
|
|
80
|
+
'Cache-Control': pathname.startsWith('/assets/') ? 'public, max-age=31536000, immutable' : 'no-cache',
|
|
81
|
+
'X-Content-Type-Options': 'nosniff',
|
|
82
|
+
}).end(req.method === 'HEAD' ? undefined : body)
|
|
83
|
+
} catch {
|
|
84
|
+
sendJson(res, isAsset ? 404 : 503, { error: isAsset ? 'Asset not found' : 'Console is not built. Run npm run build first, or use npm run dev.' })
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function dispose() {
|
|
88
|
+
if (unsubscribe) unsubscribe()
|
|
89
|
+
for (const timer of heartbeats) clearInterval(timer)
|
|
90
|
+
for (const client of clients) client.end()
|
|
91
|
+
heartbeats.clear()
|
|
92
|
+
clients.clear()
|
|
93
|
+
data.dispose()
|
|
94
|
+
}
|
|
95
|
+
server.on('close', dispose)
|
|
96
|
+
return { server, dispose }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Back-compat wrapper — `npm run demo` and existing tests import this directly. */
|
|
100
|
+
export function createDemoServer({ assetsDir = defaultAssets, simulate = true } = {}) {
|
|
101
|
+
return createServer({ mode: 'demo', assetsDir, simulate })
|
|
102
|
+
}
|