@firstflow/core 0.0.7 → 0.0.9

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.
@@ -0,0 +1,404 @@
1
+ import {
2
+ context,
3
+ propagation
4
+ } from "./chunk-QBEGXT76.mjs";
5
+
6
+ // src/wrap.ts
7
+ var INSTRUMENTED_PATHS = /* @__PURE__ */ new Set([
8
+ "chat.completions.create",
9
+ "embeddings.create",
10
+ "responses.create",
11
+ "messages.create"
12
+ ]);
13
+ var warnedIncompleteTagging = false;
14
+ function parseFirstflowMeta(args) {
15
+ const nextArgs = [...args];
16
+ for (let i = 0; i < nextArgs.length; i++) {
17
+ const a = nextArgs[i];
18
+ if (typeof a !== "object" || a === null) continue;
19
+ const rec = a;
20
+ if (!("firstflowAgentId" in rec) && !("sessionId" in rec) && !("userId" in rec)) {
21
+ continue;
22
+ }
23
+ const agentId = typeof rec.firstflowAgentId === "string" ? rec.firstflowAgentId.trim() : "";
24
+ const sessionId = typeof rec.sessionId === "string" ? rec.sessionId.trim() : "";
25
+ const userId = typeof rec.userId === "string" ? rec.userId.trim() : "";
26
+ const { firstflowAgentId: _a, sessionId: _s, userId: _u, ...rest } = rec;
27
+ nextArgs[i] = rest;
28
+ if (!agentId || !sessionId || !userId) {
29
+ if (!warnedIncompleteTagging) {
30
+ warnedIncompleteTagging = true;
31
+ console.warn(
32
+ "[@firstflow/core] LLM call tagged with some but not all of `firstflowAgentId`, `sessionId`, `userId` \u2014 the call was NOT observed. Provide all three to track it."
33
+ );
34
+ }
35
+ return { nextArgs };
36
+ }
37
+ return { nextArgs, meta: { agentId, userId, conversationId: sessionId } };
38
+ }
39
+ return { nextArgs };
40
+ }
41
+ function extractModel(nextArgs) {
42
+ const body = nextArgs[0];
43
+ if (!body || typeof body !== "object") return void 0;
44
+ const m = body.model;
45
+ return typeof m === "string" ? m : void 0;
46
+ }
47
+ function detectProvider(path) {
48
+ return path === "messages.create" ? "anthropic" : "openai";
49
+ }
50
+ function injectOpenAiStreamUsage(nextArgs) {
51
+ const body = nextArgs[0];
52
+ if (!body || typeof body !== "object") return nextArgs;
53
+ const b = body;
54
+ if (b.stream !== true) return nextArgs;
55
+ const existing = b.stream_options ?? {};
56
+ if (existing.include_usage === true) return nextArgs;
57
+ const patched = [...nextArgs];
58
+ patched[0] = { ...b, stream_options: { ...existing, include_usage: true } };
59
+ return patched;
60
+ }
61
+ function extractOpenAiResponseMeta(res) {
62
+ const usage = res.usage;
63
+ const details = usage?.prompt_tokens_details;
64
+ const choice = Array.isArray(res.choices) ? res.choices[0] : void 0;
65
+ return {
66
+ inputTokens: typeof usage?.prompt_tokens === "number" ? usage.prompt_tokens : void 0,
67
+ outputTokens: typeof usage?.completion_tokens === "number" ? usage.completion_tokens : void 0,
68
+ cacheReadTokens: typeof details?.cached_tokens === "number" ? details.cached_tokens : void 0,
69
+ finishReason: typeof choice?.finish_reason === "string" ? choice.finish_reason : void 0
70
+ };
71
+ }
72
+ function extractAnthropicResponseMeta(res) {
73
+ const usage = res.usage;
74
+ return {
75
+ inputTokens: typeof usage?.input_tokens === "number" ? usage.input_tokens : void 0,
76
+ outputTokens: typeof usage?.output_tokens === "number" ? usage.output_tokens : void 0,
77
+ cacheReadTokens: typeof usage?.cache_read_input_tokens === "number" ? usage.cache_read_input_tokens : void 0,
78
+ cacheCreationTokens: typeof usage?.cache_creation_input_tokens === "number" ? usage.cache_creation_input_tokens : void 0,
79
+ finishReason: typeof res.stop_reason === "string" ? res.stop_reason : void 0
80
+ };
81
+ }
82
+ function accumOpenAiStreamChunkMeta(chunk, acc) {
83
+ if (!chunk || typeof chunk !== "object") return;
84
+ const c = chunk;
85
+ if (c.usage && typeof c.usage === "object") {
86
+ const usage = c.usage;
87
+ const details = usage.prompt_tokens_details;
88
+ if (typeof usage.prompt_tokens === "number") acc.inputTokens = usage.prompt_tokens;
89
+ if (typeof usage.completion_tokens === "number") acc.outputTokens = usage.completion_tokens;
90
+ if (typeof details?.cached_tokens === "number") acc.cacheReadTokens = details.cached_tokens;
91
+ }
92
+ if (Array.isArray(c.choices) && c.choices[0]) {
93
+ const choice = c.choices[0];
94
+ if (typeof choice.finish_reason === "string" && choice.finish_reason) {
95
+ acc.finishReason = choice.finish_reason;
96
+ }
97
+ }
98
+ }
99
+ function accumAnthropicStreamChunkMeta(chunk, acc) {
100
+ if (!chunk || typeof chunk !== "object") return;
101
+ const c = chunk;
102
+ if (c.type === "message_start") {
103
+ const msg = c.message;
104
+ const usage = msg?.usage;
105
+ if (typeof usage?.input_tokens === "number") acc.inputTokens = usage.input_tokens;
106
+ if (typeof usage?.cache_read_input_tokens === "number")
107
+ acc.cacheReadTokens = usage.cache_read_input_tokens;
108
+ if (typeof usage?.cache_creation_input_tokens === "number")
109
+ acc.cacheCreationTokens = usage.cache_creation_input_tokens;
110
+ }
111
+ if (c.type === "message_delta") {
112
+ const usage = c.usage;
113
+ const delta = c.delta;
114
+ if (typeof usage?.output_tokens === "number") acc.outputTokens = usage.output_tokens;
115
+ if (typeof delta?.stop_reason === "string") acc.finishReason = delta.stop_reason;
116
+ }
117
+ }
118
+ function isAsyncIterable(v) {
119
+ return typeof v === "object" && v !== null && Symbol.asyncIterator in v && typeof v[Symbol.asyncIterator] === "function";
120
+ }
121
+ function streamChunkText(chunk) {
122
+ if (!chunk || typeof chunk !== "object") return "";
123
+ const c = chunk;
124
+ if (Array.isArray(c.choices)) {
125
+ const choice = c.choices[0];
126
+ if (choice && typeof choice === "object") {
127
+ const delta = choice.delta;
128
+ if (delta && typeof delta === "object") {
129
+ const content = delta.content;
130
+ if (typeof content === "string") return content;
131
+ }
132
+ }
133
+ }
134
+ if (c.type === "content_block_delta") {
135
+ const delta = c.delta;
136
+ if (delta && typeof delta === "object") {
137
+ const d = delta;
138
+ if (d.type === "text_delta" && typeof d.text === "string") return d.text;
139
+ }
140
+ }
141
+ return "";
142
+ }
143
+ function wrapAsyncIterableForAssistantHook(stream, hook, provider, startTime) {
144
+ return {
145
+ async *[Symbol.asyncIterator]() {
146
+ let content = "";
147
+ let firstChunkTime = -1;
148
+ let streamError;
149
+ const acc = {};
150
+ const accumChunkMeta = provider === "anthropic" ? accumAnthropicStreamChunkMeta : accumOpenAiStreamChunkMeta;
151
+ try {
152
+ for await (const chunk of stream) {
153
+ if (firstChunkTime === -1) firstChunkTime = Date.now();
154
+ content += streamChunkText(chunk);
155
+ accumChunkMeta(chunk, acc);
156
+ yield chunk;
157
+ }
158
+ } catch (err) {
159
+ streamError = err;
160
+ throw err;
161
+ } finally {
162
+ hook(content, {
163
+ ...acc,
164
+ httpStatus: streamError != null ? streamError.status ?? 500 : 200,
165
+ latencyMs: Date.now() - startTime,
166
+ timeToFirstTokenMs: firstChunkTime === -1 ? void 0 : firstChunkTime - startTime
167
+ });
168
+ }
169
+ }
170
+ };
171
+ }
172
+ function firstArgBodyStream(nextArgs) {
173
+ const body = nextArgs[0];
174
+ return !!(body && typeof body === "object" && body !== null && body.stream === true);
175
+ }
176
+ function extractAnthropicAssistantText(res) {
177
+ const content = res.content;
178
+ if (!Array.isArray(content)) return "";
179
+ let acc = "";
180
+ for (const item of content) {
181
+ if (item && typeof item === "object" && !Array.isArray(item)) {
182
+ const o = item;
183
+ if (o.type === "text" && typeof o.text === "string") {
184
+ acc += o.text;
185
+ }
186
+ }
187
+ }
188
+ return acc;
189
+ }
190
+ function extractOpenAiAssistantText(res) {
191
+ if (!Array.isArray(res.choices) || !res.choices[0] || typeof res.choices[0] !== "object") {
192
+ return "";
193
+ }
194
+ const message = res.choices[0].message;
195
+ return typeof message?.content === "string" ? message.content : "";
196
+ }
197
+ function isThenable(v) {
198
+ return !!v && (typeof v === "object" || typeof v === "function") && "then" in v && typeof v.then === "function";
199
+ }
200
+ function messageContentToText(content) {
201
+ if (typeof content === "string") return content;
202
+ if (!Array.isArray(content)) return "";
203
+ let acc = "";
204
+ for (const part of content) {
205
+ if (part && typeof part === "object" && !Array.isArray(part)) {
206
+ const p = part;
207
+ if (p.type === "text" && typeof p.text === "string") acc += p.text;
208
+ }
209
+ }
210
+ return acc;
211
+ }
212
+ function extractRecentMessages(nextArgs) {
213
+ const body = nextArgs[0];
214
+ if (!body || typeof body !== "object") return [];
215
+ const messages = body.messages;
216
+ if (!Array.isArray(messages)) return [];
217
+ const out = [];
218
+ for (const m of messages) {
219
+ if (!m || typeof m !== "object") continue;
220
+ const rec = m;
221
+ if (rec.role !== "user" && rec.role !== "assistant") continue;
222
+ const content = messageContentToText(rec.content).trim();
223
+ if (!content) continue;
224
+ out.push({ role: rec.role, content });
225
+ }
226
+ return out;
227
+ }
228
+ function extractTurnStartUserMessage(nextArgs) {
229
+ const body = nextArgs[0];
230
+ if (!body || typeof body !== "object") return void 0;
231
+ const messages = body.messages;
232
+ if (!Array.isArray(messages) || messages.length === 0) return void 0;
233
+ const last = messages[messages.length - 1];
234
+ if (!last || typeof last !== "object") return void 0;
235
+ const rec = last;
236
+ if (rec.role !== "user") return void 0;
237
+ const text = messageContentToText(rec.content).trim();
238
+ return text || void 0;
239
+ }
240
+ function withFirstflowBaggage(meta, fn) {
241
+ const base = propagation.getBaggage(context.active()) ?? propagation.createBaggage();
242
+ const bag = base.setEntry("firstflow.user_id", { value: meta.userId }).setEntry("firstflow.agent_id", { value: meta.agentId });
243
+ return context.with(propagation.setBaggage(context.active(), bag), fn);
244
+ }
245
+ function maybeObserveUserMessage(path, nextArgs, meta, ctx) {
246
+ if (!ctx.onUserMessage) return;
247
+ if (path !== "chat.completions.create" && path !== "messages.create") return;
248
+ if (!meta.conversationId) return;
249
+ const content = extractTurnStartUserMessage(nextArgs);
250
+ if (!content) return;
251
+ const recentMessages = extractRecentMessages(nextArgs);
252
+ ctx.onUserMessage({
253
+ agentId: meta.agentId,
254
+ conversationId: meta.conversationId,
255
+ userId: meta.userId,
256
+ role: "user",
257
+ content,
258
+ model: extractModel(nextArgs),
259
+ provider: detectProvider(path),
260
+ ...recentMessages.length ? { recentMessages } : {}
261
+ });
262
+ }
263
+ function maybeAttachAssistantHook(path, nextArgs, result, meta, ctx, startTime) {
264
+ if (!ctx.onAssistantComplete) return void 0;
265
+ if (path !== "chat.completions.create" && path !== "messages.create") {
266
+ return void 0;
267
+ }
268
+ const convId = meta.conversationId;
269
+ const recentMessages = extractRecentMessages(nextArgs);
270
+ const model = extractModel(nextArgs);
271
+ const provider = detectProvider(path);
272
+ const extractFull = path === "chat.completions.create" ? extractOpenAiAssistantText : extractAnthropicAssistantText;
273
+ const extractMeta = path === "chat.completions.create" ? extractOpenAiResponseMeta : extractAnthropicResponseMeta;
274
+ const emit = (content, llmMeta) => {
275
+ if (!convId) return;
276
+ ctx.onAssistantComplete?.({
277
+ agentId: meta.agentId,
278
+ conversationId: convId,
279
+ userId: meta.userId,
280
+ role: "assistant",
281
+ content,
282
+ model,
283
+ provider,
284
+ ...llmMeta,
285
+ ...recentMessages.length ? { recentMessages } : {}
286
+ });
287
+ };
288
+ if (firstArgBodyStream(nextArgs)) {
289
+ const wrapStream = (stream) => wrapAsyncIterableForAssistantHook(
290
+ stream,
291
+ (content, streamMeta) => emit(content, streamMeta),
292
+ provider,
293
+ startTime
294
+ );
295
+ if (isAsyncIterable(result)) {
296
+ return wrapStream(result);
297
+ }
298
+ if (isThenable(result)) {
299
+ return result.then(
300
+ (resolved) => isAsyncIterable(resolved) ? wrapStream(resolved) : resolved
301
+ );
302
+ }
303
+ return void 0;
304
+ }
305
+ if (isThenable(result)) {
306
+ return result.then((res) => {
307
+ const latencyMs = Date.now() - startTime;
308
+ const resMeta = extractMeta(res ?? {});
309
+ emit(extractFull(res ?? {}), { ...resMeta, latencyMs, httpStatus: 200 });
310
+ return res;
311
+ });
312
+ }
313
+ return void 0;
314
+ }
315
+ function wrapInstrumentedInvocation(path, original, thisArg, args, ctx) {
316
+ const parsed = parseFirstflowMeta(args);
317
+ const { meta } = parsed;
318
+ let nextArgs = parsed.nextArgs;
319
+ if (path === "chat.completions.create") {
320
+ nextArgs = injectOpenAiStreamUsage(nextArgs);
321
+ }
322
+ const startTime = Date.now();
323
+ const run = () => {
324
+ if (meta) maybeObserveUserMessage(path, nextArgs, meta, ctx);
325
+ let result;
326
+ try {
327
+ result = Reflect.apply(original, thisArg, nextArgs);
328
+ } catch (err) {
329
+ if (meta?.conversationId && ctx.onError) {
330
+ ctx.onError({
331
+ agentId: meta.agentId,
332
+ conversationId: meta.conversationId,
333
+ userId: meta.userId,
334
+ isError: true,
335
+ error: err instanceof Error ? err.message : String(err),
336
+ model: extractModel(nextArgs),
337
+ provider: detectProvider(path),
338
+ latencyMs: Date.now() - startTime,
339
+ httpStatus: err.status
340
+ });
341
+ }
342
+ throw err;
343
+ }
344
+ if (isThenable(result) && meta?.conversationId && ctx.onError) {
345
+ result = result.catch((err) => {
346
+ ctx.onError({
347
+ agentId: meta.agentId,
348
+ conversationId: meta.conversationId,
349
+ userId: meta.userId,
350
+ isError: true,
351
+ error: err instanceof Error ? err.message : String(err),
352
+ model: extractModel(nextArgs),
353
+ provider: detectProvider(path),
354
+ latencyMs: Date.now() - startTime,
355
+ httpStatus: err.status
356
+ });
357
+ return Promise.reject(err);
358
+ });
359
+ }
360
+ if (meta) {
361
+ const hooked = maybeAttachAssistantHook(path, nextArgs, result, meta, ctx, startTime);
362
+ if (hooked !== void 0) return hooked;
363
+ }
364
+ return result;
365
+ };
366
+ if (meta) {
367
+ return withFirstflowBaggage(meta, run);
368
+ }
369
+ return run();
370
+ }
371
+ function createNestedProxy(target, path, ctx) {
372
+ return new Proxy(target, {
373
+ get(_t, prop, receiver) {
374
+ const key = String(prop);
375
+ const value = Reflect.get(target, prop, receiver);
376
+ if (typeof value === "function") {
377
+ const joined = [...path, key].join(".");
378
+ return (...fnArgs) => {
379
+ if (INSTRUMENTED_PATHS.has(joined)) {
380
+ return wrapInstrumentedInvocation(
381
+ joined,
382
+ value,
383
+ target,
384
+ fnArgs,
385
+ ctx
386
+ );
387
+ }
388
+ return Reflect.apply(value, target, fnArgs);
389
+ };
390
+ }
391
+ if (value !== null && typeof value === "object") {
392
+ return createNestedProxy(value, [...path, key], ctx);
393
+ }
394
+ return value;
395
+ }
396
+ });
397
+ }
398
+ function wrapClient(client, ctx) {
399
+ return createNestedProxy(client, [], ctx);
400
+ }
401
+
402
+ export {
403
+ wrapClient
404
+ };
package/dist/index.mjs CHANGED
@@ -1,9 +1,11 @@
1
+ import {
2
+ wrapClient
3
+ } from "./chunk-WQZUOUMF.mjs";
1
4
  import {
2
5
  DEFAULT_FIRSTFLOW_BASE_URL,
3
6
  FIRSTFLOW_SERVER_PACKAGE_VERSION,
4
- getRuntime,
5
- wrapClient
6
- } from "./chunk-X3T7X4JQ.mjs";
7
+ getRuntime
8
+ } from "./chunk-QBEGXT76.mjs";
7
9
 
