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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
- "version": "10.0.1-test.113",
3
+ "version": "10.0.1-test.115",
4
4
  "name": "snow-flow-test",
5
5
  "description": "Snow-Flow Test - ServiceNow Multi-Agent Development Framework",
6
6
  "license": "Elastic-2.0",
@@ -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
- throw new Error('Flow Factory API exists (sys_id=' + existing.sys_id + ') but namespace could not be resolved via HTTP probing');
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. Probe to discover the namespace ServiceNow assigned
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
- throw new Error('Flow Factory API created (sys_id=' + apiSysId + ') but namespace could not be resolved via HTTP probing');
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() };
@@ -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
+ }