@kuralle-syrinx/aisdk 4.2.0 → 4.3.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/package.json +3 -3
- package/src/from-ai-sdk.ts +40 -1
- package/src/index.ts +144 -1
- package/src/from-ai-sdk.test.ts +0 -276
- package/src/heard-prefix-commit.test.ts +0 -291
- package/src/index.test.ts +0 -989
- package/src/speculative-on-ledger.test.ts +0 -211
- package/src/speculative-post-promotion.test.ts +0 -86
- package/tsconfig.json +0 -13
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: MIT
|
|
2
|
-
|
|
3
|
-
import { describe, expect, it } from "vitest";
|
|
4
|
-
import {
|
|
5
|
-
PipelineBusImpl,
|
|
6
|
-
Route,
|
|
7
|
-
type EndOfSpeechPacket,
|
|
8
|
-
type InMemoryIuLedger,
|
|
9
|
-
type IncrementalUnitId,
|
|
10
|
-
} from "@kuralle-syrinx/core";
|
|
11
|
-
import type { FinishReason, TextStreamPart, ToolSet } from "ai";
|
|
12
|
-
import { fromStreamFactory } from "./from-ai-sdk.js";
|
|
13
|
-
import { ReasoningBridge } from "./index.js";
|
|
14
|
-
|
|
15
|
-
const ZERO_USAGE = {
|
|
16
|
-
inputTokens: 0,
|
|
17
|
-
inputTokenDetails: { noCacheTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 },
|
|
18
|
-
outputTokens: 0,
|
|
19
|
-
outputTokenDetails: { textTokens: 0, reasoningTokens: 0 },
|
|
20
|
-
totalTokens: 0,
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
type BridgeLedgerAccess = {
|
|
24
|
-
iuLedger: InMemoryIuLedger;
|
|
25
|
-
speculativeDraft: { id: IncrementalUnitId } | null;
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
function bridgeLedger(plugin: ReasoningBridge): BridgeLedgerAccess {
|
|
29
|
-
return plugin as unknown as BridgeLedgerAccess;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
describe("ReasoningBridge speculative-on-ledger", () => {
|
|
33
|
-
it("eos.interim adds a hypothesized IU and matching eos.turn_complete commits it", async () => {
|
|
34
|
-
const plugin = new ReasoningBridge(
|
|
35
|
-
fromStreamFactory(async function* () {
|
|
36
|
-
yield textDelta("Answer.");
|
|
37
|
-
yield finish("stop");
|
|
38
|
-
}),
|
|
39
|
-
{ speculative: true },
|
|
40
|
-
);
|
|
41
|
-
const bus = new PipelineBusImpl({ onPacket: () => {} });
|
|
42
|
-
const drain = bus.start();
|
|
43
|
-
await plugin.initialize(bus, baseConfig());
|
|
44
|
-
const ledger = bridgeLedger(plugin);
|
|
45
|
-
|
|
46
|
-
bus.push(Route.Main, eosInterim("turn-1", "hello"));
|
|
47
|
-
await new Promise((resolve) => setTimeout(resolve, 30));
|
|
48
|
-
|
|
49
|
-
const draftId = ledger.speculativeDraft?.id;
|
|
50
|
-
expect(draftId).toEqual({ contextId: "turn-1", iuId: "turn-1", epoch: 1 });
|
|
51
|
-
expect(ledger.iuLedger.get(draftId!)?.state).toBe("hypothesized");
|
|
52
|
-
|
|
53
|
-
bus.push(Route.Main, turnComplete("turn-1", "hello"));
|
|
54
|
-
await new Promise((resolve) => setTimeout(resolve, 30));
|
|
55
|
-
|
|
56
|
-
expect(ledger.iuLedger.get(draftId!)?.state).toBe("committed");
|
|
57
|
-
bus.stop();
|
|
58
|
-
await drain;
|
|
59
|
-
await plugin.close();
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it("eos.retracted revokes the hypothesized IU", async () => {
|
|
63
|
-
const plugin = new ReasoningBridge(
|
|
64
|
-
fromStreamFactory(async function* () {
|
|
65
|
-
yield textDelta("Draft.");
|
|
66
|
-
yield finish("stop");
|
|
67
|
-
}),
|
|
68
|
-
{ speculative: true },
|
|
69
|
-
);
|
|
70
|
-
const bus = new PipelineBusImpl({ onPacket: () => {} });
|
|
71
|
-
const drain = bus.start();
|
|
72
|
-
await plugin.initialize(bus, baseConfig());
|
|
73
|
-
const ledger = bridgeLedger(plugin);
|
|
74
|
-
|
|
75
|
-
bus.push(Route.Main, eosInterim("turn-1", "book a"));
|
|
76
|
-
await new Promise((resolve) => setTimeout(resolve, 30));
|
|
77
|
-
const draftId = ledger.speculativeDraft?.id;
|
|
78
|
-
expect(ledger.iuLedger.get(draftId!)?.state).toBe("hypothesized");
|
|
79
|
-
|
|
80
|
-
bus.push(Route.Main, eosRetracted("turn-1"));
|
|
81
|
-
await new Promise((resolve) => setTimeout(resolve, 30));
|
|
82
|
-
|
|
83
|
-
expect(ledger.iuLedger.get(draftId!)?.state).toBe("revoked");
|
|
84
|
-
bus.stop();
|
|
85
|
-
await drain;
|
|
86
|
-
await plugin.close();
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it("double-commit fires an iu_ledger anomaly on the bus", async () => {
|
|
90
|
-
const packets: Array<{ route: Route; packet: unknown }> = [];
|
|
91
|
-
const plugin = new ReasoningBridge(
|
|
92
|
-
fromStreamFactory(async function* () {
|
|
93
|
-
yield textDelta("Answer.");
|
|
94
|
-
yield finish("stop");
|
|
95
|
-
}),
|
|
96
|
-
{ speculative: true },
|
|
97
|
-
);
|
|
98
|
-
const bus = new PipelineBusImpl({ onPacket: (route, packet) => packets.push({ route, packet }) });
|
|
99
|
-
const drain = bus.start();
|
|
100
|
-
await plugin.initialize(bus, baseConfig());
|
|
101
|
-
const ledger = bridgeLedger(plugin);
|
|
102
|
-
|
|
103
|
-
bus.push(Route.Main, eosInterim("turn-1", "hello"));
|
|
104
|
-
await new Promise((resolve) => setTimeout(resolve, 30));
|
|
105
|
-
const draftId = ledger.speculativeDraft?.id;
|
|
106
|
-
expect(draftId).toBeDefined();
|
|
107
|
-
|
|
108
|
-
bus.push(Route.Main, turnComplete("turn-1", "hello"));
|
|
109
|
-
await new Promise((resolve) => setTimeout(resolve, 30));
|
|
110
|
-
expect(ledger.iuLedger.get(draftId!)?.state).toBe("committed");
|
|
111
|
-
|
|
112
|
-
ledger.iuLedger.commit(draftId!);
|
|
113
|
-
await waitFor(() =>
|
|
114
|
-
packets.some(
|
|
115
|
-
({ packet }) =>
|
|
116
|
-
(packet as { kind?: string; component?: string }).kind === "llm.error" &&
|
|
117
|
-
(packet as { component?: string }).component === "iu_ledger",
|
|
118
|
-
),
|
|
119
|
-
);
|
|
120
|
-
|
|
121
|
-
expect(packets).toContainEqual({
|
|
122
|
-
route: Route.Background,
|
|
123
|
-
packet: expect.objectContaining({
|
|
124
|
-
kind: "llm.error",
|
|
125
|
-
component: "iu_ledger",
|
|
126
|
-
contextId: "turn-1",
|
|
127
|
-
isRecoverable: true,
|
|
128
|
-
}),
|
|
129
|
-
});
|
|
130
|
-
bus.stop();
|
|
131
|
-
await drain;
|
|
132
|
-
await plugin.close();
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it("assigns monotonic epoch across distinct contextIds", async () => {
|
|
136
|
-
const plugin = new ReasoningBridge(
|
|
137
|
-
fromStreamFactory(async function* () {
|
|
138
|
-
yield textDelta("Answer.");
|
|
139
|
-
yield finish("stop");
|
|
140
|
-
}),
|
|
141
|
-
{ speculative: true },
|
|
142
|
-
);
|
|
143
|
-
const bus = new PipelineBusImpl({ onPacket: () => {} });
|
|
144
|
-
const drain = bus.start();
|
|
145
|
-
await plugin.initialize(bus, baseConfig());
|
|
146
|
-
const ledger = bridgeLedger(plugin);
|
|
147
|
-
|
|
148
|
-
bus.push(Route.Main, eosInterim("ctx-a", "first"));
|
|
149
|
-
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
150
|
-
const idA = ledger.speculativeDraft?.id;
|
|
151
|
-
bus.push(Route.Main, turnComplete("ctx-a", "first"));
|
|
152
|
-
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
153
|
-
|
|
154
|
-
bus.push(Route.Main, eosInterim("ctx-b", "second"));
|
|
155
|
-
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
156
|
-
const idB = ledger.speculativeDraft?.id;
|
|
157
|
-
|
|
158
|
-
expect(idA?.epoch).toBe(1);
|
|
159
|
-
expect(idB?.epoch).toBe(2);
|
|
160
|
-
expect(idB!.epoch).toBeGreaterThan(idA!.epoch);
|
|
161
|
-
bus.stop();
|
|
162
|
-
await drain;
|
|
163
|
-
await plugin.close();
|
|
164
|
-
});
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
function baseConfig(): Record<string, unknown> {
|
|
168
|
-
return {
|
|
169
|
-
api_key: "test-key",
|
|
170
|
-
model: "gpt-test",
|
|
171
|
-
system_prompt: "test",
|
|
172
|
-
retry_max_attempts: 1,
|
|
173
|
-
timeout_ms: 1000,
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function turnComplete(contextId: string, text: string): EndOfSpeechPacket {
|
|
178
|
-
return { kind: "eos.turn_complete", contextId, timestampMs: Date.now(), text, transcripts: [] };
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function eosInterim(contextId: string, text: string): { kind: "eos.interim"; contextId: string; timestampMs: number; text: string } {
|
|
182
|
-
return { kind: "eos.interim", contextId, timestampMs: Date.now(), text };
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function eosRetracted(contextId: string): { kind: "eos.retracted"; contextId: string; timestampMs: number } {
|
|
186
|
-
return { kind: "eos.retracted", contextId, timestampMs: Date.now() };
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function textDelta(text: string): TextStreamPart<ToolSet> {
|
|
190
|
-
return { type: "text-delta", id: "0", text, providerMetadata: undefined } as TextStreamPart<ToolSet>;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function finish(finishReason: FinishReason, rawFinishReason?: string): TextStreamPart<ToolSet> {
|
|
194
|
-
return {
|
|
195
|
-
type: "finish",
|
|
196
|
-
finishReason,
|
|
197
|
-
rawFinishReason,
|
|
198
|
-
totalUsage: ZERO_USAGE,
|
|
199
|
-
usage: ZERO_USAGE,
|
|
200
|
-
providerMetadata: undefined,
|
|
201
|
-
response: {},
|
|
202
|
-
} as TextStreamPart<ToolSet>;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
async function waitFor(predicate: () => boolean): Promise<void> {
|
|
206
|
-
const started = Date.now();
|
|
207
|
-
while (!predicate()) {
|
|
208
|
-
if (Date.now() - started > 1000) throw new Error("Timed out waiting for packet");
|
|
209
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
210
|
-
}
|
|
211
|
-
}
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: MIT
|
|
2
|
-
//
|
|
3
|
-
// Regression guard (S1-01, RFC C2): when a speculative draft is promoted MID-STREAM,
|
|
4
|
-
// deltas produced AFTER the promotion must go live, not stay buffered. The speculative
|
|
5
|
-
// side-effect gate must re-check commit state per push — hoisting it to a single value
|
|
6
|
-
// captured at generation start loses the post-promotion tail (incl. llm.done).
|
|
7
|
-
|
|
8
|
-
import { describe, expect, it } from "vitest";
|
|
9
|
-
import { PipelineBusImpl, Route } from "@kuralle-syrinx/core";
|
|
10
|
-
import type { EndOfSpeechPacket } from "@kuralle-syrinx/core";
|
|
11
|
-
import type { FinishReason, TextStreamPart, ToolSet } from "ai";
|
|
12
|
-
import { fromStreamFactory } from "./from-ai-sdk.js";
|
|
13
|
-
import { ReasoningBridge } from "./index.js";
|
|
14
|
-
|
|
15
|
-
const ZERO_USAGE = {
|
|
16
|
-
inputTokens: 0,
|
|
17
|
-
inputTokenDetails: { noCacheTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 },
|
|
18
|
-
outputTokens: 0,
|
|
19
|
-
outputTokenDetails: { reasoningTokens: 0 },
|
|
20
|
-
totalTokens: 0,
|
|
21
|
-
reasoningTokens: 0,
|
|
22
|
-
cachedInputTokens: 0,
|
|
23
|
-
} as unknown as Record<string, unknown>;
|
|
24
|
-
|
|
25
|
-
function textDelta(text: string): TextStreamPart<ToolSet> {
|
|
26
|
-
return { type: "text-delta", id: "0", text, providerMetadata: undefined } as TextStreamPart<ToolSet>;
|
|
27
|
-
}
|
|
28
|
-
function finish(finishReason: FinishReason): TextStreamPart<ToolSet> {
|
|
29
|
-
return { type: "finish", finishReason, totalUsage: ZERO_USAGE, usage: ZERO_USAGE, providerMetadata: undefined, response: {} } as unknown as TextStreamPart<ToolSet>;
|
|
30
|
-
}
|
|
31
|
-
function eosInterim(contextId: string, text: string) {
|
|
32
|
-
return { kind: "eos.interim" as const, contextId, timestampMs: Date.now(), text };
|
|
33
|
-
}
|
|
34
|
-
function turnComplete(contextId: string, text: string): EndOfSpeechPacket {
|
|
35
|
-
return { kind: "eos.turn_complete", contextId, timestampMs: Date.now(), text, transcripts: [] };
|
|
36
|
-
}
|
|
37
|
-
function baseConfig(): Record<string, unknown> {
|
|
38
|
-
return { api_key: "test-key", model: "gpt-test", system_prompt: "test", retry_max_attempts: 1, timeout_ms: 1000 };
|
|
39
|
-
}
|
|
40
|
-
async function waitFor(predicate: () => boolean): Promise<void> {
|
|
41
|
-
for (let i = 0; i < 200; i++) {
|
|
42
|
-
if (predicate()) return;
|
|
43
|
-
await new Promise((r) => setTimeout(r, 10));
|
|
44
|
-
}
|
|
45
|
-
throw new Error("waitFor timed out");
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
describe("S1 repro — post-promotion streaming", () => {
|
|
49
|
-
it("a delta streamed AFTER a mid-stream promotion must still reach the bus", async () => {
|
|
50
|
-
const packets: Array<{ route: Route; packet: unknown }> = [];
|
|
51
|
-
let release!: () => void;
|
|
52
|
-
const gate = new Promise<void>((r) => { release = r; });
|
|
53
|
-
|
|
54
|
-
const plugin = new ReasoningBridge(
|
|
55
|
-
fromStreamFactory(async function* () {
|
|
56
|
-
yield textDelta("first "); // buffered while hypothesized
|
|
57
|
-
await gate; // still streaming — pause BEFORE promotion completes
|
|
58
|
-
yield textDelta("second"); // streamed AFTER promotion
|
|
59
|
-
yield finish("stop");
|
|
60
|
-
}),
|
|
61
|
-
{ speculative: true },
|
|
62
|
-
);
|
|
63
|
-
const bus = new PipelineBusImpl({ onPacket: (route, packet) => packets.push({ route, packet }) });
|
|
64
|
-
const drain = bus.start();
|
|
65
|
-
await plugin.initialize(bus, baseConfig());
|
|
66
|
-
|
|
67
|
-
bus.push(Route.Main, eosInterim("turn-1", "hello there"));
|
|
68
|
-
await new Promise((r) => setTimeout(r, 50)); // "first " buffered; generator paused at gate
|
|
69
|
-
|
|
70
|
-
bus.push(Route.Main, turnComplete("turn-1", "hello there")); // promote (commit) mid-stream
|
|
71
|
-
await new Promise((r) => setTimeout(r, 20));
|
|
72
|
-
release(); // now let the generator stream the post-promotion delta
|
|
73
|
-
|
|
74
|
-
await waitFor(() => packets.some((p) => (p.packet as { kind: string }).kind === "llm.done"));
|
|
75
|
-
bus.stop();
|
|
76
|
-
await drain;
|
|
77
|
-
await plugin.close();
|
|
78
|
-
|
|
79
|
-
const deltaTexts = packets
|
|
80
|
-
.filter((p) => (p.packet as { kind: string }).kind === "llm.delta")
|
|
81
|
-
.map((p) => (p.packet as { text: string }).text);
|
|
82
|
-
|
|
83
|
-
// The post-promotion "second" delta MUST have reached the bus.
|
|
84
|
-
expect(deltaTexts).toContain("second");
|
|
85
|
-
});
|
|
86
|
-
});
|
package/tsconfig.json
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2022",
|
|
4
|
-
"module": "NodeNext",
|
|
5
|
-
"moduleResolution": "NodeNext",
|
|
6
|
-
"strict": true,
|
|
7
|
-
"esModuleInterop": true,
|
|
8
|
-
"skipLibCheck": true,
|
|
9
|
-
"types": ["vitest/globals"],
|
|
10
|
-
"noEmit": true
|
|
11
|
-
},
|
|
12
|
-
"include": ["src/**/*.ts"]
|
|
13
|
-
}
|