@tangle-network/agent-app 0.8.1 → 0.9.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/dist/{chunk-SDOT7RNB.js → chunk-CPI3RILI.js} +4 -2
- package/dist/chunk-CPI3RILI.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/platform/index.d.ts +46 -6
- package/dist/platform/index.js +38 -7
- package/dist/platform/index.js.map +1 -1
- package/dist/stream/index.js +1 -1
- package/dist/trace/index.d.ts +71 -0
- package/dist/trace/index.js +145 -0
- package/dist/trace/index.js.map +1 -0
- package/dist/web-react/index.d.ts +12 -4
- package/dist/web-react/index.js +125 -56
- package/dist/web-react/index.js.map +1 -1
- package/package.json +7 -1
- package/dist/chunk-SDOT7RNB.js.map +0 -1
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// src/trace/index.ts
|
|
2
|
+
function timedEventsFromLines(lines) {
|
|
3
|
+
const out = [];
|
|
4
|
+
for (const line of lines) {
|
|
5
|
+
try {
|
|
6
|
+
const parsed = JSON.parse(line);
|
|
7
|
+
if (typeof parsed._t === "number") out.push({ t: parsed._t, event: parsed });
|
|
8
|
+
} catch {
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
return out.sort((a, b) => a.t - b.t);
|
|
12
|
+
}
|
|
13
|
+
function innerOf(e) {
|
|
14
|
+
return (e.kind === "event" ? e.event : e) ?? {};
|
|
15
|
+
}
|
|
16
|
+
function buildFlowTrace(events, opts) {
|
|
17
|
+
const spans = [];
|
|
18
|
+
let promptTokens = 0;
|
|
19
|
+
let completionTokens = 0;
|
|
20
|
+
let toolCalls = 0;
|
|
21
|
+
const first = events[0]?.t ?? 0;
|
|
22
|
+
if (first > 0) {
|
|
23
|
+
spans.push({ kind: "pipeline", name: "dispatch \u2192 first event", startMs: 0, endMs: first });
|
|
24
|
+
}
|
|
25
|
+
let segStart = null;
|
|
26
|
+
let segEnd = 0;
|
|
27
|
+
let segKinds = /* @__PURE__ */ new Set();
|
|
28
|
+
let lastDeltaT = first;
|
|
29
|
+
const openCalls = /* @__PURE__ */ new Map();
|
|
30
|
+
const closeSegment = () => {
|
|
31
|
+
if (segStart !== null) {
|
|
32
|
+
spans.push({
|
|
33
|
+
kind: "model",
|
|
34
|
+
name: segKinds.has("reasoning") ? "model turn (reasoning + text)" : "model turn",
|
|
35
|
+
startMs: segStart,
|
|
36
|
+
endMs: segEnd,
|
|
37
|
+
approx: true
|
|
38
|
+
});
|
|
39
|
+
segStart = null;
|
|
40
|
+
segKinds = /* @__PURE__ */ new Set();
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
for (const { t, event } of events) {
|
|
44
|
+
const inner = innerOf(event);
|
|
45
|
+
const type = String(event.kind === "tool_result" ? "tool_result" : inner.type ?? "");
|
|
46
|
+
if (type === "text" || type === "reasoning") {
|
|
47
|
+
if (segStart === null) segStart = t;
|
|
48
|
+
segEnd = t;
|
|
49
|
+
segKinds.add(type);
|
|
50
|
+
lastDeltaT = t;
|
|
51
|
+
} else if (type === "tool_call") {
|
|
52
|
+
closeSegment();
|
|
53
|
+
toolCalls++;
|
|
54
|
+
const call = inner.call ?? inner;
|
|
55
|
+
const id = String(call.toolCallId ?? `call_${toolCalls}`);
|
|
56
|
+
openCalls.set(id, { name: String(call.toolName ?? "tool"), emitT: t, lastDeltaT });
|
|
57
|
+
} else if (type === "tool_result") {
|
|
58
|
+
const id = String(event.toolCallId ?? inner.toolCallId ?? "");
|
|
59
|
+
const open = openCalls.get(id);
|
|
60
|
+
if (open) {
|
|
61
|
+
spans.push({
|
|
62
|
+
kind: "tool",
|
|
63
|
+
name: open.name,
|
|
64
|
+
// Execution happens between the end of the model turn that emitted
|
|
65
|
+
// the call and the result landing in the buffer.
|
|
66
|
+
startMs: open.lastDeltaT,
|
|
67
|
+
endMs: t,
|
|
68
|
+
approx: true,
|
|
69
|
+
meta: { ok: (event.outcome ?? inner.outcome)?.ok }
|
|
70
|
+
});
|
|
71
|
+
openCalls.delete(id);
|
|
72
|
+
}
|
|
73
|
+
} else if (type === "usage") {
|
|
74
|
+
const u = inner.usage ?? {};
|
|
75
|
+
promptTokens += u.promptTokens ?? 0;
|
|
76
|
+
completionTokens += u.completionTokens ?? 0;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
closeSegment();
|
|
80
|
+
const totalMs = events.length ? events[events.length - 1].t : 0;
|
|
81
|
+
const trace = { spans, totalMs, promptTokens, completionTokens, toolCalls };
|
|
82
|
+
const p = opts?.pricing;
|
|
83
|
+
if (p && (p.prompt != null || p.completion != null)) {
|
|
84
|
+
trace.costUsd = promptTokens * Number(p.prompt ?? 0) + completionTokens * Number(p.completion ?? 0);
|
|
85
|
+
}
|
|
86
|
+
return trace;
|
|
87
|
+
}
|
|
88
|
+
var fmtS = (ms) => `${(ms / 1e3).toFixed(1)}s`;
|
|
89
|
+
function renderWaterfall(trace, opts) {
|
|
90
|
+
const width = opts?.width ?? 40;
|
|
91
|
+
const scale = trace.totalMs > 0 ? width / trace.totalMs : 0;
|
|
92
|
+
const lines = [];
|
|
93
|
+
const spans = [...trace.spans].sort((a, b) => a.startMs - b.startMs);
|
|
94
|
+
for (let i = 0; i < spans.length; i++) {
|
|
95
|
+
const s = spans[i];
|
|
96
|
+
const offset = Math.round(s.startMs * scale);
|
|
97
|
+
const len = Math.max(1, Math.round((s.endMs - s.startMs) * scale));
|
|
98
|
+
const bar = " ".repeat(offset) + (s.kind === "tool" ? "\u2593" : s.kind === "pipeline" ? "\u2591" : "\u2588").repeat(len);
|
|
99
|
+
const branch = i === spans.length - 1 ? "\u2514\u2500" : "\u251C\u2500";
|
|
100
|
+
const dur = `${fmtS(s.endMs - s.startMs)}${s.approx ? "~" : ""}`;
|
|
101
|
+
lines.push(`${fmtS(s.startMs).padStart(7)} ${branch} ${bar.padEnd(width + 2)} ${s.name} (${dur})`);
|
|
102
|
+
}
|
|
103
|
+
const cost = trace.costUsd != null ? ` $${trace.costUsd.toFixed(trace.costUsd < 0.01 ? 6 : 4)}` : "";
|
|
104
|
+
lines.push(
|
|
105
|
+
`${fmtS(trace.totalMs).padStart(7)} \u2500\u2500 total \xB7 ${trace.promptTokens}p + ${trace.completionTokens}c tok \xB7 ${trace.toolCalls} tool calls${cost}`
|
|
106
|
+
);
|
|
107
|
+
return lines.join("\n");
|
|
108
|
+
}
|
|
109
|
+
function summarize(values) {
|
|
110
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
111
|
+
const q = (p) => sorted[Math.min(sorted.length - 1, Math.floor(p * sorted.length))] ?? 0;
|
|
112
|
+
return { n: sorted.length, min: sorted[0] ?? 0, p50: q(0.5), p90: q(0.9), max: sorted[sorted.length - 1] ?? 0 };
|
|
113
|
+
}
|
|
114
|
+
function renderHistogram(values, opts) {
|
|
115
|
+
if (!values.length) return "(no samples)";
|
|
116
|
+
const buckets = opts?.buckets ?? 6;
|
|
117
|
+
const width = opts?.width ?? 24;
|
|
118
|
+
const fmt = opts?.format ?? ((v) => `${Math.round(v)}${opts?.unit ?? ""}`);
|
|
119
|
+
const s = summarize(values);
|
|
120
|
+
const lo = s.min;
|
|
121
|
+
const hi = s.max === s.min ? s.min + 1 : s.max;
|
|
122
|
+
const counts = new Array(buckets).fill(0);
|
|
123
|
+
for (const v of values) {
|
|
124
|
+
counts[Math.min(buckets - 1, Math.floor((v - lo) / (hi - lo) * buckets))]++;
|
|
125
|
+
}
|
|
126
|
+
const maxCount = Math.max(...counts);
|
|
127
|
+
const lines = [
|
|
128
|
+
`n=${s.n} min=${fmt(s.min)} p50=${fmt(s.p50)} p90=${fmt(s.p90)} max=${fmt(s.max)}`
|
|
129
|
+
];
|
|
130
|
+
for (let i = 0; i < buckets; i++) {
|
|
131
|
+
const a = lo + (hi - lo) * i / buckets;
|
|
132
|
+
const b = lo + (hi - lo) * (i + 1) / buckets;
|
|
133
|
+
const bar = "\u2588".repeat(Math.max(counts[i] > 0 ? 1 : 0, Math.round(counts[i] / maxCount * width)));
|
|
134
|
+
lines.push(`${fmt(a).padStart(8)}-${fmt(b).padEnd(8)} ${bar} ${counts[i]}`);
|
|
135
|
+
}
|
|
136
|
+
return lines.join("\n");
|
|
137
|
+
}
|
|
138
|
+
export {
|
|
139
|
+
buildFlowTrace,
|
|
140
|
+
renderHistogram,
|
|
141
|
+
renderWaterfall,
|
|
142
|
+
summarize,
|
|
143
|
+
timedEventsFromLines
|
|
144
|
+
};
|
|
145
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/trace/index.ts"],"sourcesContent":["/**\n * `@tangle-network/agent-app/trace` — flow observability for agent turns.\n *\n * The turn buffer stamps `_t` (ms since turn start) on every event, so any\n * live stream OR any historical turn replayed from a TurnEventStore can be\n * reconstructed into a span trace: pipeline overhead, model segments (with\n * thinking TTFT), tool executions, token usage, and cost. Renderers turn\n * traces and multi-run samples into ASCII waterfalls and histograms — the\n * default artifact for \"how did this run actually behave\" questions across\n * evals, hill-climbs, and production debugging.\n *\n * Span boundaries derived from a buffered stream are quantized by the\n * pump's flush window and the reader's poll cadence (~100–400ms); spans\n * carry `approx: true` to keep reports honest about that.\n */\n\nexport interface TimedEvent {\n /** ms since turn start (`_t` stamped by pumpBufferedTurn). */\n t: number\n event: Record<string, unknown>\n}\n\nexport interface FlowSpan {\n kind: 'pipeline' | 'model' | 'tool'\n name: string\n startMs: number\n endMs: number\n approx?: boolean\n meta?: Record<string, unknown>\n}\n\nexport interface FlowTrace {\n spans: FlowSpan[]\n totalMs: number\n promptTokens: number\n completionTokens: number\n /** Computed when per-token pricing is supplied. */\n costUsd?: number\n toolCalls: number\n}\n\n/** Parse stored turn-event lines (JSON strings with `_t`) into TimedEvents. */\nexport function timedEventsFromLines(lines: string[]): TimedEvent[] {\n const out: TimedEvent[] = []\n for (const line of lines) {\n try {\n const parsed = JSON.parse(line) as Record<string, unknown>\n if (typeof parsed._t === 'number') out.push({ t: parsed._t, event: parsed })\n } catch {\n /* skip torn lines */\n }\n }\n return out.sort((a, b) => a.t - b.t)\n}\n\nfunction innerOf(e: Record<string, unknown>): Record<string, unknown> {\n return (e.kind === 'event' ? (e.event as Record<string, unknown>) : e) ?? {}\n}\n\n/**\n * Derive a span trace from timestamped turn events. Model segments are runs\n * of text/reasoning deltas; a tool span opens at the last delta before its\n * tool_call emission and closes at the matching tool_result.\n */\nexport function buildFlowTrace(\n events: TimedEvent[],\n opts?: { pricing?: { prompt?: string | number; completion?: string | number } },\n): FlowTrace {\n const spans: FlowSpan[] = []\n let promptTokens = 0\n let completionTokens = 0\n let toolCalls = 0\n\n const first = events[0]?.t ?? 0\n if (first > 0) {\n spans.push({ kind: 'pipeline', name: 'dispatch → first event', startMs: 0, endMs: first })\n }\n\n let segStart: number | null = null\n let segEnd = 0\n let segKinds = new Set<string>()\n let lastDeltaT = first\n const openCalls = new Map<string, { name: string; emitT: number; lastDeltaT: number }>()\n\n const closeSegment = () => {\n if (segStart !== null) {\n spans.push({\n kind: 'model',\n name: segKinds.has('reasoning') ? 'model turn (reasoning + text)' : 'model turn',\n startMs: segStart,\n endMs: segEnd,\n approx: true,\n })\n segStart = null\n segKinds = new Set()\n }\n }\n\n for (const { t, event } of events) {\n const inner = innerOf(event)\n const type = String(event.kind === 'tool_result' ? 'tool_result' : (inner.type ?? ''))\n\n if (type === 'text' || type === 'reasoning') {\n if (segStart === null) segStart = t\n segEnd = t\n segKinds.add(type)\n lastDeltaT = t\n } else if (type === 'tool_call') {\n closeSegment()\n toolCalls++\n const call = (inner.call ?? inner) as Record<string, unknown>\n const id = String(call.toolCallId ?? `call_${toolCalls}`)\n openCalls.set(id, { name: String(call.toolName ?? 'tool'), emitT: t, lastDeltaT })\n } else if (type === 'tool_result') {\n const id = String(event.toolCallId ?? inner.toolCallId ?? '')\n const open = openCalls.get(id)\n if (open) {\n spans.push({\n kind: 'tool',\n name: open.name,\n // Execution happens between the end of the model turn that emitted\n // the call and the result landing in the buffer.\n startMs: open.lastDeltaT,\n endMs: t,\n approx: true,\n meta: { ok: ((event.outcome ?? inner.outcome) as { ok?: boolean } | undefined)?.ok },\n })\n openCalls.delete(id)\n }\n } else if (type === 'usage') {\n const u = (inner.usage ?? {}) as { promptTokens?: number; completionTokens?: number }\n promptTokens += u.promptTokens ?? 0\n completionTokens += u.completionTokens ?? 0\n }\n }\n closeSegment()\n\n const totalMs = events.length ? events[events.length - 1]!.t : 0\n const trace: FlowTrace = { spans, totalMs, promptTokens, completionTokens, toolCalls }\n const p = opts?.pricing\n if (p && (p.prompt != null || p.completion != null)) {\n trace.costUsd = promptTokens * Number(p.prompt ?? 0) + completionTokens * Number(p.completion ?? 0)\n }\n return trace\n}\n\nconst fmtS = (ms: number) => `${(ms / 1000).toFixed(1)}s`\n\n/** ASCII waterfall cascade — the default artifact for explaining a flow. */\nexport function renderWaterfall(trace: FlowTrace, opts?: { width?: number }): string {\n const width = opts?.width ?? 40\n const scale = trace.totalMs > 0 ? width / trace.totalMs : 0\n const lines: string[] = []\n const spans = [...trace.spans].sort((a, b) => a.startMs - b.startMs)\n for (let i = 0; i < spans.length; i++) {\n const s = spans[i]!\n const offset = Math.round(s.startMs * scale)\n const len = Math.max(1, Math.round((s.endMs - s.startMs) * scale))\n const bar = ' '.repeat(offset) + (s.kind === 'tool' ? '▓' : s.kind === 'pipeline' ? '░' : '█').repeat(len)\n const branch = i === spans.length - 1 ? '└─' : '├─'\n const dur = `${fmtS(s.endMs - s.startMs)}${s.approx ? '~' : ''}`\n lines.push(`${fmtS(s.startMs).padStart(7)} ${branch} ${bar.padEnd(width + 2)} ${s.name} (${dur})`)\n }\n const cost = trace.costUsd != null ? ` $${trace.costUsd.toFixed(trace.costUsd < 0.01 ? 6 : 4)}` : ''\n lines.push(\n `${fmtS(trace.totalMs).padStart(7)} ── total · ${trace.promptTokens}p + ${trace.completionTokens}c tok · ${trace.toolCalls} tool calls${cost}`,\n )\n return lines.join('\\n')\n}\n\nexport interface DistributionSummary {\n n: number\n min: number\n p50: number\n p90: number\n max: number\n}\n\nexport function summarize(values: number[]): DistributionSummary {\n const sorted = [...values].sort((a, b) => a - b)\n const q = (p: number) => sorted[Math.min(sorted.length - 1, Math.floor(p * sorted.length))] ?? 0\n return { n: sorted.length, min: sorted[0] ?? 0, p50: q(0.5), p90: q(0.9), max: sorted[sorted.length - 1] ?? 0 }\n}\n\n/** ASCII histogram for multi-run samples (eval latencies, costs, scores). */\nexport function renderHistogram(\n values: number[],\n opts?: { buckets?: number; width?: number; unit?: string; format?: (v: number) => string },\n): string {\n if (!values.length) return '(no samples)'\n const buckets = opts?.buckets ?? 6\n const width = opts?.width ?? 24\n const fmt = opts?.format ?? ((v: number) => `${Math.round(v)}${opts?.unit ?? ''}`)\n const s = summarize(values)\n const lo = s.min\n const hi = s.max === s.min ? s.min + 1 : s.max\n const counts = new Array<number>(buckets).fill(0)\n for (const v of values) {\n counts[Math.min(buckets - 1, Math.floor(((v - lo) / (hi - lo)) * buckets))]!++\n }\n const maxCount = Math.max(...counts)\n const lines = [\n `n=${s.n} min=${fmt(s.min)} p50=${fmt(s.p50)} p90=${fmt(s.p90)} max=${fmt(s.max)}`,\n ]\n for (let i = 0; i < buckets; i++) {\n const a = lo + ((hi - lo) * i) / buckets\n const b = lo + ((hi - lo) * (i + 1)) / buckets\n const bar = '█'.repeat(Math.max(counts[i]! > 0 ? 1 : 0, Math.round((counts[i]! / maxCount) * width)))\n lines.push(`${fmt(a).padStart(8)}-${fmt(b).padEnd(8)} ${bar} ${counts[i]}`)\n }\n return lines.join('\\n')\n}\n"],"mappings":"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","names":[]}
|
|
@@ -185,8 +185,11 @@ interface ChatToolCallInfo {
|
|
|
185
185
|
id: string;
|
|
186
186
|
name: string;
|
|
187
187
|
status: 'running' | 'done' | 'error';
|
|
188
|
+
/** The call arguments, captured from the tool_call event — shown in the
|
|
189
|
+
* expanded card so users see exactly what the agent invoked. */
|
|
190
|
+
args?: Record<string, unknown>;
|
|
188
191
|
/** The tool outcome (`{ok, result}` shape). When `result.status` is
|
|
189
|
-
* 'queued_for_approval' the
|
|
192
|
+
* 'queued_for_approval' the card renders the approval state. */
|
|
190
193
|
result?: unknown;
|
|
191
194
|
}
|
|
192
195
|
/** Extract `{proposalId, status}` from a tool outcome when it is a proposal
|
|
@@ -216,19 +219,24 @@ interface ChatMessagesProps {
|
|
|
216
219
|
/** Approve/Reject handlers for proposals awaiting approval. When omitted the
|
|
217
220
|
* chip still shows "awaiting approval" but without action buttons. */
|
|
218
221
|
approval?: ProposalApprovalHandlers;
|
|
219
|
-
/**
|
|
222
|
+
/** Open a full-transcript view (e.g. {@link RunDrillIn}) from a tool card. */
|
|
220
223
|
onToolCallClick?: (call: ChatToolCallInfo, message: ChatUiMessage) => void;
|
|
224
|
+
/** Per-tool custom detail renderers for expanded tool cards. */
|
|
225
|
+
toolRenderers?: ToolDetailRenderers;
|
|
221
226
|
}
|
|
222
227
|
interface ProposalApprovalHandlers {
|
|
223
228
|
onApprove: (proposalId: string, toolCallId: string) => void | Promise<void>;
|
|
224
229
|
onReject: (proposalId: string, toolCallId: string) => void | Promise<void>;
|
|
225
230
|
}
|
|
231
|
+
/** Per-tool custom detail renderers for the expanded card body — keyed by
|
|
232
|
+
* tool name. Return null to fall back to the generic detail view. */
|
|
233
|
+
type ToolDetailRenderers = Record<string, (call: ChatToolCallInfo, message: ChatUiMessage) => ReactNode>;
|
|
226
234
|
/**
|
|
227
235
|
* The message thread: one centered column; user messages are right-aligned
|
|
228
236
|
* bubbles with a User label; agent messages carry an Agent meta line with
|
|
229
237
|
* model id, tokens/sec, and cost, plus a collapsible thinking section and
|
|
230
238
|
* tool-call chips.
|
|
231
239
|
*/
|
|
232
|
-
declare function ChatMessages({ messages, models, renderMarkdown, renderExtras, userLabel, agentLabel, loading, approval, onToolCallClick, }: ChatMessagesProps): react.JSX.Element;
|
|
240
|
+
declare function ChatMessages({ messages, models, renderMarkdown, renderExtras, userLabel, agentLabel, loading, approval, onToolCallClick, toolRenderers, }: ChatMessagesProps): react.JSX.Element;
|
|
233
241
|
|
|
234
|
-
export { type ChatMessageMetrics, ChatMessages, type ChatMessagesProps, type ChatStreamCallbacks, type ChatStreamToolCall, type ChatStreamToolResult, type ChatToolCallInfo, type ChatUiMessage, type ConsumeChatStreamResult, EffortPicker, type EffortPickerProps, ModelPicker, type ModelPickerProps, type ProposalApprovalHandlers, ProviderLogo, type ProviderLogoProps, RunDrillIn, type RunDrillInProps, type SmoothRevealOptions, type StreamChatOptions, type ToolRunRecord, type ToolRunStep, consumeChatStream, dispatchChatStreamLine, formatModelCost, formatTokensPerSecond, nextRevealCount, pendingApprovalOf, streamChatTurn, useSmoothText };
|
|
242
|
+
export { type ChatMessageMetrics, ChatMessages, type ChatMessagesProps, type ChatStreamCallbacks, type ChatStreamToolCall, type ChatStreamToolResult, type ChatToolCallInfo, type ChatUiMessage, type ConsumeChatStreamResult, EffortPicker, type EffortPickerProps, ModelPicker, type ModelPickerProps, type ProposalApprovalHandlers, ProviderLogo, type ProviderLogoProps, RunDrillIn, type RunDrillInProps, type SmoothRevealOptions, type StreamChatOptions, type ToolDetailRenderers, type ToolRunRecord, type ToolRunStep, consumeChatStream, dispatchChatStreamLine, formatModelCost, formatTokensPerSecond, nextRevealCount, pendingApprovalOf, streamChatTurn, useSmoothText };
|
package/dist/web-react/index.js
CHANGED
|
@@ -449,62 +449,125 @@ function pendingApprovalOf(call) {
|
|
|
449
449
|
if (!outcome?.ok || outcome.result?.status !== "queued_for_approval" || !outcome.result.proposalId) return null;
|
|
450
450
|
return { proposalId: outcome.result.proposalId };
|
|
451
451
|
}
|
|
452
|
-
function
|
|
453
|
-
|
|
452
|
+
function toolOutcomeOf(call) {
|
|
453
|
+
return call.result;
|
|
454
|
+
}
|
|
455
|
+
function friendlyToolTitle(call) {
|
|
456
|
+
const a = call.args ?? {};
|
|
457
|
+
switch (call.name) {
|
|
458
|
+
case "submit_proposal":
|
|
459
|
+
return `Proposal \xB7 ${String(a.type ?? "")}${a.title ? `: ${String(a.title)}` : ""}`;
|
|
460
|
+
case "sandbox_create":
|
|
461
|
+
return `Create sandbox (${String(a.environment ?? "universal")})`;
|
|
462
|
+
case "sandbox_run_command":
|
|
463
|
+
return `$ ${String(a.command ?? "")}`;
|
|
464
|
+
case "sandbox_destroy":
|
|
465
|
+
return `Destroy sandbox ${String(a.sandbox_id ?? "")}`;
|
|
466
|
+
case "schedule_followup":
|
|
467
|
+
return `Follow-up \xB7 ${String(a.title ?? "")}`;
|
|
468
|
+
case "render_ui":
|
|
469
|
+
return `Render view \xB7 ${String(a.title ?? "")}`;
|
|
470
|
+
case "add_citation":
|
|
471
|
+
return `Citation \xB7 ${String(a.path ?? "")}`;
|
|
472
|
+
default:
|
|
473
|
+
return call.name;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
function truncate(v, max = 240) {
|
|
477
|
+
const s = typeof v === "string" ? v : JSON.stringify(v);
|
|
478
|
+
return s.length > max ? `${s.slice(0, max)}\u2026` : s;
|
|
479
|
+
}
|
|
480
|
+
function KvRows({ data }) {
|
|
481
|
+
const entries = Object.entries(data).filter(([, v]) => v !== void 0 && v !== null && v !== "");
|
|
482
|
+
if (!entries.length) return null;
|
|
483
|
+
return /* @__PURE__ */ jsx2("dl", { className: "grid grid-cols-[auto_1fr] gap-x-3 gap-y-1", children: entries.map(([k, v]) => /* @__PURE__ */ jsxs2("div", { className: "contents", children: [
|
|
484
|
+
/* @__PURE__ */ jsx2("dt", { className: "font-mono text-[11px] text-muted-foreground/70", children: k }),
|
|
485
|
+
/* @__PURE__ */ jsx2("dd", { className: "min-w-0 whitespace-pre-wrap break-words font-mono text-[11px] text-muted-foreground", children: truncate(v) })
|
|
486
|
+
] }, k)) });
|
|
487
|
+
}
|
|
488
|
+
function DefaultToolDetail({ call }) {
|
|
489
|
+
const outcome = toolOutcomeOf(call);
|
|
490
|
+
return /* @__PURE__ */ jsxs2("div", { className: "space-y-2", children: [
|
|
491
|
+
call.args && Object.keys(call.args).length > 0 && /* @__PURE__ */ jsxs2("div", { children: [
|
|
492
|
+
/* @__PURE__ */ jsx2("p", { className: "mb-1 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground/50", children: "Called with" }),
|
|
493
|
+
/* @__PURE__ */ jsx2(KvRows, { data: call.args })
|
|
494
|
+
] }),
|
|
495
|
+
outcome && /* @__PURE__ */ jsxs2("div", { children: [
|
|
496
|
+
/* @__PURE__ */ jsx2("p", { className: "mb-1 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground/50", children: outcome.ok === false ? "Failed" : "Result" }),
|
|
497
|
+
outcome.ok === false ? /* @__PURE__ */ jsx2("p", { className: "text-xs text-red-600", children: outcome.message ?? "Tool failed" }) : outcome.result && typeof outcome.result === "object" ? /* @__PURE__ */ jsx2(KvRows, { data: outcome.result }) : /* @__PURE__ */ jsx2("p", { className: "font-mono text-[11px] text-muted-foreground", children: truncate(outcome.result) })
|
|
498
|
+
] })
|
|
499
|
+
] });
|
|
500
|
+
}
|
|
501
|
+
function ToolCallCard({
|
|
502
|
+
call,
|
|
503
|
+
message,
|
|
454
504
|
approval,
|
|
455
|
-
|
|
505
|
+
onOpenRun,
|
|
506
|
+
renderers
|
|
456
507
|
}) {
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
508
|
+
const [expanded, setExpanded] = useState2(false);
|
|
509
|
+
const pending = call.status === "done" ? pendingApprovalOf(call) : null;
|
|
510
|
+
const failed = call.status === "error" || toolOutcomeOf(call)?.ok === false;
|
|
511
|
+
const custom = renderers?.[call.name]?.(call, message);
|
|
512
|
+
return /* @__PURE__ */ jsxs2(
|
|
513
|
+
"div",
|
|
514
|
+
{
|
|
515
|
+
className: `w-fit min-w-[280px] max-w-full rounded-lg border text-xs transition ${pending ? "border-amber-300/60 bg-amber-500/5" : failed ? "border-red-300/60 bg-red-500/5" : "border-border/60 bg-muted/20"}`,
|
|
516
|
+
children: [
|
|
517
|
+
/* @__PURE__ */ jsxs2(
|
|
518
|
+
"button",
|
|
519
|
+
{
|
|
520
|
+
type: "button",
|
|
521
|
+
onClick: () => setExpanded((v) => !v),
|
|
522
|
+
className: "flex w-full items-center gap-2 px-3 py-2 text-left",
|
|
523
|
+
children: [
|
|
469
524
|
/* @__PURE__ */ jsx2(
|
|
470
|
-
"
|
|
525
|
+
"span",
|
|
471
526
|
{
|
|
472
|
-
|
|
473
|
-
onClick: () => approval.onApprove(pending.proposalId, tc.id),
|
|
474
|
-
className: "rounded bg-green-600/90 px-2 py-0.5 text-[11px] font-semibold text-white transition hover:bg-green-600",
|
|
475
|
-
children: "Approve"
|
|
527
|
+
className: `h-2 w-2 shrink-0 rounded-full ${call.status === "running" ? "animate-pulse bg-yellow-500" : pending ? "bg-amber-500" : failed ? "bg-red-500" : "bg-green-500"}`
|
|
476
528
|
}
|
|
477
529
|
),
|
|
478
|
-
/* @__PURE__ */ jsx2(
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
530
|
+
/* @__PURE__ */ jsx2("span", { className: "min-w-0 flex-1 truncate font-medium", children: friendlyToolTitle(call) }),
|
|
531
|
+
pending && approval && /* @__PURE__ */ jsxs2("span", { className: "flex shrink-0 items-center gap-1", onClick: (e) => e.stopPropagation(), children: [
|
|
532
|
+
/* @__PURE__ */ jsx2(
|
|
533
|
+
"button",
|
|
534
|
+
{
|
|
535
|
+
type: "button",
|
|
536
|
+
onClick: () => approval.onApprove(pending.proposalId, call.id),
|
|
537
|
+
className: "rounded bg-green-600/90 px-2 py-0.5 text-[11px] font-semibold text-white transition hover:bg-green-600",
|
|
538
|
+
children: "Approve"
|
|
539
|
+
}
|
|
540
|
+
),
|
|
541
|
+
/* @__PURE__ */ jsx2(
|
|
542
|
+
"button",
|
|
543
|
+
{
|
|
544
|
+
type: "button",
|
|
545
|
+
onClick: () => approval.onReject(pending.proposalId, call.id),
|
|
546
|
+
className: "rounded border border-border bg-card px-2 py-0.5 text-[11px] font-medium text-foreground transition hover:bg-accent/30",
|
|
547
|
+
children: "Reject"
|
|
548
|
+
}
|
|
549
|
+
)
|
|
550
|
+
] }),
|
|
551
|
+
/* @__PURE__ */ jsx2("span", { className: "shrink-0 text-[11px] text-muted-foreground/70", children: call.status === "running" ? "running\u2026" : pending ? "awaiting approval" : failed ? "failed" : "done" }),
|
|
552
|
+
/* @__PURE__ */ jsx2(ChevronDown, { className: `h-3 w-3 shrink-0 text-muted-foreground transition-transform ${expanded ? "rotate-180" : ""}` })
|
|
553
|
+
]
|
|
554
|
+
}
|
|
555
|
+
),
|
|
556
|
+
expanded && /* @__PURE__ */ jsxs2("div", { className: "border-t border-border/40 px-3 py-2.5", children: [
|
|
557
|
+
custom ?? /* @__PURE__ */ jsx2(DefaultToolDetail, { call }),
|
|
558
|
+
onOpenRun && call.name.startsWith("sandbox_") && /* @__PURE__ */ jsx2(
|
|
559
|
+
"button",
|
|
560
|
+
{
|
|
561
|
+
type: "button",
|
|
562
|
+
onClick: () => onOpenRun(call, message),
|
|
563
|
+
className: "mt-2 rounded border border-border bg-card px-2 py-1 text-[11px] font-medium transition hover:bg-accent/30",
|
|
564
|
+
children: "Open full transcript \u2192"
|
|
565
|
+
}
|
|
566
|
+
)
|
|
567
|
+
] })
|
|
568
|
+
]
|
|
492
569
|
}
|
|
493
|
-
|
|
494
|
-
return /* @__PURE__ */ jsxs2(
|
|
495
|
-
Tag,
|
|
496
|
-
{
|
|
497
|
-
...onClick ? { type: "button", onClick: () => onClick(tc) } : {},
|
|
498
|
-
className: `inline-flex w-fit items-center gap-2 rounded-md px-2.5 py-1 text-xs ${tc.status === "running" ? "bg-yellow-500/10 text-yellow-700" : tc.status === "error" ? "bg-red-500/10 text-red-700" : "bg-green-500/10 text-green-700"} ${onClick ? "cursor-pointer transition hover:ring-1 hover:ring-border" : ""}`,
|
|
499
|
-
children: [
|
|
500
|
-
/* @__PURE__ */ jsx2("span", { className: "font-mono opacity-70", children: tc.status === "running" ? "\u26A1" : tc.status === "error" ? "\u2717" : "\u2713" }),
|
|
501
|
-
/* @__PURE__ */ jsx2("span", { className: "font-medium", children: tc.name }),
|
|
502
|
-
/* @__PURE__ */ jsx2("span", { className: "opacity-60", children: tc.status === "running" ? "running\u2026" : tc.status === "error" ? "failed" : "done" })
|
|
503
|
-
]
|
|
504
|
-
},
|
|
505
|
-
tc.id
|
|
506
|
-
);
|
|
507
|
-
}) });
|
|
570
|
+
);
|
|
508
571
|
}
|
|
509
572
|
function AssistantMessage({
|
|
510
573
|
msg,
|
|
@@ -514,6 +577,7 @@ function AssistantMessage({
|
|
|
514
577
|
renderBody,
|
|
515
578
|
approval,
|
|
516
579
|
onToolCallClick,
|
|
580
|
+
toolRenderers,
|
|
517
581
|
renderExtras
|
|
518
582
|
}) {
|
|
519
583
|
const content = useSmoothText(msg.content, streaming);
|
|
@@ -538,14 +602,17 @@ function AssistantMessage({
|
|
|
538
602
|
/* @__PURE__ */ jsx2("div", { ref: reasoningScrollRef, className: "mt-2 max-h-48 overflow-y-auto whitespace-pre-wrap text-sm text-muted-foreground/80", children: reasoning })
|
|
539
603
|
] }),
|
|
540
604
|
/* @__PURE__ */ jsx2("div", { className: "text-base leading-[1.75]", children: renderBody(content) }),
|
|
541
|
-
msg.toolCalls && msg.toolCalls.length > 0 && /* @__PURE__ */ jsx2(
|
|
542
|
-
|
|
605
|
+
msg.toolCalls && msg.toolCalls.length > 0 && /* @__PURE__ */ jsx2("div", { className: "mt-2 flex flex-col gap-1.5", children: msg.toolCalls.map((tc) => /* @__PURE__ */ jsx2(
|
|
606
|
+
ToolCallCard,
|
|
543
607
|
{
|
|
544
|
-
|
|
608
|
+
call: tc,
|
|
609
|
+
message: msg,
|
|
545
610
|
approval,
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
611
|
+
onOpenRun: onToolCallClick,
|
|
612
|
+
renderers: toolRenderers
|
|
613
|
+
},
|
|
614
|
+
tc.id
|
|
615
|
+
)) }),
|
|
549
616
|
renderExtras?.(msg)
|
|
550
617
|
] });
|
|
551
618
|
}
|
|
@@ -573,7 +640,8 @@ function ChatMessages({
|
|
|
573
640
|
agentLabel = "Agent",
|
|
574
641
|
loading,
|
|
575
642
|
approval,
|
|
576
|
-
onToolCallClick
|
|
643
|
+
onToolCallClick,
|
|
644
|
+
toolRenderers
|
|
577
645
|
}) {
|
|
578
646
|
const renderBody = renderMarkdown ?? ((content) => /* @__PURE__ */ jsx2("p", { className: "whitespace-pre-wrap", children: content }));
|
|
579
647
|
const lastIsUser = messages[messages.length - 1]?.role === "user";
|
|
@@ -592,6 +660,7 @@ function ChatMessages({
|
|
|
592
660
|
renderBody,
|
|
593
661
|
approval,
|
|
594
662
|
onToolCallClick,
|
|
663
|
+
toolRenderers,
|
|
595
664
|
renderExtras
|
|
596
665
|
},
|
|
597
666
|
msg.id
|