ai-stream-auditor 1.0.0 → 1.0.2

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 CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  All notable changes follow [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This project uses [Semantic Versioning](https://semver.org/).
4
4
 
5
+ ## [Unreleased]
6
+
7
+ ## [1.0.2] - 2026-10-10
8
+
9
+ ### Added
10
+
11
+ - Ship the runnable OpenAI Responses lifecycle example in the npm tarball.
12
+ - Execute the example as part of the aggregate package check.
13
+ - Document how lifecycle auditing composes with async-iterator deadlines.
14
+
15
+ ## [1.0.1] - 2026-10-06
16
+
17
+ ### Fixed
18
+
19
+ - Preserve structural error state independently of bounded diagnostic storage, so warning saturation cannot turn malformed streams into successful reports.
20
+ - Reject OpenAI content-part starts, deltas, and stops without an open parent output item.
21
+
5
22
  ## [1.0.0] - 2026-10-03
6
23
 
7
24
  ### Added
package/README.md CHANGED
@@ -142,6 +142,7 @@ This function deliberately accepts only synchronous iterables. Use `createStream
142
142
 
143
143
  - `response.created` lifecycle start
144
144
  - Output-item and content-part added/done pairing
145
+ - Content-part starts, deltas, and stops require their output item to remain open
145
146
  - Delta events targeting an open item or content part
146
147
  - Non-increasing `sequence_number` values when present
147
148
  - `response.completed`, `response.failed`, `response.incomplete`, `response.cancelled`, and `error` terminals
@@ -215,7 +216,9 @@ Provider protocols can add event types. The report remains successful if no stru
215
216
 
216
217
  ### Too many diagnostics
217
218
 
218
- `droppedDiagnostics` counts records beyond `maxDiagnostics`. The first bounded set is retained. Increase the limit only when your logging and memory budgets permit.
219
+ `droppedDiagnostics` counts records beyond `maxDiagnostics`. The first bounded set is retained. Errors still affect the final outcome even when their diagnostic records are dropped. Increase the limit only when your logging and memory budgets permit.
220
+
221
+ Content parts are checked against their parent output item's lifecycle, as defined in the [OpenAI streaming events reference](https://developers.openai.com/api/reference/resources/responses/streaming-events). Orphan content parts and content activity after the output item completes make a nominal completion invalid.
219
222
 
220
223
  ## Security and privacy
221
224
 
@@ -223,6 +226,17 @@ The auditor does not retain raw event payloads or generated text by default beca
223
226
 
224
227
  See [SECURITY.md](SECURITY.md) for vulnerability reporting.
225
228
 
229
+ ## Runnable example and related tool
230
+
231
+ [`examples/audit-openai.mjs`](examples/audit-openai.mjs) feeds a minimal valid
232
+ OpenAI Responses lifecycle through the auditor. It is executed by
233
+ `npm run check` and ships in the npm tarball.
234
+
235
+ Use [`async-iterator-watchdog`](https://www.npmjs.com/package/async-iterator-watchdog)
236
+ when you also need active first-progress, idle, or total deadlines around the
237
+ source iterator. The watchdog enforces time; this package validates protocol
238
+ lifecycle and outcome.
239
+
226
240
  ## Development
227
241
 
228
242
  ```sh
@@ -0,0 +1,14 @@
1
+ import { createStreamAuditor } from 'ai-stream-auditor';
2
+
3
+ const auditor = createStreamAuditor('openai-responses');
4
+
5
+ // Feed decoded SDK events as they arrive. The auditor does not parse SSE bytes.
6
+ for (const event of [
7
+ { type: 'response.created', sequence_number: 0 },
8
+ { type: 'response.completed', sequence_number: 1, response: { status: 'completed' } },
9
+ ]) {
10
+ auditor.push(event);
11
+ }
12
+
13
+ const report = auditor.finish({ cause: 'iterator_end' });
14
+ console.log(report);
package/package.json CHANGED
@@ -1,26 +1,55 @@
1
1
  {
2
2
  "name": "ai-stream-auditor",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Audit decoded OpenAI Responses and Anthropic Messages event lifecycles for silent or malformed stream endings.",
5
- "keywords": ["ai", "streaming", "openai", "anthropic", "llm", "diagnostics", "testing"],
5
+ "keywords": [
6
+ "ai",
7
+ "streaming",
8
+ "openai",
9
+ "anthropic",
10
+ "llm",
11
+ "diagnostics",
12
+ "testing"
13
+ ],
6
14
  "license": "MIT",
7
15
  "author": "Huzaifa Asif",
8
- "repository": { "type": "git", "url": "git+https://github.com/Huzaifa-Asif/ai-stream-auditor.git" },
9
- "bugs": { "url": "https://github.com/Huzaifa-Asif/ai-stream-auditor/issues" },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/Huzaifa-Asif/ai-stream-auditor.git"
19
+ },
20
+ "bugs": {
21
+ "url": "https://github.com/Huzaifa-Asif/ai-stream-auditor/issues"
22
+ },
10
23
  "homepage": "https://github.com/Huzaifa-Asif/ai-stream-auditor#readme",
11
24
  "type": "module",
12
25
  "main": "./src/index.cjs",
13
26
  "module": "./src/index.js",
14
27
  "types": "./src/index.d.ts",
15
- "exports": { ".": { "types": "./src/index.d.ts", "import": "./src/index.js", "require": "./src/index.cjs" } },
16
- "files": ["src", "README.md", "LICENSE", "CHANGELOG.md"],
17
- "engines": { "node": ">=18" },
28
+ "exports": {
29
+ ".": {
30
+ "types": "./src/index.d.ts",
31
+ "import": "./src/index.js",
32
+ "require": "./src/index.cjs"
33
+ }
34
+ },
35
+ "files": [
36
+ "src",
37
+ "examples",
38
+ "README.md",
39
+ "LICENSE",
40
+ "CHANGELOG.md"
41
+ ],
42
+ "engines": {
43
+ "node": ">=18"
44
+ },
18
45
  "sideEffects": false,
19
46
  "scripts": {
20
47
  "lint": "node scripts/lint.mjs",
21
48
  "test": "node --test",
22
49
  "test:coverage": "node --experimental-test-coverage --test",
23
- "check": "npm run lint && npm run test:coverage && npm pack --dry-run"
50
+ "check": "npm run lint && npm run test:coverage && node examples/audit-openai.mjs && npm pack --dry-run"
24
51
  },
25
- "publishConfig": { "access": "public" }
52
+ "publishConfig": {
53
+ "access": "public"
54
+ }
26
55
  }
package/src/index.cjs CHANGED
@@ -57,6 +57,7 @@ class StreamAuditor {
57
57
  this.eventCount = 0;
58
58
  this.counts = new Map();
59
59
  this.diagnostics = [];
60
+ this.hasErrors = false;
60
61
  this.droppedDiagnostics = 0;
61
62
  this.terminal = undefined;
62
63
  this.started = false;
@@ -69,6 +70,7 @@ class StreamAuditor {
69
70
  }
70
71
 
71
72
  _diagnostic(severity, code, index, type, details) {
73
+ if (severity === 'error') this.hasErrors = true;
72
74
  if (this.diagnostics.length >= this.limits.maxDiagnostics) {
73
75
  this.droppedDiagnostics += 1;
74
76
  return;
@@ -95,6 +97,13 @@ class StreamAuditor {
95
97
  }
96
98
  }
97
99
 
100
+ _requireOpenItem(event, index, type) {
101
+ const parent = `item:${event.output_index}`;
102
+ if (!this.openUnits.has(parent)) {
103
+ this._diagnostic('error', this.doneUnits.has(parent) ? 'content_after_item_done' : 'content_without_item', index, type, { parent });
104
+ }
105
+ }
106
+
98
107
  _done(key, index, type) {
99
108
  if (!this.openUnits.has(key)) {
100
109
  this._diagnostic('error', this.doneUnits.has(key) ? 'duplicate_done' : 'done_without_start', index, type, { unit: key });
@@ -167,14 +176,17 @@ class StreamAuditor {
167
176
  return;
168
177
  }
169
178
  if (type === 'response.content_part.added') {
179
+ this._requireOpenItem(event, index, type);
170
180
  this._open(`content:${event.output_index}:${event.content_index}`, index, type);
171
181
  return;
172
182
  }
173
183
  if (type === 'response.content_part.done') {
184
+ this._requireOpenItem(event, index, type);
174
185
  this._done(`content:${event.output_index}:${event.content_index}`, index, type);
175
186
  return;
176
187
  }
177
188
  if (/\.delta$/.test(type)) {
189
+ if (Number.isSafeInteger(event.content_index)) this._requireOpenItem(event, index, type);
178
190
  const key = Number.isSafeInteger(event.content_index)
179
191
  ? `content:${event.output_index}:${event.content_index}`
180
192
  : `item:${event.output_index}`;
@@ -245,9 +257,8 @@ class StreamAuditor {
245
257
  if (!this.started && !this.terminal && this.eventCount > 0) this._diagnostic('error', 'missing_start', this.eventCount);
246
258
  if (!this.terminal && !this.boundExceeded) this._diagnostic('error', 'missing_terminal', this.eventCount);
247
259
 
248
- const hasErrors = this.diagnostics.some(({ severity }) => severity === 'error');
249
260
  let outcome;
250
- if (this.boundExceeded || hasErrors && this.terminal && this.terminal.kind === 'completed') outcome = 'invalid';
261
+ if (this.boundExceeded || this.hasErrors && this.terminal && this.terminal.kind === 'completed') outcome = 'invalid';
251
262
  else if (this.terminal) outcome = this.terminal.kind === 'completed' && this.dialect === 'anthropic-messages' && this.stopReason === 'tool_use'
252
263
  ? 'tool_use'
253
264
  : this.terminal.kind === 'completed' && this.dialect === 'anthropic-messages' && this.stopReason === 'pause_turn'