@flink-app/flink 2.0.0-alpha.91 → 2.0.0-alpha.92
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/CHANGELOG.md +45 -0
- package/dist/src/FlinkApp.d.ts +17 -0
- package/dist/src/FlinkApp.js +4 -2
- package/dist/src/ai/AgentRunner.d.ts +9 -2
- package/dist/src/ai/AgentRunner.js +507 -363
- package/dist/src/ai/FlinkAgent.d.ts +100 -1
- package/dist/src/ai/FlinkAgent.js +16 -3
- package/package.json +1 -1
- package/spec/AgentObserver.spec.ts +266 -0
- package/src/FlinkApp.ts +22 -1
- package/src/ai/AgentRunner.ts +141 -15
- package/src/ai/FlinkAgent.ts +112 -2
package/src/ai/AgentRunner.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { v4 as uuidv4 } from "uuid";
|
|
1
2
|
import {
|
|
2
3
|
FlinkAgentProps,
|
|
3
4
|
AgentExecuteResult,
|
|
@@ -7,11 +8,15 @@ import {
|
|
|
7
8
|
AgentExecuteContext,
|
|
8
9
|
AgentStepContext,
|
|
9
10
|
AgentFinishContext,
|
|
11
|
+
AgentObserver,
|
|
10
12
|
} from "./FlinkAgent";
|
|
11
13
|
import { ToolExecutor } from "./ToolExecutor";
|
|
12
14
|
import { LLMAdapter, LLMMessage, LLMContentBlock, FlinkToolSchema } from "./LLMAdapter";
|
|
15
|
+
import { FlinkLogFactory } from "../FlinkLogFactory";
|
|
13
16
|
import { log } from "../FlinkLog";
|
|
14
17
|
|
|
18
|
+
const observerLog = FlinkLogFactory.createLogger("flink.ai.observer");
|
|
19
|
+
|
|
15
20
|
export class AgentRunner {
|
|
16
21
|
private llmAdapter: LLMAdapter;
|
|
17
22
|
private maxTokens: number;
|
|
@@ -24,7 +29,8 @@ export class AgentRunner {
|
|
|
24
29
|
private tools: Map<string, ToolExecutor<any>>,
|
|
25
30
|
llmAdapters: Map<string, LLMAdapter>,
|
|
26
31
|
private agentName?: string, // Optional agent name for logging
|
|
27
|
-
private ctx?: any // FlinkContext for instruction callbacks (any for flexibility)
|
|
32
|
+
private ctx?: any, // FlinkContext for instruction callbacks (any for flexibility)
|
|
33
|
+
private observer?: AgentObserver
|
|
28
34
|
) {
|
|
29
35
|
// Get appropriate LLM adapter based on adapterId
|
|
30
36
|
const adapterId = agentProps.model?.adapterId || "default";
|
|
@@ -44,10 +50,19 @@ export class AgentRunner {
|
|
|
44
50
|
async *streamGenerator(input: AgentExecuteInput): AsyncGenerator<StreamChunk> {
|
|
45
51
|
const maxSteps = input.options?.maxSteps || this.maxSteps;
|
|
46
52
|
const toolCalls: AgentExecuteResult["toolCalls"] = [];
|
|
53
|
+
const runId = uuidv4();
|
|
54
|
+
const runStartedAt = Date.now();
|
|
55
|
+
const agentId = this.agentName || "unknown";
|
|
56
|
+
const modelInfo = {
|
|
57
|
+
adapterId: this.agentProps.model?.adapterId,
|
|
58
|
+
maxTokens: this.maxTokens,
|
|
59
|
+
temperature: this.temperature,
|
|
60
|
+
};
|
|
61
|
+
const declaredToolNames = Array.from(this.tools.keys());
|
|
47
62
|
|
|
48
63
|
// Build execution context
|
|
49
64
|
const execContext: AgentExecuteContext = {
|
|
50
|
-
agentId
|
|
65
|
+
agentId,
|
|
51
66
|
conversationId: input.conversationId,
|
|
52
67
|
user: input.user,
|
|
53
68
|
metadata: input.metadata,
|
|
@@ -83,6 +98,20 @@ export class AgentRunner {
|
|
|
83
98
|
messages.push(...this.convertMessages(input.message as Message[]));
|
|
84
99
|
}
|
|
85
100
|
|
|
101
|
+
// Dispatch observer onRun (pre-loop, before compaction / tool filtering)
|
|
102
|
+
this.safeDispatch("onRun", () =>
|
|
103
|
+
this.observer?.onRun?.({
|
|
104
|
+
runId,
|
|
105
|
+
agentId,
|
|
106
|
+
instructions: resolvedInstructions,
|
|
107
|
+
input,
|
|
108
|
+
messages: [...messages],
|
|
109
|
+
tools: declaredToolNames,
|
|
110
|
+
model: modelInfo,
|
|
111
|
+
context: execContext,
|
|
112
|
+
})
|
|
113
|
+
);
|
|
114
|
+
|
|
86
115
|
let step = 0;
|
|
87
116
|
let finalMessage = "";
|
|
88
117
|
let stoppedEarly = false;
|
|
@@ -92,6 +121,22 @@ export class AgentRunner {
|
|
|
92
121
|
let totalCacheCreationInputTokens = 0;
|
|
93
122
|
let finalProviderMetadata: Record<string, any> = {};
|
|
94
123
|
|
|
124
|
+
const buildResult = (): AgentExecuteResult => ({
|
|
125
|
+
runId,
|
|
126
|
+
message: finalMessage,
|
|
127
|
+
toolCalls,
|
|
128
|
+
stepsUsed: step,
|
|
129
|
+
stoppedEarly,
|
|
130
|
+
usage: {
|
|
131
|
+
inputTokens: totalInputTokens,
|
|
132
|
+
outputTokens: totalOutputTokens,
|
|
133
|
+
...(totalCachedInputTokens > 0 && { cachedInputTokens: totalCachedInputTokens }),
|
|
134
|
+
...(totalCacheCreationInputTokens > 0 && { cacheCreationInputTokens: totalCacheCreationInputTokens }),
|
|
135
|
+
},
|
|
136
|
+
providerMetadata: Object.keys(finalProviderMetadata).length > 0 ? finalProviderMetadata : undefined,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
try {
|
|
95
140
|
while (step < maxSteps) {
|
|
96
141
|
step++;
|
|
97
142
|
|
|
@@ -160,6 +205,26 @@ export class AgentRunner {
|
|
|
160
205
|
temperature: this.temperature,
|
|
161
206
|
});
|
|
162
207
|
|
|
208
|
+
// Dispatch observer onLlmCall — messages reflect post-compaction state;
|
|
209
|
+
// tools reflect per-step permission filtering
|
|
210
|
+
this.safeDispatch("onLlmCall", () =>
|
|
211
|
+
this.observer?.onLlmCall?.({
|
|
212
|
+
runId,
|
|
213
|
+
agentId,
|
|
214
|
+
step,
|
|
215
|
+
maxSteps,
|
|
216
|
+
instructions: resolvedInstructions,
|
|
217
|
+
messages: [...messages],
|
|
218
|
+
tools: availableTools.map((t) => t.name),
|
|
219
|
+
model: modelInfo,
|
|
220
|
+
context: execContext,
|
|
221
|
+
})
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
// Track tool call count before this step so we can slice the per-step
|
|
225
|
+
// tool calls for the onStep observer event
|
|
226
|
+
const toolCallsBeforeStep = toolCalls.length;
|
|
227
|
+
|
|
163
228
|
// Call AI model via adapter using streaming
|
|
164
229
|
const llmStream = this.llmAdapter.stream({
|
|
165
230
|
instructions: resolvedInstructions,
|
|
@@ -272,6 +337,20 @@ export class AgentRunner {
|
|
|
272
337
|
};
|
|
273
338
|
await this.agentProps.onStep(stepContext);
|
|
274
339
|
}
|
|
340
|
+
// Dispatch observer onStep (no tool calls executed this step)
|
|
341
|
+
this.safeDispatch("onStep", () =>
|
|
342
|
+
this.observer?.onStep?.({
|
|
343
|
+
runId,
|
|
344
|
+
agentId,
|
|
345
|
+
step,
|
|
346
|
+
maxSteps,
|
|
347
|
+
messages: [...messages],
|
|
348
|
+
assistantText: llmResponse.textContent,
|
|
349
|
+
toolCalls: toolCalls.slice(toolCallsBeforeStep),
|
|
350
|
+
usage,
|
|
351
|
+
context: execContext,
|
|
352
|
+
})
|
|
353
|
+
);
|
|
275
354
|
break; // No more tool calls - done
|
|
276
355
|
}
|
|
277
356
|
|
|
@@ -400,6 +479,21 @@ export class AgentRunner {
|
|
|
400
479
|
};
|
|
401
480
|
await this.agentProps.onStep(stepContext);
|
|
402
481
|
}
|
|
482
|
+
|
|
483
|
+
// Dispatch observer onStep with per-step tool calls slice
|
|
484
|
+
this.safeDispatch("onStep", () =>
|
|
485
|
+
this.observer?.onStep?.({
|
|
486
|
+
runId,
|
|
487
|
+
agentId,
|
|
488
|
+
step,
|
|
489
|
+
maxSteps,
|
|
490
|
+
messages: [...messages],
|
|
491
|
+
assistantText: llmResponse.textContent,
|
|
492
|
+
toolCalls: toolCalls.slice(toolCallsBeforeStep),
|
|
493
|
+
usage,
|
|
494
|
+
context: execContext,
|
|
495
|
+
})
|
|
496
|
+
);
|
|
403
497
|
}
|
|
404
498
|
|
|
405
499
|
if (step >= maxSteps && toolCalls.length > 0) {
|
|
@@ -407,19 +501,7 @@ export class AgentRunner {
|
|
|
407
501
|
log.warn(`Agent ${this.agentName || "unknown"} stopped early after ${maxSteps} steps`);
|
|
408
502
|
}
|
|
409
503
|
|
|
410
|
-
const result
|
|
411
|
-
message: finalMessage,
|
|
412
|
-
toolCalls,
|
|
413
|
-
stepsUsed: step,
|
|
414
|
-
stoppedEarly,
|
|
415
|
-
usage: {
|
|
416
|
-
inputTokens: totalInputTokens,
|
|
417
|
-
outputTokens: totalOutputTokens,
|
|
418
|
-
...(totalCachedInputTokens > 0 && { cachedInputTokens: totalCachedInputTokens }),
|
|
419
|
-
...(totalCacheCreationInputTokens > 0 && { cacheCreationInputTokens: totalCacheCreationInputTokens }),
|
|
420
|
-
},
|
|
421
|
-
providerMetadata: Object.keys(finalProviderMetadata).length > 0 ? finalProviderMetadata : undefined,
|
|
422
|
-
};
|
|
504
|
+
const result = buildResult();
|
|
423
505
|
|
|
424
506
|
// Call afterRun hook with full context
|
|
425
507
|
if (this.agentProps.afterRun) {
|
|
@@ -431,11 +513,55 @@ export class AgentRunner {
|
|
|
431
513
|
await this.agentProps.afterRun(result, finishContext);
|
|
432
514
|
}
|
|
433
515
|
|
|
516
|
+
// Dispatch observer onFinish (success path)
|
|
517
|
+
this.safeDispatch("onFinish", () =>
|
|
518
|
+
this.observer?.onFinish?.({
|
|
519
|
+
runId,
|
|
520
|
+
agentId,
|
|
521
|
+
result,
|
|
522
|
+
messages: [...messages],
|
|
523
|
+
durationMs: Date.now() - runStartedAt,
|
|
524
|
+
context: execContext,
|
|
525
|
+
})
|
|
526
|
+
);
|
|
527
|
+
|
|
434
528
|
// Phase 1: Yield only complete event
|
|
435
529
|
// Phase 2: Will yield text_delta and tool events during loop
|
|
436
530
|
yield { type: "complete", result };
|
|
437
531
|
|
|
438
532
|
return result;
|
|
533
|
+
} catch (err: any) {
|
|
534
|
+
// Dispatch observer onFinish with error before rethrowing
|
|
535
|
+
this.safeDispatch("onFinish", () =>
|
|
536
|
+
this.observer?.onFinish?.({
|
|
537
|
+
runId,
|
|
538
|
+
agentId,
|
|
539
|
+
result: buildResult(),
|
|
540
|
+
messages: [...messages],
|
|
541
|
+
durationMs: Date.now() - runStartedAt,
|
|
542
|
+
error: err?.message || String(err),
|
|
543
|
+
context: execContext,
|
|
544
|
+
})
|
|
545
|
+
);
|
|
546
|
+
throw err;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Fire-and-forget observer dispatch: catches synchronous throws and
|
|
552
|
+
* rejected promises so observer failures never break agent execution.
|
|
553
|
+
*/
|
|
554
|
+
private safeDispatch(eventName: string, invoke: () => void | Promise<void> | undefined): void {
|
|
555
|
+
try {
|
|
556
|
+
const maybePromise = invoke();
|
|
557
|
+
if (maybePromise && typeof (maybePromise as Promise<void>).then === "function") {
|
|
558
|
+
(maybePromise as Promise<void>).catch((err: any) => {
|
|
559
|
+
observerLog.warn(`AgentObserver.${eventName} threw (async):`, err?.message || err);
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
} catch (err: any) {
|
|
563
|
+
observerLog.warn(`AgentObserver.${eventName} threw (sync):`, err?.message || err);
|
|
564
|
+
}
|
|
439
565
|
}
|
|
440
566
|
|
|
441
567
|
/**
|
package/src/ai/FlinkAgent.ts
CHANGED
|
@@ -250,6 +250,7 @@ export abstract class FlinkAgent<Ctx extends FlinkContext, ConversationCtx = any
|
|
|
250
250
|
private _boundConversationContext?: ConversationCtx; // Conversation context bound via withConversationContext()
|
|
251
251
|
private _llmAdapters?: Map<string, any>;
|
|
252
252
|
private _tools?: { [x: string]: ToolExecutor<Ctx> };
|
|
253
|
+
private _observer?: AgentObserver;
|
|
253
254
|
|
|
254
255
|
// Abstract properties (must be defined by subclass)
|
|
255
256
|
abstract id: string;
|
|
@@ -315,9 +316,10 @@ export abstract class FlinkAgent<Ctx extends FlinkContext, ConversationCtx = any
|
|
|
315
316
|
* Internal initialization called by FlinkApp
|
|
316
317
|
* @internal
|
|
317
318
|
*/
|
|
318
|
-
__init(llmAdapters: Map<string, any>, tools: { [x: string]: ToolExecutor<Ctx> }): void {
|
|
319
|
+
__init(llmAdapters: Map<string, any>, tools: { [x: string]: ToolExecutor<Ctx> }, observer?: AgentObserver): void {
|
|
319
320
|
this._llmAdapters = llmAdapters;
|
|
320
321
|
this._tools = tools;
|
|
322
|
+
this._observer = observer;
|
|
321
323
|
}
|
|
322
324
|
|
|
323
325
|
/**
|
|
@@ -350,6 +352,9 @@ export abstract class FlinkAgent<Ctx extends FlinkContext, ConversationCtx = any
|
|
|
350
352
|
if (this._tools) {
|
|
351
353
|
bound._tools = this._tools;
|
|
352
354
|
}
|
|
355
|
+
if (this._observer) {
|
|
356
|
+
bound._observer = this._observer;
|
|
357
|
+
}
|
|
353
358
|
if (this._boundUserPermissions !== undefined) {
|
|
354
359
|
bound._boundUserPermissions = this._boundUserPermissions;
|
|
355
360
|
}
|
|
@@ -389,6 +394,9 @@ export abstract class FlinkAgent<Ctx extends FlinkContext, ConversationCtx = any
|
|
|
389
394
|
if (this._tools) {
|
|
390
395
|
bound._tools = this._tools;
|
|
391
396
|
}
|
|
397
|
+
if (this._observer) {
|
|
398
|
+
bound._observer = this._observer;
|
|
399
|
+
}
|
|
392
400
|
if (this._boundUser !== undefined) {
|
|
393
401
|
bound._boundUser = this._boundUser;
|
|
394
402
|
}
|
|
@@ -430,6 +438,9 @@ export abstract class FlinkAgent<Ctx extends FlinkContext, ConversationCtx = any
|
|
|
430
438
|
if (this._tools) {
|
|
431
439
|
bound._tools = this._tools;
|
|
432
440
|
}
|
|
441
|
+
if (this._observer) {
|
|
442
|
+
bound._observer = this._observer;
|
|
443
|
+
}
|
|
433
444
|
if (this._boundUser !== undefined) {
|
|
434
445
|
bound._boundUser = this._boundUser;
|
|
435
446
|
}
|
|
@@ -471,6 +482,9 @@ export abstract class FlinkAgent<Ctx extends FlinkContext, ConversationCtx = any
|
|
|
471
482
|
if (this._tools) {
|
|
472
483
|
bound._tools = this._tools;
|
|
473
484
|
}
|
|
485
|
+
if (this._observer) {
|
|
486
|
+
bound._observer = this._observer;
|
|
487
|
+
}
|
|
474
488
|
if (this._boundUser !== undefined) {
|
|
475
489
|
bound._boundUser = this._boundUser;
|
|
476
490
|
}
|
|
@@ -712,7 +726,8 @@ export abstract class FlinkAgent<Ctx extends FlinkContext, ConversationCtx = any
|
|
|
712
726
|
toolsMap,
|
|
713
727
|
llmAdapters,
|
|
714
728
|
this.getAgentId(),
|
|
715
|
-
this.ctx // Pass ctx to runner so callbacks can access it
|
|
729
|
+
this.ctx, // Pass ctx to runner so callbacks can access it
|
|
730
|
+
this._observer
|
|
716
731
|
);
|
|
717
732
|
}
|
|
718
733
|
return this.runner;
|
|
@@ -909,6 +924,12 @@ export interface AgentExecuteInput<ConversationCtx = any> {
|
|
|
909
924
|
}
|
|
910
925
|
|
|
911
926
|
export interface AgentExecuteResult {
|
|
927
|
+
/**
|
|
928
|
+
* Framework-generated unique ID for this agent execution.
|
|
929
|
+
* Matches the `runId` emitted in AgentObserver events, enabling apps to
|
|
930
|
+
* correlate persisted results with observer traces.
|
|
931
|
+
*/
|
|
932
|
+
runId: string;
|
|
912
933
|
message: string; // Final AI response
|
|
913
934
|
toolCalls: Array<{
|
|
914
935
|
name: string;
|
|
@@ -957,3 +978,92 @@ export interface AgentResponse {
|
|
|
957
978
|
textStream: AsyncGenerator<string>; // Stream only text deltas
|
|
958
979
|
fullStream: AsyncGenerator<StreamChunk>; // Stream all events
|
|
959
980
|
}
|
|
981
|
+
|
|
982
|
+
/**
|
|
983
|
+
* Event fired once per agent execution, before the first LLM call.
|
|
984
|
+
* Contains the resolved system instructions and the initial message array
|
|
985
|
+
* (post-history, pre-compaction, pre-tool-filtering).
|
|
986
|
+
*/
|
|
987
|
+
export interface AgentObserverRunEvent {
|
|
988
|
+
runId: string;
|
|
989
|
+
agentId: string;
|
|
990
|
+
instructions: string;
|
|
991
|
+
input: AgentExecuteInput;
|
|
992
|
+
messages: LLMMessage[];
|
|
993
|
+
tools: string[];
|
|
994
|
+
model: { adapterId?: string; maxTokens?: number; temperature?: number };
|
|
995
|
+
context: AgentExecuteContext;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
/**
|
|
999
|
+
* Event fired immediately before each LLM call in the agentic loop.
|
|
1000
|
+
* Reflects the messages and tools actually sent to the model after
|
|
1001
|
+
* compaction and per-step permission filtering.
|
|
1002
|
+
*/
|
|
1003
|
+
export interface AgentObserverLlmCallEvent {
|
|
1004
|
+
runId: string;
|
|
1005
|
+
agentId: string;
|
|
1006
|
+
step: number;
|
|
1007
|
+
maxSteps: number;
|
|
1008
|
+
instructions: string;
|
|
1009
|
+
messages: LLMMessage[];
|
|
1010
|
+
tools: string[];
|
|
1011
|
+
model: { adapterId?: string; maxTokens?: number; temperature?: number };
|
|
1012
|
+
context: AgentExecuteContext;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
/**
|
|
1016
|
+
* Event fired after each step (LLM call + tool executions) completes.
|
|
1017
|
+
* `toolCalls` contains only the tool calls executed during this step.
|
|
1018
|
+
*/
|
|
1019
|
+
export interface AgentObserverStepEvent {
|
|
1020
|
+
runId: string;
|
|
1021
|
+
agentId: string;
|
|
1022
|
+
step: number;
|
|
1023
|
+
maxSteps: number;
|
|
1024
|
+
messages: LLMMessage[];
|
|
1025
|
+
assistantText?: string;
|
|
1026
|
+
toolCalls: AgentExecuteResult["toolCalls"];
|
|
1027
|
+
usage?: import("./LLMAdapter").LLMUsage;
|
|
1028
|
+
context: AgentExecuteContext;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
/**
|
|
1032
|
+
* Event fired when an agent execution completes (successfully or with error).
|
|
1033
|
+
* On error, `result` contains whatever was accumulated before the throw.
|
|
1034
|
+
*/
|
|
1035
|
+
export interface AgentObserverFinishEvent {
|
|
1036
|
+
runId: string;
|
|
1037
|
+
agentId: string;
|
|
1038
|
+
result: AgentExecuteResult;
|
|
1039
|
+
messages: LLMMessage[];
|
|
1040
|
+
durationMs: number;
|
|
1041
|
+
error?: string;
|
|
1042
|
+
context: AgentExecuteContext;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
/**
|
|
1046
|
+
* Global agent observer for app-level tracing, APM, cost accounting, dev tools, etc.
|
|
1047
|
+
*
|
|
1048
|
+
* Register once on `FlinkOptions.ai.observer`; fires for every agent execution in the app.
|
|
1049
|
+
*
|
|
1050
|
+
* Observer callbacks are invoked fire-and-forget: they may return a Promise but the
|
|
1051
|
+
* framework does not await them, and any thrown/rejected errors are caught and logged
|
|
1052
|
+
* without affecting agent execution. Observers are read-only — they must not mutate
|
|
1053
|
+
* inputs, messages, or results. Use the per-agent `beforeRun`/`onStep`/`afterRun` hooks
|
|
1054
|
+
* for business logic that needs to block or mutate.
|
|
1055
|
+
*
|
|
1056
|
+
* Typical use cases:
|
|
1057
|
+
* - Persisted traces for a dev tools page (correlate via `context.metadata`)
|
|
1058
|
+
* - OpenTelemetry / Sentry integration
|
|
1059
|
+
* - Cost accounting and token-usage dashboards
|
|
1060
|
+
*
|
|
1061
|
+
* All events share a stable `runId` per execution so persisted records can be joined
|
|
1062
|
+
* across events. The same `runId` is returned on `AgentExecuteResult.runId`.
|
|
1063
|
+
*/
|
|
1064
|
+
export interface AgentObserver {
|
|
1065
|
+
onRun?(event: AgentObserverRunEvent): void | Promise<void>;
|
|
1066
|
+
onLlmCall?(event: AgentObserverLlmCallEvent): void | Promise<void>;
|
|
1067
|
+
onStep?(event: AgentObserverStepEvent): void | Promise<void>;
|
|
1068
|
+
onFinish?(event: AgentObserverFinishEvent): void | Promise<void>;
|
|
1069
|
+
}
|