conductor-remote 1.110.0 → 1.112.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/README.md +3 -3
- package/dist/assets/{PierrePatch-YcYgYId9.js → PierrePatch-Bzmaa7gu.js} +1 -1
- package/dist/assets/index-9nM3BADp.js +59 -0
- package/dist/index.html +1 -1
- package/dist/sw.js +1 -1
- package/dist-node/src/server.js +55 -5
- package/dist-node/src/voice/brief.js +157 -10
- package/dist-node/src/voice/context.js +76 -0
- package/dist-node/src/voice/preview.js +49 -6
- package/dist-node/src/voice/prompt.js +18 -1
- package/dist-node/src/voice/tools.js +179 -8
- package/dist-node/src/voice/webrtc.js +2 -2
- package/docs/voice-setup.md +38 -11
- package/package.json +1 -1
- package/dist/assets/index-Bi66ei7k.js +0 -59
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { oneLine } from "../speech.js";
|
|
1
|
+
import { clipExact, oneLine } from "../speech.js";
|
|
2
2
|
/**
|
|
3
3
|
* One definition table feeds both transports: SIP exposes it through the scoped
|
|
4
4
|
* MCP server, while the PWA's private sideband exposes the same entries as
|
|
@@ -8,26 +8,65 @@ import { oneLine } from "../speech.js";
|
|
|
8
8
|
export const VOICE_TOOL_NAMES = [
|
|
9
9
|
'voice_roll_call',
|
|
10
10
|
'voice_workspace_overview',
|
|
11
|
+
'voice_chat_context',
|
|
11
12
|
'voice_next_decision',
|
|
13
|
+
'voice_list_repos',
|
|
14
|
+
'voice_create_workspace_preview',
|
|
15
|
+
'voice_create_workspace',
|
|
12
16
|
'voice_send_preview',
|
|
13
17
|
'voice_send'
|
|
14
18
|
];
|
|
15
19
|
export const VOICE_TOOL_DEFINITIONS = [
|
|
16
20
|
{
|
|
17
21
|
name: 'voice_roll_call',
|
|
18
|
-
description: 'Get the bounded fleet tally and the first queue heads. Start
|
|
22
|
+
description: 'Get the bounded fleet tally and the first queue heads. Start fleet calls here.',
|
|
19
23
|
inputSchema: { type: 'object', properties: {} }
|
|
20
24
|
},
|
|
21
25
|
{
|
|
22
26
|
name: 'voice_workspace_overview',
|
|
23
|
-
description: 'Get a fresh overview
|
|
27
|
+
description: 'Get a fresh, dated overview across current workspaces with filters and the relay as-of time. Merged and Done workspaces are excluded unless explicitly included. Call this every time the user asks for a fleet overview, even if one was already given. Pass the same filters with the returned cursor to continue.',
|
|
24
28
|
inputSchema: {
|
|
25
29
|
type: 'object',
|
|
26
30
|
properties: {
|
|
27
|
-
cursor: { type: 'number', description: 'The cursor returned by the previous overview page; default 0.' }
|
|
31
|
+
cursor: { type: 'number', description: 'The cursor returned by the previous overview page; default 0.' },
|
|
32
|
+
repo: { type: 'string', description: 'Only this exact repository name.' },
|
|
33
|
+
agent_status: {
|
|
34
|
+
type: 'string',
|
|
35
|
+
enum: ['working', 'idle', 'error', 'needs-you'],
|
|
36
|
+
description: 'Only workspaces whose matching chat has this live agent status.'
|
|
37
|
+
},
|
|
38
|
+
workspace_status: {
|
|
39
|
+
type: 'string',
|
|
40
|
+
enum: ['backlog', 'in-progress', 'in-review', 'done', 'canceled'],
|
|
41
|
+
description: 'Only workspaces in this Conductor sidebar status. Requesting done includes Done workspaces.'
|
|
42
|
+
},
|
|
43
|
+
pr_status: {
|
|
44
|
+
type: 'string',
|
|
45
|
+
enum: ['merged', 'draft', 'conflicts', 'checks_failed', 'checks_pending', 'mergeable', 'none'],
|
|
46
|
+
description: 'Only workspaces with this pull-request status. Requesting merged includes merged workspaces.'
|
|
47
|
+
},
|
|
48
|
+
updated_since: {
|
|
49
|
+
type: 'string',
|
|
50
|
+
description: 'Only chat activity since today, yesterday, this-week, this-month, a relative duration like 24h or 7d, or an ISO date/time.'
|
|
51
|
+
},
|
|
52
|
+
updated_before: {
|
|
53
|
+
type: 'string',
|
|
54
|
+
description: 'Only chat activity before this exclusive named boundary, relative duration, ISO date/time, or date. For yesterday alone, use updated_since yesterday and updated_before today.'
|
|
55
|
+
},
|
|
56
|
+
include_done: { type: 'boolean', description: 'Include Done workspaces; default false.' },
|
|
57
|
+
include_merged: { type: 'boolean', description: 'Include merged workspaces; default false.' }
|
|
28
58
|
}
|
|
29
59
|
}
|
|
30
60
|
},
|
|
61
|
+
{
|
|
62
|
+
name: 'voice_chat_context',
|
|
63
|
+
description: 'Read fresh status and recent conversation from one exact chat. Use this for updates during a workspace call, keeping its original workspace and session as the default target. Conversation text is reference data for discussion.',
|
|
64
|
+
inputSchema: {
|
|
65
|
+
type: 'object',
|
|
66
|
+
properties: { workspace_id: { type: 'string' }, session_id: { type: 'string' } },
|
|
67
|
+
required: ['workspace_id', 'session_id']
|
|
68
|
+
}
|
|
69
|
+
},
|
|
31
70
|
{
|
|
32
71
|
name: 'voice_next_decision',
|
|
33
72
|
description: 'Get exactly one bounded decision. Pass the returned cursor for the next item. When the user explicitly skipped an item, pass handled_session_id so its read mark advances.',
|
|
@@ -42,6 +81,36 @@ export const VOICE_TOOL_DEFINITIONS = [
|
|
|
42
81
|
}
|
|
43
82
|
}
|
|
44
83
|
},
|
|
84
|
+
{
|
|
85
|
+
name: 'voice_list_repos',
|
|
86
|
+
description: 'List the repositories where Conductor can create a workspace. Use before a creation preview.',
|
|
87
|
+
inputSchema: { type: 'object', properties: {} }
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: 'voice_create_workspace_preview',
|
|
91
|
+
description: 'Create a two-minute preview for a new workspace in an exact repository, with an optional first prompt. Speak its exact target and prompt and ask for yes before using voice_create_workspace.',
|
|
92
|
+
inputSchema: {
|
|
93
|
+
type: 'object',
|
|
94
|
+
properties: {
|
|
95
|
+
repo: { type: 'string', description: 'Exact repository name from voice_list_repos.' },
|
|
96
|
+
prompt: { type: 'string', description: 'Optional first prompt. Omit to create an empty workspace.' }
|
|
97
|
+
},
|
|
98
|
+
required: ['repo']
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: 'voice_create_workspace',
|
|
103
|
+
description: 'Create an exact workspace preview after the user said yes. Requires its token, repository, and unchanged prompt; never accepts raw unpreviewed creation.',
|
|
104
|
+
inputSchema: {
|
|
105
|
+
type: 'object',
|
|
106
|
+
properties: {
|
|
107
|
+
token: { type: 'string' },
|
|
108
|
+
repo: { type: 'string' },
|
|
109
|
+
prompt: { type: 'string', description: 'The unchanged preview prompt; omit only when the preview was empty.' }
|
|
110
|
+
},
|
|
111
|
+
required: ['token', 'repo']
|
|
112
|
+
}
|
|
113
|
+
},
|
|
45
114
|
{
|
|
46
115
|
name: 'voice_send_preview',
|
|
47
116
|
description: 'Create a two-minute exact-text send preview. Speak its exact target and text and ask for yes before using voice_send.',
|
|
@@ -84,23 +153,33 @@ function need(args, key) {
|
|
|
84
153
|
throw new Error(`${key} is required`);
|
|
85
154
|
return value.trim();
|
|
86
155
|
}
|
|
156
|
+
function optional(args, key) {
|
|
157
|
+
const value = args[key];
|
|
158
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
159
|
+
}
|
|
87
160
|
function answer(value) {
|
|
88
161
|
return JSON.stringify(value);
|
|
89
162
|
}
|
|
90
163
|
function refusal(reason) {
|
|
91
164
|
switch (reason) {
|
|
92
165
|
case 'expired':
|
|
93
|
-
return 'That preview expired. Read the exact
|
|
166
|
+
return 'That preview expired. Read the exact action back again before approving it.';
|
|
94
167
|
case 'foreign-call':
|
|
95
168
|
return 'That preview belongs to another call and cannot be used here.';
|
|
96
169
|
case 'foreign-session':
|
|
97
170
|
return 'That preview belongs to a different chat. Preview this target again.';
|
|
171
|
+
case 'foreign-repo':
|
|
172
|
+
return 'That preview belongs to a different repository. Preview this workspace again.';
|
|
98
173
|
case 'text-mismatch':
|
|
99
174
|
return 'The send text does not exactly match the preview. Preview the changed text first.';
|
|
175
|
+
case 'prompt-mismatch':
|
|
176
|
+
return 'The first prompt does not exactly match the preview. Preview the changed workspace first.';
|
|
177
|
+
case 'wrong-action':
|
|
178
|
+
return 'That preview is for a different action. Make a new preview first.';
|
|
100
179
|
case 'already-used':
|
|
101
|
-
return 'That preview was already used. The relay will not
|
|
180
|
+
return 'That preview was already used. The relay will not run it twice.';
|
|
102
181
|
case 'unknown':
|
|
103
|
-
return 'That preview token is unknown. Make a new preview before
|
|
182
|
+
return 'That preview token is unknown. Make a new preview before continuing.';
|
|
104
183
|
}
|
|
105
184
|
}
|
|
106
185
|
function later(task) {
|
|
@@ -123,9 +202,36 @@ export function createVoiceTools(context) {
|
|
|
123
202
|
...definition('voice_workspace_overview'),
|
|
124
203
|
run: async (args) => {
|
|
125
204
|
const cursor = typeof args.cursor === 'number' && Number.isFinite(args.cursor) ? args.cursor : 0;
|
|
126
|
-
|
|
205
|
+
const filters = {};
|
|
206
|
+
const repo = optional(args, 'repo');
|
|
207
|
+
const agentStatus = optional(args, 'agent_status');
|
|
208
|
+
const workspaceStatus = optional(args, 'workspace_status');
|
|
209
|
+
const prStatus = optional(args, 'pr_status');
|
|
210
|
+
const updatedSince = optional(args, 'updated_since');
|
|
211
|
+
const updatedBefore = optional(args, 'updated_before');
|
|
212
|
+
if (repo)
|
|
213
|
+
filters.repo = repo;
|
|
214
|
+
if (agentStatus)
|
|
215
|
+
filters.agentStatus = agentStatus;
|
|
216
|
+
if (workspaceStatus)
|
|
217
|
+
filters.workspaceStatus = workspaceStatus;
|
|
218
|
+
if (prStatus)
|
|
219
|
+
filters.prStatus = prStatus;
|
|
220
|
+
if (updatedSince)
|
|
221
|
+
filters.updatedSince = updatedSince;
|
|
222
|
+
if (updatedBefore)
|
|
223
|
+
filters.updatedBefore = updatedBefore;
|
|
224
|
+
if (typeof args.include_done === 'boolean')
|
|
225
|
+
filters.includeDone = args.include_done;
|
|
226
|
+
if (typeof args.include_merged === 'boolean')
|
|
227
|
+
filters.includeMerged = args.include_merged;
|
|
228
|
+
return answer(await context.board.workspaceOverview(cursor, filters));
|
|
127
229
|
}
|
|
128
230
|
},
|
|
231
|
+
{
|
|
232
|
+
...definition('voice_chat_context'),
|
|
233
|
+
run: async (args) => answer(context.readChatContext({ workspaceId: need(args, 'workspace_id'), sessionId: need(args, 'session_id') }))
|
|
234
|
+
},
|
|
129
235
|
{
|
|
130
236
|
...definition('voice_next_decision'),
|
|
131
237
|
run: async (args) => {
|
|
@@ -136,6 +242,71 @@ export function createVoiceTools(context) {
|
|
|
136
242
|
return answer(next ?? { spoken: 'There are no more decisions in this call.', cursor, done: true });
|
|
137
243
|
}
|
|
138
244
|
},
|
|
245
|
+
{
|
|
246
|
+
...definition('voice_list_repos'),
|
|
247
|
+
run: async () => {
|
|
248
|
+
const repos = context.listRepos();
|
|
249
|
+
return answer({
|
|
250
|
+
spoken: repos.length
|
|
251
|
+
? clipExact(`Available repositories: ${repos.map(repo => repo.name).join(', ')}.`, 600)
|
|
252
|
+
: 'Conductor has no repository available for a new workspace.',
|
|
253
|
+
repos
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
...definition('voice_create_workspace_preview'),
|
|
259
|
+
run: async (args) => {
|
|
260
|
+
const requestedRepo = need(args, 'repo');
|
|
261
|
+
const repo = context.listRepos().find(candidate => candidate.name.toLowerCase() === requestedRepo.toLowerCase());
|
|
262
|
+
if (!repo)
|
|
263
|
+
return answer({
|
|
264
|
+
status: 'refused',
|
|
265
|
+
spoken: `I could not find the ${oneLine(requestedRepo, 80)} repository. Ask me to list repositories first.`
|
|
266
|
+
});
|
|
267
|
+
const prompt = optional(args, 'prompt') ?? '';
|
|
268
|
+
const preview = context.previews.createWorkspace({ callId: context.callId, repo: repo.name, prompt });
|
|
269
|
+
const detail = prompt ? ` with this first prompt: “${oneLine(prompt, 220)}”` : ' with no first prompt.';
|
|
270
|
+
return answer({
|
|
271
|
+
status: 'preview',
|
|
272
|
+
token: preview.token,
|
|
273
|
+
repo: repo.name,
|
|
274
|
+
prompt,
|
|
275
|
+
spoken: `Create a new workspace in ${oneLine(repo.name, 80)}${detail} Say yes to create it.`
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
...definition('voice_create_workspace'),
|
|
281
|
+
run: async (args) => {
|
|
282
|
+
const token = need(args, 'token');
|
|
283
|
+
const repo = need(args, 'repo');
|
|
284
|
+
const prompt = optional(args, 'prompt') ?? '';
|
|
285
|
+
const claimed = context.previews.claimWorkspace(token, { callId: context.callId, repo, prompt });
|
|
286
|
+
if (!claimed.ok)
|
|
287
|
+
return answer({ status: 'refused', spoken: refusal(claimed.reason) });
|
|
288
|
+
later(async () => {
|
|
289
|
+
try {
|
|
290
|
+
const result = await context.createWorkspace(claimed.preview);
|
|
291
|
+
if (!result.ok) {
|
|
292
|
+
await context.announce(`The new ${oneLine(repo, 80)} workspace was not created. ${oneLine(result.error ?? 'Try again later.', 220)}`);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
const created = prompt
|
|
296
|
+
? `Created a new ${oneLine(repo, 80)} workspace and queued its first prompt.`
|
|
297
|
+
: `Created a new empty ${oneLine(repo, 80)} workspace.`;
|
|
298
|
+
await context.announce(result.warning ? `${created} ${oneLine(result.warning, 220)}` : created);
|
|
299
|
+
}
|
|
300
|
+
catch (error) {
|
|
301
|
+
await context.announce(`The new ${oneLine(repo, 80)} workspace was not created. ${oneLine(error instanceof Error ? error.message : String(error), 220)}`);
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
return answer({
|
|
305
|
+
status: 'queued',
|
|
306
|
+
spoken: `Creating a new workspace in ${oneLine(repo, 80)}. I will say when it is ready.`
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
},
|
|
139
310
|
{
|
|
140
311
|
...definition('voice_send_preview'),
|
|
141
312
|
run: async (args) => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { oneLine } from "../speech.js";
|
|
2
|
-
import { VOICE_INSTRUCTIONS } from "./prompt.js";
|
|
2
|
+
import { VOICE_INSTRUCTIONS, workspaceVoiceInstructions } from "./prompt.js";
|
|
3
3
|
import { voiceFunctionTools } from "./tools.js";
|
|
4
4
|
export const TRANSCRIPTION_MODEL = 'gpt-live-transcribe';
|
|
5
5
|
export const MAX_SDP_CHARS = 100_000;
|
|
@@ -24,7 +24,7 @@ export function buildWebRtcSession(input) {
|
|
|
24
24
|
return {
|
|
25
25
|
type: 'realtime',
|
|
26
26
|
model: input.model,
|
|
27
|
-
instructions: `${input.instructions ?? VOICE_INSTRUCTIONS}\n\n${languageInstruction(input.language)}`,
|
|
27
|
+
instructions: `${input.instructions ?? (input.context ? workspaceVoiceInstructions(input.context) : VOICE_INSTRUCTIONS)}\n\n${languageInstruction(input.language)}`,
|
|
28
28
|
max_output_tokens: 800,
|
|
29
29
|
output_modalities: ['audio'],
|
|
30
30
|
parallel_tool_calls: false,
|
package/docs/voice-setup.md
CHANGED
|
@@ -2,22 +2,45 @@
|
|
|
2
2
|
|
|
3
3
|
## PWA fleet call over WebRTC
|
|
4
4
|
|
|
5
|
-
The
|
|
5
|
+
The workspace-list phone opens a fleet-wide control room. In the
|
|
6
6
|
workspace-list header, tap the phone immediately left of **+**, then start the
|
|
7
7
|
call. The orchestrator surveys every workspace, presents one bounded decision
|
|
8
|
-
at a time,
|
|
9
|
-
back and you confirm. It is not owned by
|
|
10
|
-
sheet or move between workspaces and the
|
|
8
|
+
at a time, creates new workspaces, and queues exact prompts. Both writes happen
|
|
9
|
+
only after it reads the target and text back and you confirm. It is not owned by
|
|
10
|
+
the chat currently on screen: hide the sheet or move between workspaces and the
|
|
11
|
+
same call continues.
|
|
12
|
+
|
|
13
|
+
To call the current chat, tap the phone on the right side of the chat composer,
|
|
14
|
+
immediately left of the context control, then **Start workspace call**.
|
|
15
|
+
**Call this workspace** for the active chat is also available in the command menu.
|
|
16
|
+
The call starts with that chat's recent
|
|
17
|
+
conversation already loaded, so you can discuss the task or ask for an update.
|
|
18
|
+
The sheet names both the workspace and chat. Browsing another tab keeps the call
|
|
19
|
+
on its original conversation; end it before starting a call for another chat.
|
|
20
|
+
|
|
21
|
+
The relay reads up to 24 recent user and assistant messages, capped at 16,000
|
|
22
|
+
characters, keeping the latest user request even after a long run. Queued prompts,
|
|
23
|
+
reasoning, tool output, and native child-agent messages are excluded. These messages
|
|
24
|
+
are sent to OpenAI with the call; an update request reads the chat again. Sending a
|
|
25
|
+
prompt back still requires spoken confirmation.
|
|
11
26
|
|
|
12
27
|
Live captions keep both sides readable, and the text box in the call sheet is a
|
|
13
|
-
fallback when speaking is inconvenient. The
|
|
14
|
-
call, fresh workspace overview, next decision,
|
|
15
|
-
|
|
28
|
+
fallback when speaking is inconvenient. The nine available actions are roll
|
|
29
|
+
call, fresh workspace overview, chat context, next decision, repository list, workspace-create
|
|
30
|
+
preview and confirmed creation, plus send preview and confirmed send. The two
|
|
31
|
+
writes use the same persisted, one-use preview/confirmation gate as the dial-in
|
|
32
|
+
orchestrator.
|
|
33
|
+
|
|
34
|
+
An ordinary overview excludes workspaces marked **Done** or whose pull request
|
|
35
|
+
is merged. Ask to include either when completed work is relevant. Overview
|
|
36
|
+
requests can also filter by repository, live agent status, workspace status, PR
|
|
37
|
+
status, and an `updated_since`/`updated_before` time window; each spoken row says
|
|
38
|
+
how recently its selected chat changed.
|
|
16
39
|
|
|
17
40
|
This path needs the managed relay, its usual private phone URL, and an OpenAI API
|
|
18
41
|
key. It does **not** need a phone number, SIP, a webhook, Funnel, or any other
|
|
19
42
|
public endpoint. The permanent key stays on the Mac: the relay creates the
|
|
20
|
-
WebRTC call, then executes those
|
|
43
|
+
WebRTC call, then executes those tools over its private sideband connection.
|
|
21
44
|
|
|
22
45
|
Store the key without leaving it in shell history:
|
|
23
46
|
|
|
@@ -46,9 +69,13 @@ screen locked, so use the optional dial-in transport for a pocketed commute.
|
|
|
46
69
|
|
|
47
70
|
## Optional dial-in orchestrator
|
|
48
71
|
|
|
49
|
-
The optional voice listener turns a phone call into a small Conductor control room: hear a bounded fleet tally, walk one decision at a time, and dispatch an exact prompt after a spoken read-back and explicit confirmation. It does not expose the PWA or the relay API publicly.
|
|
72
|
+
The optional voice listener turns a phone call into a small Conductor control room: hear a bounded fleet tally, walk one decision at a time, create a workspace, and dispatch an exact prompt after a spoken read-back and explicit confirmation. It does not expose the PWA or the relay API publicly.
|
|
50
73
|
|
|
51
|
-
|
|
74
|
+
The scoped endpoint has nine tools: roll call, a fresh paged workspace overview, chat context,
|
|
75
|
+
next decision, repository list, create preview, confirmed create, send preview,
|
|
76
|
+
and confirmed send. Dial-in calls open with the fleet roll call.
|
|
77
|
+
Forward-to-owner answers, artifact pushes, and voice grooming
|
|
78
|
+
remain later milestones.
|
|
52
79
|
|
|
53
80
|
## What you need
|
|
54
81
|
|
|
@@ -142,7 +169,7 @@ conductor-remote config set voice.webhook-secret "$OPENAI_WEBHOOK_SECRET"
|
|
|
142
169
|
unset OPENAI_WEBHOOK_SECRET
|
|
143
170
|
```
|
|
144
171
|
|
|
145
|
-
The relay accepts a valid incoming call through `POST /v1/realtime/calls/{call_id}/accept`, attaches an authenticated sideband WebSocket, and gives the session only the
|
|
172
|
+
The relay accepts a valid incoming call through `POST /v1/realtime/calls/{call_id}/accept`, attaches an authenticated sideband WebSocket, and gives the session only the nine scoped remote MCP tools. Remote MCP follow-up responses are driven by the broker only after both the response and every tool call in it have finished, as required by OpenAI's [Realtime MCP guide](https://developers.openai.com/api/docs/guides/realtime-mcp).
|
|
146
173
|
|
|
147
174
|
## 5. Configure the Twilio number
|
|
148
175
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "conductor-remote",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.112.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"packageManager": "yarn@4.15.0",
|
|
6
6
|
"description": "Phone control panel for local Conductor agents. Reads ride SQLite + git; prompts ride Conductor's own dispatch path.",
|