stitchkit 0.95.1 → 0.95.3
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/agent-runtime/runtime-internals.d.ts +6 -0
- package/dist/agent-runtime/runtime-internals.d.ts.map +1 -1
- package/dist/entrypoints/agent-runtime/harness.js +1 -1
- package/dist/entrypoints/agent-runtime.js +1 -1
- package/dist/entrypoints/live.js +6 -1
- package/dist/entrypoints/voice.d.ts +6 -0
- package/dist/entrypoints/voice.d.ts.map +1 -0
- package/dist/entrypoints/voice.js +221 -0
- package/dist/{index-8zja4pkf.js → index-nts4a2br.js} +5 -1
- package/dist/live/watch-client.d.ts.map +1 -1
- package/dist/voice/phases.d.ts +4 -0
- package/dist/voice/phases.d.ts.map +1 -0
- package/dist/voice/sentences.d.ts +8 -0
- package/dist/voice/sentences.d.ts.map +1 -0
- package/dist/voice/speakable.d.ts +2 -0
- package/dist/voice/speakable.d.ts.map +1 -0
- package/dist/voice/speech-queue.d.ts +42 -0
- package/dist/voice/speech-queue.d.ts.map +1 -0
- package/llms/stitchkit.txt +1 -0
- package/llms/voice.txt +106 -0
- package/llms-full.txt +98 -0
- package/llms.txt +3 -1
- package/package.json +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -15,6 +15,51 @@ additive**; the first breaking change landed in 0.10.0. Grep the file for
|
|
|
15
15
|
|
|
16
16
|
## [Unreleased]
|
|
17
17
|
|
|
18
|
+
## [0.95.3] — 2026-09-23
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- `stitchkit/live` — the watch client took a revision as a floor across two
|
|
23
|
+
lives of the hub. Revisions count one hub's life of a key, so after an API
|
|
24
|
+
restart the new hub answered the reopen with a `full` value at revision 1
|
|
25
|
+
and the client dropped it, and every later one, as "no newer" until the new
|
|
26
|
+
counter passed the old — while showing the key live. The first answer to
|
|
27
|
+
each `open` is now taken whatever its revision; within one open the
|
|
28
|
+
counter orders frames as before.
|
|
29
|
+
- `stitchkit/agent-runtime` — a turn total no longer knows more than its
|
|
30
|
+
steps. `mergeModelTotals` took each field from the SDK aggregate, which the
|
|
31
|
+
OpenRouter adapter had already summed with `cached_tokens ?? 0`, so a turn
|
|
32
|
+
whose every request reported no cache figure ended as
|
|
33
|
+
`{ value: 0, provenance: 'computed' }`. A field every step reported as
|
|
34
|
+
`unavailable` now stays `unavailable` in the total; an explicit zero and a
|
|
35
|
+
positive count are kept.
|
|
36
|
+
|
|
37
|
+
## [0.95.2] — 2026-09-23
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
|
|
41
|
+
- `stitchkit/voice` (new, browser **and** server, evolving, no dependencies) — the
|
|
42
|
+
mechanics of a live voice conversation with an agent that applications were each
|
|
43
|
+
carrying a copy of:
|
|
44
|
+
- **`SentenceCutter`** — `next(text, final)` cuts a reply into finished sentences
|
|
45
|
+
while it streams, so a voice starts on the first one; not at `3.5`, `т. е.` or an
|
|
46
|
+
ellipsis before lowercase, never inside a fenced code block; `reset()` starts a
|
|
47
|
+
new turn.
|
|
48
|
+
- **`speakableText(markdown)`** — what a reader sees, without Markdown delimiters,
|
|
49
|
+
link and image addresses, bare addresses, fenced code blocks or tables.
|
|
50
|
+
- **`SpeechQueue<Prepared>({ prepare, play, lookahead, onError, discard })`** —
|
|
51
|
+
speaks sentences in order and synthesises up to `lookahead` (default 1) while one
|
|
52
|
+
plays, so a synthesiser that returns whole clips leaves no gap between sentences;
|
|
53
|
+
`close()` finishes what is queued, `cancel()` aborts the playing sentence and every
|
|
54
|
+
synthesis in flight and settles `done` at once; a failed sentence is reported and
|
|
55
|
+
skipped.
|
|
56
|
+
- **`LIVE_VOICE_PHASES` / `LiveVoicePhase`** — `opening`, `idle`, `hearing`,
|
|
57
|
+
`thinking`, `speaking`.
|
|
58
|
+
|
|
59
|
+
Migration for an application with its own copy: import these four from
|
|
60
|
+
`stitchkit/voice` and delete the copy. A sequential queue over a streaming
|
|
61
|
+
synthesiser is `lookahead: 0`.
|
|
62
|
+
|
|
18
63
|
## [0.95.1] — 2026-09-23
|
|
19
64
|
|
|
20
65
|
### Added
|
|
@@ -60,6 +60,12 @@ export declare function addUsage(total: AgentUsage | undefined, step: AgentUsage
|
|
|
60
60
|
* handed us. Which loop did the adding is not a difference a caller filtering
|
|
61
61
|
* for a billable figure cares about, and pretending otherwise made one run
|
|
62
62
|
* report `provider-reported` on success and `computed` on abort.
|
|
63
|
+
*
|
|
64
|
+
* A field every step reported as `unavailable` stays `unavailable`: the
|
|
65
|
+
* aggregate is summed from the SDK's normalized usage, where an adapter may
|
|
66
|
+
* already have written `cached_tokens ?? 0`, so its zero is the adapter's word
|
|
67
|
+
* and not the provider's. The OpenRouter adapter did exactly that, and a turn
|
|
68
|
+
* whose every request said "no cache figure" ended as a confident "cache 0".
|
|
63
69
|
*/
|
|
64
70
|
export declare function mergeModelTotals(sdkTotal: AgentUsage, accumulated: AgentUsage | undefined): AgentUsage;
|
|
65
71
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime-internals.d.ts","sourceRoot":"","sources":["../../src/agent-runtime/runtime-internals.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,IAAI,CAAC;AAC7C,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAEL,KAAK,gBAAgB,EAGrB,KAAK,QAAQ,EACb,KAAK,mBAAmB,EACxB,KAAK,UAAU,EAChB,MAAM,WAAW,CAAC;AAGnB;;;;;GAKG;AAEH,wBAAgB,OAAO,CAAC,IAAI,EAAE,SAAS,QAAQ,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,QAAQ,CAI1E;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAG5E;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO;;;;cAI9C;AAED;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,SAAS;;;EAKhF;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,gBAAgB,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAQxE;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,WAAW,GAAG,mBAAmB,CAK5E;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,UAAU,GAAG,SAAS,EAAE,IAAI,EAAE,UAAU,GAAG,UAAU,CAuDpF;AAED
|
|
1
|
+
{"version":3,"file":"runtime-internals.d.ts","sourceRoot":"","sources":["../../src/agent-runtime/runtime-internals.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,IAAI,CAAC;AAC7C,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAEL,KAAK,gBAAgB,EAGrB,KAAK,QAAQ,EACb,KAAK,mBAAmB,EACxB,KAAK,UAAU,EAChB,MAAM,WAAW,CAAC;AAGnB;;;;;GAKG;AAEH,wBAAgB,OAAO,CAAC,IAAI,EAAE,SAAS,QAAQ,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,QAAQ,CAI1E;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAG5E;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO;;;;cAI9C;AAED;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,SAAS;;;EAKhF;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,gBAAgB,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAQxE;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,WAAW,GAAG,mBAAmB,CAK5E;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,UAAU,GAAG,SAAS,EAAE,IAAI,EAAE,UAAU,GAAG,UAAU,CAuDpF;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,UAAU,EACpB,WAAW,EAAE,UAAU,GAAG,SAAS,GAClC,UAAU,CAmBZ;AAED;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,UAAU,GAAG,SAAS,GAAG,UAAU,CAYrE;AAED,8EAA8E;AAC9E,wBAAgB,YAAY,IAAI,UAAU,CAUzC;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,kBAAkB,GAAG,UAAU,CAmBvE;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,GAAG,SAAS;;iBAEhE,IAAI;;;;;;EAgDxB"}
|
package/dist/entrypoints/live.js
CHANGED
|
@@ -86,6 +86,7 @@ async function openWatch(config, openTimeoutMs, entry) {
|
|
|
86
86
|
if (entry.opened)
|
|
87
87
|
return;
|
|
88
88
|
entry.opened = true;
|
|
89
|
+
entry.answered = false;
|
|
89
90
|
try {
|
|
90
91
|
const have = entry.hasValue && entry.fingerprint !== undefined ? { revision: entry.revision, fingerprint: entry.fingerprint } : undefined;
|
|
91
92
|
const acknowledgement = await config.transport.request(WATCH_OPEN, { key: entry.key, args: entry.args, ...have !== undefined && { have } }, { timeoutMs: openTimeoutMs });
|
|
@@ -121,10 +122,12 @@ function applyWatchFrame(entry, frame) {
|
|
|
121
122
|
}
|
|
122
123
|
entry.revision = frame.revision;
|
|
123
124
|
entry.fingerprint = frame.fingerprint;
|
|
125
|
+
entry.answered = true;
|
|
124
126
|
return { kind: "confirmed" };
|
|
125
127
|
}
|
|
126
|
-
if (entry.hasValue && frame.revision <= entry.revision)
|
|
128
|
+
if (entry.hasValue && entry.answered && frame.revision <= entry.revision) {
|
|
127
129
|
return { kind: "stale" };
|
|
130
|
+
}
|
|
128
131
|
let value;
|
|
129
132
|
if (frame.kind === "full") {
|
|
130
133
|
value = frame.value;
|
|
@@ -154,6 +157,7 @@ function applyWatchFrame(entry, frame) {
|
|
|
154
157
|
entry.value = value;
|
|
155
158
|
entry.fingerprint = frame.fingerprint;
|
|
156
159
|
entry.hasValue = true;
|
|
160
|
+
entry.answered = true;
|
|
157
161
|
return { kind: "value", value };
|
|
158
162
|
}
|
|
159
163
|
function createWatchClient(contract, config) {
|
|
@@ -226,6 +230,7 @@ function createWatchClient(contract, config) {
|
|
|
226
230
|
revision: 0,
|
|
227
231
|
hasValue: false,
|
|
228
232
|
opened: false,
|
|
233
|
+
answered: false,
|
|
229
234
|
state: { key, phase: "opening" }
|
|
230
235
|
};
|
|
231
236
|
entries.set(id, entry);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Isomorphic primitives of a live voice conversation: sentences, speakable text, speech queue. */
|
|
2
|
+
export { LIVE_VOICE_PHASES, type LiveVoicePhase } from '../voice/phases.js';
|
|
3
|
+
export { SentenceCutter } from '../voice/sentences.js';
|
|
4
|
+
export { speakableText } from '../voice/speakable.js';
|
|
5
|
+
export { SpeechQueue, type SpeechQueueOptions } from '../voice/speech-queue.js';
|
|
6
|
+
//# sourceMappingURL=voice.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"voice.d.ts","sourceRoot":"","sources":["../../src/entrypoints/voice.ts"],"names":[],"mappings":"AAAA,mGAAmG;AACnG,OAAO,EAAE,iBAAiB,EAAE,KAAK,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACzE,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,WAAW,EAAE,KAAK,kBAAkB,EAAE,MAAM,uBAAuB,CAAC"}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// src/voice/phases.ts
|
|
2
|
+
var LIVE_VOICE_PHASES = [
|
|
3
|
+
"opening",
|
|
4
|
+
"idle",
|
|
5
|
+
"hearing",
|
|
6
|
+
"thinking",
|
|
7
|
+
"speaking"
|
|
8
|
+
];
|
|
9
|
+
// src/voice/sentences.ts
|
|
10
|
+
var TERMINAL = /[.!?…]/;
|
|
11
|
+
var CLOSING = /["»”)]/;
|
|
12
|
+
var ENDS_SENTENCE = /[.!?…]+["»”)]*$/;
|
|
13
|
+
var OPENER = /^[\p{Lu}\p{N}"«“(—–-]/u;
|
|
14
|
+
var NEXT_WORD = /^\s+(\S)/u;
|
|
15
|
+
|
|
16
|
+
class SentenceCutter {
|
|
17
|
+
said = 0;
|
|
18
|
+
next(text, final) {
|
|
19
|
+
const sentences = [];
|
|
20
|
+
let start = this.said;
|
|
21
|
+
let fence = fenceFrom(text, this.said);
|
|
22
|
+
for (let index = this.said;index < text.length; index += 1) {
|
|
23
|
+
if (index >= fence.end)
|
|
24
|
+
fence = fenceFrom(text, index);
|
|
25
|
+
if (index >= fence.start && index < fence.end)
|
|
26
|
+
continue;
|
|
27
|
+
if (!isBoundary(text, index, start))
|
|
28
|
+
continue;
|
|
29
|
+
const sentence = text.slice(start, index + 1).trim();
|
|
30
|
+
if (sentence !== "")
|
|
31
|
+
sentences.push(sentence);
|
|
32
|
+
start = index + 1;
|
|
33
|
+
}
|
|
34
|
+
this.said = start;
|
|
35
|
+
if (final) {
|
|
36
|
+
const rest = text.slice(start).trim();
|
|
37
|
+
if (rest !== "")
|
|
38
|
+
sentences.push(rest);
|
|
39
|
+
this.said = text.length;
|
|
40
|
+
}
|
|
41
|
+
return sentences;
|
|
42
|
+
}
|
|
43
|
+
reset() {
|
|
44
|
+
this.said = 0;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function isBoundary(text, index, start) {
|
|
48
|
+
const char = text[index] ?? "";
|
|
49
|
+
if (char === `
|
|
50
|
+
` && text[index + 1] === `
|
|
51
|
+
`)
|
|
52
|
+
return true;
|
|
53
|
+
if (!TERMINAL.test(char) && !CLOSING.test(char))
|
|
54
|
+
return false;
|
|
55
|
+
const following = text[index + 1] ?? "";
|
|
56
|
+
if (TERMINAL.test(following) || CLOSING.test(following))
|
|
57
|
+
return false;
|
|
58
|
+
if (!ENDS_SENTENCE.test(text.slice(start, index + 1)))
|
|
59
|
+
return false;
|
|
60
|
+
const next = text.slice(index + 1, index + 64).match(NEXT_WORD);
|
|
61
|
+
return next !== null && OPENER.test(next[1] ?? "");
|
|
62
|
+
}
|
|
63
|
+
function fenceFrom(text, from) {
|
|
64
|
+
const open = text.indexOf("```", from);
|
|
65
|
+
if (open === -1)
|
|
66
|
+
return { start: text.length, end: text.length };
|
|
67
|
+
const close = text.indexOf("```", open + 3);
|
|
68
|
+
return { start: open, end: close === -1 ? text.length : close + 3 };
|
|
69
|
+
}
|
|
70
|
+
// src/voice/speakable.ts
|
|
71
|
+
var FENCED_BLOCK = /^[ \t]*(```|~~~)[^\n]*\n[\s\S]*?(?:^[ \t]*\1[^\n]*$|(?![\s\S]))/gm;
|
|
72
|
+
var TABLE_ROW = /^[ \t]*\|.*$/gm;
|
|
73
|
+
var THEMATIC_BREAK = /^[ \t]*([-*_])(?:[ \t]*\1){2,}[ \t]*$/gm;
|
|
74
|
+
var IMAGE = /!\[[^\]]*\]\((?:<[^>]*>|[^)])*\)/g;
|
|
75
|
+
var LINK = /\[([^\]]+)\]\((?:<[^>]*>|[^)])*\)/g;
|
|
76
|
+
var REFERENCE_LINK = /\[([^\]]+)\]\[[^\]]*\]/g;
|
|
77
|
+
var LINK_DEFINITION = /^[ \t]{0,3}\[[^\]]+\]:[ \t]*\S+.*$/gm;
|
|
78
|
+
var AUTOLINK = /<(?:https?|mailto):[^>\s]*>/g;
|
|
79
|
+
var BARE_URL = /\bhttps?:\/\/[^\s<>()]+/g;
|
|
80
|
+
var HTML_TAG = /<\/?[A-Za-z][^>]*>/g;
|
|
81
|
+
var LINE_MARKER = /^[ \t]{0,3}(?:#{1,6}[ \t]+|>[ \t]?|[-*+][ \t]+(?:\[[ xX]\][ \t]+)?)/gm;
|
|
82
|
+
function speakableText(markdown) {
|
|
83
|
+
return markdown.replace(FENCED_BLOCK, " ").replace(TABLE_ROW, " ").replace(THEMATIC_BREAK, " ").replace(LINK_DEFINITION, " ").replace(IMAGE, "").replace(LINK, "$1").replace(REFERENCE_LINK, "$1").replace(AUTOLINK, "").replace(BARE_URL, "").replace(HTML_TAG, "").replace(LINE_MARKER, "").replace(/\*\*([^*]+)\*\*/g, "$1").replace(/__([^_]+)__/g, "$1").replace(/~~([^~]+)~~/g, "$1").replace(/(^|[^\p{L}\p{N}*])\*([^*\s][^*]*?)\*(?![\p{L}\p{N}*])/gu, "$1$2").replace(/(^|[^\p{L}\p{N}_])_([^_\s][^_]*?)_(?![\p{L}\p{N}_])/gu, "$1$2").replace(/`([^`]+)`/g, "$1").replace(/\s+/g, " ").replace(/ ([.,!?;:…])/g, "$1").trim();
|
|
84
|
+
}
|
|
85
|
+
// src/voice/speech-queue.ts
|
|
86
|
+
var CANCELLED = Symbol("cancelled");
|
|
87
|
+
function settle(run) {
|
|
88
|
+
return Promise.resolve().then(run).then((value) => ({ ok: true, value }), (error) => ({ ok: false, error }));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
class SpeechQueue {
|
|
92
|
+
options;
|
|
93
|
+
done;
|
|
94
|
+
lookahead;
|
|
95
|
+
pending = [];
|
|
96
|
+
ahead = [];
|
|
97
|
+
current = null;
|
|
98
|
+
playing = false;
|
|
99
|
+
closed = false;
|
|
100
|
+
cancelled = false;
|
|
101
|
+
wake = null;
|
|
102
|
+
stop;
|
|
103
|
+
stopped = new Promise((resolve) => {
|
|
104
|
+
this.stop = resolve;
|
|
105
|
+
});
|
|
106
|
+
constructor(options) {
|
|
107
|
+
this.options = options;
|
|
108
|
+
const lookahead = options.lookahead ?? 1;
|
|
109
|
+
if (!Number.isInteger(lookahead) || lookahead < 0) {
|
|
110
|
+
throw new RangeError(`lookahead must be a non-negative integer, got ${lookahead}`);
|
|
111
|
+
}
|
|
112
|
+
this.lookahead = lookahead;
|
|
113
|
+
this.done = this.run();
|
|
114
|
+
}
|
|
115
|
+
get isCancelled() {
|
|
116
|
+
return this.cancelled;
|
|
117
|
+
}
|
|
118
|
+
get isSpeaking() {
|
|
119
|
+
return this.playing;
|
|
120
|
+
}
|
|
121
|
+
push(text) {
|
|
122
|
+
if (this.closed || this.cancelled || text.trim() === "")
|
|
123
|
+
return;
|
|
124
|
+
this.pending.push(text);
|
|
125
|
+
this.fill();
|
|
126
|
+
this.notify();
|
|
127
|
+
}
|
|
128
|
+
close() {
|
|
129
|
+
this.closed = true;
|
|
130
|
+
this.notify();
|
|
131
|
+
}
|
|
132
|
+
cancel() {
|
|
133
|
+
if (this.cancelled)
|
|
134
|
+
return;
|
|
135
|
+
this.cancelled = true;
|
|
136
|
+
this.closed = true;
|
|
137
|
+
this.pending.length = 0;
|
|
138
|
+
const unplayed = this.ahead.splice(0);
|
|
139
|
+
if (this.current && !this.playing)
|
|
140
|
+
unplayed.push(this.current);
|
|
141
|
+
this.current?.controller.abort();
|
|
142
|
+
for (const slot of unplayed) {
|
|
143
|
+
slot.controller.abort();
|
|
144
|
+
slot.result.then((outcome) => {
|
|
145
|
+
if (outcome.ok)
|
|
146
|
+
this.release(outcome.value);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
this.stop(CANCELLED);
|
|
150
|
+
this.notify();
|
|
151
|
+
}
|
|
152
|
+
notify() {
|
|
153
|
+
this.wake?.();
|
|
154
|
+
}
|
|
155
|
+
fill() {
|
|
156
|
+
const capacity = this.lookahead + 1 - (this.current ? 1 : 0);
|
|
157
|
+
while (!this.cancelled && this.ahead.length < capacity && this.pending.length > 0) {
|
|
158
|
+
const text = this.pending.shift();
|
|
159
|
+
const controller = new AbortController;
|
|
160
|
+
this.ahead.push({
|
|
161
|
+
text,
|
|
162
|
+
controller,
|
|
163
|
+
result: settle(() => this.options.prepare(text, controller.signal))
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
async run() {
|
|
168
|
+
while (!this.cancelled) {
|
|
169
|
+
this.fill();
|
|
170
|
+
const slot = this.ahead.shift();
|
|
171
|
+
if (!slot) {
|
|
172
|
+
if (this.closed)
|
|
173
|
+
return;
|
|
174
|
+
await new Promise((resolve) => {
|
|
175
|
+
this.wake = resolve;
|
|
176
|
+
});
|
|
177
|
+
this.wake = null;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
this.current = slot;
|
|
181
|
+
try {
|
|
182
|
+
const prepared = await Promise.race([slot.result, this.stopped]);
|
|
183
|
+
if (prepared === CANCELLED)
|
|
184
|
+
return;
|
|
185
|
+
if (!prepared.ok) {
|
|
186
|
+
this.report(prepared.error, slot.text);
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
this.playing = true;
|
|
190
|
+
this.fill();
|
|
191
|
+
const played = await Promise.race([
|
|
192
|
+
settle(() => this.options.play(prepared.value, slot.controller.signal)),
|
|
193
|
+
this.stopped
|
|
194
|
+
]);
|
|
195
|
+
if (played === CANCELLED)
|
|
196
|
+
return;
|
|
197
|
+
if (!played.ok)
|
|
198
|
+
this.report(played.error, slot.text);
|
|
199
|
+
} finally {
|
|
200
|
+
this.playing = false;
|
|
201
|
+
this.current = null;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
report(error, text) {
|
|
206
|
+
try {
|
|
207
|
+
this.options.onError?.(error, text);
|
|
208
|
+
} catch {}
|
|
209
|
+
}
|
|
210
|
+
release(prepared) {
|
|
211
|
+
try {
|
|
212
|
+
this.options.discard?.(prepared);
|
|
213
|
+
} catch {}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
export {
|
|
217
|
+
speakableText,
|
|
218
|
+
SpeechQueue,
|
|
219
|
+
SentenceCutter,
|
|
220
|
+
LIVE_VOICE_PHASES
|
|
221
|
+
};
|
|
@@ -1152,7 +1152,11 @@ function addUsage(total, step) {
|
|
|
1152
1152
|
};
|
|
1153
1153
|
}
|
|
1154
1154
|
function mergeModelTotals(sdkTotal, accumulated) {
|
|
1155
|
-
const pick = (total, ours) =>
|
|
1155
|
+
const pick = (total, ours) => {
|
|
1156
|
+
if (ours?.provenance === "unavailable")
|
|
1157
|
+
return ours;
|
|
1158
|
+
return total?.value !== undefined ? { value: total.value, provenance: "computed" } : ours ?? { provenance: "unavailable" };
|
|
1159
|
+
};
|
|
1156
1160
|
return {
|
|
1157
1161
|
inputTokens: pick(sdkTotal.inputTokens, accumulated?.inputTokens),
|
|
1158
1162
|
outputTokens: pick(sdkTotal.outputTokens, accumulated?.outputTokens),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"watch-client.d.ts","sourceRoot":"","sources":["../../src/live/watch-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEnE,OAAO,EACL,WAAW,EACX,UAAU,EACV,WAAW,EACX,WAAW,EACX,KAAK,SAAS,EACd,KAAK,QAAQ,EACb,KAAK,eAAe,EACpB,KAAK,eAAe,EAErB,MAAM,kBAAkB,CAAC;AAG1B,MAAM,WAAW,cAAc,CAAC,MAAM;IACpC,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,kEAAkE;IAClE,KAAK,CAAC,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI,CAAC;CACtC;AAED,MAAM,WAAW,WAAW,CAAC,MAAM;IACjC,sFAAsF;IACtF,SAAS,CAAC,SAAS,EAAE,cAAc,CAAC,MAAM,CAAC,GAAG,MAAM,IAAI,CAAC;IACzD,wEAAwE;IACxE,KAAK,IAAI,IAAI,CAAC;CACf;AAED,sDAAsD;AACtD,MAAM,WAAW,kBAAkB;IACjC,CAAC,WAAW,CAAC,EAAE,eAAe,CAAC;IAC/B,CAAC,WAAW,CAAC,EAAE,eAAe,CAAC;CAChC;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,CAAC;IAClC,IAAI,EAAE,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,CAAC;IACpC,OAAO,EAAE,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,CAAC;IACvC,kBAAkB,EAAE,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,CAAC;CACnD;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,kBAAkB,GAAG,cAAc,CAKzE;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,cAAc;IAC7B,EAAE,CAAC,MAAM,SAAS,MAAM,kBAAkB,EACxC,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,CAAC,OAAO,EAAE,kBAAkB,CAAC,MAAM,CAAC,KAAK,IAAI,GACrD,MAAM,IAAI,CAAC;IACd,IAAI,CAAC,KAAK,EAAE,OAAO,WAAW,EAAE,OAAO,EAAE;QAAE,GAAG,EAAE,QAAQ,CAAA;KAAE,GAAG,OAAO,CAAC;IACrE,OAAO,CACL,KAAK,EAAE,OAAO,UAAU,EACxB,OAAO,EAAE;QAAE,GAAG,EAAE,QAAQ,CAAC;QAAC,IAAI,EAAE,OAAO,CAAC;QAAC,IAAI,CAAC,EAAE,SAAS,CAAA;KAAE,EAC3D,OAAO,EAAE;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,GAC7B,OAAO,CAAC;QAAE,QAAQ,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACnD;;;;;;;OAOG;IACH,kBAAkB,CAAC,QAAQ,EAAE,CAAC,SAAS,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CACzF;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,SAAS,EAAE,cAAc,CAAC;IACnC,+EAA+E;IAC/E,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,8DAA8D;IAC9D,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,wEAAwE;IACxE,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9D;
|
|
1
|
+
{"version":3,"file":"watch-client.d.ts","sourceRoot":"","sources":["../../src/live/watch-client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** The phases of a live voice conversation, one vocabulary for every side of it. */
|
|
2
|
+
export declare const LIVE_VOICE_PHASES: readonly ['opening', 'idle', 'hearing', 'thinking', 'speaking'];
|
|
3
|
+
export type LiveVoicePhase = (typeof LIVE_VOICE_PHASES)[number];
|
|
4
|
+
//# sourceMappingURL=phases.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"phases.d.ts","sourceRoot":"","sources":["../../src/voice/phases.ts"],"names":[],"mappings":"AAAA,oFAAoF;AACpF,eAAO,MAAM,iBAAiB,YAC5B,SAAS,EACT,MAAM,EACN,SAAS,EACT,UAAU,EACV,UAAU,CACF,CAAC;AACX,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,iBAAiB,CAAC,CAAC,MAAM,CAAC,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare class SentenceCutter {
|
|
2
|
+
private said;
|
|
3
|
+
/** The sentences finished since the previous call; with `final`, the rest of the turn too. */
|
|
4
|
+
next(text: string, final: boolean): string[];
|
|
5
|
+
/** Start a new turn. */
|
|
6
|
+
reset(): void;
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=sentences.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sentences.d.ts","sourceRoot":"","sources":["../../src/voice/sentences.ts"],"names":[],"mappings":"AAkBA,qBAAa,cAAc;IACzB,OAAO,CAAC,IAAI,CAAK;IAEjB,8FAA8F;IAC9F,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,MAAM,EAAE,CAmB3C;IAED,wBAAwB;IACxB,KAAK,IAAI,IAAI,CAEZ;CACF"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"speakable.d.ts","sourceRoot":"","sources":["../../src/voice/speakable.ts"],"names":[],"mappings":"AAiBA,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAsBtD"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export interface SpeechQueueOptions<Prepared> {
|
|
2
|
+
/** Synthesise one sentence. Aborted when the queue is cancelled. */
|
|
3
|
+
prepare: (text: string, signal: AbortSignal) => Promise<Prepared>;
|
|
4
|
+
/** Play one prepared sentence to its end. Aborted when the queue is cancelled. */
|
|
5
|
+
play: (prepared: Prepared, signal: AbortSignal) => Promise<void>;
|
|
6
|
+
/** Sentences prepared ahead of the one playing. Default 1; 0 prepares only after playing. */
|
|
7
|
+
lookahead?: number;
|
|
8
|
+
/** A sentence that failed to prepare or play is skipped; the queue goes on. */
|
|
9
|
+
onError?: (error: unknown, text: string) => void;
|
|
10
|
+
/** A prepared sentence that will never play — cancelled — to release what it holds. */
|
|
11
|
+
discard?: (prepared: Prepared) => void;
|
|
12
|
+
}
|
|
13
|
+
export declare class SpeechQueue<Prepared> {
|
|
14
|
+
private readonly options;
|
|
15
|
+
readonly done: Promise<void>;
|
|
16
|
+
private readonly lookahead;
|
|
17
|
+
private readonly pending;
|
|
18
|
+
private readonly ahead;
|
|
19
|
+
private current;
|
|
20
|
+
private playing;
|
|
21
|
+
private closed;
|
|
22
|
+
private cancelled;
|
|
23
|
+
private wake;
|
|
24
|
+
private stop;
|
|
25
|
+
private readonly stopped;
|
|
26
|
+
constructor(options: SpeechQueueOptions<Prepared>);
|
|
27
|
+
get isCancelled(): boolean;
|
|
28
|
+
get isSpeaking(): boolean;
|
|
29
|
+
/** Add one sentence. Blank text and anything after `close` or `cancel` is ignored. */
|
|
30
|
+
push(text: string): void;
|
|
31
|
+
/** The turn is over: say what is queued, then finish. */
|
|
32
|
+
close(): void;
|
|
33
|
+
/** Interrupted: abort the sentence playing and every synthesis, drop the rest. */
|
|
34
|
+
cancel(): void;
|
|
35
|
+
private notify;
|
|
36
|
+
/** Start synthesis up to the lookahead: the sentence playing or next, plus `lookahead`. */
|
|
37
|
+
private fill;
|
|
38
|
+
private run;
|
|
39
|
+
private report;
|
|
40
|
+
private release;
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=speech-queue.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"speech-queue.d.ts","sourceRoot":"","sources":["../../src/voice/speech-queue.ts"],"names":[],"mappings":"AAYA,MAAM,WAAW,kBAAkB,CAAC,QAAQ;IAC1C,oEAAoE;IACpE,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;IAClE,kFAAkF;IAClF,IAAI,EAAE,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACjE,6FAA6F;IAC7F,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,+EAA+E;IAC/E,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACjD,uFAAuF;IACvF,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,KAAK,IAAI,CAAC;CACxC;AAqBD,qBAAa,WAAW,CAAC,QAAQ;IAenB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAdpC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAgB;IACxC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAwB;IAC9C,OAAO,CAAC,OAAO,CAA+B;IAC9C,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,IAAI,CAA6B;IACzC,OAAO,CAAC,IAAI,CAAqC;IACjD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAErB;IAEH,YAA6B,OAAO,EAAE,kBAAkB,CAAC,QAAQ,CAAC,EAOjE;IAED,IAAI,WAAW,IAAI,OAAO,CAEzB;IAED,IAAI,UAAU,IAAI,OAAO,CAExB;IAED,sFAAsF;IACtF,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAKvB;IAED,yDAAyD;IACzD,KAAK,IAAI,IAAI,CAGZ;IAED,kFAAkF;IAClF,MAAM,IAAI,IAAI,CAgBb;IAED,OAAO,CAAC,MAAM;IAId,2FAA2F;IAC3F,OAAO,CAAC,IAAI;YAaE,GAAG;IAmCjB,OAAO,CAAC,MAAM;IAQd,OAAO,CAAC,OAAO;CAOhB"}
|
package/llms/stitchkit.txt
CHANGED
|
@@ -73,6 +73,7 @@ own, recorded as an ADR.
|
|
|
73
73
|
| `stitchkit/tracking` | browser **and** server | evolving | visitor-tracking mechanics — `createTrackingClient`, the tab-shared outbox, the page-leave beacon, attribution, the contract factory; no event vocabulary, no React |
|
|
74
74
|
| `stitchkit/tracking/server` | server (Bun or Node) | evolving | the decisions a tracking backend makes — dispositions, visit lease over an application-owned store, active intervals, presence; no database |
|
|
75
75
|
| `stitchkit/release` | browser **and** server | evolving | a page follows the release it was built for — `createReleaseMarker` on the server, `createReleaseWatcher` in the browser, the `X-Build-Id` header and a socket event between them |
|
|
76
|
+
| `stitchkit/voice` | browser **and** server | evolving | the mechanics of a live voice reply — `SentenceCutter`, `speakableText`, a `SpeechQueue` that synthesises ahead and stops on interruption, the conversation phases |
|
|
76
77
|
| `stitchkit/geo` | server (Bun or Node) | evolving | managed GeoIP reader generations, last-known-good reload and the optional MaxMind adapter |
|
|
77
78
|
| `stitchkit/oauth` | browser **and** server | evolving | provider-neutral Authorization Code + PKCE request and one-shot callback transaction mechanics |
|
|
78
79
|
| `stitchkit/google` | server (Bun or Node) | evolving | optional Google code exchange and verified OIDC identity adapter |
|
package/llms/voice.txt
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# stitchkit/voice — stitchkit docs slice
|
|
2
|
+
|
|
3
|
+
> Generated from the `docs/` tree by `bun run gen:llms`. The index of all slices is
|
|
4
|
+
> `llms.txt` in this package.
|
|
5
|
+
|
|
6
|
+
Contains: Guide: Voice (docs/guide/voice.md) · API reference: `stitchkit/voice` (docs/api/reference.md)
|
|
7
|
+
|
|
8
|
+
==============================================================================
|
|
9
|
+
# Guide: Voice (docs/guide/voice.md)
|
|
10
|
+
==============================================================================
|
|
11
|
+
|
|
12
|
+
# Voice
|
|
13
|
+
|
|
14
|
+
A live voice conversation with an agent repeats the same mechanics in every application
|
|
15
|
+
that has one: the reply is cut into sentences while the model is still writing it, each
|
|
16
|
+
sentence is stripped down to what can be said aloud, and a queue speaks them in order — and
|
|
17
|
+
stops at once when the listener interrupts. `stitchkit/voice` is those mechanics, the same in
|
|
18
|
+
the browser and on Bun or Node, with no dependencies. It is **evolving**.
|
|
19
|
+
|
|
20
|
+
Speech recognition, synthesis providers, the microphone and the player stay with the
|
|
21
|
+
application: the queue takes them as two functions.
|
|
22
|
+
|
|
23
|
+
## Sentences while the reply streams
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { SentenceCutter, SpeechQueue, speakableText } from 'stitchkit/voice'
|
|
27
|
+
|
|
28
|
+
const cutter = new SentenceCutter()
|
|
29
|
+
|
|
30
|
+
for await (const text of reply) { // the whole reply so far, on every chunk
|
|
31
|
+
for (const sentence of cutter.next(text, false)) say(sentence)
|
|
32
|
+
}
|
|
33
|
+
for (const sentence of cutter.next(finalText, true)) say(sentence) // the rest of the turn
|
|
34
|
+
cutter.reset() // the next turn starts from zero
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
A sentence ends at `. ! ? …` (with any closing quote or bracket) followed by whitespace and a
|
|
38
|
+
capital letter, a digit or an opening mark, or at a blank line between paragraphs. `3.5`,
|
|
39
|
+
`т. е. так` and `e.g. this` are not cut; the end of the text is not the end of a sentence
|
|
40
|
+
until `final`; nothing is cut inside a fenced code block, even one that is still open.
|
|
41
|
+
|
|
42
|
+
## What is said
|
|
43
|
+
|
|
44
|
+
`speakableText(markdown)` keeps what a reader sees and drops what cannot be said: headings,
|
|
45
|
+
emphasis and list markers lose their delimiters, a link keeps its text and loses its
|
|
46
|
+
address, and images, bare addresses, fenced code blocks and tables are dropped whole. It does
|
|
47
|
+
not summarise. A sentence that was only code becomes `''`, and the queue ignores blank text.
|
|
48
|
+
|
|
49
|
+
## Speaking in order, synthesising ahead
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
const queue = new SpeechQueue<Blob>({
|
|
53
|
+
prepare: (text, signal) => synthesise(text, { signal }), // the whole clip
|
|
54
|
+
play: (clip, signal) => player.play(clip, { signal }), // resolves when it has played
|
|
55
|
+
lookahead: 1, // the default
|
|
56
|
+
onError: (error, text) => log.warn('sentence skipped', { error, text }),
|
|
57
|
+
discard: (clip) => release(clip), // prepared, never played
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
const say = (sentence: string) => queue.push(speakableText(sentence))
|
|
61
|
+
|
|
62
|
+
// the turn is over: say what is queued, then `done` settles
|
|
63
|
+
queue.close()
|
|
64
|
+
// the listener interrupted: the playing sentence and every synthesis are aborted
|
|
65
|
+
queue.cancel()
|
|
66
|
+
await queue.done
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
While one sentence plays, up to `lookahead` more are already synthesising, so a synthesiser
|
|
70
|
+
that returns the whole clip does not leave a gap the length of its synthesis between two
|
|
71
|
+
sentences. `lookahead: 0` synthesises each sentence only after the previous one played.
|
|
72
|
+
|
|
73
|
+
A sentence that fails to synthesise or to play is reported to `onError` and skipped; the
|
|
74
|
+
queue goes on. `cancel()` aborts the signal of the sentence playing and of every synthesis in
|
|
75
|
+
flight, drops the rest, calls `discard` for every clip that was prepared and will not play,
|
|
76
|
+
and settles `done` without waiting for a `play` that ignores its signal. `isSpeaking` and
|
|
77
|
+
`isCancelled` say where the queue is.
|
|
78
|
+
|
|
79
|
+
## Phases
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import { LIVE_VOICE_PHASES, type LiveVoicePhase } from 'stitchkit/voice'
|
|
83
|
+
// 'opening' | 'idle' | 'hearing' | 'thinking' | 'speaking'
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
One vocabulary for every side of the conversation — the server that runs the agent and the
|
|
87
|
+
page that shows the microphone — so a phase sent over the wire means the same on both ends.
|
|
88
|
+
|
|
89
|
+
==============================================================================
|
|
90
|
+
# API reference: `stitchkit/voice` (docs/api/reference.md)
|
|
91
|
+
==============================================================================
|
|
92
|
+
|
|
93
|
+
## `stitchkit/voice`
|
|
94
|
+
|
|
95
|
+
Isomorphic, dependency-free mechanics of a live voice conversation. Recognition, synthesis
|
|
96
|
+
providers, the microphone and the player remain application-owned. See the
|
|
97
|
+
[voice guide](../guide/voice.md).
|
|
98
|
+
|
|
99
|
+
| Export | Kind | Summary |
|
|
100
|
+
|--------|------|---------|
|
|
101
|
+
| `SentenceCutter` | class | `next(text, final)` returns the sentences finished since the previous call of the reply streamed so far — never inside a fenced code block, never at `3.5` or `т. е.`; `reset()` starts a new turn |
|
|
102
|
+
| `speakableText` | function | the visible text of Markdown without delimiters, link and image addresses, bare addresses, fenced code blocks or tables |
|
|
103
|
+
| `SpeechQueue` / `SpeechQueueOptions` | class / _type_ | speak sentences in order with `lookahead` (default 1) synthesised while one plays; `close()` finishes the queue, `cancel()` aborts the playing sentence and every synthesis; a failed sentence is reported to `onError` and skipped |
|
|
104
|
+
| `LIVE_VOICE_PHASES` / `LiveVoicePhase` | constant / _type_ | `opening`, `idle`, `hearing`, `thinking`, `speaking` |
|
|
105
|
+
|
|
106
|
+
---
|
package/llms-full.txt
CHANGED
|
@@ -64,6 +64,7 @@ own, recorded as an ADR.
|
|
|
64
64
|
| `stitchkit/tracking` | browser **and** server | evolving | visitor-tracking mechanics — `createTrackingClient`, the tab-shared outbox, the page-leave beacon, attribution, the contract factory; no event vocabulary, no React |
|
|
65
65
|
| `stitchkit/tracking/server` | server (Bun or Node) | evolving | the decisions a tracking backend makes — dispositions, visit lease over an application-owned store, active intervals, presence; no database |
|
|
66
66
|
| `stitchkit/release` | browser **and** server | evolving | a page follows the release it was built for — `createReleaseMarker` on the server, `createReleaseWatcher` in the browser, the `X-Build-Id` header and a socket event between them |
|
|
67
|
+
| `stitchkit/voice` | browser **and** server | evolving | the mechanics of a live voice reply — `SentenceCutter`, `speakableText`, a `SpeechQueue` that synthesises ahead and stops on interruption, the conversation phases |
|
|
67
68
|
| `stitchkit/geo` | server (Bun or Node) | evolving | managed GeoIP reader generations, last-known-good reload and the optional MaxMind adapter |
|
|
68
69
|
| `stitchkit/oauth` | browser **and** server | evolving | provider-neutral Authorization Code + PKCE request and one-shot callback transaction mechanics |
|
|
69
70
|
| `stitchkit/google` | server (Bun or Node) | evolving | optional Google code exchange and verified OIDC identity adapter |
|
|
@@ -12633,6 +12634,88 @@ session query with `cache`; the generic starter does not invent a `getSession`
|
|
|
12633
12634
|
endpoint or cookie vocabulary.
|
|
12634
12635
|
|
|
12635
12636
|
|
|
12637
|
+
==============================================================================
|
|
12638
|
+
# Guide: Voice (docs/guide/voice.md)
|
|
12639
|
+
==============================================================================
|
|
12640
|
+
|
|
12641
|
+
# Voice
|
|
12642
|
+
|
|
12643
|
+
A live voice conversation with an agent repeats the same mechanics in every application
|
|
12644
|
+
that has one: the reply is cut into sentences while the model is still writing it, each
|
|
12645
|
+
sentence is stripped down to what can be said aloud, and a queue speaks them in order — and
|
|
12646
|
+
stops at once when the listener interrupts. `stitchkit/voice` is those mechanics, the same in
|
|
12647
|
+
the browser and on Bun or Node, with no dependencies. It is **evolving**.
|
|
12648
|
+
|
|
12649
|
+
Speech recognition, synthesis providers, the microphone and the player stay with the
|
|
12650
|
+
application: the queue takes them as two functions.
|
|
12651
|
+
|
|
12652
|
+
## Sentences while the reply streams
|
|
12653
|
+
|
|
12654
|
+
```ts
|
|
12655
|
+
import { SentenceCutter, SpeechQueue, speakableText } from 'stitchkit/voice'
|
|
12656
|
+
|
|
12657
|
+
const cutter = new SentenceCutter()
|
|
12658
|
+
|
|
12659
|
+
for await (const text of reply) { // the whole reply so far, on every chunk
|
|
12660
|
+
for (const sentence of cutter.next(text, false)) say(sentence)
|
|
12661
|
+
}
|
|
12662
|
+
for (const sentence of cutter.next(finalText, true)) say(sentence) // the rest of the turn
|
|
12663
|
+
cutter.reset() // the next turn starts from zero
|
|
12664
|
+
```
|
|
12665
|
+
|
|
12666
|
+
A sentence ends at `. ! ? …` (with any closing quote or bracket) followed by whitespace and a
|
|
12667
|
+
capital letter, a digit or an opening mark, or at a blank line between paragraphs. `3.5`,
|
|
12668
|
+
`т. е. так` and `e.g. this` are not cut; the end of the text is not the end of a sentence
|
|
12669
|
+
until `final`; nothing is cut inside a fenced code block, even one that is still open.
|
|
12670
|
+
|
|
12671
|
+
## What is said
|
|
12672
|
+
|
|
12673
|
+
`speakableText(markdown)` keeps what a reader sees and drops what cannot be said: headings,
|
|
12674
|
+
emphasis and list markers lose their delimiters, a link keeps its text and loses its
|
|
12675
|
+
address, and images, bare addresses, fenced code blocks and tables are dropped whole. It does
|
|
12676
|
+
not summarise. A sentence that was only code becomes `''`, and the queue ignores blank text.
|
|
12677
|
+
|
|
12678
|
+
## Speaking in order, synthesising ahead
|
|
12679
|
+
|
|
12680
|
+
```ts
|
|
12681
|
+
const queue = new SpeechQueue<Blob>({
|
|
12682
|
+
prepare: (text, signal) => synthesise(text, { signal }), // the whole clip
|
|
12683
|
+
play: (clip, signal) => player.play(clip, { signal }), // resolves when it has played
|
|
12684
|
+
lookahead: 1, // the default
|
|
12685
|
+
onError: (error, text) => log.warn('sentence skipped', { error, text }),
|
|
12686
|
+
discard: (clip) => release(clip), // prepared, never played
|
|
12687
|
+
})
|
|
12688
|
+
|
|
12689
|
+
const say = (sentence: string) => queue.push(speakableText(sentence))
|
|
12690
|
+
|
|
12691
|
+
// the turn is over: say what is queued, then `done` settles
|
|
12692
|
+
queue.close()
|
|
12693
|
+
// the listener interrupted: the playing sentence and every synthesis are aborted
|
|
12694
|
+
queue.cancel()
|
|
12695
|
+
await queue.done
|
|
12696
|
+
```
|
|
12697
|
+
|
|
12698
|
+
While one sentence plays, up to `lookahead` more are already synthesising, so a synthesiser
|
|
12699
|
+
that returns the whole clip does not leave a gap the length of its synthesis between two
|
|
12700
|
+
sentences. `lookahead: 0` synthesises each sentence only after the previous one played.
|
|
12701
|
+
|
|
12702
|
+
A sentence that fails to synthesise or to play is reported to `onError` and skipped; the
|
|
12703
|
+
queue goes on. `cancel()` aborts the signal of the sentence playing and of every synthesis in
|
|
12704
|
+
flight, drops the rest, calls `discard` for every clip that was prepared and will not play,
|
|
12705
|
+
and settles `done` without waiting for a `play` that ignores its signal. `isSpeaking` and
|
|
12706
|
+
`isCancelled` say where the queue is.
|
|
12707
|
+
|
|
12708
|
+
## Phases
|
|
12709
|
+
|
|
12710
|
+
```ts
|
|
12711
|
+
import { LIVE_VOICE_PHASES, type LiveVoicePhase } from 'stitchkit/voice'
|
|
12712
|
+
// 'opening' | 'idle' | 'hearing' | 'thinking' | 'speaking'
|
|
12713
|
+
```
|
|
12714
|
+
|
|
12715
|
+
One vocabulary for every side of the conversation — the server that runs the agent and the
|
|
12716
|
+
page that shows the microphone — so a phase sent over the wire means the same on both ends.
|
|
12717
|
+
|
|
12718
|
+
|
|
12636
12719
|
==============================================================================
|
|
12637
12720
|
# Guide: GeoIP (docs/guide/geo.md)
|
|
12638
12721
|
==============================================================================
|
|
@@ -18300,6 +18383,21 @@ signal stay with the application. → ADR 0167 — [guide](../guide/release.md)
|
|
|
18300
18383
|
|
|
18301
18384
|
---
|
|
18302
18385
|
|
|
18386
|
+
## `stitchkit/voice`
|
|
18387
|
+
|
|
18388
|
+
Isomorphic, dependency-free mechanics of a live voice conversation. Recognition, synthesis
|
|
18389
|
+
providers, the microphone and the player remain application-owned. See the
|
|
18390
|
+
[voice guide](../guide/voice.md).
|
|
18391
|
+
|
|
18392
|
+
| Export | Kind | Summary |
|
|
18393
|
+
|--------|------|---------|
|
|
18394
|
+
| `SentenceCutter` | class | `next(text, final)` returns the sentences finished since the previous call of the reply streamed so far — never inside a fenced code block, never at `3.5` or `т. е.`; `reset()` starts a new turn |
|
|
18395
|
+
| `speakableText` | function | the visible text of Markdown without delimiters, link and image addresses, bare addresses, fenced code blocks or tables |
|
|
18396
|
+
| `SpeechQueue` / `SpeechQueueOptions` | class / _type_ | speak sentences in order with `lookahead` (default 1) synthesised while one plays; `close()` finishes the queue, `cancel()` aborts the playing sentence and every synthesis; a failed sentence is reported to `onError` and skipped |
|
|
18397
|
+
| `LIVE_VOICE_PHASES` / `LiveVoicePhase` | constant / _type_ | `opening`, `idle`, `hearing`, `thinking`, `speaking` |
|
|
18398
|
+
|
|
18399
|
+
---
|
|
18400
|
+
|
|
18303
18401
|
## `stitchkit/geo`
|
|
18304
18402
|
|
|
18305
18403
|
Server-only managed GeoIP generations. Applications own database acquisition,
|
package/llms.txt
CHANGED
|
@@ -9,7 +9,7 @@ Build with stitchkit: define a contract once, then `implement` it and serve it (
|
|
|
9
9
|
Load the slice for each entrypoint your code imports — `import … from 'stitchkit/server'` → `llms/server.txt`; the root `stitchkit` import → `llms/stitchkit.txt`. A slice holds that entrypoint's API reference section and the guide pages whose home it is; every guide lives in exactly one slice, and a slice that needs another's guide names it under "Also read". Each file is at most 50.0 KB; a longer one continues in numbered parts, each naming the next at its top. Moving across versions: load `llms/upgrading.txt` and the range slices it lists between your version and the target. `llms-full.txt` inlines everything; it is far larger than a context window, so do not read it whole.
|
|
10
10
|
|
|
11
11
|
## Entrypoint slices
|
|
12
|
-
- **stitchkit**: [llms/stitchkit.txt](llms/stitchkit.txt) 15.
|
|
12
|
+
- **stitchkit**: [llms/stitchkit.txt](llms/stitchkit.txt) 15.5 KB → [llms/stitchkit.2.txt](llms/stitchkit.2.txt) 35.0 KB → [llms/stitchkit.3.txt](llms/stitchkit.3.txt) 14.2 KB
|
|
13
13
|
- **stitchkit/live**: [llms/live.txt](llms/live.txt) 21.2 KB
|
|
14
14
|
- **stitchkit/react**: [llms/react.txt](llms/react.txt) 8.6 KB
|
|
15
15
|
- **stitchkit/tools**: [llms/tools.txt](llms/tools.txt) 44.9 KB → [llms/tools.2.txt](llms/tools.2.txt) 26.3 KB → [llms/tools.3.txt](llms/tools.3.txt) 25.4 KB
|
|
@@ -44,6 +44,7 @@ Load the slice for each entrypoint your code imports — `import … from 'stitc
|
|
|
44
44
|
- **stitchkit/release**: [llms/release.txt](llms/release.txt) 8.9 KB
|
|
45
45
|
- **stitchkit/tracking/server**: [llms/tracking-server.txt](llms/tracking-server.txt) 2.5 KB
|
|
46
46
|
- **stitchkit/geo**: [llms/geo.txt](llms/geo.txt) 4.3 KB
|
|
47
|
+
- **stitchkit/voice**: [llms/voice.txt](llms/voice.txt) 5.2 KB
|
|
47
48
|
- **stitchkit/oauth**: [llms/oauth.txt](llms/oauth.txt) 6.7 KB
|
|
48
49
|
- **stitchkit/google**: [llms/google.txt](llms/google.txt) 1.4 KB
|
|
49
50
|
- **stitchkit/declaration**: [llms/declaration.txt](llms/declaration.txt) 16.0 KB
|
|
@@ -78,6 +79,7 @@ Load the slice for each entrypoint your code imports — `import … from 'stitc
|
|
|
78
79
|
- [Multi-tenant](https://github.com/max-listov/stitchkit/blob/master/docs/guide/multi-tenant.md): a /tenants/:id/… scenario end-to-end — scopePrefixes, scoped client, extend
|
|
79
80
|
- [Frontend integrations](https://github.com/max-listov/stitchkit/blob/master/docs/guide/frontend-integrations.md): React Router resource routes and a separate Vite development proxy
|
|
80
81
|
- [React Query policy](https://github.com/max-listov/stitchkit/blob/master/docs/guide/react.md): request-local SSR clients, a browser singleton and ApiError retry rules
|
|
82
|
+
- [Voice](https://github.com/max-listov/stitchkit/blob/master/docs/guide/voice.md): stitchkit/voice — sentences cut while a reply streams, the text a voice reads, a speech queue that synthesises ahead and stops on interruption
|
|
81
83
|
- [GeoIP](https://github.com/max-listov/stitchkit/blob/master/docs/guide/geo.md): stitchkit/geo — a server-only managed reader, three observable states and last-known-good generation reloads
|
|
82
84
|
- [Upgrading](https://github.com/max-listov/stitchkit/blob/master/docs/guide/upgrading.md): moving a project across stitchkit versions; how breaking changes are marked
|
|
83
85
|
- [Project declaration](https://github.com/max-listov/stitchkit/blob/master/docs/guide/declaration.md): the optional machine-readable statement a repository makes about itself — identity, roles, build, requirements, release steps and the names of the values a deployment supplies
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stitchkit",
|
|
3
|
-
"version": "0.95.
|
|
3
|
+
"version": "0.95.3",
|
|
4
4
|
"description": "Contract-first backend framework — one defineContract() into an HTTP API, MCP tools, AI-agent tools and a typed client. Bun and Node.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bun",
|
|
@@ -190,6 +190,10 @@
|
|
|
190
190
|
"types": "./dist/entrypoints/geo.d.ts",
|
|
191
191
|
"import": "./dist/entrypoints/geo.js"
|
|
192
192
|
},
|
|
193
|
+
"./voice": {
|
|
194
|
+
"types": "./dist/entrypoints/voice.d.ts",
|
|
195
|
+
"import": "./dist/entrypoints/voice.js"
|
|
196
|
+
},
|
|
193
197
|
"./oauth": {
|
|
194
198
|
"types": "./dist/entrypoints/oauth.d.ts",
|
|
195
199
|
"import": "./dist/entrypoints/oauth.js"
|