@pellux/goodvibes-agent 0.1.1 → 0.1.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 +14 -0
- package/README.md +12 -1
- package/docs/README.md +2 -0
- package/docs/getting-started.md +19 -1
- package/docs/release-and-publishing.md +3 -1
- package/package.json +10 -1
- package/src/agent/persona-registry.ts +379 -0
- package/src/agent/skill-registry.ts +360 -0
- package/src/audio/spoken-turn-model-routing.ts +2 -1
- package/src/cli/agent-knowledge-command.ts +525 -0
- package/src/cli/help.ts +35 -0
- package/src/cli/management-commands.ts +3 -1
- package/src/cli/management.ts +33 -9
- package/src/cli/parser.ts +7 -0
- package/src/cli/types.ts +3 -0
- package/src/config/surface.ts +1 -0
- package/src/input/agent-workspace.ts +33 -3
- package/src/input/command-registry.ts +4 -1
- package/src/input/commands/agent-skills-runtime.ts +216 -0
- package/src/input/commands/delegation-runtime.ts +129 -0
- package/src/input/commands/knowledge.ts +18 -18
- package/src/input/commands/personas-runtime.ts +219 -0
- package/src/input/commands/shell-core.ts +9 -6
- package/src/input/commands/skills-runtime.ts +7 -2
- package/src/input/commands.ts +6 -0
- package/src/input/panel-integration-actions.ts +0 -52
- package/src/input/submission-router.ts +1 -1
- package/src/main.ts +2 -1
- package/src/panels/builtin/agent.ts +0 -14
- package/src/panels/builtin/session.ts +4 -3
- package/src/panels/index.ts +0 -5
- package/src/panels/orchestration-panel.ts +4 -5
- package/src/panels/qr-panel.ts +3 -2
- package/src/panels/tasks-panel.ts +4 -4
- package/src/renderer/agent-workspace.ts +2 -0
- package/src/runtime/bootstrap-command-context.ts +3 -0
- package/src/runtime/bootstrap-command-parts.ts +6 -2
- package/src/runtime/bootstrap-core.ts +8 -4
- package/src/runtime/bootstrap-shell.ts +5 -2
- package/src/runtime/bootstrap.ts +10 -2
- package/src/runtime/cloudflare-control-plane.ts +2 -1
- package/src/version.ts +1 -1
- package/src/daemon/cli.ts +0 -55
- package/src/daemon/safe-serve.ts +0 -61
- package/src/panels/diff-panel.ts +0 -520
- package/src/panels/file-explorer-panel.ts +0 -584
- package/src/panels/file-preview-panel.ts +0 -434
- package/src/panels/git-panel.ts +0 -638
- package/src/panels/sandbox-panel.ts +0 -283
- package/src/panels/symbol-outline-panel.ts +0 -486
- package/src/panels/worktree-panel.ts +0 -182
- package/src/panels/wrfc-panel.ts +0 -609
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import type { ShellPathService } from '@/runtime/index.ts';
|
|
4
|
+
import { GOODVIBES_AGENT_SURFACE_ROOT } from '../config/surface.ts';
|
|
5
|
+
import { assertNoSecretLikeText } from './persona-registry.ts';
|
|
6
|
+
|
|
7
|
+
export type AgentSkillSource = 'user' | 'agent' | 'imported' | 'system';
|
|
8
|
+
export type AgentSkillReviewState = 'fresh' | 'reviewed' | 'stale';
|
|
9
|
+
|
|
10
|
+
export interface AgentSkillRecord {
|
|
11
|
+
readonly id: string;
|
|
12
|
+
readonly name: string;
|
|
13
|
+
readonly description: string;
|
|
14
|
+
readonly procedure: string;
|
|
15
|
+
readonly triggers: readonly string[];
|
|
16
|
+
readonly tags: readonly string[];
|
|
17
|
+
readonly enabled: boolean;
|
|
18
|
+
readonly source: AgentSkillSource;
|
|
19
|
+
readonly provenance: string;
|
|
20
|
+
readonly reviewState: AgentSkillReviewState;
|
|
21
|
+
readonly staleReason?: string;
|
|
22
|
+
readonly createdAt: string;
|
|
23
|
+
readonly updatedAt: string;
|
|
24
|
+
readonly reviewedAt?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface AgentSkillCreateInput {
|
|
28
|
+
readonly name: string;
|
|
29
|
+
readonly description: string;
|
|
30
|
+
readonly procedure: string;
|
|
31
|
+
readonly triggers?: readonly string[];
|
|
32
|
+
readonly tags?: readonly string[];
|
|
33
|
+
readonly enabled?: boolean;
|
|
34
|
+
readonly source?: AgentSkillSource;
|
|
35
|
+
readonly provenance?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface AgentSkillUpdateInput {
|
|
39
|
+
readonly name?: string;
|
|
40
|
+
readonly description?: string;
|
|
41
|
+
readonly procedure?: string;
|
|
42
|
+
readonly triggers?: readonly string[];
|
|
43
|
+
readonly tags?: readonly string[];
|
|
44
|
+
readonly provenance?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface AgentSkillSnapshot {
|
|
48
|
+
readonly path: string;
|
|
49
|
+
readonly skills: readonly AgentSkillRecord[];
|
|
50
|
+
readonly enabledSkills: readonly AgentSkillRecord[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface SkillStoreFile {
|
|
54
|
+
readonly version: 1;
|
|
55
|
+
readonly skills: readonly AgentSkillRecord[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const STORE_VERSION = 1;
|
|
59
|
+
|
|
60
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
61
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function readString(value: unknown, fallback = ''): string {
|
|
65
|
+
return typeof value === 'string' ? value : fallback;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function readStringArray(value: unknown): string[] {
|
|
69
|
+
if (!Array.isArray(value)) return [];
|
|
70
|
+
return value.filter((entry): entry is string => typeof entry === 'string').map((entry) => entry.trim()).filter(Boolean);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function normalizeName(name: string): string {
|
|
74
|
+
return name.trim().replace(/\s+/g, ' ');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function normalizeList(values: readonly string[] | undefined): string[] {
|
|
78
|
+
const seen = new Set<string>();
|
|
79
|
+
const result: string[] = [];
|
|
80
|
+
for (const value of values ?? []) {
|
|
81
|
+
const trimmed = value.trim();
|
|
82
|
+
if (!trimmed) continue;
|
|
83
|
+
const key = trimmed.toLowerCase();
|
|
84
|
+
if (seen.has(key)) continue;
|
|
85
|
+
seen.add(key);
|
|
86
|
+
result.push(trimmed);
|
|
87
|
+
}
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function slugify(value: string): string {
|
|
92
|
+
const slug = value.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
93
|
+
return slug || 'skill';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function nowIso(): string {
|
|
97
|
+
return new Date().toISOString();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function parseSkill(value: unknown): AgentSkillRecord | null {
|
|
101
|
+
if (!isRecord(value)) return null;
|
|
102
|
+
const id = readString(value.id).trim();
|
|
103
|
+
const name = normalizeName(readString(value.name));
|
|
104
|
+
const description = readString(value.description).trim();
|
|
105
|
+
const procedure = readString(value.procedure).trim();
|
|
106
|
+
if (!id || !name || !description || !procedure) return null;
|
|
107
|
+
const reviewState = value.reviewState === 'reviewed' || value.reviewState === 'stale' ? value.reviewState : 'fresh';
|
|
108
|
+
const source = value.source === 'agent' || value.source === 'imported' || value.source === 'system' ? value.source : 'user';
|
|
109
|
+
const staleReason = readString(value.staleReason).trim();
|
|
110
|
+
const reviewedAt = readString(value.reviewedAt).trim();
|
|
111
|
+
return {
|
|
112
|
+
id,
|
|
113
|
+
name,
|
|
114
|
+
description,
|
|
115
|
+
procedure,
|
|
116
|
+
triggers: readStringArray(value.triggers),
|
|
117
|
+
tags: readStringArray(value.tags),
|
|
118
|
+
enabled: value.enabled === true,
|
|
119
|
+
source,
|
|
120
|
+
provenance: readString(value.provenance, source).trim() || source,
|
|
121
|
+
reviewState,
|
|
122
|
+
staleReason: staleReason || undefined,
|
|
123
|
+
createdAt: readString(value.createdAt, nowIso()),
|
|
124
|
+
updatedAt: readString(value.updatedAt, nowIso()),
|
|
125
|
+
reviewedAt: reviewedAt || undefined,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function parseStore(raw: string): SkillStoreFile {
|
|
130
|
+
const parsed: unknown = JSON.parse(raw);
|
|
131
|
+
if (!isRecord(parsed)) return { version: STORE_VERSION, skills: [] };
|
|
132
|
+
return {
|
|
133
|
+
version: STORE_VERSION,
|
|
134
|
+
skills: Array.isArray(parsed.skills)
|
|
135
|
+
? parsed.skills.map(parseSkill).filter((entry): entry is AgentSkillRecord => entry !== null)
|
|
136
|
+
: [],
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function formatStore(store: SkillStoreFile): string {
|
|
141
|
+
return `${JSON.stringify(store, null, 2)}\n`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function skillStorePath(shellPaths: ShellPathService): string {
|
|
145
|
+
return shellPaths.resolveUserPath(GOODVIBES_AGENT_SURFACE_ROOT, 'skills', 'skills.json');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export class AgentSkillRegistry {
|
|
149
|
+
public constructor(private readonly storePath: string) {}
|
|
150
|
+
|
|
151
|
+
public static fromShellPaths(shellPaths: ShellPathService): AgentSkillRegistry {
|
|
152
|
+
return new AgentSkillRegistry(skillStorePath(shellPaths));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
public snapshot(): AgentSkillSnapshot {
|
|
156
|
+
const store = this.readStore();
|
|
157
|
+
return {
|
|
158
|
+
path: this.storePath,
|
|
159
|
+
skills: [...store.skills],
|
|
160
|
+
enabledSkills: store.skills.filter((skill) => skill.enabled),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
public list(): readonly AgentSkillRecord[] {
|
|
165
|
+
return this.snapshot().skills;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
public search(query: string): readonly AgentSkillRecord[] {
|
|
169
|
+
const normalized = query.trim().toLowerCase();
|
|
170
|
+
if (!normalized) return this.list();
|
|
171
|
+
return this.list().filter((skill) => [
|
|
172
|
+
skill.id,
|
|
173
|
+
skill.name,
|
|
174
|
+
skill.description,
|
|
175
|
+
skill.procedure,
|
|
176
|
+
...skill.tags,
|
|
177
|
+
...skill.triggers,
|
|
178
|
+
].some((field) => field.toLowerCase().includes(normalized)));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
public get(idOrName: string): AgentSkillRecord | null {
|
|
182
|
+
const lookup = idOrName.trim().toLowerCase();
|
|
183
|
+
if (!lookup) return null;
|
|
184
|
+
return this.list().find((skill) => skill.id.toLowerCase() === lookup || skill.name.toLowerCase() === lookup) ?? null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
public create(input: AgentSkillCreateInput): AgentSkillRecord {
|
|
188
|
+
const store = this.readStore();
|
|
189
|
+
const name = normalizeName(input.name);
|
|
190
|
+
const description = input.description.trim();
|
|
191
|
+
const procedure = input.procedure.trim();
|
|
192
|
+
this.validateRequired(name, description, procedure);
|
|
193
|
+
assertNoSecretLikeText([name, description, procedure, ...(input.tags ?? []), ...(input.triggers ?? [])]);
|
|
194
|
+
const duplicate = store.skills.find((skill) => skill.name.toLowerCase() === name.toLowerCase());
|
|
195
|
+
if (duplicate) throw new Error(`Skill already exists: ${duplicate.id}`);
|
|
196
|
+
const timestamp = nowIso();
|
|
197
|
+
const skill: AgentSkillRecord = {
|
|
198
|
+
id: this.nextId(name, store.skills),
|
|
199
|
+
name,
|
|
200
|
+
description,
|
|
201
|
+
procedure,
|
|
202
|
+
triggers: normalizeList(input.triggers),
|
|
203
|
+
tags: normalizeList(input.tags),
|
|
204
|
+
enabled: input.enabled === true,
|
|
205
|
+
source: input.source ?? 'user',
|
|
206
|
+
provenance: input.provenance?.trim() || input.source || 'user',
|
|
207
|
+
reviewState: 'fresh',
|
|
208
|
+
createdAt: timestamp,
|
|
209
|
+
updatedAt: timestamp,
|
|
210
|
+
};
|
|
211
|
+
this.writeStore({ ...store, skills: [...store.skills, skill] });
|
|
212
|
+
return skill;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
public update(idOrName: string, input: AgentSkillUpdateInput): AgentSkillRecord {
|
|
216
|
+
const store = this.readStore();
|
|
217
|
+
const existing = this.findInStore(store, idOrName);
|
|
218
|
+
if (!existing) throw new Error(`Unknown skill: ${idOrName}`);
|
|
219
|
+
const name = input.name === undefined ? existing.name : normalizeName(input.name);
|
|
220
|
+
const description = input.description === undefined ? existing.description : input.description.trim();
|
|
221
|
+
const procedure = input.procedure === undefined ? existing.procedure : input.procedure.trim();
|
|
222
|
+
this.validateRequired(name, description, procedure);
|
|
223
|
+
assertNoSecretLikeText([name, description, procedure, ...(input.tags ?? []), ...(input.triggers ?? [])]);
|
|
224
|
+
const duplicate = store.skills.find((skill) => skill.id !== existing.id && skill.name.toLowerCase() === name.toLowerCase());
|
|
225
|
+
if (duplicate) throw new Error(`Skill already exists: ${duplicate.id}`);
|
|
226
|
+
const updated: AgentSkillRecord = {
|
|
227
|
+
...existing,
|
|
228
|
+
name,
|
|
229
|
+
description,
|
|
230
|
+
procedure,
|
|
231
|
+
triggers: input.triggers === undefined ? existing.triggers : normalizeList(input.triggers),
|
|
232
|
+
tags: input.tags === undefined ? existing.tags : normalizeList(input.tags),
|
|
233
|
+
provenance: input.provenance === undefined ? existing.provenance : input.provenance.trim() || existing.provenance,
|
|
234
|
+
reviewState: 'fresh',
|
|
235
|
+
staleReason: undefined,
|
|
236
|
+
reviewedAt: undefined,
|
|
237
|
+
updatedAt: nowIso(),
|
|
238
|
+
};
|
|
239
|
+
this.writeStore({
|
|
240
|
+
...store,
|
|
241
|
+
skills: store.skills.map((skill) => skill.id === existing.id ? updated : skill),
|
|
242
|
+
});
|
|
243
|
+
return updated;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
public setEnabled(idOrName: string, enabled: boolean): AgentSkillRecord {
|
|
247
|
+
const store = this.readStore();
|
|
248
|
+
const existing = this.findInStore(store, idOrName);
|
|
249
|
+
if (!existing) throw new Error(`Unknown skill: ${idOrName}`);
|
|
250
|
+
const updated: AgentSkillRecord = { ...existing, enabled, updatedAt: nowIso() };
|
|
251
|
+
this.writeStore({
|
|
252
|
+
...store,
|
|
253
|
+
skills: store.skills.map((skill) => skill.id === existing.id ? updated : skill),
|
|
254
|
+
});
|
|
255
|
+
return updated;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
public markReviewed(idOrName: string): AgentSkillRecord {
|
|
259
|
+
const store = this.readStore();
|
|
260
|
+
const existing = this.findInStore(store, idOrName);
|
|
261
|
+
if (!existing) throw new Error(`Unknown skill: ${idOrName}`);
|
|
262
|
+
const updated: AgentSkillRecord = {
|
|
263
|
+
...existing,
|
|
264
|
+
reviewState: 'reviewed',
|
|
265
|
+
staleReason: undefined,
|
|
266
|
+
reviewedAt: nowIso(),
|
|
267
|
+
updatedAt: nowIso(),
|
|
268
|
+
};
|
|
269
|
+
this.writeStore({
|
|
270
|
+
...store,
|
|
271
|
+
skills: store.skills.map((skill) => skill.id === existing.id ? updated : skill),
|
|
272
|
+
});
|
|
273
|
+
return updated;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
public markStale(idOrName: string, reason: string): AgentSkillRecord {
|
|
277
|
+
const store = this.readStore();
|
|
278
|
+
const existing = this.findInStore(store, idOrName);
|
|
279
|
+
if (!existing) throw new Error(`Unknown skill: ${idOrName}`);
|
|
280
|
+
const updated: AgentSkillRecord = {
|
|
281
|
+
...existing,
|
|
282
|
+
reviewState: 'stale',
|
|
283
|
+
staleReason: reason.trim() || 'Marked stale by user.',
|
|
284
|
+
updatedAt: nowIso(),
|
|
285
|
+
};
|
|
286
|
+
this.writeStore({
|
|
287
|
+
...store,
|
|
288
|
+
skills: store.skills.map((skill) => skill.id === existing.id ? updated : skill),
|
|
289
|
+
});
|
|
290
|
+
return updated;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
public deleteSkill(idOrName: string): AgentSkillRecord {
|
|
294
|
+
const store = this.readStore();
|
|
295
|
+
const existing = this.findInStore(store, idOrName);
|
|
296
|
+
if (!existing) throw new Error(`Unknown skill: ${idOrName}`);
|
|
297
|
+
this.writeStore({
|
|
298
|
+
...store,
|
|
299
|
+
skills: store.skills.filter((skill) => skill.id !== existing.id),
|
|
300
|
+
});
|
|
301
|
+
return existing;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
private validateRequired(name: string, description: string, procedure: string): void {
|
|
305
|
+
if (!name) throw new Error('Skill name is required.');
|
|
306
|
+
if (!description) throw new Error('Skill description is required.');
|
|
307
|
+
if (!procedure) throw new Error('Skill procedure is required.');
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
private nextId(name: string, skills: readonly AgentSkillRecord[]): string {
|
|
311
|
+
const base = slugify(name);
|
|
312
|
+
const ids = new Set(skills.map((skill) => skill.id));
|
|
313
|
+
if (!ids.has(base)) return base;
|
|
314
|
+
for (let index = 2; index < 1000; index += 1) {
|
|
315
|
+
const candidate = `${base}-${index}`;
|
|
316
|
+
if (!ids.has(candidate)) return candidate;
|
|
317
|
+
}
|
|
318
|
+
throw new Error(`Could not allocate skill id for ${name}.`);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
private findInStore(store: SkillStoreFile, idOrName: string): AgentSkillRecord | null {
|
|
322
|
+
const lookup = idOrName.trim().toLowerCase();
|
|
323
|
+
if (!lookup) return null;
|
|
324
|
+
return store.skills.find((skill) => skill.id.toLowerCase() === lookup || skill.name.toLowerCase() === lookup) ?? null;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private readStore(): SkillStoreFile {
|
|
328
|
+
if (!existsSync(this.storePath)) return { version: STORE_VERSION, skills: [] };
|
|
329
|
+
try {
|
|
330
|
+
return parseStore(readFileSync(this.storePath, 'utf-8'));
|
|
331
|
+
} catch (error) {
|
|
332
|
+
throw new Error(`Could not read Agent skill store: ${error instanceof Error ? error.message : String(error)}`);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
private writeStore(store: SkillStoreFile): void {
|
|
337
|
+
mkdirSync(dirname(this.storePath), { recursive: true });
|
|
338
|
+
const tmpPath = `${this.storePath}.tmp`;
|
|
339
|
+
writeFileSync(tmpPath, formatStore(store), 'utf-8');
|
|
340
|
+
renameSync(tmpPath, this.storePath);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export function buildEnabledSkillsPrompt(shellPaths: ShellPathService): string | null {
|
|
345
|
+
const enabled = AgentSkillRegistry.fromShellPaths(shellPaths).snapshot().enabledSkills;
|
|
346
|
+
if (enabled.length === 0) return null;
|
|
347
|
+
return [
|
|
348
|
+
'## Enabled GoodVibes Agent Skills',
|
|
349
|
+
'Use these local reusable procedures inside the same serial assistant conversation when they fit the user request.',
|
|
350
|
+
'',
|
|
351
|
+
...enabled.slice(0, 8).flatMap((skill) => [
|
|
352
|
+
`### ${skill.name}`,
|
|
353
|
+
`Description: ${skill.description}`,
|
|
354
|
+
`Review state: ${skill.reviewState}`,
|
|
355
|
+
`Triggers: ${skill.triggers.join(', ') || '(manual)'}`,
|
|
356
|
+
skill.procedure,
|
|
357
|
+
'',
|
|
358
|
+
]),
|
|
359
|
+
].join('\n').trim();
|
|
360
|
+
}
|
|
@@ -2,6 +2,7 @@ import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
|
2
2
|
import type { ModelDefinition, ProviderRegistry } from '@pellux/goodvibes-sdk/platform/providers';
|
|
3
3
|
import type { ContentPart } from '@pellux/goodvibes-sdk/platform/providers';
|
|
4
4
|
import type { Orchestrator, OrchestratorUserInputOptions } from '../core/orchestrator.ts';
|
|
5
|
+
import { GOODVIBES_AGENT_PAIRING_SURFACE } from '../config/surface.ts';
|
|
5
6
|
|
|
6
7
|
const SPOKEN_TURN_SOURCE = 'tts';
|
|
7
8
|
|
|
@@ -27,7 +28,7 @@ export function createSpokenTurnInputOptions(): OrchestratorUserInputOptions {
|
|
|
27
28
|
return {
|
|
28
29
|
origin: {
|
|
29
30
|
source: SPOKEN_TURN_SOURCE,
|
|
30
|
-
surface:
|
|
31
|
+
surface: GOODVIBES_AGENT_PAIRING_SURFACE,
|
|
31
32
|
metadata: { spokenOutput: true },
|
|
32
33
|
},
|
|
33
34
|
};
|