@omnivoice/page-agent 0.1.0 → 0.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.
@@ -0,0 +1,309 @@
1
+ /**
2
+ * Sessão de voz: gerencia o contexto compartilhado, enfileira inputs,
3
+ * emite eventos públicos e coordena alternância entre voz e chat.
4
+ *
5
+ * Desenho:
6
+ * - Estado mutável fica tudo aqui (contexto, histórico, turno).
7
+ * - Um dono por estado: só VoiceSession muda VoiceContext.
8
+ * - Eventos públicos são propagados via AsyncIterator.
9
+ * - Bridge e controlador observam; nunca escrevem direto.
10
+ * - Controle opcional de mutação (padrão: true / ativado).
11
+ * Pode ser desativado para modo read-only (debug/auditoria).
12
+ */
13
+
14
+ import { EventEmitter } from "node:events";
15
+ import type {
16
+ VoiceContext,
17
+ VoiceEvent,
18
+ VoiceSnapshot,
19
+ InputChannel,
20
+ TurnState,
21
+ Message,
22
+ SpeechMode,
23
+ } from "./types.ts";
24
+ import type { UserInput, VoiceSessionReader } from "./bridge.ts";
25
+ import type { VoiceEngine } from "./types.ts";
26
+ import type { VoiceSettings } from "./config.ts";
27
+
28
+ /**
29
+ * Sessão de voz: o coração do sistema.
30
+ * Implementa VoiceSessionReader para o bridge.
31
+ */
32
+ export class VoiceSession implements VoiceSessionReader {
33
+ readonly sessionId: string;
34
+ readonly context: VoiceContext;
35
+
36
+ private engine: VoiceEngine | null = null;
37
+ private settings: VoiceSettings;
38
+ private inputQueue: UserInput[] = [];
39
+ private eventEmitter: EventEmitter = new EventEmitter();
40
+ private isClosed = false;
41
+
42
+ /** Flag para ativar/desativar mutação de estado. Padrão: true (ativado). */
43
+ private allowMutations = true;
44
+
45
+ constructor(settings: VoiceSettings, allowMutations: boolean = true) {
46
+ this.settings = settings;
47
+ this.sessionId = settings.sessionId;
48
+ this.allowMutations = allowMutations;
49
+
50
+ this.context = {
51
+ mode: settings.speechMode,
52
+ channel: "voice", // começa com voz
53
+ turn: "idle",
54
+ sessionId: this.sessionId,
55
+ conversationHistory: [],
56
+ muted: !settings.mic,
57
+ lastTurnTime: undefined,
58
+ };
59
+ }
60
+
61
+ /**
62
+ * Abre a sessão: inicializa a engine, começa a escutar eventos.
63
+ */
64
+ async abrir(engine: VoiceEngine): Promise<void> {
65
+ if (this.engine) throw new Error("sessão já está aberta");
66
+
67
+ this.engine = engine;
68
+ this.isClosed = false;
69
+
70
+ await engine.enter();
71
+
72
+ // Emite evento "pronto"
73
+ this.emitPublic({
74
+ kind: "session.ready",
75
+ data: this.snapshot(),
76
+ });
77
+
78
+ // Loop de eventos da engine: transforma em públicos (se aplicável)
79
+ // e processa inputs enfileirados.
80
+ this.processEngineEvents();
81
+ }
82
+
83
+ /**
84
+ * Fecha a sessão.
85
+ */
86
+ async fechar(): Promise<void> {
87
+ this.isClosed = true;
88
+
89
+ if (this.engine) {
90
+ try {
91
+ await this.engine.exit();
92
+ } catch (err) {
93
+ console.error("[VoiceSession.fechar]", err);
94
+ }
95
+ this.engine = null;
96
+ }
97
+
98
+ this.eventEmitter.removeAllListeners();
99
+ }
100
+
101
+ /**
102
+ * Snapshot atual para o painel.
103
+ */
104
+ snapshot(): VoiceSnapshot {
105
+ return {
106
+ mode: this.context.mode,
107
+ channel: this.context.channel,
108
+ state: this.context.turn,
109
+ muted: this.context.muted,
110
+ sessionId: this.sessionId,
111
+ messageCount: this.context.conversationHistory.length,
112
+ lastMessageTime: this.context.lastTurnTime,
113
+ };
114
+ }
115
+
116
+ /**
117
+ * Habilita/desabilita mutação de estado.
118
+ * Padrão: true (ativado). false = modo auditoria (read-only).
119
+ */
120
+ setMutationsAllowed(allowed: boolean): void {
121
+ this.allowMutations = allowed;
122
+ }
123
+
124
+ /**
125
+ * Enfileira entrada do usuário (voz transcrita ou texto).
126
+ * Sem efeito se mutações desabilitadas.
127
+ */
128
+ enqueueInput(input: UserInput): void {
129
+ if (!this.allowMutations) return;
130
+
131
+ this.inputQueue.push(input);
132
+
133
+ // Sinaliza ao loop que há nova entrada
134
+ this.processNextInput();
135
+ }
136
+
137
+ /**
138
+ * Muta/desmuta o microfone.
139
+ * Sem efeito se mutações desabilitadas.
140
+ */
141
+ setMuted(muted: boolean): void {
142
+ if (!this.allowMutations) return;
143
+
144
+ this.context.muted = muted;
145
+ if (this.engine) {
146
+ this.engine.setMuted(muted);
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Itera eventos públicos até fechar.
152
+ */
153
+ async *events(): AsyncIterable<VoiceEvent> {
154
+ const iterator = this.eventEmitter;
155
+
156
+ while (!this.isClosed) {
157
+ const event: VoiceEvent = await new Promise((resolve) => {
158
+ iterator.once("public-event", (ev: VoiceEvent) => resolve(ev));
159
+ if (this.isClosed) resolve({ kind: "error", data: { message: "sessão fechada", code: "CLOSED" } });
160
+ });
161
+
162
+ yield event;
163
+
164
+ if (event.kind === "error") break;
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Processa entrada do fila.
170
+ * Atualiza histórico, emite evento ao bridge, passa ao agente.
171
+ * Sem efeito se mutações desabilitadas.
172
+ */
173
+ private async processNextInput(): Promise<void> {
174
+ if (!this.allowMutations || this.inputQueue.length === 0) return;
175
+
176
+ const input = this.inputQueue.shift()!;
177
+
178
+ // Atualiza turno
179
+ this.context.turn = "listening";
180
+ this.context.channel = input.channel;
181
+ this.context.lastTurnTime = input.timestamp;
182
+
183
+ // Registra no histórico
184
+ this.context.conversationHistory.push({
185
+ role: "user",
186
+ content: input.utterance,
187
+ timestamp: input.timestamp,
188
+ channel: input.channel,
189
+ confidence: input.confidence,
190
+ });
191
+
192
+ // Emite evento público
193
+ this.emitPublic({
194
+ kind: "turn.heard",
195
+ data: {
196
+ text: input.utterance,
197
+ confidence: input.confidence,
198
+ },
199
+ });
200
+
201
+ // Marca turno como "processando" (o agente vai responder)
202
+ this.context.turn = "processing";
203
+ }
204
+
205
+ /**
206
+ * Processa eventos da engine (Local ou Realtime) e transforma em públicos.
207
+ * Roda em background enquanto a sessão está aberta.
208
+ */
209
+ private async processEngineEvents(): Promise<void> {
210
+ if (!this.engine) return;
211
+
212
+ try {
213
+ for await (const engineEvent of this.engine.events()) {
214
+ if (this.isClosed) break;
215
+
216
+ // Transforma evento internal em público (se aplicável)
217
+ this.handleEngineEvent(engineEvent);
218
+ }
219
+ } catch (err) {
220
+ this.emitPublic({
221
+ kind: "error",
222
+ data: {
223
+ message: err instanceof Error ? err.message : "erro desconhecido",
224
+ code: "ENGINE_ERROR",
225
+ },
226
+ });
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Transforma evento da engine em público.
232
+ */
233
+ private handleEngineEvent(event: VoiceEvent): void {
234
+ // Apenas certos eventos são públicos; ver types.ts
235
+ if (
236
+ [
237
+ "session.ready",
238
+ "user.started",
239
+ "turn.heard",
240
+ "agent.started",
241
+ "agent.finished",
242
+ "sound.started",
243
+ "speaker.idle",
244
+ "error",
245
+ ].includes(event.kind)
246
+ ) {
247
+ this.emitPublic(event);
248
+ }
249
+
250
+ // Registra resposta do agente no histórico
251
+ if (event.kind === "agent.finished") {
252
+ const { text } = event.data;
253
+ this.context.conversationHistory.push({
254
+ role: "assistant",
255
+ content: text,
256
+ timestamp: Date.now(),
257
+ channel: this.context.channel,
258
+ });
259
+ this.context.turn = "idle";
260
+ }
261
+
262
+ // Atualiza turno conforme eventos
263
+ if (event.kind === "agent.started") {
264
+ this.context.turn = "speaking";
265
+ }
266
+ }
267
+
268
+ /**
269
+ * Emite evento público (para bridge e painel).
270
+ */
271
+ private emitPublic(event: VoiceEvent): void {
272
+ this.eventEmitter.emit("public-event", event);
273
+ }
274
+
275
+ /**
276
+ * Alterna entre voz e chat.
277
+ * O histórico é preservado; apenas a origem do próximo input muda.
278
+ * Sem efeito se mutações desabilitadas.
279
+ */
280
+ switchToChannel(channel: InputChannel): void {
281
+ if (!this.allowMutations) return;
282
+ if (this.context.channel === channel) return;
283
+
284
+ const from = this.context.channel;
285
+ this.context.channel = channel;
286
+
287
+ this.emitPublic({
288
+ kind: "channel.switched",
289
+ data: { from, to: channel },
290
+ });
291
+ }
292
+ }
293
+
294
+ /**
295
+ * Factory: cria e abre uma sessão de voz com a engine apropriada.
296
+ *
297
+ * Usa o settings.speechMode para escolher Local ou Realtime,
298
+ * e carrega a engine correspondente.
299
+ */
300
+ export async function createVoiceSession(settings: VoiceSettings): Promise<VoiceSession> {
301
+ const session = new VoiceSession(settings);
302
+
303
+ const { createEngine } = await import("./engines.ts");
304
+ const engine = await createEngine(settings);
305
+
306
+ await session.abrir(engine);
307
+
308
+ return session;
309
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Types compartilhados para a integração de voz.
3
+ * Baseado no pattern Conversation + Engine de NoStopPageTest.
4
+ */
5
+
6
+ /** Modo de fala: local (modelos no device) ou realtime (OpenAI full-duplex). */
7
+ export type SpeechMode = "local" | "realtime";
8
+
9
+ /** Canal ativo de entrada: voz (microfone) ou chat (texto). */
10
+ export type InputChannel = "voice" | "chat";
11
+
12
+ /** Tipo de efeito observável após ação. */
13
+ export type EffectKind = "notification" | "navigation" | "loading";
14
+
15
+ /** Resultado da observação de efeito. */
16
+ export type EffectOutcome =
17
+ | { status: "observed"; kind: EffectKind; detail: string }
18
+ | { status: "unknown"; detail: string };
19
+
20
+ /** Uma mensagem no histórico de conversa. */
21
+ export type Message = {
22
+ role: "user" | "assistant";
23
+ content: string;
24
+ timestamp: number; // milliseconds since epoch
25
+ channel: InputChannel; // voz ou chat
26
+ confidence?: number; // 0-1, para speech-to-text
27
+ };
28
+
29
+ /** Estado de um turno: agente escutando, processando, falando. */
30
+ export type TurnState = "idle" | "listening" | "processing" | "speaking";
31
+
32
+ /** Snapshot do estado de voz para o painel. */
33
+ export type VoiceSnapshot = {
34
+ mode: SpeechMode;
35
+ channel: InputChannel;
36
+ state: TurnState;
37
+ muted: boolean;
38
+ sessionId: string;
39
+ messageCount: number;
40
+ lastMessageTime?: number;
41
+ error?: string;
42
+ };
43
+
44
+ /** Estado compartilhado entre voz e navegação. */
45
+ export type VoiceContext = {
46
+ mode: SpeechMode;
47
+ channel: InputChannel;
48
+ turn: TurnState;
49
+ sessionId: string;
50
+ conversationHistory: Message[];
51
+ muted: boolean;
52
+ lastTurnTime?: number;
53
+ };
54
+
55
+ /** Evento emitido pela sessão de voz. */
56
+ export type VoiceEvent =
57
+ | { kind: "session.ready"; data: VoiceSnapshot }
58
+ | { kind: "user.started"; data: { channel: InputChannel } }
59
+ | { kind: "turn.heard"; data: { text: string; confidence?: number } }
60
+ | { kind: "agent.started"; data: { turn: TurnState } }
61
+ | { kind: "agent.finished"; data: { text: string } }
62
+ | { kind: "sound.started"; data: object }
63
+ | { kind: "speaker.idle"; data: object }
64
+ | { kind: "channel.switched"; data: { from: InputChannel; to: InputChannel } }
65
+ | { kind: "error"; data: { message: string; code: string } };
66
+
67
+ /** Capacidades oferecidas pela engine de voz. */
68
+ export const TEXT_INPUT = "text_input";
69
+ export const MUTE = "mute";
70
+ export const REPLAY = "replay";
71
+
72
+ export type Capability = typeof TEXT_INPUT | typeof MUTE | typeof REPLAY | string;
73
+
74
+ /**
75
+ * Protocol para uma engine de voz (Local ou Realtime).
76
+ * Baseado no EngineLike de NoStopPageTest.
77
+ */
78
+ export interface VoiceEngine {
79
+ readonly capabilities: ReadonlySet<Capability>;
80
+
81
+ /** Abre a engine (inicializa modelos, conexões, etc). */
82
+ enter(): Promise<void>;
83
+
84
+ /** Fecha a engine. */
85
+ exit(exc?: Error): Promise<void>;
86
+
87
+ /** Snapshot atual para o painel. */
88
+ snapshot(): VoiceSnapshot;
89
+
90
+ /** Muta ou desmuta o microfone. */
91
+ setMuted(muted: boolean): void;
92
+
93
+ /** Submete texto como se tivesse sido ouvido (se TEXT_INPUT). */
94
+ submit(text: string): void;
95
+
96
+ /** Itera eventos públicos até fecha. */
97
+ events(): AsyncIterable<VoiceEvent>;
98
+ }
package/LICENSE DELETED
@@ -1,22 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 SimonLuvRamen
4
- Copyright (c) 2026 Alibaba Group Holding Limited
5
-
6
- Permission is hereby granted, free of charge, to any person obtaining a copy
7
- of this software and associated documentation files (the "Software"), to deal
8
- in the Software without restriction, including without limitation the rights
9
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
- copies of the Software, and to permit persons to whom the Software is
11
- furnished to do so, subject to the following conditions:
12
-
13
- The above copyright notice and this permission notice shall be included in all
14
- copies or substantial portions of the Software.
15
-
16
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
- SOFTWARE.
@@ -1,26 +0,0 @@
1
- import { AgentConfig } from '@omnivoice/core';
2
- import { PageAgentCore } from '@omnivoice/core';
3
- import { PageControllerConfig } from '@omnivoice/page-controller';
4
- import { Panel } from '@omnivoice/ui';
5
- import { PanelConfig } from '@omnivoice/ui';
6
-
7
- export declare class PageAgent extends PageAgentCore {
8
- panel: Panel;
9
- constructor(config: PageAgentConfig);
10
- }
11
-
12
- export declare type PageAgentConfig = AgentConfig & PageControllerConfig & Omit<PanelConfig, 'language'>;
13
-
14
-
15
- export * from "@omnivoice/core";
16
-
17
- export { }
18
-
19
-
20
-
21
- declare global {
22
- interface Window {
23
- pageAgent?: PageAgent
24
- PageAgent: typeof PageAgent
25
- }
26
- }
@@ -1,30 +0,0 @@
1
- import { PageAgentCore } from "@omnivoice/core";
2
- import { PageController } from "@omnivoice/page-controller";
3
- import { Panel } from "@omnivoice/ui";
4
- export * from "@omnivoice/core";
5
- //#region src/PageAgent.ts
6
- /**
7
- * Copyright (C) 2025 Alibaba Group Holding Limited
8
- * All rights reserved.
9
- */
10
- var PageAgent = class extends PageAgentCore {
11
- panel;
12
- constructor(config) {
13
- const pageController = new PageController({
14
- ...config,
15
- enableMask: config.enableMask ?? true
16
- });
17
- super({
18
- ...config,
19
- pageController
20
- });
21
- this.panel = new Panel(this, {
22
- language: config.language,
23
- promptForNextTask: config.promptForNextTask
24
- });
25
- }
26
- };
27
- //#endregion
28
- export { PageAgent };
29
-
30
- //# sourceMappingURL=page-agent.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"page-agent.js","names":[],"sources":["../../src/PageAgent.ts"],"sourcesContent":["/**\n * Copyright (C) 2025 Alibaba Group Holding Limited\n * All rights reserved.\n */\nimport { type AgentConfig, PageAgentCore } from '@omnivoice/core'\nimport { PageController, type PageControllerConfig } from '@omnivoice/page-controller'\nimport { Panel, type PanelConfig } from '@omnivoice/ui'\n\nexport * from '@omnivoice/core'\n\nexport type PageAgentConfig = AgentConfig & PageControllerConfig & Omit<PanelConfig, 'language'>\n\nexport class PageAgent extends PageAgentCore {\n\tpanel: Panel\n\n\tconstructor(config: PageAgentConfig) {\n\t\tconst pageController = new PageController({\n\t\t\t...config,\n\t\t\tenableMask: config.enableMask ?? true,\n\t\t})\n\n\t\tsuper({ ...config, pageController })\n\n\t\tthis.panel = new Panel(this, {\n\t\t\tlanguage: config.language,\n\t\t\tpromptForNextTask: config.promptForNextTask,\n\t\t})\n\t}\n}\n"],"mappings":";;;;;;;;;AAYA,IAAa,YAAb,cAA+B,cAAc;CAC5C;CAEA,YAAY,QAAyB;EACpC,MAAM,iBAAiB,IAAI,eAAe;GACzC,GAAG;GACH,YAAY,OAAO,cAAc;EAClC,CAAC;EAED,MAAM;GAAE,GAAG;GAAQ;EAAe,CAAC;EAEnC,KAAK,QAAQ,IAAI,MAAM,MAAM;GAC5B,UAAU,OAAO;GACjB,mBAAmB,OAAO;EAC3B,CAAC;CACF;AACD"}