8
10
  // src/standalone.ts
9
11
  function observe(input) {
@@ -0,0 +1,56 @@
1
+ import { BaseCallbackHandler } from '@langchain/core/callbacks/base';
2
+
3
+ /** The three identifiers every observed turn is attributed to. */
4
+ type FirstflowIdentity = {
5
+ /** Agent this run is attributed to (primary routing key). */
6
+ agentId: string;
7
+ /** Session / conversation / thread id (groups messages in Sessions). */
8
+ sessionId: string;
9
+ /** The end-user this run belongs to (stable across sessions). */
10
+ userId: string;
11
+ };
12
+ type Rec = Record<string, unknown>;
13
+ /**
14
+ * LangChain callback handler that forwards each model turn to Firstflow
15
+ * (Sessions + Analytics), tagged from the run `metadata`. Attach it via
16
+ * `callbacks: [handler]` and pass `metadata: { firstflowAgentId, sessionId,
17
+ * userId }`, or use {@link withFirstflow} to do both at once.
18
+ *
19
+ * Stateless apart from a short-lived per-run map; one shared instance is safe to
20
+ * reuse across the whole process (see {@link firstflowCallback}).
21
+ */
22
+ declare class FirstflowCallbackHandler extends BaseCallbackHandler {
23
+ name: string;
24
+ private readonly runs;
25
+ handleChatModelStart(_llm: unknown, messages: unknown, runId: string, _parentRunId?: string, extraParams?: Rec, _tags?: string[], metadata?: Rec): void;
26
+ handleLLMStart(_llm: unknown, prompts: unknown, runId: string, _parentRunId?: string, extraParams?: Rec, _tags?: string[], metadata?: Rec): void;
27
+ private begin;
28
+ handleLLMNewToken(_token: string, _idx: unknown, runId: string): void;
29
+ handleLLMEnd(output: unknown, runId: string): void;
30
+ handleLLMError(err: unknown, runId: string): void;
31
+ }
32
+ /**
33
+ * The process-wide shared handler. Prefer this over allocating a new handler per
34
+ * call — it is safe to reuse everywhere.
35
+ */
36
+ declare function firstflowCallback(): FirstflowCallbackHandler;
37
+ /** A minimal LangChain run-config shape (avoids a hard type dep on the peer). */
38
+ type RunnableConfigLike = {
39
+ callbacks?: unknown;
40
+ metadata?: Record<string, unknown>;
41
+ [key: string]: unknown;
42
+ };
43
+ /**
44
+ * Merge the Firstflow handler + identity metadata into a LangChain run config.
45
+ * Pass the result as the second arg to `invoke` / `stream` / `graph.stream`:
46
+ *
47
+ * await graph.stream(state, withFirstflow({ agentId, sessionId, userId }));
48
+ *
49
+ * Existing array callbacks and metadata are preserved.
50
+ */
51
+ declare function withFirstflow<T extends RunnableConfigLike>(identity: FirstflowIdentity, config?: T): T & {
52
+ callbacks: unknown[];
53
+ metadata: Record<string, unknown>;
54
+ };
55
+
56
+ export { FirstflowCallbackHandler, type FirstflowIdentity, firstflowCallback, withFirstflow };
@@ -0,0 +1,56 @@
1
+ import { BaseCallbackHandler } from '@langchain/core/callbacks/base';
2
+
3
+ /** The three identifiers every observed turn is attributed to. */
4
+ type FirstflowIdentity = {
5
+ /** Agent this run is attributed to (primary routing key). */
6
+ agentId: string;
7
+ /** Session / conversation / thread id (groups messages in Sessions). */
8
+ sessionId: string;
9
+ /** The end-user this run belongs to (stable across sessions). */
10
+ userId: string;
11
+ };
12
+ type Rec = Record<string, unknown>;
13
+ /**
14
+ * LangChain callback handler that forwards each model turn to Firstflow
15
+ * (Sessions + Analytics), tagged from the run `metadata`. Attach it via
16
+ * `callbacks: [handler]` and pass `metadata: { firstflowAgentId, sessionId,
17
+ * userId }`, or use {@link withFirstflow} to do both at once.
18
+ *
19
+ * Stateless apart from a short-lived per-run map; one shared instance is safe to
20
+ * reuse across the whole process (see {@link firstflowCallback}).
21
+ */
22
+ declare class FirstflowCallbackHandler extends BaseCallbackHandler {
23
+ name: string;
24
+ private readonly runs;
25
+ handleChatModelStart(_llm: unknown, messages: unknown, runId: string, _parentRunId?: string, extraParams?: Rec, _tags?: string[], metadata?: Rec): void;
26
+ handleLLMStart(_llm: unknown, prompts: unknown, runId: string, _parentRunId?: string, extraParams?: Rec, _tags?: string[], metadata?: Rec): void;
27
+ private begin;
28
+ handleLLMNewToken(_token: string, _idx: unknown, runId: string): void;
29
+ handleLLMEnd(output: unknown, runId: string): void;
30
+ handleLLMError(err: unknown, runId: string): void;
31
+ }
32
+ /**
33
+ * The process-wide shared handler. Prefer this over allocating a new handler per
34
+ * call — it is safe to reuse everywhere.
35
+ */
36
+ declare function firstflowCallback(): FirstflowCallbackHandler;
37
+ /** A minimal LangChain run-config shape (avoids a hard type dep on the peer). */
38
+ type RunnableConfigLike = {
39
+ callbacks?: unknown;
40
+ metadata?: Record<string, unknown>;
41
+ [key: string]: unknown;
42
+ };
43
+ /**
44
+ * Merge the Firstflow handler + identity metadata into a LangChain run config.
45
+ * Pass the result as the second arg to `invoke` / `stream` / `graph.stream`:
46
+ *
47
+ * await graph.stream(state, withFirstflow({ agentId, sessionId, userId }));
48
+ *
49
+ * Existing array callbacks and metadata are preserved.
50
+ */
51
+ declare function withFirstflow<T extends RunnableConfigLike>(identity: FirstflowIdentity, config?: T): T & {
52
+ callbacks: unknown[];
53
+ metadata: Record<string, unknown>;
54
+ };
55
+
56
+ export { FirstflowCallbackHandler, type FirstflowIdentity, firstflowCallback, withFirstflow };