Repository navigation
feat(middleware): add lifecycle middleware with onFinish, onError, onChunk callbacks #888
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,201 @@ | ||
| /** | ||
| * Lifecycle Middleware | ||
| * | ||
| * Provides onFinish, onError, and onChunk callbacks for observing | ||
| * generation and streaming lifecycle events. | ||
| * | ||
| * This middleware is automatically enabled when lifecycle callbacks | ||
| * (onFinish, onError, onChunk) are passed in GenerateOptions or StreamOptions. | ||
| */ | ||
|
|
||
| import type { LanguageModelV1Middleware } from "ai"; | ||
| import type { | ||
| NeuroLinkMiddleware, | ||
| NeuroLinkMiddlewareMetadata, | ||
| LifecycleMiddlewareConfig, | ||
| } from "../../types/middlewareTypes.js"; | ||
| import { logger } from "../../utils/logger.js"; | ||
| import { isRecoverableError } from "../../utils/errorHandling.js"; | ||
|
|
||
| export function createLifecycleMiddleware( | ||
| config: LifecycleMiddlewareConfig = {}, | ||
| ): NeuroLinkMiddleware { | ||
| const metadata: NeuroLinkMiddlewareMetadata = { | ||
| id: "lifecycle", | ||
| name: "Lifecycle Callbacks", | ||
| description: | ||
| "Provides onFinish, onError, and onChunk callbacks for generation and streaming lifecycle events", | ||
| priority: 110, | ||
| defaultEnabled: false, | ||
| }; | ||
|
|
||
| const middleware: LanguageModelV1Middleware = { | ||
| wrapGenerate: async ({ doGenerate }) => { | ||
| const startTime = Date.now(); | ||
|
|
||
| try { | ||
| const result = await doGenerate(); | ||
|
|
||
| if (config.onFinish) { | ||
| try { | ||
| const callbackResult = config.onFinish({ | ||
| text: result.text ?? "", | ||
| usage: result.usage | ||
| ? { | ||
| promptTokens: result.usage.promptTokens ?? 0, | ||
| completionTokens: result.usage.completionTokens ?? 0, | ||
| } | ||
| : undefined, | ||
| duration: Date.now() - startTime, | ||
| finishReason: result.finishReason, | ||
| }); | ||
| Promise.resolve(callbackResult).catch((e) => { | ||
| logger.warn("[LifecycleMiddleware] onFinish callback error:", e); | ||
| }); | ||
| } catch (e) { | ||
| logger.warn("[LifecycleMiddleware] onFinish callback error:", e); | ||
| } | ||
| } | ||
|
|
||
| return result; | ||
| } catch (error) { | ||
| if (config.onError) { | ||
| const err = error instanceof Error ? error : new Error(String(error)); | ||
| try { | ||
| const callbackResult = config.onError({ | ||
| error: err, | ||
| duration: Date.now() - startTime, | ||
| recoverable: isRecoverableError(err), | ||
| }); | ||
| Promise.resolve(callbackResult).catch((e) => { | ||
| logger.warn("[LifecycleMiddleware] onError callback error:", e); | ||
| }); | ||
| } catch (e) { | ||
| logger.warn("[LifecycleMiddleware] onError callback error:", e); | ||
| } | ||
| } | ||
|
|
||
| throw error; | ||
| } | ||
| }, | ||
|
|
||
| wrapStream: async ({ doStream }) => { | ||
| const startTime = Date.now(); | ||
|
|
||
| try { | ||
| const result = await doStream(); | ||
|
|
||
| if (!config.onChunk && !config.onFinish && !config.onError) { | ||
| return result; | ||
| } | ||
|
|
||
| let sequenceNumber = 0; | ||
| let accumulatedText = ""; | ||
|
|
||
| const transformStream = new TransformStream({ | ||
| transform(chunk, controller) { | ||
| try { | ||
| if (chunk.type === "text-delta") { | ||
| accumulatedText += chunk.textDelta; | ||
| } | ||
|
|
||
| if (config.onChunk && chunk.type) { | ||
| try { | ||
| const callbackResult = config.onChunk({ | ||
| type: chunk.type, | ||
| textDelta: | ||
| chunk.type === "text-delta" ? chunk.textDelta : undefined, | ||
| sequenceNumber: sequenceNumber++, | ||
| }); | ||
| Promise.resolve(callbackResult).catch((e) => { | ||
| logger.warn( | ||
| "[LifecycleMiddleware] onChunk callback error:", | ||
| e, | ||
| ); | ||
| }); | ||
| } catch (e) { | ||
| logger.warn( | ||
| "[LifecycleMiddleware] onChunk callback error:", | ||
| e, | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| controller.enqueue(chunk); | ||
| } catch (error) { | ||
| if (config.onError) { | ||
| const err = | ||
| error instanceof Error ? error : new Error(String(error)); | ||
| try { | ||
| const callbackResult = config.onError({ | ||
| error: err, | ||
| duration: Date.now() - startTime, | ||
| recoverable: isRecoverableError(err), | ||
| }); | ||
| Promise.resolve(callbackResult).catch((e) => { | ||
| logger.warn( | ||
| "[LifecycleMiddleware] onError callback error:", | ||
| e, | ||
| ); | ||
| }); | ||
| } catch (e) { | ||
| logger.warn( | ||
| "[LifecycleMiddleware] onError callback error:", | ||
| e, | ||
| ); | ||
| } | ||
| } | ||
| throw error; | ||
| } | ||
| }, | ||
| flush() { | ||
| if (config.onFinish) { | ||
| try { | ||
| const callbackResult = config.onFinish({ | ||
| text: accumulatedText, | ||
| duration: Date.now() - startTime, | ||
| }); | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| Promise.resolve(callbackResult).catch((e) => { | ||
| logger.warn( | ||
| "[LifecycleMiddleware] onFinish callback error:", | ||
| e, | ||
| ); | ||
| }); | ||
| } catch (e) { | ||
| logger.warn( | ||
| "[LifecycleMiddleware] onFinish callback error:", | ||
| e, | ||
| ); | ||
| } | ||
| } | ||
| }, | ||
| }); | ||
|
|
||
| return { | ||
| ...result, | ||
| stream: result.stream.pipeThrough(transformStream), | ||
| }; | ||
|
Comment on lines
+174
to
+177
|
||
| } catch (error) { | ||
| if (config.onError) { | ||
| const err = error instanceof Error ? error : new Error(String(error)); | ||
| try { | ||
| const callbackResult = config.onError({ | ||
| error: err, | ||
| duration: Date.now() - startTime, | ||
| recoverable: isRecoverableError(err), | ||
| }); | ||
| Promise.resolve(callbackResult).catch((e) => { | ||
| logger.warn("[LifecycleMiddleware] onError callback error:", e); | ||
| }); | ||
| } catch (e) { | ||
| logger.warn("[LifecycleMiddleware] onError callback error:", e); | ||
| } | ||
| } | ||
|
|
||
| throw error; | ||
|
Comment on lines
+82
to
+195
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: cat -n src/lib/middleware/builtin/lifecycle.ts | head -200Repository: juspay/neurolink Length of output: 6120 🏁 Script executed: rg "pipeThrough|stream.*error|\.catch\(\)" src/lib --type ts -A 2 -B 2 | head -60Repository: juspay/neurolink Length of output: 3372 🏁 Script executed: rg "wrapStream|TransformStream" src/lib --type ts -B 2 -A 5 | grep -A 10 "TransformStream"Repository: juspay/neurolink Length of output: 1941 🏁 Script executed: rg "\.stream\s*=" src/lib --type ts -B 2 -A 3 | head -80Repository: juspay/neurolink Length of output: 1593 🏁 Script executed: cat -n src/lib/neurolink.ts | sed -n '3200,3350p'Repository: juspay/neurolink Length of output: 8261 🏁 Script executed: rg "for await.*stream|\.text\(\)|\.json\(\)" src/lib --type ts -B 2 -A 5 | head -100Repository: juspay/neurolink Length of output: 5907 🏁 Script executed: rg "error.*stream|stream.*error" src/lib/middleware --type ts -B 3 -A 3Repository: juspay/neurolink Length of output: 610 🏁 Script executed: rg "wrapStream.*async.*doStream" src/lib/middleware -A 50 | head -150Repository: juspay/neurolink Length of output: 10781 🏁 Script executed: rg "stream.pipeThrough|pipeThrough.*TransformStream" src/lib -B 5 -A 10 | head -120Repository: juspay/neurolink Length of output: 3034 🏁 Script executed: cat -n src/lib/middleware/builtin/autoEvaluation.ts | sed -n '80,130p'Repository: juspay/neurolink Length of output: 1701 🏁 Script executed: rg "readableStream.*error|addEventListener.*error" src/lib --type ts -B 2 -A 3Repository: juspay/neurolink Length of output: 42 🏁 Script executed: rg "consumeAndEvaluateStream" src/lib --type ts -A 30 | head -80Repository: juspay/neurolink Length of output: 4380 🏁 Script executed: cat -n src/lib/middleware/builtin/autoEvaluation.ts | sed -n '160,220p'Repository: juspay/neurolink Length of output: 2309 🏁 Script executed: cat -n src/lib/middleware/builtin/autoEvaluation.ts | sed -n '220,260p'Repository: juspay/neurolink Length of output: 959 🏁 Script executed: rg "generateStream|\.stream\)" src/lib/neurolink.ts -B 3 -A 5 | head -100Repository: juspay/neurolink Length of output: 848 🏁 Script executed: rg "stream\.getReader|reader\.read|ReadableStream" src/lib/middleware --type ts -B 2 -A 5Repository: juspay/neurolink Length of output: 2603 Mid-stream failures never reach The To fix this, wrap the returned stream with a reader loop that:
Reference the pattern in 🤖 Prompt for AI Agents |
||
| } | ||
| }, | ||
| }; | ||
|
|
||
| return { ...middleware, metadata }; | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.