whale-code 6.4.0 → 6.5.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/swagmanager-mcp.js +51 -0
- package/dist/cli/app.js +30 -2
- package/dist/cli/chat/ChatApp.d.ts +4 -4
- package/dist/cli/chat/ChatApp.js +114 -44
- package/dist/cli/chat/ChatInput.d.ts +13 -6
- package/dist/cli/chat/ChatInput.js +433 -89
- package/dist/cli/chat/MemoryManager.d.ts +15 -0
- package/dist/cli/chat/MemoryManager.js +61 -0
- package/dist/cli/chat/MessageList.d.ts +8 -0
- package/dist/cli/chat/MessageList.js +1 -1
- package/dist/cli/chat/NodeManager.d.ts +30 -0
- package/dist/cli/chat/NodeManager.js +89 -0
- package/dist/cli/chat/NodeSelector.d.ts +19 -0
- package/dist/cli/chat/NodeSelector.js +37 -0
- package/dist/cli/chat/PlanApproval.d.ts +17 -0
- package/dist/cli/chat/PlanApproval.js +82 -0
- package/dist/cli/chat/SessionManager.d.ts +16 -0
- package/dist/cli/chat/SessionManager.js +43 -0
- package/dist/cli/chat/SlashMenu.d.ts +38 -0
- package/dist/cli/chat/SlashMenu.js +208 -0
- package/dist/cli/chat/StatusBar.d.ts +16 -0
- package/dist/cli/chat/StatusBar.js +22 -0
- package/dist/cli/chat/ThemeSelector.d.ts +14 -0
- package/dist/cli/chat/ThemeSelector.js +29 -0
- package/dist/cli/chat/ToolIndicator.d.ts +8 -0
- package/dist/cli/chat/ToolIndicator.js +33 -9
- package/dist/cli/chat/hooks/useAgentLoop.d.ts +2 -1
- package/dist/cli/chat/hooks/useAgentLoop.js +22 -17
- package/dist/cli/chat/hooks/useSlashCommands.d.ts +19 -0
- package/dist/cli/chat/hooks/useSlashCommands.js +254 -15
- package/dist/cli/commands/config-cmd.js +4 -25
- package/dist/cli/commands/db.d.ts +13 -0
- package/dist/cli/commands/db.js +243 -0
- package/dist/cli/commands/doctor.js +6 -9
- package/dist/cli/commands/mcp.js +1 -20
- package/dist/cli/services/agent-events.d.ts +22 -1
- package/dist/cli/services/agent-events.js +9 -0
- package/dist/cli/services/agent-loop.js +65 -8
- package/dist/cli/services/agent-worker-base.js +21 -6
- package/dist/cli/services/api-retry.d.ts +25 -0
- package/dist/cli/services/api-retry.js +91 -0
- package/dist/cli/services/auth-service.d.ts +1 -1
- package/dist/cli/services/auth-service.js +40 -19
- package/dist/cli/services/background-processes.js +26 -2
- package/dist/cli/services/config-store.d.ts +13 -1
- package/dist/cli/services/config-store.js +116 -13
- package/dist/cli/services/format-server-response.js +12 -6
- package/dist/cli/services/ink-resize-fix.d.ts +18 -0
- package/dist/cli/services/ink-resize-fix.js +66 -0
- package/dist/cli/services/interactive-tools.d.ts +14 -0
- package/dist/cli/services/interactive-tools.js +47 -2
- package/dist/cli/services/keybinding-manager.js +1 -1
- package/dist/cli/services/local-tools.js +35 -2
- package/dist/cli/services/server-tools.js +175 -3
- package/dist/cli/services/subagent.js +7 -6
- package/dist/cli/services/system-prompt.js +5 -3
- package/dist/cli/services/task-decomposer.d.ts +35 -0
- package/dist/cli/services/task-decomposer.js +199 -0
- package/dist/cli/services/team-lead.d.ts +18 -0
- package/dist/cli/services/team-lead.js +80 -0
- package/dist/cli/services/teammate.js +5 -5
- package/dist/cli/services/telemetry.d.ts +8 -2
- package/dist/cli/services/telemetry.js +116 -92
- package/dist/cli/services/tools/agent-tools.d.ts +1 -0
- package/dist/cli/services/tools/agent-tools.js +50 -4
- package/dist/cli/services/tools/file-ops.d.ts +2 -0
- package/dist/cli/services/tools/file-ops.js +85 -19
- package/dist/cli/services/tools/shell-exec.js +22 -12
- package/dist/cli/shared/Theme.d.ts +1 -2
- package/dist/cli/shared/Theme.js +1 -1
- package/dist/cli/shared/WhaleBanner.d.ts +4 -1
- package/dist/cli/shared/WhaleBanner.js +12 -8
- package/dist/cli/shared/markdown.d.ts +5 -4
- package/dist/cli/shared/markdown.js +376 -334
- package/dist/cli/shared/theme-manager.d.ts +27 -0
- package/dist/cli/shared/theme-manager.js +178 -0
- package/dist/cli/shared/theme-presets.d.ts +16 -0
- package/dist/cli/shared/theme-presets.js +265 -0
- package/dist/index.js +0 -51
- package/dist/node/adapters/imessage.d.ts +10 -0
- package/dist/node/adapters/imessage.js +45 -6
- package/dist/node/cli.js +459 -8
- package/dist/node/config.d.ts +17 -0
- package/dist/node/gateway-client.d.ts +55 -0
- package/dist/node/gateway-client.js +201 -0
- package/dist/node/portal/clipboard.d.ts +28 -0
- package/dist/node/portal/clipboard.js +183 -0
- package/dist/node/portal/discovery.d.ts +29 -0
- package/dist/node/portal/discovery.js +61 -0
- package/dist/node/portal/forward.d.ts +30 -0
- package/dist/node/portal/forward.js +90 -0
- package/dist/node/portal/index.d.ts +47 -0
- package/dist/node/portal/index.js +250 -0
- package/dist/node/portal/multiplexer.d.ts +48 -0
- package/dist/node/portal/multiplexer.js +207 -0
- package/dist/node/portal/permissions.d.ts +36 -0
- package/dist/node/portal/permissions.js +131 -0
- package/dist/node/portal/protocol.d.ts +140 -0
- package/dist/node/portal/protocol.js +193 -0
- package/dist/node/portal/screen.d.ts +18 -0
- package/dist/node/portal/screen.js +93 -0
- package/dist/node/portal/session.d.ts +68 -0
- package/dist/node/portal/session.js +127 -0
- package/dist/node/portal/shell.d.ts +26 -0
- package/dist/node/portal/shell.js +142 -0
- package/dist/node/portal/stream.d.ts +43 -0
- package/dist/node/portal/stream.js +90 -0
- package/dist/node/portal/transfer.d.ts +33 -0
- package/dist/node/portal/transfer.js +231 -0
- package/dist/node/portal/ui.d.ts +16 -0
- package/dist/node/portal/ui.js +148 -0
- package/dist/node/remote-desktop/compile-helper.d.ts +13 -0
- package/dist/node/remote-desktop/compile-helper.js +73 -0
- package/dist/node/remote-desktop/index.d.ts +67 -0
- package/dist/node/remote-desktop/index.js +220 -0
- package/dist/node/remote-desktop/protocol.d.ts +96 -0
- package/dist/node/remote-desktop/protocol.js +67 -0
- package/dist/node/runtime.d.ts +8 -1
- package/dist/node/runtime.js +117 -9
- package/dist/server/handlers/__test-utils__/test-db.d.ts +25 -0
- package/dist/server/handlers/__test-utils__/test-db.js +128 -0
- package/dist/server/handlers/api-keys.js +26 -2
- package/dist/server/handlers/browser.d.ts +0 -4
- package/dist/server/handlers/browser.js +0 -46
- package/dist/server/handlers/catalog.js +37 -14
- package/dist/server/handlers/clickhouse.d.ts +10 -0
- package/dist/server/handlers/clickhouse.js +215 -0
- package/dist/server/handlers/comms.d.ts +308 -4
- package/dist/server/handlers/comms.js +444 -11
- package/dist/server/handlers/creations.js +1 -1
- package/dist/server/handlers/crm.d.ts +54 -8
- package/dist/server/handlers/crm.js +353 -68
- package/dist/server/handlers/embeddings.js +3 -3
- package/dist/server/handlers/enrichment.js +39 -55
- package/dist/server/handlers/inventory.js +1 -1
- package/dist/server/handlers/kali.d.ts +9 -1
- package/dist/server/handlers/kali.js +50 -1
- package/dist/server/handlers/media.d.ts +8 -0
- package/dist/server/handlers/media.js +902 -0
- package/dist/server/handlers/meta-ads.js +6 -3
- package/dist/server/handlers/nodes.d.ts +2 -0
- package/dist/server/handlers/nodes.js +331 -40
- package/dist/server/handlers/operations.d.ts +4 -6
- package/dist/server/handlers/operations.js +99 -38
- package/dist/server/handlers/platform.js +224 -107
- package/dist/server/handlers/remove-bg.d.ts +6 -0
- package/dist/server/handlers/remove-bg.js +96 -0
- package/dist/server/handlers/storefront.d.ts +6 -0
- package/dist/server/handlers/storefront.js +477 -0
- package/dist/server/handlers/supply-chain.js +21 -3
- package/dist/server/handlers/workflow-steps.js +87 -31
- package/dist/server/handlers/workflows.js +4 -1
- package/dist/server/index.js +334 -88
- package/dist/server/lib/clickhouse-buffer.d.ts +48 -0
- package/dist/server/lib/clickhouse-buffer.js +175 -0
- package/dist/server/lib/clickhouse-client.d.ts +112 -0
- package/dist/server/lib/clickhouse-client.js +141 -0
- package/dist/server/lib/coa-renderer.d.ts +91 -0
- package/dist/server/lib/coa-renderer.js +411 -0
- package/dist/server/lib/compaction-service.js +46 -1
- package/dist/server/lib/pdf-renderer.d.ts +143 -0
- package/dist/server/lib/pdf-renderer.js +867 -0
- package/dist/server/lib/react-pdf-layout.d.ts +40 -0
- package/dist/server/lib/react-pdf-layout.js +437 -0
- package/dist/server/lib/server-agent-loop.d.ts +2 -0
- package/dist/server/lib/server-agent-loop.js +36 -17
- package/dist/server/lib/server-subagent.d.ts +3 -0
- package/dist/server/lib/server-subagent.js +9 -6
- package/dist/server/lib/supabase-client.js +51 -3
- package/dist/server/lib/template-resolver.js +14 -4
- package/dist/server/lib/utils.js +15 -0
- package/dist/server/local-agent-gateway.d.ts +44 -0
- package/dist/server/local-agent-gateway.js +389 -49
- package/dist/server/providers/anthropic.js +12 -2
- package/dist/server/providers/gemini.js +17 -2
- package/dist/server/proxy-handlers.js +151 -0
- package/dist/server/tool-router.d.ts +2 -2
- package/dist/server/tool-router.js +25 -35
- package/dist/shared/agent-core.d.ts +25 -2
- package/dist/shared/agent-core.js +66 -5
- package/dist/shared/api-client.js +54 -3
- package/dist/shared/sse-parser.d.ts +1 -1
- package/dist/shared/sse-parser.js +5 -2
- package/dist/shared/tool-dispatch.js +15 -1
- package/package.json +16 -10
- package/dist/server/handlers/__test-utils__/mock-supabase.d.ts +0 -11
- package/dist/server/handlers/__test-utils__/mock-supabase.js +0 -393
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type LayoutElement, type PageConfig, type StyleConfig } from "./pdf-renderer.js";
|
|
2
|
+
export declare function renderLayoutToPdf(layout: LayoutElement[] | Record<string, unknown> | undefined | null, pageConfig: PageConfig | undefined | null, styles: StyleConfig | undefined | null, data: Record<string, unknown>): Promise<Buffer>;
|
|
3
|
+
interface LabelPageConfig {
|
|
4
|
+
cols: number;
|
|
5
|
+
rows: number;
|
|
6
|
+
pageWidth: number;
|
|
7
|
+
pageHeight: number;
|
|
8
|
+
labelWidth: number;
|
|
9
|
+
labelHeight: number;
|
|
10
|
+
margins: {
|
|
11
|
+
top: number;
|
|
12
|
+
left: number;
|
|
13
|
+
};
|
|
14
|
+
gutters?: {
|
|
15
|
+
horizontal: number;
|
|
16
|
+
vertical: number;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
interface LabelElement {
|
|
20
|
+
type: string;
|
|
21
|
+
field?: string;
|
|
22
|
+
x: number;
|
|
23
|
+
y: number;
|
|
24
|
+
width: number;
|
|
25
|
+
height: number;
|
|
26
|
+
style?: Record<string, unknown>;
|
|
27
|
+
text?: string;
|
|
28
|
+
format?: string;
|
|
29
|
+
color?: string;
|
|
30
|
+
backgroundColor?: string;
|
|
31
|
+
}
|
|
32
|
+
export declare function renderLabelToPdf(layout: {
|
|
33
|
+
version: number;
|
|
34
|
+
elements: LabelElement[];
|
|
35
|
+
}, pageConfig: LabelPageConfig | any, items: Record<string, unknown>[]): Promise<Buffer>;
|
|
36
|
+
export declare function renderHtmlToPdf(html: string, options?: {
|
|
37
|
+
format?: string;
|
|
38
|
+
landscape?: boolean;
|
|
39
|
+
}): Promise<Buffer>;
|
|
40
|
+
export {};
|
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
// server/lib/react-pdf-layout.ts — Generic React-PDF layout engine
|
|
2
|
+
// Replaces Playwright HTML→PDF with pure Node.js React-PDF rendering.
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { Document, Page, Text, View, Image, renderToBuffer, } from "@react-pdf/renderer";
|
|
5
|
+
import { resolveBinding, resolveText, generateQRSvg, } from "./pdf-renderer.js";
|
|
6
|
+
const e = React.createElement;
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// Unit Helpers
|
|
9
|
+
// ============================================================================
|
|
10
|
+
/** Convert margin string (e.g. "15mm", "1cm", "72pt", number) to points */
|
|
11
|
+
function toPoints(val, fallback = 42.5) {
|
|
12
|
+
if (val === undefined || val === null)
|
|
13
|
+
return fallback;
|
|
14
|
+
if (typeof val === "number")
|
|
15
|
+
return val;
|
|
16
|
+
const num = parseFloat(val);
|
|
17
|
+
if (isNaN(num))
|
|
18
|
+
return fallback;
|
|
19
|
+
if (val.endsWith("mm"))
|
|
20
|
+
return num * 2.835;
|
|
21
|
+
if (val.endsWith("cm"))
|
|
22
|
+
return num * 28.35;
|
|
23
|
+
if (val.endsWith("in"))
|
|
24
|
+
return num * 72;
|
|
25
|
+
if (val.endsWith("pt"))
|
|
26
|
+
return num;
|
|
27
|
+
if (val.endsWith("px"))
|
|
28
|
+
return num * 0.75;
|
|
29
|
+
return num; // default to points
|
|
30
|
+
}
|
|
31
|
+
/** Map page size string to React-PDF page size */
|
|
32
|
+
function mapPageSize(size) {
|
|
33
|
+
if (!size)
|
|
34
|
+
return "A4";
|
|
35
|
+
const s = size.toUpperCase();
|
|
36
|
+
if (s === "LETTER")
|
|
37
|
+
return "LETTER";
|
|
38
|
+
if (s === "LEGAL")
|
|
39
|
+
return "LEGAL";
|
|
40
|
+
if (s === "TABLOID")
|
|
41
|
+
return "TABLOID";
|
|
42
|
+
return s; // A4, A3, etc. — React-PDF supports these directly
|
|
43
|
+
}
|
|
44
|
+
// ============================================================================
|
|
45
|
+
// Element Renderer — ports renderElement() from pdf-renderer.ts
|
|
46
|
+
// ============================================================================
|
|
47
|
+
function renderElement(el, data, key) {
|
|
48
|
+
switch (el.type) {
|
|
49
|
+
case "text": {
|
|
50
|
+
const text = resolveText(el.content || el.text || "", data);
|
|
51
|
+
return e(View, { key, style: { marginBottom: 2 } }, e(Text, {
|
|
52
|
+
style: {
|
|
53
|
+
fontSize: el.fontSize || undefined,
|
|
54
|
+
color: el.color || undefined,
|
|
55
|
+
fontFamily: el.bold ? "Helvetica-Bold" : undefined,
|
|
56
|
+
textAlign: el.align || undefined,
|
|
57
|
+
},
|
|
58
|
+
}, text));
|
|
59
|
+
}
|
|
60
|
+
case "header": {
|
|
61
|
+
const text = resolveText(el.content || el.text || "", data);
|
|
62
|
+
return e(View, {
|
|
63
|
+
key,
|
|
64
|
+
style: {
|
|
65
|
+
backgroundColor: el.backgroundColor || undefined,
|
|
66
|
+
padding: typeof el.padding === "number" ? el.padding : el.padding ? toPoints(el.padding, 0) : undefined,
|
|
67
|
+
marginBottom: 4,
|
|
68
|
+
},
|
|
69
|
+
}, e(Text, {
|
|
70
|
+
style: {
|
|
71
|
+
fontFamily: "Helvetica-Bold",
|
|
72
|
+
fontSize: el.fontSize || undefined,
|
|
73
|
+
color: el.color || undefined,
|
|
74
|
+
textAlign: el.align || undefined,
|
|
75
|
+
},
|
|
76
|
+
}, text));
|
|
77
|
+
}
|
|
78
|
+
case "image": {
|
|
79
|
+
let src = el.src || el.url || "";
|
|
80
|
+
if (el.bind) {
|
|
81
|
+
const resolved = resolveBinding(el.bind, data);
|
|
82
|
+
if (typeof resolved === "string")
|
|
83
|
+
src = resolved;
|
|
84
|
+
}
|
|
85
|
+
if (!src)
|
|
86
|
+
return e(View, { key });
|
|
87
|
+
const style = {};
|
|
88
|
+
if (el.width)
|
|
89
|
+
style.width = typeof el.width === "number" ? el.width : toPoints(el.width, 100);
|
|
90
|
+
if (el.height)
|
|
91
|
+
style.height = typeof el.height === "number" ? el.height : toPoints(el.height, 100);
|
|
92
|
+
return e(Image, { key, src, style });
|
|
93
|
+
}
|
|
94
|
+
case "qrcode": {
|
|
95
|
+
let qrData = el.data || el.content || "";
|
|
96
|
+
if (el.bind) {
|
|
97
|
+
const resolved = resolveBinding(el.bind, data);
|
|
98
|
+
if (typeof resolved === "string")
|
|
99
|
+
qrData = resolved;
|
|
100
|
+
}
|
|
101
|
+
qrData = resolveText(qrData, data);
|
|
102
|
+
const qrSize = el.size || (typeof el.width === "number" ? el.width : 100);
|
|
103
|
+
const svg = generateQRSvg(qrData, qrSize);
|
|
104
|
+
const dataUri = `data:image/svg+xml;base64,${Buffer.from(svg).toString("base64")}`;
|
|
105
|
+
return e(Image, { key, src: dataUri, style: { width: qrSize, height: qrSize } });
|
|
106
|
+
}
|
|
107
|
+
case "spacer": {
|
|
108
|
+
const h = el.height || 10;
|
|
109
|
+
return e(View, { key, style: { height: typeof h === "number" ? h : toPoints(h, 10) } });
|
|
110
|
+
}
|
|
111
|
+
case "line": {
|
|
112
|
+
const color = el.color || "#000";
|
|
113
|
+
const thickness = el.thickness || 1;
|
|
114
|
+
return e(View, {
|
|
115
|
+
key,
|
|
116
|
+
style: {
|
|
117
|
+
borderTopWidth: thickness,
|
|
118
|
+
borderTopColor: color,
|
|
119
|
+
borderTopStyle: "solid",
|
|
120
|
+
marginVertical: 8,
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
case "columns": {
|
|
125
|
+
const cols = el.columns || el.children || [];
|
|
126
|
+
const children = cols.map((child, i) => {
|
|
127
|
+
if (Array.isArray(child)) {
|
|
128
|
+
return e(View, { key: `col-${i}`, style: { flex: 1 } }, ...child.map((c, j) => renderElement(c, data, `col-${i}-${j}`)));
|
|
129
|
+
}
|
|
130
|
+
return e(View, { key: `col-${i}`, style: { flex: 1 } }, renderElement(child, data, `col-${i}-inner`));
|
|
131
|
+
});
|
|
132
|
+
return e(View, { key, style: { flexDirection: "row", gap: 8 } }, ...children);
|
|
133
|
+
}
|
|
134
|
+
case "page_break": {
|
|
135
|
+
return e(View, { key, break: true });
|
|
136
|
+
}
|
|
137
|
+
case "grid": {
|
|
138
|
+
const items = (Array.isArray(el.items) ? el.items : Array.isArray(el.content) ? el.content : []);
|
|
139
|
+
const rows = items.map((item, i) => {
|
|
140
|
+
const label = item.label || item.value || "";
|
|
141
|
+
let value = "";
|
|
142
|
+
if (item.bind) {
|
|
143
|
+
const resolved = resolveBinding(item.bind, data);
|
|
144
|
+
value = resolved !== undefined && resolved !== null ? String(resolved) : "";
|
|
145
|
+
}
|
|
146
|
+
value = resolveText(value || "", data);
|
|
147
|
+
return e(View, { key: `row-${i}`, style: { flexDirection: "row", borderBottomWidth: 0.5, borderBottomColor: "#ddd" } }, e(View, { style: { width: "40%", padding: 4 } }, e(Text, { style: { fontFamily: "Helvetica-Bold", fontSize: 9 } }, label)), e(View, { style: { width: "60%", padding: 4 } }, e(Text, { style: { fontSize: 9 } }, value)));
|
|
148
|
+
});
|
|
149
|
+
return e(View, { key, style: { width: "100%" } }, ...rows);
|
|
150
|
+
}
|
|
151
|
+
case "table": {
|
|
152
|
+
const headers = el.headers || [];
|
|
153
|
+
const rowFields = el.row_fields;
|
|
154
|
+
let bodyRows = el.rows || [];
|
|
155
|
+
if (el.bind) {
|
|
156
|
+
const resolved = resolveBinding(el.bind, data);
|
|
157
|
+
if (Array.isArray(resolved))
|
|
158
|
+
bodyRows = resolved;
|
|
159
|
+
}
|
|
160
|
+
const colCount = headers.length || (rowFields?.length || 0);
|
|
161
|
+
const colWidth = colCount > 0 ? `${(100 / colCount).toFixed(1)}%` : "100%";
|
|
162
|
+
const cellStyle = { padding: 5, borderWidth: 0.5, borderColor: "#ddd" };
|
|
163
|
+
const headerCellStyle = { ...cellStyle, backgroundColor: "#f5f5f5" };
|
|
164
|
+
const headerRow = headers.length
|
|
165
|
+
? e(View, { key: "thead", style: { flexDirection: "row" } }, ...headers.map((h, i) => e(View, { key: `th-${i}`, style: { ...headerCellStyle, width: colWidth } }, e(Text, { style: { fontSize: 8, fontFamily: "Helvetica-Bold" } }, h))))
|
|
166
|
+
: null;
|
|
167
|
+
const dataRows = bodyRows.map((row, ri) => {
|
|
168
|
+
let cells;
|
|
169
|
+
if (Array.isArray(row)) {
|
|
170
|
+
cells = row.map((c) => String(c ?? ""));
|
|
171
|
+
}
|
|
172
|
+
else if (rowFields && rowFields.length) {
|
|
173
|
+
cells = rowFields.map(field => String(row[field] ?? ""));
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
cells = headers.map((h) => {
|
|
177
|
+
const key2 = h.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
178
|
+
return String(row[h] ?? row[key2] ?? row[h.replace(/\s+/g, "")] ?? "");
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
return e(View, { key: `tr-${ri}`, style: { flexDirection: "row" } }, ...cells.map((cell, ci) => e(View, { key: `td-${ri}-${ci}`, style: { ...cellStyle, width: colWidth } }, e(Text, { style: { fontSize: 8 } }, cell))));
|
|
182
|
+
});
|
|
183
|
+
const tableChildren = [];
|
|
184
|
+
if (headerRow)
|
|
185
|
+
tableChildren.push(headerRow);
|
|
186
|
+
tableChildren.push(...dataRows);
|
|
187
|
+
return e(View, { key, style: { width: "100%" } }, ...tableChildren);
|
|
188
|
+
}
|
|
189
|
+
case "box": {
|
|
190
|
+
const inner = (el.children || []).map((child, i) => renderElement(child, data, `box-${i}`));
|
|
191
|
+
const text = el.content || el.text ? resolveText(el.content || el.text || "", data) : "";
|
|
192
|
+
const boxChildren = [];
|
|
193
|
+
if (text)
|
|
194
|
+
boxChildren.push(e(Text, { key: "box-text", style: { textAlign: "center" } }, text));
|
|
195
|
+
boxChildren.push(...inner);
|
|
196
|
+
return e(View, {
|
|
197
|
+
key,
|
|
198
|
+
style: {
|
|
199
|
+
textAlign: "center",
|
|
200
|
+
border: el.border ? undefined : undefined,
|
|
201
|
+
borderWidth: el.border ? 1 : undefined,
|
|
202
|
+
borderColor: el.border ? (el.border.includes("#") ? el.border.split(" ").find(s => s.startsWith("#")) : "#000") : undefined,
|
|
203
|
+
backgroundColor: el.background || el.backgroundColor || undefined,
|
|
204
|
+
padding: typeof el.padding === "number" ? el.padding : el.padding ? toPoints(el.padding, 0) : undefined,
|
|
205
|
+
},
|
|
206
|
+
}, ...boxChildren);
|
|
207
|
+
}
|
|
208
|
+
default: {
|
|
209
|
+
if (el.children && Array.isArray(el.children)) {
|
|
210
|
+
return e(View, { key }, ...el.children.map((child, i) => renderElement(child, data, `child-${i}`)));
|
|
211
|
+
}
|
|
212
|
+
return e(View, { key });
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
// ============================================================================
|
|
217
|
+
// Layout Normalization — same logic as renderLayoutToHtml lines 896-924
|
|
218
|
+
// ============================================================================
|
|
219
|
+
function normalizeLayout(layout) {
|
|
220
|
+
if (!layout)
|
|
221
|
+
return [];
|
|
222
|
+
if (Array.isArray(layout))
|
|
223
|
+
return layout;
|
|
224
|
+
const obj = layout;
|
|
225
|
+
const elements = [];
|
|
226
|
+
if (Array.isArray(obj.pages)) {
|
|
227
|
+
for (const page of obj.pages) {
|
|
228
|
+
if (page.header)
|
|
229
|
+
elements.push(page.header);
|
|
230
|
+
if (Array.isArray(page.body))
|
|
231
|
+
elements.push(...page.body);
|
|
232
|
+
else if (page.body)
|
|
233
|
+
elements.push(page.body);
|
|
234
|
+
if (Array.isArray(page.sections))
|
|
235
|
+
elements.push(...page.sections);
|
|
236
|
+
if (Array.isArray(page.elements))
|
|
237
|
+
elements.push(...page.elements);
|
|
238
|
+
if (page.footer)
|
|
239
|
+
elements.push(page.footer);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
else if (Array.isArray(obj.sections)) {
|
|
243
|
+
elements.push(...obj.sections);
|
|
244
|
+
}
|
|
245
|
+
else if (Array.isArray(obj.children)) {
|
|
246
|
+
elements.push(...obj.children);
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
if (obj.header)
|
|
250
|
+
elements.push(obj.header);
|
|
251
|
+
if (obj.body) {
|
|
252
|
+
if (Array.isArray(obj.body))
|
|
253
|
+
elements.push(...obj.body);
|
|
254
|
+
else
|
|
255
|
+
elements.push(obj.body);
|
|
256
|
+
}
|
|
257
|
+
if (obj.footer)
|
|
258
|
+
elements.push(obj.footer);
|
|
259
|
+
}
|
|
260
|
+
return elements;
|
|
261
|
+
}
|
|
262
|
+
// ============================================================================
|
|
263
|
+
// renderLayoutToPdf — Generic element-based layout → PDF buffer
|
|
264
|
+
// ============================================================================
|
|
265
|
+
export async function renderLayoutToPdf(layout, pageConfig, styles, data) {
|
|
266
|
+
const elements = normalizeLayout(layout);
|
|
267
|
+
const pageSize = mapPageSize(pageConfig?.size);
|
|
268
|
+
const margins = pageConfig?.margins || {};
|
|
269
|
+
const orientation = pageConfig?.orientation === "landscape" ? "landscape" : "portrait";
|
|
270
|
+
const fontFamily = styles?.fontFamily?.split(",")[0]?.trim() || "Helvetica";
|
|
271
|
+
const fontSize = styles?.fontSize || 12;
|
|
272
|
+
const color = styles?.color || "#000";
|
|
273
|
+
const pageStyle = {
|
|
274
|
+
paddingTop: toPoints(margins.top, 42.5),
|
|
275
|
+
paddingRight: toPoints(margins.right, 42.5),
|
|
276
|
+
paddingBottom: toPoints(margins.bottom, 42.5),
|
|
277
|
+
paddingLeft: toPoints(margins.left, 42.5),
|
|
278
|
+
fontFamily,
|
|
279
|
+
fontSize,
|
|
280
|
+
color,
|
|
281
|
+
lineHeight: 1.4,
|
|
282
|
+
};
|
|
283
|
+
const rendered = elements.map((el, i) => renderElement(el, data, `el-${i}`));
|
|
284
|
+
const doc = e(Document, {}, e(Page, { size: pageSize, orientation, style: pageStyle }, ...rendered));
|
|
285
|
+
const buffer = await renderToBuffer(doc);
|
|
286
|
+
return Buffer.from(buffer);
|
|
287
|
+
}
|
|
288
|
+
export async function renderLabelToPdf(layout, pageConfig, items) {
|
|
289
|
+
const IN = 72; // 1 inch = 72 points
|
|
290
|
+
const cols = pageConfig.cols || 2;
|
|
291
|
+
const rows = pageConfig.rows || 5;
|
|
292
|
+
const pageW = (pageConfig.pageWidth || 8.5) * IN;
|
|
293
|
+
const pageH = (pageConfig.pageHeight || 11) * IN;
|
|
294
|
+
const labelW = (pageConfig.labelWidth || 4) * IN;
|
|
295
|
+
const labelH = (pageConfig.labelHeight || 2) * IN;
|
|
296
|
+
const mTop = (pageConfig.margins?.top || 0.5) * IN;
|
|
297
|
+
const mLeft = (pageConfig.margins?.left || 0.15625) * IN;
|
|
298
|
+
const gH = (pageConfig.gutters?.horizontal || 0.125) * IN;
|
|
299
|
+
const gV = (pageConfig.gutters?.vertical || 0) * IN;
|
|
300
|
+
const labelsPerPage = cols * rows;
|
|
301
|
+
const pageCount = Math.ceil(items.length / labelsPerPage);
|
|
302
|
+
const pages = [];
|
|
303
|
+
for (let p = 0; p < pageCount; p++) {
|
|
304
|
+
const labelViews = [];
|
|
305
|
+
for (let slot = 0; slot < labelsPerPage; slot++) {
|
|
306
|
+
const itemIdx = p * labelsPerPage + slot;
|
|
307
|
+
if (itemIdx >= items.length)
|
|
308
|
+
break;
|
|
309
|
+
const item = items[itemIdx];
|
|
310
|
+
const row = Math.floor(slot / cols);
|
|
311
|
+
const col = slot % cols;
|
|
312
|
+
const x = mLeft + col * (labelW + gH);
|
|
313
|
+
const y = mTop + row * (labelH + gV);
|
|
314
|
+
const labelElements = layout.elements.map((le, ei) => {
|
|
315
|
+
const elX = le.x * labelW;
|
|
316
|
+
const elY = le.y * labelH;
|
|
317
|
+
const elW = le.width * labelW;
|
|
318
|
+
const elH = le.height * labelH;
|
|
319
|
+
const value = le.field ? String(item[le.field] ?? "") : (le.text || "");
|
|
320
|
+
switch (le.type) {
|
|
321
|
+
case "text":
|
|
322
|
+
return e(View, {
|
|
323
|
+
key: `le-${ei}`,
|
|
324
|
+
style: { position: "absolute", left: elX, top: elY, width: elW, height: elH },
|
|
325
|
+
}, e(Text, {
|
|
326
|
+
style: {
|
|
327
|
+
fontSize: le.style?.fontSize || 8,
|
|
328
|
+
color: le.color || "#000",
|
|
329
|
+
fontFamily: (le.style?.bold || le.style?.fontWeight === "bold") ? "Helvetica-Bold" : "Helvetica",
|
|
330
|
+
},
|
|
331
|
+
}, value));
|
|
332
|
+
case "badge":
|
|
333
|
+
return e(View, {
|
|
334
|
+
key: `le-${ei}`,
|
|
335
|
+
style: {
|
|
336
|
+
position: "absolute", left: elX, top: elY, width: elW, height: elH,
|
|
337
|
+
backgroundColor: le.backgroundColor || "#4CAF50",
|
|
338
|
+
borderRadius: 4,
|
|
339
|
+
justifyContent: "center",
|
|
340
|
+
alignItems: "center",
|
|
341
|
+
},
|
|
342
|
+
}, e(Text, { style: { fontSize: 7, color: le.color || "#fff", fontFamily: "Helvetica-Bold" } }, value));
|
|
343
|
+
case "image": {
|
|
344
|
+
const src = le.field ? String(item[le.field] ?? "") : "";
|
|
345
|
+
if (!src)
|
|
346
|
+
return e(View, { key: `le-${ei}` });
|
|
347
|
+
return e(Image, {
|
|
348
|
+
key: `le-${ei}`,
|
|
349
|
+
src,
|
|
350
|
+
style: { position: "absolute", left: elX, top: elY, width: elW, height: elH },
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
case "qr_code": {
|
|
354
|
+
const svg = generateQRSvg(value, Math.min(elW, elH));
|
|
355
|
+
const dataUri = `data:image/svg+xml;base64,${Buffer.from(svg).toString("base64")}`;
|
|
356
|
+
return e(Image, {
|
|
357
|
+
key: `le-${ei}`,
|
|
358
|
+
src: dataUri,
|
|
359
|
+
style: { position: "absolute", left: elX, top: elY, width: Math.min(elW, elH), height: Math.min(elW, elH) },
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
case "date": {
|
|
363
|
+
const dateVal = le.field ? item[le.field] : null;
|
|
364
|
+
const formatted = dateVal ? new Date(String(dateVal)).toLocaleDateString() : value;
|
|
365
|
+
return e(View, {
|
|
366
|
+
key: `le-${ei}`,
|
|
367
|
+
style: { position: "absolute", left: elX, top: elY, width: elW, height: elH },
|
|
368
|
+
}, e(Text, { style: { fontSize: le.style?.fontSize || 8, color: le.color || "#000" } }, formatted));
|
|
369
|
+
}
|
|
370
|
+
default:
|
|
371
|
+
return e(View, { key: `le-${ei}` });
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
labelViews.push(e(View, {
|
|
375
|
+
key: `label-${slot}`,
|
|
376
|
+
style: {
|
|
377
|
+
position: "absolute",
|
|
378
|
+
left: x,
|
|
379
|
+
top: y,
|
|
380
|
+
width: labelW,
|
|
381
|
+
height: labelH,
|
|
382
|
+
},
|
|
383
|
+
}, ...labelElements));
|
|
384
|
+
}
|
|
385
|
+
pages.push(e(Page, {
|
|
386
|
+
key: `page-${p}`,
|
|
387
|
+
size: [pageW, pageH],
|
|
388
|
+
style: { position: "relative" },
|
|
389
|
+
}, ...labelViews));
|
|
390
|
+
}
|
|
391
|
+
const doc = e(Document, {}, ...pages);
|
|
392
|
+
const buffer = await renderToBuffer(doc);
|
|
393
|
+
return Buffer.from(buffer);
|
|
394
|
+
}
|
|
395
|
+
// ============================================================================
|
|
396
|
+
// renderHtmlToPdf — Direct HTML string → PDF (replaces Playwright)
|
|
397
|
+
// ============================================================================
|
|
398
|
+
export async function renderHtmlToPdf(html, options) {
|
|
399
|
+
// Sanitize: strip dangerous tags (same P0 security as the old Playwright path)
|
|
400
|
+
const sanitizedHtml = html
|
|
401
|
+
.replace(/<script[\s\S]*?<\/script>/gi, "")
|
|
402
|
+
.replace(/<script[^>]*\/?>/gi, "")
|
|
403
|
+
.replace(/<iframe[\s\S]*?<\/iframe>/gi, "")
|
|
404
|
+
.replace(/<iframe[^>]*\/?>/gi, "")
|
|
405
|
+
.replace(/<object[\s\S]*?<\/object>/gi, "")
|
|
406
|
+
.replace(/<object[^>]*\/?>/gi, "")
|
|
407
|
+
.replace(/<embed[^>]*\/?>/gi, "")
|
|
408
|
+
.replace(/<link[^>]*\/?>/gi, "");
|
|
409
|
+
// Use react-pdf-html to render HTML inside React-PDF
|
|
410
|
+
let Html;
|
|
411
|
+
try {
|
|
412
|
+
const mod = await import("react-pdf-html");
|
|
413
|
+
Html = mod.default || mod.Html || mod;
|
|
414
|
+
}
|
|
415
|
+
catch {
|
|
416
|
+
// Fallback: extract text content and render as plain text
|
|
417
|
+
const textContent = sanitizedHtml.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
|
|
418
|
+
const pageSize = mapPageSize(options?.format);
|
|
419
|
+
const orientation = options?.landscape ? "landscape" : "portrait";
|
|
420
|
+
const doc = e(Document, {}, e(Page, {
|
|
421
|
+
size: pageSize,
|
|
422
|
+
orientation,
|
|
423
|
+
style: { padding: 28, fontFamily: "Helvetica", fontSize: 12 },
|
|
424
|
+
}, e(Text, {}, textContent)));
|
|
425
|
+
const buffer = await renderToBuffer(doc);
|
|
426
|
+
return Buffer.from(buffer);
|
|
427
|
+
}
|
|
428
|
+
const pageSize = mapPageSize(options?.format);
|
|
429
|
+
const orientation = options?.landscape ? "landscape" : "portrait";
|
|
430
|
+
const doc = e(Document, {}, e(Page, {
|
|
431
|
+
size: pageSize,
|
|
432
|
+
orientation,
|
|
433
|
+
style: { padding: 28, fontFamily: "Helvetica", fontSize: 12, lineHeight: 1.4 },
|
|
434
|
+
}, e(Html, { style: { fontSize: 12 } }, sanitizedHtml)));
|
|
435
|
+
const buffer = await renderToBuffer(doc);
|
|
436
|
+
return Buffer.from(buffer);
|
|
437
|
+
}
|
|
@@ -102,6 +102,8 @@ export interface ServerAgentLoopResult {
|
|
|
102
102
|
consecutiveFailedTurns: number;
|
|
103
103
|
};
|
|
104
104
|
turns: TurnMetrics[];
|
|
105
|
+
/** Final stop reason from the last API response (end_turn, tool_use, max_tokens) */
|
|
106
|
+
stopReason: string;
|
|
105
107
|
}
|
|
106
108
|
export type { SubagentProgressCallback, SubagentProgressEvent };
|
|
107
109
|
export declare function runServerAgentLoop(opts: ServerAgentLoopOptions): Promise<ServerAgentLoopResult>;
|
|
@@ -8,11 +8,12 @@
|
|
|
8
8
|
* Consolidates: streaming, prompt caching, context management betas, compaction,
|
|
9
9
|
* loop detection, parallel tool execution, subagent delegation, retry, cost tracking.
|
|
10
10
|
*/
|
|
11
|
-
import { LoopDetector, getContextManagement, getMaxOutputTokens, getThinkingConfig, addPromptCaching, estimateCostUsd, isRetryableError, sanitizeError, routeModel, resolveToolChoice, COMPACTION_TRIGGER_TOKENS, COMPACTION_TOTAL_BUDGET, DEFAULT_SESSION_COST_BUDGET_USD, } from "../../shared/agent-core.js";
|
|
11
|
+
import { LoopDetector, getContextManagement, getMaxOutputTokens, getThinkingConfig, addPromptCaching, estimateCostUsd, isRetryableError, sanitizeError, routeModel, resolveToolChoice, emitCostWarningIfNeeded, demoteSubagentModel, COMPACTION_TRIGGER_TOKENS, COMPACTION_TOTAL_BUDGET, DEFAULT_SESSION_COST_BUDGET_USD, } from "../../shared/agent-core.js";
|
|
12
12
|
import { processStreamWithCallbacks } from "../../shared/sse-parser.js";
|
|
13
13
|
import { MODELS } from "../../shared/constants.js";
|
|
14
14
|
import { dispatchTools, buildAssistantContent } from "../../shared/tool-dispatch.js";
|
|
15
15
|
import { getCachedToolDefs, getFullToolSchemas } from "../tool-router.js";
|
|
16
|
+
import { queueSpan, auditRowToSpan } from "./clickhouse-buffer.js";
|
|
16
17
|
import { DELEGATE_TASK_TOOL_DEF, runServerSubagent, } from "./server-subagent.js";
|
|
17
18
|
import { handleTranscribe } from "../handlers/transcription.js";
|
|
18
19
|
import { preCompact } from "./compaction-service.js";
|
|
@@ -49,7 +50,10 @@ function mapToolChoiceForAnthropic(tc) {
|
|
|
49
50
|
// UNIFIED AGENT LOOP
|
|
50
51
|
// ============================================================================
|
|
51
52
|
export async function runServerAgentLoop(opts) {
|
|
52
|
-
const { anthropic, model, systemPrompt, messages, tools: inputTools, maxTurns, temperature, enableDelegation = true, enablePromptCaching = true, enableStreaming = true, maxConcurrentTools = DEFAULT_MAX_CONCURRENT_TOOLS, maxCostUsd
|
|
53
|
+
const { anthropic, model, systemPrompt, messages, tools: inputTools, maxTurns, temperature, enableDelegation = true, enablePromptCaching = true, enableStreaming = true, maxConcurrentTools = DEFAULT_MAX_CONCURRENT_TOOLS, maxCostUsd: maxCostUsdOpt, onText, onToolStart, onCitation, documents, clientDisconnected = { value: false }, startedAt = Date.now(), maxDurationMs = 15 * 60 * 1000, } = opts;
|
|
54
|
+
// Resolve cost budget: explicit opt > env var > default
|
|
55
|
+
const envBudget = parseFloat(process.env.WHALE_COST_BUDGET_USD || "");
|
|
56
|
+
const maxCostUsd = maxCostUsdOpt ?? (isFinite(envBudget) ? envBudget : DEFAULT_SESSION_COST_BUDGET_USD);
|
|
53
57
|
// Auto-inject delegate_task for all models (subagents always use Claude Haiku/Sonnet)
|
|
54
58
|
// activeTools is mutable — discover_tools adds to it during the session
|
|
55
59
|
const activeTools = [...inputTools];
|
|
@@ -92,10 +96,12 @@ export async function runServerAgentLoop(opts) {
|
|
|
92
96
|
let sessionCostUsd = 0;
|
|
93
97
|
let compactionCount = 0;
|
|
94
98
|
let finalResponse = "";
|
|
99
|
+
let lastStopReason = "end_turn";
|
|
95
100
|
const allTextResponses = [];
|
|
96
101
|
const allToolNames = [];
|
|
97
102
|
const allCitations = [];
|
|
98
103
|
const turnMetrics = [];
|
|
104
|
+
const costWarningsEmitted = new Set();
|
|
99
105
|
while (turnCount < maxTurns) {
|
|
100
106
|
// Abort checks
|
|
101
107
|
if (clientDisconnected.value) {
|
|
@@ -164,13 +170,17 @@ export async function runServerAgentLoop(opts) {
|
|
|
164
170
|
];
|
|
165
171
|
// Resolve tool_choice for this turn
|
|
166
172
|
const recentToolUses = turnMetrics.slice(-3).flatMap(t => t.toolsUsed);
|
|
167
|
-
|
|
173
|
+
let resolvedToolChoice = resolveToolChoice({
|
|
168
174
|
toolChoice: opts.toolChoice,
|
|
169
175
|
turnCount,
|
|
170
176
|
recentToolUses,
|
|
171
177
|
availableToolNames: tools.map(t => t.name),
|
|
172
178
|
userMessage: firstUserText,
|
|
173
179
|
});
|
|
180
|
+
// Anthropic API: forced tool_choice ("any" or specific tool) is incompatible with thinking — downgrade to "auto"
|
|
181
|
+
if (thinkingCfg.thinking.type !== "disabled" && resolvedToolChoice !== "auto" && resolvedToolChoice !== "none") {
|
|
182
|
+
resolvedToolChoice = "auto";
|
|
183
|
+
}
|
|
174
184
|
const { toolChoice: anthropicToolChoice, omitTools } = mapToolChoiceForAnthropic(resolvedToolChoice);
|
|
175
185
|
if (omitTools) {
|
|
176
186
|
log.info({ turn: turnCount, resolvedToolChoice }, "tool_choice=none — omitting tools");
|
|
@@ -254,6 +264,8 @@ export async function runServerAgentLoop(opts) {
|
|
|
254
264
|
cacheReadTokens += turnCacheRead;
|
|
255
265
|
// Update cost (include cache tokens for accurate pricing)
|
|
256
266
|
sessionCostUsd = estimateCostUsd(totalIn, totalOut, model, 0, cacheReadTokens, cacheCreationTokens);
|
|
267
|
+
// Graduated cost warnings — give the LLM visibility into spend
|
|
268
|
+
emitCostWarningIfNeeded(sessionCostUsd, maxCostUsd, costWarningsEmitted, onText);
|
|
257
269
|
// Record per-turn metrics for observability
|
|
258
270
|
const turnToolNames = toolUseBlocks.map(b => b.name);
|
|
259
271
|
turnMetrics.push({
|
|
@@ -271,6 +283,7 @@ export async function runServerAgentLoop(opts) {
|
|
|
271
283
|
// Compaction handling — API paused after generating summary.
|
|
272
284
|
// Preserve last 2 messages (1 user + 1 assistant turn) for continuity,
|
|
273
285
|
// then resume. This is NOT a new turn — just context compression.
|
|
286
|
+
lastStopReason = streamResult.stopReason || "end_turn";
|
|
274
287
|
if (streamResult.stopReason === "compaction" && compactionContent) {
|
|
275
288
|
compactionCount++;
|
|
276
289
|
log.info({ compactionCount }, "compaction — preserving last 2 messages, resuming");
|
|
@@ -331,6 +344,8 @@ export async function runServerAgentLoop(opts) {
|
|
|
331
344
|
totalIn += subagentTokens.input;
|
|
332
345
|
totalOut += subagentTokens.output;
|
|
333
346
|
sessionCostUsd = estimateCostUsd(totalIn, totalOut, model, 0, cacheReadTokens, cacheCreationTokens) + subagentTokens.costUsd;
|
|
347
|
+
// Cost warnings after subagent aggregation (subagents can be expensive)
|
|
348
|
+
emitCostWarningIfNeeded(sessionCostUsd, maxCostUsd, costWarningsEmitted, onText);
|
|
334
349
|
const assistantContent = buildAssistantContent({ text: currentText, toolUseBlocks, compactionContent });
|
|
335
350
|
messages.push({ role: "assistant", content: assistantContent });
|
|
336
351
|
messages.push({ role: "user", content: toolResults });
|
|
@@ -426,6 +441,8 @@ export async function runServerAgentLoop(opts) {
|
|
|
426
441
|
}
|
|
427
442
|
}
|
|
428
443
|
sessionCostUsd = estimateCostUsd(totalIn, totalOut, model, 0, cacheReadTokens, cacheCreationTokens);
|
|
444
|
+
// Graduated cost warnings (non-streaming path)
|
|
445
|
+
emitCostWarningIfNeeded(sessionCostUsd, maxCostUsd, costWarningsEmitted, onText);
|
|
429
446
|
// Record per-turn metrics (non-streaming)
|
|
430
447
|
const nsTurnToolNames = toolUseBlocks.map(b => b.name);
|
|
431
448
|
turnMetrics.push({
|
|
@@ -440,6 +457,7 @@ export async function runServerAgentLoop(opts) {
|
|
|
440
457
|
});
|
|
441
458
|
if (currentText)
|
|
442
459
|
allTextResponses.push(currentText);
|
|
460
|
+
lastStopReason = response.stop_reason || "end_turn";
|
|
443
461
|
// Compaction handling (non-streaming) — same logic as streaming path
|
|
444
462
|
if (response.stop_reason === "compaction" && nsCompactionContent !== null) {
|
|
445
463
|
compactionCount++;
|
|
@@ -488,6 +506,8 @@ export async function runServerAgentLoop(opts) {
|
|
|
488
506
|
totalIn += nonStreamSubTokens.input;
|
|
489
507
|
totalOut += nonStreamSubTokens.output;
|
|
490
508
|
sessionCostUsd = estimateCostUsd(totalIn, totalOut, model, 0, cacheReadTokens, cacheCreationTokens) + nonStreamSubTokens.costUsd;
|
|
509
|
+
// Cost warnings after subagent aggregation (non-streaming)
|
|
510
|
+
emitCostWarningIfNeeded(sessionCostUsd, maxCostUsd, costWarningsEmitted, onText);
|
|
491
511
|
const assistantContent = buildAssistantContent({ text: currentText, toolUseBlocks });
|
|
492
512
|
messages.push({ role: "assistant", content: assistantContent });
|
|
493
513
|
messages.push({ role: "user", content: toolResults });
|
|
@@ -514,21 +534,20 @@ export async function runServerAgentLoop(opts) {
|
|
|
514
534
|
loopDetectorStats: loopDetector.getSessionStats(),
|
|
515
535
|
turns: turnMetrics,
|
|
516
536
|
citations: allCitations,
|
|
537
|
+
stopReason: lastStopReason,
|
|
517
538
|
};
|
|
518
539
|
}
|
|
519
540
|
// ============================================================================
|
|
520
541
|
// TOOL EXECUTOR FACTORY — creates executor for dispatchTools with delegation
|
|
521
542
|
// ============================================================================
|
|
522
543
|
function makeToolExecutor(opts, tools, allToolNames, subagentTokens, discoveredToolNames) {
|
|
523
|
-
const { anthropic, supabase, storeId, traceId, userId, userEmail, conversationId, agentId, executeTool, onToolResult, onToolProgress, onSubagentProgress, clientDisconnected = { value: false }, startedAt = Date.now(), maxDurationMs =
|
|
544
|
+
const { anthropic, supabase, storeId, traceId, userId, userEmail, conversationId, agentId, executeTool, onToolResult, onToolProgress, onSubagentProgress, clientDisconnected = { value: false }, startedAt = Date.now(), maxDurationMs = 15 * 60 * 1000, } = opts;
|
|
524
545
|
return async (name, input) => {
|
|
525
546
|
allToolNames.push(name);
|
|
526
|
-
// Subagent delegation
|
|
547
|
+
// Subagent delegation — demote models to control cost (sub-agents should never run Opus)
|
|
527
548
|
if (name === "delegate_task") {
|
|
528
549
|
const subPrompt = String(input.prompt || "");
|
|
529
|
-
const
|
|
530
|
-
const subModel = (subModelInput === "opus" ? "opus" :
|
|
531
|
-
subModelInput === "sonnet" ? "sonnet" : "haiku");
|
|
550
|
+
const subModel = demoteSubagentModel(input.model ? String(input.model) : undefined);
|
|
532
551
|
const subMaxTurns = Math.min(Math.max(1, Number(input.max_turns) || 6), 12);
|
|
533
552
|
const subTools = tools.filter((t) => t.name !== "delegate_task");
|
|
534
553
|
const subId = `sub-${Date.now().toString(36)}`;
|
|
@@ -547,37 +566,37 @@ function makeToolExecutor(opts, tools, allToolNames, subagentTokens, discoveredT
|
|
|
547
566
|
: subModel === "sonnet" ? MODELS.SONNET : MODELS.HAIKU;
|
|
548
567
|
try {
|
|
549
568
|
const subEndTime = Date.now();
|
|
550
|
-
|
|
551
|
-
crypto.getRandomValues(subBytes);
|
|
552
|
-
const subSpanId = Array.from(subBytes).map(b => b.toString(16).padStart(2, "0")).join("");
|
|
553
|
-
await supabase.from("audit_logs").insert({
|
|
569
|
+
queueSpan(auditRowToSpan({
|
|
554
570
|
action: "chat.subagent_complete", severity: "info",
|
|
555
571
|
store_id: storeId || null, resource_type: "chat_subagent",
|
|
556
572
|
resource_id: agentId || null, request_id: traceId || null,
|
|
557
573
|
conversation_id: conversationId || null, source: "server_subagent",
|
|
558
|
-
user_id: userId || null,
|
|
574
|
+
user_id: userId || null,
|
|
575
|
+
user_email: userEmail || null,
|
|
559
576
|
input_tokens: subResult.tokensUsed.input, output_tokens: subResult.tokensUsed.output,
|
|
560
577
|
total_cost: subResult.costUsd, model: subModelId, duration_ms: subDurationMs,
|
|
561
|
-
// OTEL fields
|
|
562
578
|
trace_id: traceId || null,
|
|
563
|
-
span_id: subSpanId,
|
|
564
579
|
span_kind: "INTERNAL",
|
|
565
580
|
service_name: "agent-server",
|
|
566
581
|
status_code: subResult.success ? "OK" : "ERROR",
|
|
567
582
|
start_time: new Date(subEndTime - subDurationMs).toISOString(),
|
|
568
583
|
end_time: new Date(subEndTime).toISOString(),
|
|
584
|
+
stop_reason: subResult.stopReason || undefined,
|
|
585
|
+
turn_number: subResult.turnCount || 1,
|
|
586
|
+
parent_conversation_id: conversationId || undefined,
|
|
569
587
|
details: {
|
|
570
588
|
subagent_model: subModel, turn_count: subResult.turnCount,
|
|
571
589
|
tool_calls: subResult.toolsUsed.length, tool_names: subResult.toolsUsed,
|
|
572
590
|
cost_usd: subResult.costUsd, success: subResult.success,
|
|
573
591
|
prompt_preview: subPrompt.substring(0, 200),
|
|
574
|
-
// gen_ai fields for SwiftUI cost display
|
|
575
592
|
"gen_ai.request.model": subModelId,
|
|
576
593
|
"gen_ai.usage.input_tokens": subResult.tokensUsed.input,
|
|
577
594
|
"gen_ai.usage.output_tokens": subResult.tokensUsed.output,
|
|
595
|
+
"gen_ai.usage.cache_read_tokens": subResult.tokensUsed.cacheRead || 0,
|
|
596
|
+
"gen_ai.usage.cache_creation_tokens": subResult.tokensUsed.cacheCreation || 0,
|
|
578
597
|
"gen_ai.usage.cost": subResult.costUsd,
|
|
579
598
|
},
|
|
580
|
-
});
|
|
599
|
+
}));
|
|
581
600
|
}
|
|
582
601
|
catch (err) {
|
|
583
602
|
log.error({ err: err.message }, "failed to log subagent delegation audit");
|