@cat-factory/orchestration 0.6.0
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/LICENSE +21 -0
- package/dist/container.d.ts +460 -0
- package/dist/container.d.ts.map +1 -0
- package/dist/container.js +657 -0
- package/dist/container.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/modules/board/BoardService.d.ts +125 -0
- package/dist/modules/board/BoardService.d.ts.map +1 -0
- package/dist/modules/board/BoardService.js +496 -0
- package/dist/modules/board/BoardService.js.map +1 -0
- package/dist/modules/board/board.logic.d.ts +17 -0
- package/dist/modules/board/board.logic.d.ts.map +1 -0
- package/dist/modules/board/board.logic.js +51 -0
- package/dist/modules/board/board.logic.js.map +1 -0
- package/dist/modules/boardScan/BoardScanService.d.ts +35 -0
- package/dist/modules/boardScan/BoardScanService.d.ts.map +1 -0
- package/dist/modules/boardScan/BoardScanService.js +91 -0
- package/dist/modules/boardScan/BoardScanService.js.map +1 -0
- package/dist/modules/boardScan/board-scan.logic.d.ts +10 -0
- package/dist/modules/boardScan/board-scan.logic.d.ts.map +1 -0
- package/dist/modules/boardScan/board-scan.logic.js +26 -0
- package/dist/modules/boardScan/board-scan.logic.js.map +1 -0
- package/dist/modules/bootstrap/BootstrapService.d.ts +114 -0
- package/dist/modules/bootstrap/BootstrapService.d.ts.map +1 -0
- package/dist/modules/bootstrap/BootstrapService.js +516 -0
- package/dist/modules/bootstrap/BootstrapService.js.map +1 -0
- package/dist/modules/clarity/ClarityReviewService.d.ts +48 -0
- package/dist/modules/clarity/ClarityReviewService.d.ts.map +1 -0
- package/dist/modules/clarity/ClarityReviewService.js +63 -0
- package/dist/modules/clarity/ClarityReviewService.js.map +1 -0
- package/dist/modules/clarity/clarity.logic.d.ts +36 -0
- package/dist/modules/clarity/clarity.logic.d.ts.map +1 -0
- package/dist/modules/clarity/clarity.logic.js +98 -0
- package/dist/modules/clarity/clarity.logic.js.map +1 -0
- package/dist/modules/estimation/estimate.logic.d.ts +11 -0
- package/dist/modules/estimation/estimate.logic.d.ts.map +1 -0
- package/dist/modules/estimation/estimate.logic.js +37 -0
- package/dist/modules/estimation/estimate.logic.js.map +1 -0
- package/dist/modules/execution/AgentContextBuilder.d.ts +114 -0
- package/dist/modules/execution/AgentContextBuilder.d.ts.map +1 -0
- package/dist/modules/execution/AgentContextBuilder.js +316 -0
- package/dist/modules/execution/AgentContextBuilder.js.map +1 -0
- package/dist/modules/execution/CompanionController.d.ts +60 -0
- package/dist/modules/execution/CompanionController.d.ts.map +1 -0
- package/dist/modules/execution/CompanionController.js +216 -0
- package/dist/modules/execution/CompanionController.js.map +1 -0
- package/dist/modules/execution/ExecutionService.d.ts +874 -0
- package/dist/modules/execution/ExecutionService.d.ts.map +1 -0
- package/dist/modules/execution/ExecutionService.js +2921 -0
- package/dist/modules/execution/ExecutionService.js.map +1 -0
- package/dist/modules/execution/MergeResolver.d.ts +34 -0
- package/dist/modules/execution/MergeResolver.d.ts.map +1 -0
- package/dist/modules/execution/MergeResolver.js +81 -0
- package/dist/modules/execution/MergeResolver.js.map +1 -0
- package/dist/modules/execution/ReviewGateController.d.ts +163 -0
- package/dist/modules/execution/ReviewGateController.d.ts.map +1 -0
- package/dist/modules/execution/ReviewGateController.js +251 -0
- package/dist/modules/execution/ReviewGateController.js.map +1 -0
- package/dist/modules/execution/TesterController.d.ts +61 -0
- package/dist/modules/execution/TesterController.d.ts.map +1 -0
- package/dist/modules/execution/TesterController.js +215 -0
- package/dist/modules/execution/TesterController.js.map +1 -0
- package/dist/modules/execution/advance.d.ts +84 -0
- package/dist/modules/execution/advance.d.ts.map +1 -0
- package/dist/modules/execution/advance.js +2 -0
- package/dist/modules/execution/advance.js.map +1 -0
- package/dist/modules/execution/artifact-review.logic.d.ts +25 -0
- package/dist/modules/execution/artifact-review.logic.d.ts.map +1 -0
- package/dist/modules/execution/artifact-review.logic.js +39 -0
- package/dist/modules/execution/artifact-review.logic.js.map +1 -0
- package/dist/modules/execution/ci.logic.d.ts +101 -0
- package/dist/modules/execution/ci.logic.d.ts.map +1 -0
- package/dist/modules/execution/ci.logic.js +117 -0
- package/dist/modules/execution/ci.logic.js.map +1 -0
- package/dist/modules/execution/drive.d.ts +47 -0
- package/dist/modules/execution/drive.d.ts.map +1 -0
- package/dist/modules/execution/drive.js +112 -0
- package/dist/modules/execution/drive.js.map +1 -0
- package/dist/modules/execution/gates.d.ts +97 -0
- package/dist/modules/execution/gates.d.ts.map +1 -0
- package/dist/modules/execution/gates.js +2 -0
- package/dist/modules/execution/gates.js.map +1 -0
- package/dist/modules/execution/individualVendors.logic.d.ts +22 -0
- package/dist/modules/execution/individualVendors.logic.d.ts.map +1 -0
- package/dist/modules/execution/individualVendors.logic.js +33 -0
- package/dist/modules/execution/individualVendors.logic.js.map +1 -0
- package/dist/modules/execution/job.logic.d.ts +52 -0
- package/dist/modules/execution/job.logic.d.ts.map +1 -0
- package/dist/modules/execution/job.logic.js +56 -0
- package/dist/modules/execution/job.logic.js.map +1 -0
- package/dist/modules/execution/release.logic.d.ts +43 -0
- package/dist/modules/execution/release.logic.d.ts.map +1 -0
- package/dist/modules/execution/release.logic.js +49 -0
- package/dist/modules/execution/release.logic.js.map +1 -0
- package/dist/modules/execution/retry.logic.d.ts +40 -0
- package/dist/modules/execution/retry.logic.d.ts.map +1 -0
- package/dist/modules/execution/retry.logic.js +83 -0
- package/dist/modules/execution/retry.logic.js.map +1 -0
- package/dist/modules/execution/stepGating.logic.d.ts +15 -0
- package/dist/modules/execution/stepGating.logic.d.ts.map +1 -0
- package/dist/modules/execution/stepGating.logic.js +29 -0
- package/dist/modules/execution/stepGating.logic.js.map +1 -0
- package/dist/modules/execution/stepResolvers.d.ts +41 -0
- package/dist/modules/execution/stepResolvers.d.ts.map +1 -0
- package/dist/modules/execution/stepResolvers.js +2 -0
- package/dist/modules/execution/stepResolvers.js.map +1 -0
- package/dist/modules/execution/tester-infra.logic.d.ts +42 -0
- package/dist/modules/execution/tester-infra.logic.d.ts.map +1 -0
- package/dist/modules/execution/tester-infra.logic.js +46 -0
- package/dist/modules/execution/tester-infra.logic.js.map +1 -0
- package/dist/modules/merge/MergePresetService.d.ts +32 -0
- package/dist/modules/merge/MergePresetService.d.ts.map +1 -0
- package/dist/modules/merge/MergePresetService.js +109 -0
- package/dist/modules/merge/MergePresetService.js.map +1 -0
- package/dist/modules/modelDefaults/ModelDefaultsService.d.ts +22 -0
- package/dist/modules/modelDefaults/ModelDefaultsService.d.ts.map +1 -0
- package/dist/modules/modelDefaults/ModelDefaultsService.js +28 -0
- package/dist/modules/modelDefaults/ModelDefaultsService.js.map +1 -0
- package/dist/modules/notifications/NotificationService.d.ts +74 -0
- package/dist/modules/notifications/NotificationService.d.ts.map +1 -0
- package/dist/modules/notifications/NotificationService.js +131 -0
- package/dist/modules/notifications/NotificationService.js.map +1 -0
- package/dist/modules/observability/LlmObservabilityService.d.ts +121 -0
- package/dist/modules/observability/LlmObservabilityService.d.ts.map +1 -0
- package/dist/modules/observability/LlmObservabilityService.js +140 -0
- package/dist/modules/observability/LlmObservabilityService.js.map +1 -0
- package/dist/modules/observability/observability.logic.d.ts +57 -0
- package/dist/modules/observability/observability.logic.d.ts.map +1 -0
- package/dist/modules/observability/observability.logic.js +186 -0
- package/dist/modules/observability/observability.logic.js.map +1 -0
- package/dist/modules/pipelines/PipelineService.d.ts +54 -0
- package/dist/modules/pipelines/PipelineService.d.ts.map +1 -0
- package/dist/modules/pipelines/PipelineService.js +226 -0
- package/dist/modules/pipelines/PipelineService.js.map +1 -0
- package/dist/modules/pipelines/pipelineShape.d.ts +53 -0
- package/dist/modules/pipelines/pipelineShape.d.ts.map +1 -0
- package/dist/modules/pipelines/pipelineShape.js +74 -0
- package/dist/modules/pipelines/pipelineShape.js.map +1 -0
- package/dist/modules/recurring/RecurringPipelineService.d.ts +76 -0
- package/dist/modules/recurring/RecurringPipelineService.d.ts.map +1 -0
- package/dist/modules/recurring/RecurringPipelineService.js +295 -0
- package/dist/modules/recurring/RecurringPipelineService.js.map +1 -0
- package/dist/modules/recurring/TrackerSettingsService.d.ts +16 -0
- package/dist/modules/recurring/TrackerSettingsService.d.ts.map +1 -0
- package/dist/modules/recurring/TrackerSettingsService.js +30 -0
- package/dist/modules/recurring/TrackerSettingsService.js.map +1 -0
- package/dist/modules/recurring/schedule.logic.d.ts +14 -0
- package/dist/modules/recurring/schedule.logic.d.ts.map +1 -0
- package/dist/modules/recurring/schedule.logic.js +85 -0
- package/dist/modules/recurring/schedule.logic.js.map +1 -0
- package/dist/modules/releaseHealth/ReleaseHealthService.d.ts +38 -0
- package/dist/modules/releaseHealth/ReleaseHealthService.d.ts.map +1 -0
- package/dist/modules/releaseHealth/ReleaseHealthService.js +96 -0
- package/dist/modules/releaseHealth/ReleaseHealthService.js.map +1 -0
- package/dist/modules/requirements/RequirementReviewService.d.ts +48 -0
- package/dist/modules/requirements/RequirementReviewService.d.ts.map +1 -0
- package/dist/modules/requirements/RequirementReviewService.js +83 -0
- package/dist/modules/requirements/RequirementReviewService.js.map +1 -0
- package/dist/modules/requirements/requirements.logic.d.ts +93 -0
- package/dist/modules/requirements/requirements.logic.d.ts.map +1 -0
- package/dist/modules/requirements/requirements.logic.js +203 -0
- package/dist/modules/requirements/requirements.logic.js.map +1 -0
- package/dist/modules/review/IterativeReviewService.d.ts +175 -0
- package/dist/modules/review/IterativeReviewService.d.ts.map +1 -0
- package/dist/modules/review/IterativeReviewService.js +327 -0
- package/dist/modules/review/IterativeReviewService.js.map +1 -0
- package/dist/modules/serviceFragmentDefaults/ServiceFragmentDefaultsService.d.ts +20 -0
- package/dist/modules/serviceFragmentDefaults/ServiceFragmentDefaultsService.d.ts.map +1 -0
- package/dist/modules/serviceFragmentDefaults/ServiceFragmentDefaultsService.js +26 -0
- package/dist/modules/serviceFragmentDefaults/ServiceFragmentDefaultsService.js.map +1 -0
- package/dist/modules/services/ServiceMountService.d.ts +48 -0
- package/dist/modules/services/ServiceMountService.d.ts.map +1 -0
- package/dist/modules/services/ServiceMountService.js +90 -0
- package/dist/modules/services/ServiceMountService.js.map +1 -0
- package/dist/modules/settings/WorkspaceSettingsService.d.ts +22 -0
- package/dist/modules/settings/WorkspaceSettingsService.d.ts.map +1 -0
- package/dist/modules/settings/WorkspaceSettingsService.js +50 -0
- package/dist/modules/settings/WorkspaceSettingsService.js.map +1 -0
- package/package.json +41 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { Clock, IdGenerator } from '@cat-factory/kernel';
|
|
2
|
+
import type { LlmCallMetric, LlmCallMetricRepository, LlmCallMetricSummary, LlmTraceSink } from '@cat-factory/kernel';
|
|
3
|
+
import type { LlmMetricsExport } from '@cat-factory/contracts';
|
|
4
|
+
export interface LlmObservabilityServiceDependencies {
|
|
5
|
+
llmCallMetricRepository: LlmCallMetricRepository;
|
|
6
|
+
idGenerator: IdGenerator;
|
|
7
|
+
clock: Clock;
|
|
8
|
+
/**
|
|
9
|
+
* Whether to persist the full prompt body with each metric. Defaults to true. When
|
|
10
|
+
* false, every numeric field (tokens, timing, finish reason, message/tool counts)
|
|
11
|
+
* is still recorded but the prompt is stored empty — for deployments that must not
|
|
12
|
+
* retain the complete prompts sent to the model. Governed by `LLM_RECORD_PROMPTS`.
|
|
13
|
+
*/
|
|
14
|
+
recordPrompts?: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Optional external trace sink (e.g. Langfuse). When wired, every recorded call is
|
|
17
|
+
* ALSO emitted here as a generation — the same code path the inline executor's
|
|
18
|
+
* instrumented model provider feeds, so proxied and inline calls land in one place.
|
|
19
|
+
* Fan-out is best-effort and never blocks or breaks the local recording.
|
|
20
|
+
*/
|
|
21
|
+
traceSink?: LlmTraceSink;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Defensive upper bound on a stored prompt/response body (characters). Real agent
|
|
25
|
+
* prompts sit far below this; the cap exists only so a pathological body can't blow
|
|
26
|
+
* past the store's per-row/value limit and make the whole metric fail to record
|
|
27
|
+
* (which would drop the call from observability entirely). A truncated-but-recorded
|
|
28
|
+
* body is strictly more useful than a silently dropped one.
|
|
29
|
+
*/
|
|
30
|
+
export declare const MAX_BODY_CHARS: number;
|
|
31
|
+
/** Default cap on how many (newest) calls a list/export returns. */
|
|
32
|
+
export declare const DEFAULT_LIST_LIMIT = 1000;
|
|
33
|
+
/**
|
|
34
|
+
* Details of one proxied LLM call, handed in by the LLM proxy. The proxy owns the
|
|
35
|
+
* timing (it wraps the upstream call): {@link totalMs} is the end-to-end time it
|
|
36
|
+
* spent and {@link upstreamMs} the slice waiting on the model — the difference is
|
|
37
|
+
* transport/proxy overhead, derived here so the two can never disagree.
|
|
38
|
+
*/
|
|
39
|
+
export interface RecordLlmCallInput {
|
|
40
|
+
/**
|
|
41
|
+
* The call's id. The proxy mints it so the same id is carried on the live `llmCall`
|
|
42
|
+
* activity event AND this persisted row — the drill-down panel keys its lazy body
|
|
43
|
+
* load by it. Optional: when omitted the service mints one (existing callers).
|
|
44
|
+
*/
|
|
45
|
+
id?: string;
|
|
46
|
+
workspaceId: string;
|
|
47
|
+
executionId: string | null;
|
|
48
|
+
agentKind: string;
|
|
49
|
+
provider: string;
|
|
50
|
+
model: string;
|
|
51
|
+
streaming: boolean;
|
|
52
|
+
messageCount: number;
|
|
53
|
+
toolCount: number;
|
|
54
|
+
requestMaxTokens: number | null;
|
|
55
|
+
promptTokens: number;
|
|
56
|
+
cachedPromptTokens: number;
|
|
57
|
+
completionTokens: number;
|
|
58
|
+
totalTokens: number;
|
|
59
|
+
finishReason: string | null;
|
|
60
|
+
/** End-to-end time the proxy spent on the call (ms). */
|
|
61
|
+
totalMs: number;
|
|
62
|
+
/** Time spent waiting on the upstream model (ms). */
|
|
63
|
+
upstreamMs: number;
|
|
64
|
+
ok: boolean;
|
|
65
|
+
httpStatus: number | null;
|
|
66
|
+
errorMessage: string | null;
|
|
67
|
+
promptText: string;
|
|
68
|
+
responseText: string;
|
|
69
|
+
/** The model's reasoning/thinking trace, when emitted on a separate channel (else ''). */
|
|
70
|
+
reasoningText: string;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The LLM observability sink. The proxy meters every container-agent model call
|
|
74
|
+
* here; the engine rolls the per-run aggregates onto pipeline steps for the board,
|
|
75
|
+
* and a query endpoint lists the full per-call detail for the drill-down panel. It
|
|
76
|
+
* is the observability sibling of {@link SpendService} (which keeps only billed
|
|
77
|
+
* totals): this keeps the full prompt/response, the output-limit headroom and the
|
|
78
|
+
* transport-vs-execution latency split. Wired only when a metric repository is
|
|
79
|
+
* present, so tests and unconfigured facades are unaffected.
|
|
80
|
+
*/
|
|
81
|
+
export declare class LlmObservabilityService {
|
|
82
|
+
private readonly repository;
|
|
83
|
+
private readonly idGenerator;
|
|
84
|
+
private readonly clock;
|
|
85
|
+
private readonly recordPrompts;
|
|
86
|
+
private readonly traceSink?;
|
|
87
|
+
constructor({ llmCallMetricRepository, idGenerator, clock, recordPrompts, traceSink, }: LlmObservabilityServiceDependencies);
|
|
88
|
+
/**
|
|
89
|
+
* Persist one metered call, assigning its id + timestamp and deriving the overhead.
|
|
90
|
+
* When prompt recording is enabled, the prompt is stored as a DELTA against the
|
|
91
|
+
* previous call in the same `(execution, agentKind)` conversation — a container
|
|
92
|
+
* agent re-sends its whole growing history every call, so storing only the new
|
|
93
|
+
* messages collapses ~21× of redundant prompt bytes (see `computeStoredPrompt`). The
|
|
94
|
+
* full prompt is rebuilt on export. The chain-tip lookup is off the response path
|
|
95
|
+
* (the proxy records via `waitUntil`), so the extra read is free of user latency.
|
|
96
|
+
* When prompt recording is disabled (`recordPrompts: false`) the prompt body is
|
|
97
|
+
* stored empty and the chain-tip read is skipped entirely — the numeric telemetry is
|
|
98
|
+
* still recorded.
|
|
99
|
+
*/
|
|
100
|
+
record(input: RecordLlmCallInput): Promise<void>;
|
|
101
|
+
/**
|
|
102
|
+
* Resolve this call's prompt to a delta against the chain tip of its
|
|
103
|
+
* `(workspace, execution, agentKind)` conversation (or the full array when it can't
|
|
104
|
+
* be chained). Only reached when prompt recording is enabled.
|
|
105
|
+
*/
|
|
106
|
+
private computeStoredPromptForChain;
|
|
107
|
+
/**
|
|
108
|
+
* Calls recorded for a run, newest first (full prompt/response included), capped
|
|
109
|
+
* at {@link DEFAULT_LIST_LIMIT} so a long run can't produce an unbounded payload.
|
|
110
|
+
*/
|
|
111
|
+
listByExecution(workspaceId: string, executionId: string, limit?: number): Promise<LlmCallMetric[]>;
|
|
112
|
+
/** Per-agent-kind aggregates for a run, for the board step rollups. */
|
|
113
|
+
summarizeByExecution(workspaceId: string, executionId: string): Promise<LlmCallMetricSummary[]>;
|
|
114
|
+
/**
|
|
115
|
+
* Build the LLM-friendly export for a run: a self-describing JSON bundle (totals +
|
|
116
|
+
* per-agent insights + every call, with derived ratios) meant to be handed to a
|
|
117
|
+
* model for analysis. Stamped with the service clock.
|
|
118
|
+
*/
|
|
119
|
+
exportForExecution(workspaceId: string, executionId: string): Promise<LlmMetricsExport>;
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=LlmObservabilityService.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LlmObservabilityService.d.ts","sourceRoot":"","sources":["../../../src/modules/observability/LlmObservabilityService.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AAC7D,OAAO,KAAK,EACV,aAAa,EACb,uBAAuB,EACvB,oBAAoB,EACpB,YAAY,EACb,MAAM,qBAAqB,CAAA;AAC5B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AAI9D,MAAM,WAAW,mCAAmC;IAClD,uBAAuB,EAAE,uBAAuB,CAAA;IAChD,WAAW,EAAE,WAAW,CAAA;IACxB,KAAK,EAAE,KAAK,CAAA;IACZ;;;;;OAKG;IACH,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,YAAY,CAAA;CACzB;AAED;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,QAAa,CAAA;AAQxC,oEAAoE;AACpE,eAAO,MAAM,kBAAkB,OAAO,CAAA;AAKtC;;;;;GAKG;AACH,MAAM,WAAW,kBAAkB;IACjC;;;;OAIG;IACH,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,WAAW,EAAE,MAAM,CAAA;IACnB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,OAAO,CAAA;IAClB,YAAY,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,MAAM,CAAA;IACjB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B,YAAY,EAAE,MAAM,CAAA;IACpB,kBAAkB,EAAE,MAAM,CAAA;IAC1B,gBAAgB,EAAE,MAAM,CAAA;IACxB,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,wDAAwD;IACxD,OAAO,EAAE,MAAM,CAAA;IACf,qDAAqD;IACrD,UAAU,EAAE,MAAM,CAAA;IAClB,EAAE,EAAE,OAAO,CAAA;IACX,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,UAAU,EAAE,MAAM,CAAA;IAClB,YAAY,EAAE,MAAM,CAAA;IACpB,0FAA0F;IAC1F,aAAa,EAAE,MAAM,CAAA;CACtB;AAED;;;;;;;;GAQG;AACH,qBAAa,uBAAuB;IAClC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAyB;IACpD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAa;IACzC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAO;IAC7B,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAc;IAEzC,YAAY,EACV,uBAAuB,EACvB,WAAW,EACX,KAAK,EACL,aAAoB,EACpB,SAAS,GACV,EAAE,mCAAmC,EAMrC;IAED;;;;;;;;;;;OAWG;IACG,MAAM,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAsDrD;IAED;;;;OAIG;YACW,2BAA2B;IAYzC;;;OAGG;IACH,eAAe,CACb,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,EACnB,KAAK,GAAE,MAA2B,GACjC,OAAO,CAAC,aAAa,EAAE,CAAC,CAE1B;IAED,uEAAuE;IACvE,oBAAoB,CAAC,WAAW,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,EAAE,CAAC,CAE9F;IAED;;;;OAIG;IACG,kBAAkB,CAAC,WAAW,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAG5F;CACF"}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { buildLlmMetricsExport, computeStoredPrompt } from './observability.logic.js';
|
|
2
|
+
/**
|
|
3
|
+
* Defensive upper bound on a stored prompt/response body (characters). Real agent
|
|
4
|
+
* prompts sit far below this; the cap exists only so a pathological body can't blow
|
|
5
|
+
* past the store's per-row/value limit and make the whole metric fail to record
|
|
6
|
+
* (which would drop the call from observability entirely). A truncated-but-recorded
|
|
7
|
+
* body is strictly more useful than a silently dropped one.
|
|
8
|
+
*/
|
|
9
|
+
export const MAX_BODY_CHARS = 512 * 1024;
|
|
10
|
+
/** Cap a body to {@link MAX_BODY_CHARS}, marking where it was cut. */
|
|
11
|
+
function clampBody(text) {
|
|
12
|
+
if (text.length <= MAX_BODY_CHARS)
|
|
13
|
+
return text;
|
|
14
|
+
return `${text.slice(0, MAX_BODY_CHARS)}\n…[truncated ${text.length - MAX_BODY_CHARS} chars]`;
|
|
15
|
+
}
|
|
16
|
+
/** Default cap on how many (newest) calls a list/export returns. */
|
|
17
|
+
export const DEFAULT_LIST_LIMIT = 1000;
|
|
18
|
+
/** What to store for a call's prompt when prompt recording is turned off: nothing. */
|
|
19
|
+
const EMPTY_STORED_PROMPT = { promptText: '', promptPrefixCount: 0, promptHash: '' };
|
|
20
|
+
/**
|
|
21
|
+
* The LLM observability sink. The proxy meters every container-agent model call
|
|
22
|
+
* here; the engine rolls the per-run aggregates onto pipeline steps for the board,
|
|
23
|
+
* and a query endpoint lists the full per-call detail for the drill-down panel. It
|
|
24
|
+
* is the observability sibling of {@link SpendService} (which keeps only billed
|
|
25
|
+
* totals): this keeps the full prompt/response, the output-limit headroom and the
|
|
26
|
+
* transport-vs-execution latency split. Wired only when a metric repository is
|
|
27
|
+
* present, so tests and unconfigured facades are unaffected.
|
|
28
|
+
*/
|
|
29
|
+
export class LlmObservabilityService {
|
|
30
|
+
repository;
|
|
31
|
+
idGenerator;
|
|
32
|
+
clock;
|
|
33
|
+
recordPrompts;
|
|
34
|
+
traceSink;
|
|
35
|
+
constructor({ llmCallMetricRepository, idGenerator, clock, recordPrompts = true, traceSink, }) {
|
|
36
|
+
this.repository = llmCallMetricRepository;
|
|
37
|
+
this.idGenerator = idGenerator;
|
|
38
|
+
this.clock = clock;
|
|
39
|
+
this.recordPrompts = recordPrompts;
|
|
40
|
+
this.traceSink = traceSink;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Persist one metered call, assigning its id + timestamp and deriving the overhead.
|
|
44
|
+
* When prompt recording is enabled, the prompt is stored as a DELTA against the
|
|
45
|
+
* previous call in the same `(execution, agentKind)` conversation — a container
|
|
46
|
+
* agent re-sends its whole growing history every call, so storing only the new
|
|
47
|
+
* messages collapses ~21× of redundant prompt bytes (see `computeStoredPrompt`). The
|
|
48
|
+
* full prompt is rebuilt on export. The chain-tip lookup is off the response path
|
|
49
|
+
* (the proxy records via `waitUntil`), so the extra read is free of user latency.
|
|
50
|
+
* When prompt recording is disabled (`recordPrompts: false`) the prompt body is
|
|
51
|
+
* stored empty and the chain-tip read is skipped entirely — the numeric telemetry is
|
|
52
|
+
* still recorded.
|
|
53
|
+
*/
|
|
54
|
+
async record(input) {
|
|
55
|
+
const overheadMs = Math.max(0, input.totalMs - input.upstreamMs);
|
|
56
|
+
const stored = this.recordPrompts
|
|
57
|
+
? await this.computeStoredPromptForChain(input)
|
|
58
|
+
: EMPTY_STORED_PROMPT;
|
|
59
|
+
const metric = {
|
|
60
|
+
createdAt: this.clock.now(),
|
|
61
|
+
...input,
|
|
62
|
+
// Derived/bounded fields last, so they win over any same-named input field.
|
|
63
|
+
// `id` here (not above `...input`) so an absent `input.id` falls back to a mint
|
|
64
|
+
// rather than being spread in as `undefined`.
|
|
65
|
+
id: input.id ?? this.idGenerator.next('llm'),
|
|
66
|
+
overheadMs,
|
|
67
|
+
promptText: clampBody(stored.promptText),
|
|
68
|
+
promptPrefixCount: stored.promptPrefixCount,
|
|
69
|
+
promptHash: stored.promptHash,
|
|
70
|
+
responseText: clampBody(input.responseText),
|
|
71
|
+
reasoningText: clampBody(input.reasoningText),
|
|
72
|
+
};
|
|
73
|
+
await this.repository.record(metric);
|
|
74
|
+
// Fan out to the external trace sink (Langfuse), if wired. We send the FULL prompt
|
|
75
|
+
// (not the stored delta) so the trace is self-contained, honouring the same
|
|
76
|
+
// `recordPrompts` privacy switch as the local store. Best-effort and NON-blocking:
|
|
77
|
+
// dispatched without awaiting (like the inline feeder) so the sink's network round
|
|
78
|
+
// trip never extends the metering path, and isolated so a sink failure can't break
|
|
79
|
+
// local recording. The sink itself swallows + logs and bounds its own request.
|
|
80
|
+
if (this.traceSink) {
|
|
81
|
+
const endedAt = metric.createdAt;
|
|
82
|
+
try {
|
|
83
|
+
void Promise.resolve(this.traceSink.recordGeneration({
|
|
84
|
+
workspaceId: input.workspaceId,
|
|
85
|
+
executionId: input.executionId,
|
|
86
|
+
agentKind: input.agentKind,
|
|
87
|
+
provider: input.provider,
|
|
88
|
+
model: input.model,
|
|
89
|
+
startedAt: Math.max(0, endedAt - input.upstreamMs),
|
|
90
|
+
endedAt,
|
|
91
|
+
promptTokens: input.promptTokens,
|
|
92
|
+
completionTokens: input.completionTokens,
|
|
93
|
+
totalTokens: input.totalTokens,
|
|
94
|
+
finishReason: input.finishReason,
|
|
95
|
+
ok: input.ok,
|
|
96
|
+
errorMessage: input.errorMessage,
|
|
97
|
+
input: this.recordPrompts ? input.promptText : '',
|
|
98
|
+
// Fall back to the reasoning trace when the turn produced no response text
|
|
99
|
+
// (a thinking model that spent its budget reasoning) so the trace isn't blank.
|
|
100
|
+
output: this.recordPrompts ? input.responseText || input.reasoningText : '',
|
|
101
|
+
})).catch(() => { });
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// Swallowed: the sink itself logs; observability never breaks the proxy.
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Resolve this call's prompt to a delta against the chain tip of its
|
|
110
|
+
* `(workspace, execution, agentKind)` conversation (or the full array when it can't
|
|
111
|
+
* be chained). Only reached when prompt recording is enabled.
|
|
112
|
+
*/
|
|
113
|
+
async computeStoredPromptForChain(input) {
|
|
114
|
+
const prev = input.executionId != null
|
|
115
|
+
? await this.repository.latestChainTip(input.workspaceId, input.executionId, input.agentKind)
|
|
116
|
+
: null;
|
|
117
|
+
return computeStoredPrompt(input.promptText, prev);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Calls recorded for a run, newest first (full prompt/response included), capped
|
|
121
|
+
* at {@link DEFAULT_LIST_LIMIT} so a long run can't produce an unbounded payload.
|
|
122
|
+
*/
|
|
123
|
+
listByExecution(workspaceId, executionId, limit = DEFAULT_LIST_LIMIT) {
|
|
124
|
+
return this.repository.listByExecution(workspaceId, executionId, limit);
|
|
125
|
+
}
|
|
126
|
+
/** Per-agent-kind aggregates for a run, for the board step rollups. */
|
|
127
|
+
summarizeByExecution(workspaceId, executionId) {
|
|
128
|
+
return this.repository.summarizeByExecution(workspaceId, executionId);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Build the LLM-friendly export for a run: a self-describing JSON bundle (totals +
|
|
132
|
+
* per-agent insights + every call, with derived ratios) meant to be handed to a
|
|
133
|
+
* model for analysis. Stamped with the service clock.
|
|
134
|
+
*/
|
|
135
|
+
async exportForExecution(workspaceId, executionId) {
|
|
136
|
+
const calls = await this.listByExecution(workspaceId, executionId);
|
|
137
|
+
return buildLlmMetricsExport(executionId, calls, this.clock.now());
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
//# sourceMappingURL=LlmObservabilityService.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LlmObservabilityService.js","sourceRoot":"","sources":["../../../src/modules/observability/LlmObservabilityService.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { type LlmCallMetric } from '@cat-factory/kernel';
|
|
2
|
+
import type { LlmMetricsExport } from '@cat-factory/contracts';
|
|
3
|
+
export type LlmCallOutcome = 'ok' | 'warning' | 'error';
|
|
4
|
+
/** Whether a finish reason is a (non-fatal) warning — output truncated or filtered. */
|
|
5
|
+
export declare function isWarningFinishReason(finishReason: string | null): boolean;
|
|
6
|
+
/**
|
|
7
|
+
* Classify a recorded call: a non-2xx/failed call is an `error`; a successful call
|
|
8
|
+
* cut short by the output limit or content filter is a `warning`; otherwise `ok`.
|
|
9
|
+
*/
|
|
10
|
+
export declare function classifyCall(metric: Pick<LlmCallMetric, 'ok' | 'finishReason'>): LlmCallOutcome;
|
|
11
|
+
/**
|
|
12
|
+
* Fraction (0..1) of the output budget the largest single completion consumed, or
|
|
13
|
+
* null when the ceiling is unknown. 1 (or a `length` finish) means a call was
|
|
14
|
+
* truncated. Drives the board's "output-limit headroom" bar.
|
|
15
|
+
*/
|
|
16
|
+
export declare function outputHeadroomRatio(peakCompletionTokens: number, maxOutputTokens: number | null): number | null;
|
|
17
|
+
/** Share of total latency spent in transport/proxy overhead (0..1), or null when no timing. */
|
|
18
|
+
export declare function transportOverheadRatio(upstreamMs: number, overheadMs: number): number | null;
|
|
19
|
+
/** The previous call's chain tip: enough to decide if the next call extends it. */
|
|
20
|
+
export interface PromptChainTip {
|
|
21
|
+
/** The previous call's full message count (its `messageCount`). */
|
|
22
|
+
messageCount: number;
|
|
23
|
+
/** The previous call's {@link LlmCallMetric.promptHash} (hash of its full array). */
|
|
24
|
+
promptHash: string;
|
|
25
|
+
}
|
|
26
|
+
/** What to store for a call's prompt after delta compression. */
|
|
27
|
+
export interface StoredPrompt {
|
|
28
|
+
promptText: string;
|
|
29
|
+
promptPrefixCount: number;
|
|
30
|
+
promptHash: string;
|
|
31
|
+
}
|
|
32
|
+
/** Fast, stable, dependency-free hash (FNV-1a, length-salted) for chain validation. */
|
|
33
|
+
export declare function hashPrompt(text: string): string;
|
|
34
|
+
/**
|
|
35
|
+
* Compute what to store for a call's prompt: the delta (new messages only) when this
|
|
36
|
+
* call provably extends the previous one in its chain, else the full array. The
|
|
37
|
+
* returned {@link StoredPrompt.promptHash} is always over the FULL array so the next
|
|
38
|
+
* call can chain onto this one. `fullPromptText` is the proxy's `JSON.stringify(messages)`.
|
|
39
|
+
*/
|
|
40
|
+
export declare function computeStoredPrompt(fullPromptText: string, prev: PromptChainTip | null): StoredPrompt;
|
|
41
|
+
/**
|
|
42
|
+
* Rebuild each call's FULL prompt from the stored deltas. Calls are grouped by agent
|
|
43
|
+
* kind (each kind is its own conversation chain) and replayed oldest-first,
|
|
44
|
+
* accumulating the running message array; a call with `promptPrefixCount === 0` resets
|
|
45
|
+
* the chain. Best-effort: if a chain's head is missing (e.g. truncated by a list
|
|
46
|
+
* limit) a delta that can't be rebuilt is returned as-is. The returned calls preserve
|
|
47
|
+
* the input order, with `promptText` set to the full array and `promptPrefixCount` 0.
|
|
48
|
+
*/
|
|
49
|
+
export declare function reconstructPrompts(calls: LlmCallMetric[]): LlmCallMetric[];
|
|
50
|
+
/**
|
|
51
|
+
* Build the LLM-friendly export bundle for a run from its recorded calls: a
|
|
52
|
+
* self-describing JSON document (totals + per-agent insights + every call, with
|
|
53
|
+
* derived ratios precomputed) meant to be handed straight to a model for analysis.
|
|
54
|
+
* Pure so it is unit-testable; `generatedAt` is injected (no clock here).
|
|
55
|
+
*/
|
|
56
|
+
export declare function buildLlmMetricsExport(executionId: string, storedCalls: LlmCallMetric[], generatedAt: number): LlmMetricsExport;
|
|
57
|
+
//# sourceMappingURL=observability.logic.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"observability.logic.d.ts","sourceRoot":"","sources":["../../../src/modules/observability/observability.logic.ts"],"names":[],"mappings":"AAAA,OAAO,EAA8B,KAAK,aAAa,EAAE,MAAM,qBAAqB,CAAA;AACpF,OAAO,KAAK,EAAoB,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AAMhF,MAAM,MAAM,cAAc,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAAA;AAEvD,uFAAuF;AACvF,wBAAgB,qBAAqB,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAI1E;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,IAAI,CAAC,aAAa,EAAE,IAAI,GAAG,cAAc,CAAC,GAAG,cAAc,CAI/F;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,oBAAoB,EAAE,MAAM,EAC5B,eAAe,EAAE,MAAM,GAAG,IAAI,GAC7B,MAAM,GAAG,IAAI,CAGf;AAED,+FAA+F;AAC/F,wBAAgB,sBAAsB,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAG5F;AAkBD,mFAAmF;AACnF,MAAM,WAAW,cAAc;IAC7B,mEAAmE;IACnE,YAAY,EAAE,MAAM,CAAA;IACpB,qFAAqF;IACrF,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,iEAAiE;AACjE,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,MAAM,CAAA;IAClB,iBAAiB,EAAE,MAAM,CAAA;IACzB,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,uFAAuF;AACvF,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAS/C;AAWD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,cAAc,EAAE,MAAM,EACtB,IAAI,EAAE,cAAc,GAAG,IAAI,GAC1B,YAAY,CAkBd;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,aAAa,EAAE,GAAG,aAAa,EAAE,CAgC1E;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,aAAa,EAAE,EAC5B,WAAW,EAAE,MAAM,GAClB,gBAAgB,CAwDlB"}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { LLM_WARNING_FINISH_REASONS } from '@cat-factory/kernel';
|
|
2
|
+
/** Whether a finish reason is a (non-fatal) warning — output truncated or filtered. */
|
|
3
|
+
export function isWarningFinishReason(finishReason) {
|
|
4
|
+
return (finishReason != null && LLM_WARNING_FINISH_REASONS.includes(finishReason));
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Classify a recorded call: a non-2xx/failed call is an `error`; a successful call
|
|
8
|
+
* cut short by the output limit or content filter is a `warning`; otherwise `ok`.
|
|
9
|
+
*/
|
|
10
|
+
export function classifyCall(metric) {
|
|
11
|
+
if (!metric.ok)
|
|
12
|
+
return 'error';
|
|
13
|
+
if (isWarningFinishReason(metric.finishReason))
|
|
14
|
+
return 'warning';
|
|
15
|
+
return 'ok';
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Fraction (0..1) of the output budget the largest single completion consumed, or
|
|
19
|
+
* null when the ceiling is unknown. 1 (or a `length` finish) means a call was
|
|
20
|
+
* truncated. Drives the board's "output-limit headroom" bar.
|
|
21
|
+
*/
|
|
22
|
+
export function outputHeadroomRatio(peakCompletionTokens, maxOutputTokens) {
|
|
23
|
+
if (maxOutputTokens == null || maxOutputTokens <= 0)
|
|
24
|
+
return null;
|
|
25
|
+
return Math.min(1, peakCompletionTokens / maxOutputTokens);
|
|
26
|
+
}
|
|
27
|
+
/** Share of total latency spent in transport/proxy overhead (0..1), or null when no timing. */
|
|
28
|
+
export function transportOverheadRatio(upstreamMs, overheadMs) {
|
|
29
|
+
const total = upstreamMs + overheadMs;
|
|
30
|
+
return total > 0 ? overheadMs / total : null;
|
|
31
|
+
}
|
|
32
|
+
/** Fast, stable, dependency-free hash (FNV-1a, length-salted) for chain validation. */
|
|
33
|
+
export function hashPrompt(text) {
|
|
34
|
+
let h = 0x811c9dc5;
|
|
35
|
+
for (let i = 0; i < text.length; i++) {
|
|
36
|
+
h ^= text.charCodeAt(i);
|
|
37
|
+
// FNV prime, kept in 32-bit range via Math.imul.
|
|
38
|
+
h = Math.imul(h, 0x01000193);
|
|
39
|
+
}
|
|
40
|
+
// Salt with the length so two same-hash-different-length strings can't collide.
|
|
41
|
+
return `${text.length.toString(36)}:${(h >>> 0).toString(36)}`;
|
|
42
|
+
}
|
|
43
|
+
function parseMessages(promptText) {
|
|
44
|
+
try {
|
|
45
|
+
const parsed = JSON.parse(promptText);
|
|
46
|
+
return Array.isArray(parsed) ? parsed : null;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Compute what to store for a call's prompt: the delta (new messages only) when this
|
|
54
|
+
* call provably extends the previous one in its chain, else the full array. The
|
|
55
|
+
* returned {@link StoredPrompt.promptHash} is always over the FULL array so the next
|
|
56
|
+
* call can chain onto this one. `fullPromptText` is the proxy's `JSON.stringify(messages)`.
|
|
57
|
+
*/
|
|
58
|
+
export function computeStoredPrompt(fullPromptText, prev) {
|
|
59
|
+
const promptHash = hashPrompt(fullPromptText);
|
|
60
|
+
const full = parseMessages(fullPromptText);
|
|
61
|
+
// No previous tip, unparseable, or not actually longer ⇒ store the full array.
|
|
62
|
+
if (!full || !prev || prev.messageCount <= 0 || full.length < prev.messageCount) {
|
|
63
|
+
return { promptText: fullPromptText, promptPrefixCount: 0, promptHash };
|
|
64
|
+
}
|
|
65
|
+
// Only elide when the leading `prev.messageCount` messages match what the previous
|
|
66
|
+
// call stored (append-only). A mismatch (fresh conversation / compaction) ⇒ full.
|
|
67
|
+
const prefix = JSON.stringify(full.slice(0, prev.messageCount));
|
|
68
|
+
if (hashPrompt(prefix) !== prev.promptHash) {
|
|
69
|
+
return { promptText: fullPromptText, promptPrefixCount: 0, promptHash };
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
promptText: JSON.stringify(full.slice(prev.messageCount)),
|
|
73
|
+
promptPrefixCount: prev.messageCount,
|
|
74
|
+
promptHash,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Rebuild each call's FULL prompt from the stored deltas. Calls are grouped by agent
|
|
79
|
+
* kind (each kind is its own conversation chain) and replayed oldest-first,
|
|
80
|
+
* accumulating the running message array; a call with `promptPrefixCount === 0` resets
|
|
81
|
+
* the chain. Best-effort: if a chain's head is missing (e.g. truncated by a list
|
|
82
|
+
* limit) a delta that can't be rebuilt is returned as-is. The returned calls preserve
|
|
83
|
+
* the input order, with `promptText` set to the full array and `promptPrefixCount` 0.
|
|
84
|
+
*/
|
|
85
|
+
export function reconstructPrompts(calls) {
|
|
86
|
+
// Order by time, then message count, then id. Records are written off the response
|
|
87
|
+
// path (`waitUntil`) so two calls can share a `createdAt` millisecond; `messageCount`
|
|
88
|
+
// is monotonic within an append-only chain, so it breaks such ties in true
|
|
89
|
+
// conversation order (id is the last resort when even that is equal).
|
|
90
|
+
const asc = [...calls].sort((a, b) => a.createdAt - b.createdAt || a.messageCount - b.messageCount || a.id.localeCompare(b.id));
|
|
91
|
+
const running = new Map();
|
|
92
|
+
const fullById = new Map();
|
|
93
|
+
for (const c of asc) {
|
|
94
|
+
const delta = parseMessages(c.promptText) ?? [];
|
|
95
|
+
let full;
|
|
96
|
+
if (c.promptPrefixCount > 0) {
|
|
97
|
+
const prev = running.get(c.agentKind) ?? [];
|
|
98
|
+
// Only rebuild when we actually hold the referenced prefix; else keep the delta.
|
|
99
|
+
full =
|
|
100
|
+
prev.length >= c.promptPrefixCount
|
|
101
|
+
? [...prev.slice(0, c.promptPrefixCount), ...delta]
|
|
102
|
+
: delta;
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
full = delta;
|
|
106
|
+
}
|
|
107
|
+
running.set(c.agentKind, full);
|
|
108
|
+
fullById.set(c.id, JSON.stringify(full));
|
|
109
|
+
}
|
|
110
|
+
return calls.map((c) => ({
|
|
111
|
+
...c,
|
|
112
|
+
promptText: fullById.get(c.id) ?? c.promptText,
|
|
113
|
+
promptPrefixCount: 0,
|
|
114
|
+
}));
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Build the LLM-friendly export bundle for a run from its recorded calls: a
|
|
118
|
+
* self-describing JSON document (totals + per-agent insights + every call, with
|
|
119
|
+
* derived ratios precomputed) meant to be handed straight to a model for analysis.
|
|
120
|
+
* Pure so it is unit-testable; `generatedAt` is injected (no clock here).
|
|
121
|
+
*/
|
|
122
|
+
export function buildLlmMetricsExport(executionId, storedCalls, generatedAt) {
|
|
123
|
+
// The export is a self-contained analysis bundle, so rebuild each call's full
|
|
124
|
+
// prompt from the stored deltas before assembling it.
|
|
125
|
+
const calls = reconstructPrompts(storedCalls);
|
|
126
|
+
const byKind = new Map();
|
|
127
|
+
for (const call of calls) {
|
|
128
|
+
const list = byKind.get(call.agentKind);
|
|
129
|
+
if (list)
|
|
130
|
+
list.push(call);
|
|
131
|
+
else
|
|
132
|
+
byKind.set(call.agentKind, [call]);
|
|
133
|
+
}
|
|
134
|
+
const insights = [...byKind.entries()].map(([agentKind, kindCalls]) => {
|
|
135
|
+
const promptTokens = sum(kindCalls, (c) => c.promptTokens);
|
|
136
|
+
const completionTokens = sum(kindCalls, (c) => c.completionTokens);
|
|
137
|
+
const peakCompletionTokens = kindCalls.reduce((m, c) => Math.max(m, c.completionTokens), 0);
|
|
138
|
+
const maxOutputTokens = maxNullable(kindCalls.map((c) => c.requestMaxTokens));
|
|
139
|
+
const upstreamMs = sum(kindCalls, (c) => c.upstreamMs);
|
|
140
|
+
const overheadMs = sum(kindCalls, (c) => c.overheadMs);
|
|
141
|
+
return {
|
|
142
|
+
agentKind,
|
|
143
|
+
calls: kindCalls.length,
|
|
144
|
+
promptTokens,
|
|
145
|
+
completionTokens,
|
|
146
|
+
peakCompletionTokens,
|
|
147
|
+
maxOutputTokens,
|
|
148
|
+
outputHeadroomRatio: outputHeadroomRatio(peakCompletionTokens, maxOutputTokens),
|
|
149
|
+
truncatedCalls: kindCalls.filter((c) => c.finishReason === 'length').length,
|
|
150
|
+
upstreamMs,
|
|
151
|
+
overheadMs,
|
|
152
|
+
transportOverheadRatio: transportOverheadRatio(upstreamMs, overheadMs),
|
|
153
|
+
errors: kindCalls.filter((c) => !c.ok).length,
|
|
154
|
+
warnings: kindCalls.filter((c) => c.ok && isWarningFinishReason(c.finishReason)).length,
|
|
155
|
+
};
|
|
156
|
+
});
|
|
157
|
+
const upstreamMs = sum(calls, (c) => c.upstreamMs);
|
|
158
|
+
const overheadMs = sum(calls, (c) => c.overheadMs);
|
|
159
|
+
return {
|
|
160
|
+
kind: 'cat-factory.llm-metrics-export',
|
|
161
|
+
version: 1,
|
|
162
|
+
executionId,
|
|
163
|
+
generatedAt,
|
|
164
|
+
totals: {
|
|
165
|
+
calls: calls.length,
|
|
166
|
+
promptTokens: sum(calls, (c) => c.promptTokens),
|
|
167
|
+
completionTokens: sum(calls, (c) => c.completionTokens),
|
|
168
|
+
upstreamMs,
|
|
169
|
+
overheadMs,
|
|
170
|
+
transportOverheadRatio: transportOverheadRatio(upstreamMs, overheadMs),
|
|
171
|
+
errors: calls.filter((c) => !c.ok).length,
|
|
172
|
+
warnings: calls.filter((c) => c.ok && isWarningFinishReason(c.finishReason)).length,
|
|
173
|
+
truncatedCalls: calls.filter((c) => c.finishReason === 'length').length,
|
|
174
|
+
},
|
|
175
|
+
insights,
|
|
176
|
+
calls,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
function sum(items, pick) {
|
|
180
|
+
return items.reduce((acc, item) => acc + pick(item), 0);
|
|
181
|
+
}
|
|
182
|
+
function maxNullable(values) {
|
|
183
|
+
const present = values.filter((v) => v != null);
|
|
184
|
+
return present.length > 0 ? Math.max(...present) : null;
|
|
185
|
+
}
|
|
186
|
+
//# sourceMappingURL=observability.logic.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"observability.logic.js","sourceRoot":"","sources":["../../../src/modules/observability/observability.logic.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { ClonePipelineInput, CreatePipelineInput, OrganizePipelineInput, UpdatePipelineInput } from '@cat-factory/contracts';
|
|
2
|
+
import type { Pipeline } from '@cat-factory/kernel';
|
|
3
|
+
import type { DatadogConnectionRepository, PipelineRepository, WorkspaceRepository } from '@cat-factory/kernel';
|
|
4
|
+
import type { IdGenerator } from '@cat-factory/kernel';
|
|
5
|
+
export interface PipelineServiceDependencies {
|
|
6
|
+
workspaceRepository: WorkspaceRepository;
|
|
7
|
+
pipelineRepository: PipelineRepository;
|
|
8
|
+
idGenerator: IdGenerator;
|
|
9
|
+
/**
|
|
10
|
+
* Resolves whether the workspace has any observability integration enabled (today: a
|
|
11
|
+
* Datadog connection). When absent (no observability persistence wired at all), the
|
|
12
|
+
* observability-gated step can never be added.
|
|
13
|
+
*/
|
|
14
|
+
datadogConnectionRepository?: DatadogConnectionRepository;
|
|
15
|
+
}
|
|
16
|
+
/** Saved, reusable pipelines (the pipeline palette). */
|
|
17
|
+
export declare class PipelineService {
|
|
18
|
+
private readonly workspaceRepository;
|
|
19
|
+
private readonly pipelineRepository;
|
|
20
|
+
private readonly idGenerator;
|
|
21
|
+
private readonly datadogConnectionRepository?;
|
|
22
|
+
constructor({ workspaceRepository, pipelineRepository, idGenerator, datadogConnectionRepository, }: PipelineServiceDependencies);
|
|
23
|
+
/**
|
|
24
|
+
* The post-release-health gate is only meaningful with an observability integration, so
|
|
25
|
+
* reject a chain that includes an ENABLED post-release-health step unless the workspace
|
|
26
|
+
* has one wired. Validated only when the chain/enable mask is being authored (create, or
|
|
27
|
+
* an update that changes them) so an unrelated edit to an existing pipeline never trips.
|
|
28
|
+
*/
|
|
29
|
+
private assertObservabilityGatedStepAllowed;
|
|
30
|
+
private requireWorkspace;
|
|
31
|
+
list(workspaceId: string): Promise<Pipeline[]>;
|
|
32
|
+
create(workspaceId: string, input: CreatePipelineInput): Promise<Pipeline>;
|
|
33
|
+
/**
|
|
34
|
+
* Clone any pipeline (built-in or custom) into a new, editable copy. The copy keeps
|
|
35
|
+
* the source's steps / gates / thresholds / enable flags but is never `builtin`, so
|
|
36
|
+
* it can be edited — this is how a built-in template is "made editable".
|
|
37
|
+
*/
|
|
38
|
+
clone(workspaceId: string, sourceId: string, input: ClonePipelineInput): Promise<Pipeline>;
|
|
39
|
+
/**
|
|
40
|
+
* Edit a custom pipeline in place. Only the supplied fields change; passing
|
|
41
|
+
* `agentKinds` replaces the whole chain and re-aligns the parallel arrays. Built-in
|
|
42
|
+
* catalog templates are read-only and reject this — clone them first.
|
|
43
|
+
*/
|
|
44
|
+
update(workspaceId: string, id: string, input: UpdatePipelineInput): Promise<Pipeline>;
|
|
45
|
+
remove(workspaceId: string, id: string): Promise<void>;
|
|
46
|
+
/**
|
|
47
|
+
* Set a pipeline's organizational metadata (labels and/or archive state). This is the
|
|
48
|
+
* ONLY mutation allowed on a BUILT-IN pipeline — it touches the library view, not the
|
|
49
|
+
* pipeline's structure, so a built-in can be tagged or archived while staying read-only
|
|
50
|
+
* for its steps. Only the supplied fields change.
|
|
51
|
+
*/
|
|
52
|
+
organize(workspaceId: string, id: string, input: OrganizePipelineInput): Promise<Pipeline>;
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=PipelineService.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PipelineService.d.ts","sourceRoot":"","sources":["../../../src/modules/pipelines/PipelineService.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,kBAAkB,EAClB,mBAAmB,EACnB,qBAAqB,EACrB,mBAAmB,EACpB,MAAM,wBAAwB,CAAA;AAC/B,OAAO,KAAK,EAAuB,QAAQ,EAAc,MAAM,qBAAqB,CAAA;AAEpF,OAAO,KAAK,EACV,2BAA2B,EAC3B,kBAAkB,EAClB,mBAAmB,EACpB,MAAM,qBAAqB,CAAA;AAC5B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AAWtD,MAAM,WAAW,2BAA2B;IAC1C,mBAAmB,EAAE,mBAAmB,CAAA;IACxC,kBAAkB,EAAE,kBAAkB,CAAA;IACtC,WAAW,EAAE,WAAW,CAAA;IACxB;;;;OAIG;IACH,2BAA2B,CAAC,EAAE,2BAA2B,CAAA;CAC1D;AAED,wDAAwD;AACxD,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAqB;IACzD,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAoB;IACvD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAa;IACzC,OAAO,CAAC,QAAQ,CAAC,2BAA2B,CAAC,CAA6B;IAE1E,YAAY,EACV,mBAAmB,EACnB,kBAAkB,EAClB,WAAW,EACX,2BAA2B,GAC5B,EAAE,2BAA2B,EAK7B;IAED;;;;;OAKG;YACW,mCAAmC;IAiBjD,OAAO,CAAC,gBAAgB;IAIlB,IAAI,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CAGnD;IAEK,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAAC,QAAQ,CAAC,CAsB/E;IAED;;;;OAIG;IACG,KAAK,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,QAAQ,CAAC,CA6B/F;IAED;;;;OAIG;IACG,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAAC,QAAQ,CAAC,CAuC3F;IAEK,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAS3D;IAED;;;;;OAKG;IACG,QAAQ,CAAC,WAAW,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,qBAAqB,GAAG,OAAO,CAAC,QAAQ,CAAC,CAc/F;CACF"}
|