snow-flow-test 10.0.1-test.113 → 10.0.1-test.115
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/package.json
CHANGED
|
@@ -473,10 +473,19 @@ async function ensureFlowFactoryAPI(
|
|
|
473
473
|
var existing = checkResp.data.result[0];
|
|
474
474
|
var ns = await probeFlowFactoryNamespace(client, existing.sys_id, instanceUrl);
|
|
475
475
|
if (!ns) {
|
|
476
|
-
|
|
476
|
+
// Namespace can't be resolved — the API is stale (e.g. created by an older version
|
|
477
|
+
// without the /discover endpoint, or ServiceNow REST framework hasn't registered it).
|
|
478
|
+
// Delete and redeploy with current v5 scripts.
|
|
479
|
+
try {
|
|
480
|
+
await client.delete('/api/now/table/sys_ws_definition/' + existing.sys_id);
|
|
481
|
+
} catch (_) {
|
|
482
|
+
// If delete fails, try to continue anyway — deployment step will error if API ID conflicts
|
|
483
|
+
}
|
|
484
|
+
// Fall through to step 4 (deploy fresh)
|
|
485
|
+
} else {
|
|
486
|
+
_flowFactoryCache = { apiSysId: existing.sys_id, namespace: ns, timestamp: Date.now() };
|
|
487
|
+
return { namespace: ns, apiSysId: existing.sys_id };
|
|
477
488
|
}
|
|
478
|
-
_flowFactoryCache = { apiSysId: existing.sys_id, namespace: ns, timestamp: Date.now() };
|
|
479
|
-
return { namespace: ns, apiSysId: existing.sys_id };
|
|
480
489
|
}
|
|
481
490
|
|
|
482
491
|
// 4. Deploy the Scripted REST API (do NOT set namespace — let ServiceNow assign it)
|
|
@@ -531,10 +540,18 @@ async function ensureFlowFactoryAPI(
|
|
|
531
540
|
// Non-fatal: create endpoint is more important than discover
|
|
532
541
|
}
|
|
533
542
|
|
|
534
|
-
// 6.
|
|
543
|
+
// 6. Wait for ServiceNow REST framework to register the new endpoints
|
|
544
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
545
|
+
|
|
546
|
+
// 7. Probe to discover the namespace ServiceNow assigned
|
|
535
547
|
var resolvedNs = await probeFlowFactoryNamespace(client, apiSysId, instanceUrl);
|
|
536
548
|
if (!resolvedNs) {
|
|
537
|
-
|
|
549
|
+
// Retry once after extra delay — some instances are slow to register
|
|
550
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
551
|
+
resolvedNs = await probeFlowFactoryNamespace(client, apiSysId, instanceUrl);
|
|
552
|
+
}
|
|
553
|
+
if (!resolvedNs) {
|
|
554
|
+
throw new Error('Flow Factory API created (sys_id=' + apiSysId + ') but namespace could not be resolved via HTTP probing after 6s delay');
|
|
538
555
|
}
|
|
539
556
|
|
|
540
557
|
_flowFactoryCache = { apiSysId: apiSysId, namespace: resolvedNs, timestamp: Date.now() };
|
package/src/session/prompt.ts
CHANGED
|
@@ -45,6 +45,7 @@ import { LLM } from "./llm"
|
|
|
45
45
|
import { iife } from "@/util/iife"
|
|
46
46
|
import { Shell } from "@/shell/shell"
|
|
47
47
|
import { Truncate } from "@/tool/truncation"
|
|
48
|
+
import { UsageReporter } from "@/usage"
|
|
48
49
|
|
|
49
50
|
// @ts-ignore
|
|
50
51
|
globalThis.AI_SDK_LOG_WARNINGS = false
|
|
@@ -152,6 +153,9 @@ export namespace SessionPrompt {
|
|
|
152
153
|
const session = await Session.get(input.sessionID)
|
|
153
154
|
await SessionRevert.cleanup(session)
|
|
154
155
|
|
|
156
|
+
// Initialize TUI usage reporting (lazy, no-op if no enterprise auth)
|
|
157
|
+
UsageReporter.init()
|
|
158
|
+
|
|
155
159
|
const message = await createUserMessage(input)
|
|
156
160
|
await Session.touch(input.sessionID)
|
|
157
161
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { UsageReporter } from "./reporter"
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { Instance } from "@/project/instance"
|
|
2
|
+
import { Bus } from "@/bus"
|
|
3
|
+
import { MessageV2 } from "@/session/message-v2"
|
|
4
|
+
import { Auth } from "@/auth"
|
|
5
|
+
import { Log } from "@/util/log"
|
|
6
|
+
|
|
7
|
+
const log = Log.create({ service: "usage.reporter" })
|
|
8
|
+
|
|
9
|
+
const BUILTIN_TOOLS = new Set([
|
|
10
|
+
"Read",
|
|
11
|
+
"Write",
|
|
12
|
+
"Edit",
|
|
13
|
+
"Bash",
|
|
14
|
+
"Glob",
|
|
15
|
+
"Grep",
|
|
16
|
+
"Task",
|
|
17
|
+
"WebFetch",
|
|
18
|
+
"WebSearch",
|
|
19
|
+
"Skill",
|
|
20
|
+
"TodoRead",
|
|
21
|
+
"TodoWrite",
|
|
22
|
+
"NotebookEdit",
|
|
23
|
+
"AskUserQuestion",
|
|
24
|
+
"EnterPlanMode",
|
|
25
|
+
"ExitPlanMode",
|
|
26
|
+
])
|
|
27
|
+
|
|
28
|
+
interface UsageEvent {
|
|
29
|
+
eventType: "llm" | "tool"
|
|
30
|
+
sessionId: string
|
|
31
|
+
machineId?: string
|
|
32
|
+
model?: string
|
|
33
|
+
provider?: string
|
|
34
|
+
agent?: string
|
|
35
|
+
tokensInput?: number
|
|
36
|
+
tokensOutput?: number
|
|
37
|
+
tokensReasoning?: number
|
|
38
|
+
tokensCacheRead?: number
|
|
39
|
+
tokensCacheWrite?: number
|
|
40
|
+
costUsd?: number
|
|
41
|
+
toolName?: string
|
|
42
|
+
toolCategory?: string
|
|
43
|
+
durationMs?: number
|
|
44
|
+
success?: boolean
|
|
45
|
+
errorMessage?: string
|
|
46
|
+
timestamp: number
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface AuthCache {
|
|
50
|
+
licenseKey: string
|
|
51
|
+
portalUrl: string
|
|
52
|
+
cachedAt: number
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const AUTH_CACHE_TTL = 5 * 60 * 1000 // 5 minutes
|
|
56
|
+
const FLUSH_INTERVAL = 30_000 // 30 seconds
|
|
57
|
+
const MAX_BATCH = 500
|
|
58
|
+
|
|
59
|
+
export namespace UsageReporter {
|
|
60
|
+
// Message metadata cache: maps messageID -> { modelID, providerID, agent }
|
|
61
|
+
// Populated from Message.Updated events so we can correlate step-finish parts
|
|
62
|
+
const messageMetadata = new Map<string, { modelID: string; providerID: string; agent: string }>()
|
|
63
|
+
|
|
64
|
+
const state = Instance.state(
|
|
65
|
+
() => {
|
|
66
|
+
const queue: UsageEvent[] = []
|
|
67
|
+
let authCache: AuthCache | undefined
|
|
68
|
+
let flushTimer: ReturnType<typeof setInterval> | undefined
|
|
69
|
+
|
|
70
|
+
const unsubs = [
|
|
71
|
+
// Track assistant message metadata for correlating step-finish parts
|
|
72
|
+
Bus.subscribe(MessageV2.Event.Updated, (event) => {
|
|
73
|
+
const info = event.properties.info
|
|
74
|
+
if (info.role !== "assistant") return
|
|
75
|
+
messageMetadata.set(info.id, {
|
|
76
|
+
modelID: info.modelID,
|
|
77
|
+
providerID: info.providerID,
|
|
78
|
+
agent: info.agent,
|
|
79
|
+
})
|
|
80
|
+
}),
|
|
81
|
+
|
|
82
|
+
// Track part updates for usage events
|
|
83
|
+
Bus.subscribe(MessageV2.Event.PartUpdated, (event) => {
|
|
84
|
+
const part = event.properties.part
|
|
85
|
+
|
|
86
|
+
if (part.type === "step-finish") {
|
|
87
|
+
const meta = messageMetadata.get(part.messageID)
|
|
88
|
+
queue.push({
|
|
89
|
+
eventType: "llm",
|
|
90
|
+
sessionId: part.sessionID,
|
|
91
|
+
model: meta?.modelID,
|
|
92
|
+
provider: meta?.providerID,
|
|
93
|
+
agent: meta?.agent,
|
|
94
|
+
tokensInput: part.tokens.input,
|
|
95
|
+
tokensOutput: part.tokens.output,
|
|
96
|
+
tokensReasoning: part.tokens.reasoning,
|
|
97
|
+
tokensCacheRead: part.tokens.cache.read,
|
|
98
|
+
tokensCacheWrite: part.tokens.cache.write,
|
|
99
|
+
costUsd: part.cost,
|
|
100
|
+
timestamp: Date.now(),
|
|
101
|
+
})
|
|
102
|
+
if (queue.length >= MAX_BATCH) flush(queue, authCache).catch(() => {})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (part.type === "tool") {
|
|
106
|
+
if (part.state.status === "completed") {
|
|
107
|
+
queue.push({
|
|
108
|
+
eventType: "tool",
|
|
109
|
+
sessionId: part.sessionID,
|
|
110
|
+
toolName: part.tool,
|
|
111
|
+
toolCategory: BUILTIN_TOOLS.has(part.tool) ? "builtin" : "mcp",
|
|
112
|
+
durationMs: part.state.time.end - part.state.time.start,
|
|
113
|
+
success: true,
|
|
114
|
+
timestamp: Date.now(),
|
|
115
|
+
})
|
|
116
|
+
if (queue.length >= MAX_BATCH) flush(queue, authCache).catch(() => {})
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (part.state.status === "error") {
|
|
120
|
+
queue.push({
|
|
121
|
+
eventType: "tool",
|
|
122
|
+
sessionId: part.sessionID,
|
|
123
|
+
toolName: part.tool,
|
|
124
|
+
toolCategory: BUILTIN_TOOLS.has(part.tool) ? "builtin" : "mcp",
|
|
125
|
+
durationMs: part.state.time.end - part.state.time.start,
|
|
126
|
+
success: false,
|
|
127
|
+
errorMessage: part.state.error,
|
|
128
|
+
timestamp: Date.now(),
|
|
129
|
+
})
|
|
130
|
+
if (queue.length >= MAX_BATCH) flush(queue, authCache).catch(() => {})
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}),
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
// Start periodic flush
|
|
137
|
+
resolveAuth().then((auth) => {
|
|
138
|
+
if (!auth) {
|
|
139
|
+
log.info("no enterprise/portal auth found, TUI usage reporting disabled")
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
authCache = auth
|
|
143
|
+
flushTimer = setInterval(() => {
|
|
144
|
+
flush(queue, authCache).catch(() => {})
|
|
145
|
+
}, FLUSH_INTERVAL)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
return { queue, unsubs, flushTimer, authCache }
|
|
149
|
+
},
|
|
150
|
+
async (current) => {
|
|
151
|
+
// Dispose: final flush + cleanup
|
|
152
|
+
if (current.flushTimer) clearInterval(current.flushTimer)
|
|
153
|
+
for (const unsub of current.unsubs) unsub()
|
|
154
|
+
if (current.queue.length > 0 && current.authCache) {
|
|
155
|
+
await flush(current.queue, current.authCache).catch(() => {})
|
|
156
|
+
}
|
|
157
|
+
messageMetadata.clear()
|
|
158
|
+
},
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
/** Initialize the reporter. Call once per Instance lifecycle. */
|
|
162
|
+
export function init() {
|
|
163
|
+
state() // triggers lazy init
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function resolveAuth(): Promise<AuthCache | undefined> {
|
|
168
|
+
const all = await Auth.all()
|
|
169
|
+
|
|
170
|
+
// Check enterprise auth first
|
|
171
|
+
for (const entry of Object.values(all)) {
|
|
172
|
+
if (entry.type === "enterprise" && entry.licenseKey) {
|
|
173
|
+
return {
|
|
174
|
+
licenseKey: entry.licenseKey,
|
|
175
|
+
portalUrl: entry.enterpriseUrl || process.env.SNOW_FLOW_PORTAL_URL || "https://portal.snow-flow.dev",
|
|
176
|
+
cachedAt: Date.now(),
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Check portal auth (Teams users have organization license key)
|
|
182
|
+
// Portal auth doesn't carry a license key directly, but we can use the token
|
|
183
|
+
// The portal backend also accepts organization license keys
|
|
184
|
+
for (const entry of Object.values(all)) {
|
|
185
|
+
if (entry.type === "portal") {
|
|
186
|
+
// Portal users need their org license key; we don't have it directly
|
|
187
|
+
// They authenticate via token, not license key
|
|
188
|
+
// For now, skip portal-only users (they use the portal chat which already tracks usage)
|
|
189
|
+
return undefined
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return undefined
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function flush(queue: UsageEvent[], authCache: AuthCache | undefined): Promise<void> {
|
|
197
|
+
if (queue.length === 0 || !authCache) return
|
|
198
|
+
|
|
199
|
+
// Drain the queue
|
|
200
|
+
const batch = queue.splice(0, MAX_BATCH)
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
const response = await fetch(`${authCache.portalUrl}/api/tui/usage/ingest`, {
|
|
204
|
+
method: "POST",
|
|
205
|
+
headers: {
|
|
206
|
+
"Content-Type": "application/json",
|
|
207
|
+
"X-License-Key": authCache.licenseKey,
|
|
208
|
+
},
|
|
209
|
+
body: JSON.stringify({ events: batch }),
|
|
210
|
+
signal: AbortSignal.timeout(10_000),
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
if (!response.ok) {
|
|
214
|
+
if (response.status === 401) {
|
|
215
|
+
log.warn("TUI usage flush got 401, clearing auth cache")
|
|
216
|
+
// Re-resolve auth on next flush
|
|
217
|
+
const fresh = await resolveAuth()
|
|
218
|
+
if (fresh) {
|
|
219
|
+
authCache.licenseKey = fresh.licenseKey
|
|
220
|
+
authCache.portalUrl = fresh.portalUrl
|
|
221
|
+
authCache.cachedAt = fresh.cachedAt
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// Put events back if flush failed (with limit to avoid unbounded growth)
|
|
225
|
+
if (queue.length + batch.length <= MAX_BATCH * 2) {
|
|
226
|
+
queue.unshift(...batch)
|
|
227
|
+
}
|
|
228
|
+
log.warn("TUI usage flush failed", { status: response.status })
|
|
229
|
+
}
|
|
230
|
+
} catch (error) {
|
|
231
|
+
// Fire-and-forget: network errors are expected when offline
|
|
232
|
+
if (queue.length + batch.length <= MAX_BATCH * 2) {
|
|
233
|
+
queue.unshift(...batch)
|
|
234
|
+
}
|
|
235
|
+
log.warn("TUI usage flush error", { error: String(error) })
|
|
236
|
+
}
|
|
237
|
+
}
|