ai-contract-observer 0.1.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/README.md +253 -0
- package/package.json +38 -0
- package/src/index.js +3 -0
- package/src/logger.js +46 -0
- package/src/observer.js +100 -0
- package/src/summary.js +49 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Brandon Himpfen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
# ai-contract-observer
|
|
2
|
+
|
|
3
|
+
A lightweight observability layer for AI systems.
|
|
4
|
+
|
|
5
|
+
This project solves a common problem in AI applications: even when requests and responses are structured, teams still struggle to understand what actually happened during a model interaction. Inputs are hard to trace, failures are difficult to diagnose, and system behavior becomes harder to improve over time.
|
|
6
|
+
|
|
7
|
+
`ai-contract-observer` provides a small, practical logging and summary layer for AI interactions. It is designed to work especially well with `ai-contract-kit` but it can also be used with any structured request and response objects.
|
|
8
|
+
|
|
9
|
+
It can be used as both a reference implementation and a lightweight standard for observing AI interactions.
|
|
10
|
+
|
|
11
|
+
## Why this project exists
|
|
12
|
+
|
|
13
|
+
Most AI systems are still hard to inspect.
|
|
14
|
+
|
|
15
|
+
- One request succeeds but no one knows why.
|
|
16
|
+
- Another fails and the error is only visible in application logs.
|
|
17
|
+
- A third returns an uncertain output, but the system records it as a generic success.
|
|
18
|
+
- A fourth performs poorly over time, but there is no consistent way to measure behavior across runs.
|
|
19
|
+
|
|
20
|
+
The result is weak traceability, poor debugging, and limited operational visibility.
|
|
21
|
+
|
|
22
|
+
This package defines a simple observation layer that sits around the model interaction boundary.
|
|
23
|
+
|
|
24
|
+
## Mental model
|
|
25
|
+
|
|
26
|
+
Think of the package as a small observability boundary:
|
|
27
|
+
|
|
28
|
+
`App -> Request Contract -> Model -> Response Contract -> Observation Layer`
|
|
29
|
+
|
|
30
|
+
The observer sits beside the application flow and records what happened in a consistent format.
|
|
31
|
+
|
|
32
|
+
This does not replace model SDKs, tracing platforms, or application logging.
|
|
33
|
+
|
|
34
|
+
It standardizes how AI interactions are logged, summarized, and inspected.
|
|
35
|
+
|
|
36
|
+
## What is included
|
|
37
|
+
|
|
38
|
+
- Observer instance for recording AI interactions.
|
|
39
|
+
- Normalized event entries with IDs, timestamps, and durations.
|
|
40
|
+
- Console logger for development visibility.
|
|
41
|
+
- JSON logger for machine-readable output.
|
|
42
|
+
- Summary helpers for status counts and success rates.
|
|
43
|
+
- Example usage demonstrating real-world integration.
|
|
44
|
+
- Test coverage for core observer behavior.
|
|
45
|
+
|
|
46
|
+
## Install
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npm install ai-contract-observer
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Example
|
|
53
|
+
|
|
54
|
+
```js
|
|
55
|
+
import { createObserver } from "ai-contract-observer";
|
|
56
|
+
|
|
57
|
+
const observer = createObserver();
|
|
58
|
+
|
|
59
|
+
const request = {
|
|
60
|
+
version: "1.0",
|
|
61
|
+
task: "summarization",
|
|
62
|
+
prompt: "Summarize this text in 3 sentences."
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const response = {
|
|
66
|
+
version: "1.0",
|
|
67
|
+
status: "success",
|
|
68
|
+
output: {
|
|
69
|
+
text: "This is the summary."
|
|
70
|
+
},
|
|
71
|
+
meta: {
|
|
72
|
+
model: "example-model-v1"
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
observer.observe({
|
|
77
|
+
request,
|
|
78
|
+
response,
|
|
79
|
+
durationMs: 842
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
console.log(observer.getSummary());
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Event structure
|
|
86
|
+
|
|
87
|
+
An observation entry looks like this:
|
|
88
|
+
|
|
89
|
+
```json
|
|
90
|
+
{
|
|
91
|
+
"id": "obs_000001",
|
|
92
|
+
"timestamp": "2026-03-22T00:00:00.000Z",
|
|
93
|
+
"durationMs": 842,
|
|
94
|
+
"request": {
|
|
95
|
+
"version": "1.0",
|
|
96
|
+
"task": "summarization",
|
|
97
|
+
"prompt": "Summarize this text in 3 sentences."
|
|
98
|
+
},
|
|
99
|
+
"response": {
|
|
100
|
+
"version": "1.0",
|
|
101
|
+
"status": "success",
|
|
102
|
+
"output": {
|
|
103
|
+
"text": "This is the summary."
|
|
104
|
+
},
|
|
105
|
+
"meta": {
|
|
106
|
+
"model": "example-model-v1"
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Status summary
|
|
113
|
+
|
|
114
|
+
The observer tracks the following response statuses:
|
|
115
|
+
|
|
116
|
+
- `success`
|
|
117
|
+
- `uncertain`
|
|
118
|
+
- `refusal`
|
|
119
|
+
- `error`
|
|
120
|
+
- `unknown`
|
|
121
|
+
|
|
122
|
+
This makes it easier to distinguish between valid results, low-confidence outputs, refusals, system errors, and malformed responses.
|
|
123
|
+
|
|
124
|
+
## Quick wrapper example
|
|
125
|
+
|
|
126
|
+
```js
|
|
127
|
+
import { createObserver } from "ai-contract-observer";
|
|
128
|
+
|
|
129
|
+
async function runModel(modelClient, request) {
|
|
130
|
+
const observer = createObserver();
|
|
131
|
+
|
|
132
|
+
const start = Date.now();
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
const raw = await modelClient.generate(request);
|
|
136
|
+
|
|
137
|
+
const response = {
|
|
138
|
+
version: "1.0",
|
|
139
|
+
status: raw?.status ?? "unknown",
|
|
140
|
+
output: {
|
|
141
|
+
text: raw?.text ?? null,
|
|
142
|
+
structured: raw?.structured ?? null
|
|
143
|
+
},
|
|
144
|
+
meta: {
|
|
145
|
+
model: raw?.model ?? "unknown"
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const entry = observer.observe({
|
|
150
|
+
request,
|
|
151
|
+
response,
|
|
152
|
+
durationMs: Date.now() - start
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
return { response, entry };
|
|
156
|
+
} catch (error) {
|
|
157
|
+
const response = {
|
|
158
|
+
version: "1.0",
|
|
159
|
+
status: "error",
|
|
160
|
+
output: {
|
|
161
|
+
text: null,
|
|
162
|
+
structured: null
|
|
163
|
+
},
|
|
164
|
+
issues: [
|
|
165
|
+
{
|
|
166
|
+
type: "exception",
|
|
167
|
+
message: error instanceof Error ? error.message : "Unknown error"
|
|
168
|
+
}
|
|
169
|
+
],
|
|
170
|
+
meta: {
|
|
171
|
+
model: "unknown"
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const entry = observer.observe({
|
|
176
|
+
request,
|
|
177
|
+
response,
|
|
178
|
+
durationMs: Date.now() - start
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
return { response, entry };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## API
|
|
187
|
+
|
|
188
|
+
### `createObserver(options?)`
|
|
189
|
+
|
|
190
|
+
Creates a new observer instance.
|
|
191
|
+
|
|
192
|
+
Supported options:
|
|
193
|
+
|
|
194
|
+
- `logger`: an object with a `log(entry)` function.
|
|
195
|
+
- `idPrefix`: string prefix for generated observation IDs.
|
|
196
|
+
- `startSequence`: starting integer for generated IDs.
|
|
197
|
+
|
|
198
|
+
### `observer.observe({ request, response, durationMs, timestamp? })`
|
|
199
|
+
|
|
200
|
+
Records a single AI interaction and returns the normalized observation entry.
|
|
201
|
+
|
|
202
|
+
### `observer.getEntries()`
|
|
203
|
+
|
|
204
|
+
Returns a copy of all recorded entries.
|
|
205
|
+
|
|
206
|
+
### `observer.getSummary()`
|
|
207
|
+
|
|
208
|
+
Returns aggregate counts and derived metrics across recorded entries.
|
|
209
|
+
|
|
210
|
+
### `observer.clear()`
|
|
211
|
+
|
|
212
|
+
Removes all stored entries from the current observer instance.
|
|
213
|
+
|
|
214
|
+
## Design Principles
|
|
215
|
+
|
|
216
|
+
This project is intentionally minimal.
|
|
217
|
+
|
|
218
|
+
It defines a small, explicit observability layer rather than a full telemetry platform. The goal is to provide a stable and understandable boundary for recording AI interactions without introducing unnecessary complexity.
|
|
219
|
+
|
|
220
|
+
The design emphasizes:
|
|
221
|
+
|
|
222
|
+
- Simplicity over abstraction
|
|
223
|
+
- Explicit event structure over implicit logging
|
|
224
|
+
- Reliability over feature sprawl
|
|
225
|
+
- Composability over completeness
|
|
226
|
+
|
|
227
|
+
This allows the observer to work across different models, tools, and systems while remaining easy to adopt.
|
|
228
|
+
|
|
229
|
+
## Non-Goals
|
|
230
|
+
|
|
231
|
+
This project does not attempt to:
|
|
232
|
+
|
|
233
|
+
- Replace model SDKs or providers.
|
|
234
|
+
- Replace full observability platforms.
|
|
235
|
+
- Provide dashboards or hosted telemetry infrastructure.
|
|
236
|
+
- Enforce a specific prompt strategy or orchestration framework.
|
|
237
|
+
|
|
238
|
+
It focuses only on defining a consistent observation layer for AI interactions.
|
|
239
|
+
|
|
240
|
+
## Roadmap
|
|
241
|
+
|
|
242
|
+
This project is designed as a foundation for more observable AI systems. Future extensions may include:
|
|
243
|
+
|
|
244
|
+
- File persistence adapters.
|
|
245
|
+
- Streaming event hooks.
|
|
246
|
+
- OpenTelemetry-compatible emitters.
|
|
247
|
+
- Dashboard and reporting utilities.
|
|
248
|
+
- Evaluation pipeline integration.
|
|
249
|
+
- Trace correlation across multi-step AI workflows.
|
|
250
|
+
|
|
251
|
+
## License
|
|
252
|
+
|
|
253
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ai-contract-observer",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A lightweight observability layer for AI systems with structured request and response logging.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"test": "node --test",
|
|
15
|
+
"example": "node examples/basic-usage.js"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"ai",
|
|
19
|
+
"llm",
|
|
20
|
+
"observability",
|
|
21
|
+
"logging",
|
|
22
|
+
"tracing",
|
|
23
|
+
"ai systems",
|
|
24
|
+
"ai infrastructure",
|
|
25
|
+
"structured outputs",
|
|
26
|
+
"inference"
|
|
27
|
+
],
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/brandonhimpfen/ai-contract-observer.git"
|
|
31
|
+
},
|
|
32
|
+
"bugs": {
|
|
33
|
+
"url": "https://github.com/brandonhimpfen/ai-contract-observer/issues"
|
|
34
|
+
},
|
|
35
|
+
"homepage": "https://github.com/brandonhimpfen/ai-contract-observer#readme",
|
|
36
|
+
"author": "Brandon Himpfen",
|
|
37
|
+
"license": "MIT"
|
|
38
|
+
}
|
package/src/index.js
ADDED
package/src/logger.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
function writeLine(writer, line) {
|
|
2
|
+
if (typeof writer === "function") {
|
|
3
|
+
writer(line);
|
|
4
|
+
}
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function createConsoleLogger(options = {}) {
|
|
8
|
+
const {
|
|
9
|
+
enabled = true,
|
|
10
|
+
writer = console.log
|
|
11
|
+
} = options;
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
log(entry) {
|
|
15
|
+
if (!enabled) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const status = entry?.response?.status ?? "unknown";
|
|
20
|
+
const task = entry?.request?.task ?? "unknown";
|
|
21
|
+
const duration = entry?.durationMs ?? "n/a";
|
|
22
|
+
|
|
23
|
+
writeLine(
|
|
24
|
+
writer,
|
|
25
|
+
`[ai-contract-observer] ${entry.id} status=${status} task=${task} durationMs=${duration}`
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function createJsonLogger(options = {}) {
|
|
32
|
+
const {
|
|
33
|
+
writer = console.log,
|
|
34
|
+
pretty = false
|
|
35
|
+
} = options;
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
log(entry) {
|
|
39
|
+
const json = pretty
|
|
40
|
+
? JSON.stringify(entry, null, 2)
|
|
41
|
+
: JSON.stringify(entry);
|
|
42
|
+
|
|
43
|
+
writeLine(writer, json);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
package/src/observer.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { createConsoleLogger } from "./logger.js";
|
|
2
|
+
import { buildSummary, normalizeStatus } from "./summary.js";
|
|
3
|
+
|
|
4
|
+
function formatSequence(sequence) {
|
|
5
|
+
return String(sequence).padStart(6, "0");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function clone(value) {
|
|
9
|
+
return value == null ? value : JSON.parse(JSON.stringify(value));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function normalizeTimestamp(timestamp) {
|
|
13
|
+
const date = timestamp ? new Date(timestamp) : new Date();
|
|
14
|
+
|
|
15
|
+
if (Number.isNaN(date.getTime())) {
|
|
16
|
+
throw new TypeError("timestamp must be a valid date or ISO string");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return date.toISOString();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function normalizeDuration(durationMs) {
|
|
23
|
+
if (durationMs == null) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!Number.isFinite(durationMs) || durationMs < 0) {
|
|
28
|
+
throw new TypeError("durationMs must be a non-negative finite number");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return durationMs;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function createEntry({ id, timestamp, durationMs, request, response }) {
|
|
35
|
+
return {
|
|
36
|
+
id,
|
|
37
|
+
timestamp,
|
|
38
|
+
durationMs,
|
|
39
|
+
request: clone(request) ?? null,
|
|
40
|
+
response: clone(response) ?? null
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function createObserver(options = {}) {
|
|
45
|
+
const {
|
|
46
|
+
logger = createConsoleLogger({ enabled: false }),
|
|
47
|
+
idPrefix = "obs",
|
|
48
|
+
startSequence = 1
|
|
49
|
+
} = options;
|
|
50
|
+
|
|
51
|
+
if (!logger || typeof logger.log !== "function") {
|
|
52
|
+
throw new TypeError("logger must be an object with a log(entry) function");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!Number.isInteger(startSequence) || startSequence < 1) {
|
|
56
|
+
throw new TypeError("startSequence must be a positive integer");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let sequence = startSequence;
|
|
60
|
+
const entries = [];
|
|
61
|
+
|
|
62
|
+
function observe({ request = null, response = null, durationMs = null, timestamp } = {}) {
|
|
63
|
+
const id = `${idPrefix}_${formatSequence(sequence++)}`;
|
|
64
|
+
const normalizedTimestamp = normalizeTimestamp(timestamp);
|
|
65
|
+
const normalizedDuration = normalizeDuration(durationMs);
|
|
66
|
+
|
|
67
|
+
const entry = createEntry({
|
|
68
|
+
id,
|
|
69
|
+
timestamp: normalizedTimestamp,
|
|
70
|
+
durationMs: normalizedDuration,
|
|
71
|
+
request,
|
|
72
|
+
response
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
logger.log(entry);
|
|
76
|
+
entries.push(entry);
|
|
77
|
+
|
|
78
|
+
return clone(entry);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function getEntries() {
|
|
82
|
+
return clone(entries);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function getSummary() {
|
|
86
|
+
return buildSummary(entries);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function clear() {
|
|
90
|
+
entries.length = 0;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
observe,
|
|
95
|
+
getEntries,
|
|
96
|
+
getSummary,
|
|
97
|
+
clear,
|
|
98
|
+
normalizeStatus
|
|
99
|
+
};
|
|
100
|
+
}
|
package/src/summary.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const KNOWN_STATUSES = new Set(["success", "uncertain", "refusal", "error"]);
|
|
2
|
+
|
|
3
|
+
export function normalizeStatus(status) {
|
|
4
|
+
if (typeof status !== "string") {
|
|
5
|
+
return "unknown";
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const value = status.trim().toLowerCase();
|
|
9
|
+
return KNOWN_STATUSES.has(value) ? value : "unknown";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function buildSummary(entries = []) {
|
|
13
|
+
const summary = {
|
|
14
|
+
total: 0,
|
|
15
|
+
success: 0,
|
|
16
|
+
uncertain: 0,
|
|
17
|
+
refusal: 0,
|
|
18
|
+
error: 0,
|
|
19
|
+
unknown: 0,
|
|
20
|
+
successRate: 0,
|
|
21
|
+
averageDurationMs: null
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
let durationTotal = 0;
|
|
25
|
+
let durationCount = 0;
|
|
26
|
+
|
|
27
|
+
for (const entry of entries) {
|
|
28
|
+
summary.total += 1;
|
|
29
|
+
|
|
30
|
+
const status = normalizeStatus(entry?.response?.status);
|
|
31
|
+
summary[status] += 1;
|
|
32
|
+
|
|
33
|
+
const duration = entry?.durationMs;
|
|
34
|
+
if (Number.isFinite(duration)) {
|
|
35
|
+
durationTotal += duration;
|
|
36
|
+
durationCount += 1;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (summary.total > 0) {
|
|
41
|
+
summary.successRate = Number((summary.success / summary.total).toFixed(4));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (durationCount > 0) {
|
|
45
|
+
summary.averageDurationMs = Number((durationTotal / durationCount).toFixed(2));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return summary;
|
|
49
|
+
}
|