conductor-remote 1.129.0 → 1.130.1
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/dist/assets/AutoModelSettings-DQAuzuHG.js +1 -0
- package/dist/assets/{PierrePatch-DBoBtHa-.js → PierrePatch-CSMuVBjh.js} +1 -1
- package/dist/assets/index-B4mWU6o0.js +67 -0
- package/dist/assets/index-DD0rU7nR.css +1 -0
- package/dist/index.html +2 -2
- package/dist/sw.js +1 -1
- package/dist-node/src/agents/auto-model/config.js +153 -0
- package/dist-node/src/agents/auto-model/decision.js +101 -0
- package/dist-node/src/agents/auto-model/provider.js +228 -0
- package/dist-node/src/agents/auto-model/queue.js +266 -0
- package/dist-node/src/agents/auto-model/types.js +1 -0
- package/dist-node/src/contracts/agent-inputs.js +2 -0
- package/dist-node/src/http/router.js +2 -0
- package/dist-node/src/http/routes/auto-model.js +26 -0
- package/dist-node/src/http/routes/create-workspace.js +36 -1
- package/dist-node/src/http/routes/prompts.js +56 -4
- package/dist-node/src/http/routes/sessions.js +8 -0
- package/dist-node/src/http/routes/state.js +3 -2
- package/dist-node/src/http/routes/workflows.js +7 -0
- package/dist-node/src/http/routes/workspaces.js +3 -1
- package/dist-node/src/http/services/auto-model.js +108 -0
- package/dist-node/src/http/services/delivery.js +7 -2
- package/dist-node/src/http/services.js +3 -0
- package/dist-node/src/mcp/tools/workspaces.js +2 -0
- package/dist-node/src/prefs.js +2 -0
- package/dist-node/src/routes.js +2 -0
- package/dist-node/src/server.js +2 -1
- package/package.json +1 -1
- package/dist/assets/index-D1kDyIDN.js +0 -67
- package/dist/assets/index-q-Z5Zz9y.css +0 -1
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { routerProvider } from "./config.js";
|
|
6
|
+
import { ROUTER_INSTRUCTIONS } from "./decision.js";
|
|
7
|
+
/** Picker labels and CLI IDs are distinct. Restrict routing to known label shapes. */
|
|
8
|
+
export function codexRouterId(label) {
|
|
9
|
+
if (!/^(?:GPT-)?\d[\d.]*(?: (?:Luna|Terra|Sol|Astra))?$/.test(label))
|
|
10
|
+
throw new Error('Unsupported Codex router.');
|
|
11
|
+
return `gpt-${label.replace(/^GPT-/, '').toLowerCase().replaceAll(' ', '-')}`;
|
|
12
|
+
}
|
|
13
|
+
function codexBinary() {
|
|
14
|
+
const root = path.join(os.homedir(), 'Library/Application Support/com.conductor.app/agent-binaries/codex');
|
|
15
|
+
try {
|
|
16
|
+
for (const version of fs.readdirSync(root).sort((a, b) => b.localeCompare(a, undefined, { numeric: true }))) {
|
|
17
|
+
const file = path.join(root, version, 'codex');
|
|
18
|
+
try {
|
|
19
|
+
fs.accessSync(file, fs.constants.X_OK);
|
|
20
|
+
return file;
|
|
21
|
+
}
|
|
22
|
+
catch { }
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
catch { }
|
|
26
|
+
return 'codex';
|
|
27
|
+
}
|
|
28
|
+
/** Build an isolated classifier invocation; no workspace instructions or editing tools. */
|
|
29
|
+
export function routerCommand(router, prompt, images, directory) {
|
|
30
|
+
const provider = routerProvider(router);
|
|
31
|
+
if (provider === 'codex') {
|
|
32
|
+
const instructions = path.join(directory, 'instructions.txt');
|
|
33
|
+
const schema = path.join(directory, 'output-schema.json');
|
|
34
|
+
fs.writeFileSync(instructions, ROUTER_INSTRUCTIONS);
|
|
35
|
+
fs.writeFileSync(schema, JSON.stringify({
|
|
36
|
+
type: 'object',
|
|
37
|
+
additionalProperties: false,
|
|
38
|
+
required: ['profile', 'reason', 'missingContext'],
|
|
39
|
+
properties: { profile: { type: 'string' }, reason: { type: 'string' }, missingContext: { type: 'boolean' } }
|
|
40
|
+
}));
|
|
41
|
+
return {
|
|
42
|
+
binary: codexBinary(),
|
|
43
|
+
cwd: directory,
|
|
44
|
+
env: { ...process.env },
|
|
45
|
+
stdin: prompt,
|
|
46
|
+
args: [
|
|
47
|
+
'exec',
|
|
48
|
+
'--ephemeral',
|
|
49
|
+
'--ignore-user-config',
|
|
50
|
+
'--ignore-rules',
|
|
51
|
+
'--skip-git-repo-check',
|
|
52
|
+
'--sandbox',
|
|
53
|
+
'read-only',
|
|
54
|
+
'--json',
|
|
55
|
+
'--output-schema',
|
|
56
|
+
schema,
|
|
57
|
+
'-m',
|
|
58
|
+
codexRouterId(router.model),
|
|
59
|
+
'-c',
|
|
60
|
+
`model_instructions_file=${JSON.stringify(instructions)}`,
|
|
61
|
+
'-c',
|
|
62
|
+
'approval_policy="never"',
|
|
63
|
+
'-c',
|
|
64
|
+
'web_search="disabled"',
|
|
65
|
+
'-c',
|
|
66
|
+
'agents.enabled=false',
|
|
67
|
+
...[
|
|
68
|
+
'shell_tool',
|
|
69
|
+
'apply_patch_freeform',
|
|
70
|
+
'code_mode',
|
|
71
|
+
'apps',
|
|
72
|
+
'plugins',
|
|
73
|
+
'hooks',
|
|
74
|
+
'codex_hooks',
|
|
75
|
+
'multi_agent',
|
|
76
|
+
'multi_agent_v2',
|
|
77
|
+
'browser_use',
|
|
78
|
+
'computer_use',
|
|
79
|
+
'image_generation',
|
|
80
|
+
'memories'
|
|
81
|
+
].flatMap(name => ['-c', `features.${name}=false`]),
|
|
82
|
+
'-c',
|
|
83
|
+
`model_reasoning_effort=${JSON.stringify(router.effort ?? 'low')}`,
|
|
84
|
+
'-c',
|
|
85
|
+
`service_tier=${JSON.stringify(router.fast ? 'priority' : 'default')}`,
|
|
86
|
+
...images.flatMap(file => ['--image', file]),
|
|
87
|
+
'-'
|
|
88
|
+
]
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
if (provider !== 'opencode')
|
|
92
|
+
throw new Error('Unsupported Auto router.');
|
|
93
|
+
const configDir = path.join(directory, 'config');
|
|
94
|
+
fs.mkdirSync(configDir);
|
|
95
|
+
const config = path.join(configDir, 'opencode.json');
|
|
96
|
+
fs.writeFileSync(config, JSON.stringify({
|
|
97
|
+
share: 'disabled',
|
|
98
|
+
autoupdate: false,
|
|
99
|
+
permission: { '*': 'deny' },
|
|
100
|
+
agent: {
|
|
101
|
+
'auto-model-router': {
|
|
102
|
+
description: 'Select a model profile',
|
|
103
|
+
mode: 'primary',
|
|
104
|
+
prompt: ROUTER_INSTRUCTIONS,
|
|
105
|
+
permission: { '*': 'deny' },
|
|
106
|
+
steps: 1
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}));
|
|
110
|
+
return {
|
|
111
|
+
binary: 'opencode',
|
|
112
|
+
cwd: directory,
|
|
113
|
+
stdin: prompt,
|
|
114
|
+
env: {
|
|
115
|
+
...process.env,
|
|
116
|
+
XDG_CONFIG_HOME: configDir,
|
|
117
|
+
OPENCODE_CONFIG: config,
|
|
118
|
+
OPENCODE_CONFIG_DIR: configDir,
|
|
119
|
+
OPENCODE_CONFIG_CONTENT: '{}',
|
|
120
|
+
OPENCODE_DISABLE_PROJECT_CONFIG: 'true',
|
|
121
|
+
OPENCODE_DISABLE_CLAUDE_CODE: 'true'
|
|
122
|
+
},
|
|
123
|
+
args: [
|
|
124
|
+
'--pure',
|
|
125
|
+
'run',
|
|
126
|
+
'--format',
|
|
127
|
+
'json',
|
|
128
|
+
'--model',
|
|
129
|
+
router.model,
|
|
130
|
+
'--agent',
|
|
131
|
+
'auto-model-router',
|
|
132
|
+
'--dir',
|
|
133
|
+
directory,
|
|
134
|
+
...images.flatMap(file => ['--file', file])
|
|
135
|
+
]
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
export function runRouterCommand(command, signal) {
|
|
139
|
+
return new Promise((resolve, reject) => {
|
|
140
|
+
signal.throwIfAborted();
|
|
141
|
+
const child = spawn(command.binary, command.args, {
|
|
142
|
+
cwd: command.cwd,
|
|
143
|
+
env: command.env,
|
|
144
|
+
detached: true,
|
|
145
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
146
|
+
});
|
|
147
|
+
let stdout = '';
|
|
148
|
+
let failure;
|
|
149
|
+
const stop = () => {
|
|
150
|
+
failure ??= new Error('Router cancelled or timed out.');
|
|
151
|
+
try {
|
|
152
|
+
if (child.pid)
|
|
153
|
+
process.kill(-child.pid, 'SIGKILL');
|
|
154
|
+
}
|
|
155
|
+
catch { }
|
|
156
|
+
};
|
|
157
|
+
signal.addEventListener('abort', stop, { once: true });
|
|
158
|
+
child.stdout.setEncoding('utf8');
|
|
159
|
+
child.stdout.on('data', chunk => {
|
|
160
|
+
stdout += chunk;
|
|
161
|
+
if (stdout.length > 512_000)
|
|
162
|
+
stop();
|
|
163
|
+
});
|
|
164
|
+
// Provider diagnostics can contain the prompt or authentication details.
|
|
165
|
+
child.stderr.resume();
|
|
166
|
+
child.stdin.on('error', () => undefined);
|
|
167
|
+
child.on('error', () => {
|
|
168
|
+
failure = new Error('The router CLI could not start.');
|
|
169
|
+
});
|
|
170
|
+
child.on('close', code => {
|
|
171
|
+
signal.removeEventListener('abort', stop);
|
|
172
|
+
if (failure || code !== 0)
|
|
173
|
+
reject(failure ?? new Error('The router CLI failed.'));
|
|
174
|
+
else
|
|
175
|
+
resolve(stdout);
|
|
176
|
+
});
|
|
177
|
+
child.stdin.end(command.stdin);
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
export function routerOutput(output, provider) {
|
|
181
|
+
const events = output
|
|
182
|
+
.split('\n')
|
|
183
|
+
.filter(line => line.trim())
|
|
184
|
+
.map(line => JSON.parse(line));
|
|
185
|
+
if (events.some(event => event.type === 'error' || event.type === 'turn.failed' || event.type === 'tool_use'))
|
|
186
|
+
throw new Error('Router failed.');
|
|
187
|
+
return events
|
|
188
|
+
.flatMap(event => {
|
|
189
|
+
if (provider === 'codex' && event.type === 'item.completed' && event.item?.type === 'agent_message')
|
|
190
|
+
return [event.item.text];
|
|
191
|
+
if (provider === 'opencode' && event.type === 'text' && event.part?.type === 'text')
|
|
192
|
+
return [event.part.text];
|
|
193
|
+
return [];
|
|
194
|
+
})
|
|
195
|
+
.join('');
|
|
196
|
+
}
|
|
197
|
+
/** Authentication stays owned by the installed CLIs; no credential files are read here. */
|
|
198
|
+
export async function runRouter(router, prompt, images, signal) {
|
|
199
|
+
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'conductor-auto-model-'));
|
|
200
|
+
try {
|
|
201
|
+
const command = routerCommand(router, prompt, images, directory);
|
|
202
|
+
const output = await runRouterCommand(command, signal);
|
|
203
|
+
const provider = routerProvider(router);
|
|
204
|
+
if (!provider)
|
|
205
|
+
throw new Error('Unsupported router.');
|
|
206
|
+
if (provider === 'opencode') {
|
|
207
|
+
const sessionId = output
|
|
208
|
+
.split('\n')
|
|
209
|
+
.filter(Boolean)
|
|
210
|
+
.map(line => {
|
|
211
|
+
try {
|
|
212
|
+
return JSON.parse(line).sessionID;
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
return undefined;
|
|
216
|
+
}
|
|
217
|
+
})
|
|
218
|
+
.find(id => typeof id === 'string' && /^ses_[a-zA-Z0-9]+$/.test(id));
|
|
219
|
+
if (sessionId) {
|
|
220
|
+
await runRouterCommand({ ...command, args: ['--pure', 'session', 'delete', sessionId], stdin: '' }, AbortSignal.timeout(2000)).catch(() => undefined);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return routerOutput(output, provider);
|
|
224
|
+
}
|
|
225
|
+
finally {
|
|
226
|
+
fs.rmSync(directory, { recursive: true, force: true });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { atomicJson } from "./config.js";
|
|
5
|
+
const pending = (job) => ['selecting', 'waiting', 'failed'].includes(job.status);
|
|
6
|
+
/** One durable file per target; a process lease also keeps dev and installed relays from routing twice. */
|
|
7
|
+
export class AutoModelQueue {
|
|
8
|
+
running = false;
|
|
9
|
+
now;
|
|
10
|
+
directory;
|
|
11
|
+
deps;
|
|
12
|
+
constructor(directory, deps) {
|
|
13
|
+
this.directory = directory;
|
|
14
|
+
this.deps = deps;
|
|
15
|
+
this.now = deps.now ?? Date.now;
|
|
16
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
17
|
+
}
|
|
18
|
+
file(job) {
|
|
19
|
+
return path.join(this.directory, `${job.id}.json`);
|
|
20
|
+
}
|
|
21
|
+
list() {
|
|
22
|
+
return fs
|
|
23
|
+
.readdirSync(this.directory)
|
|
24
|
+
.filter(name => /^[a-f0-9]{64}\.json$/.test(name))
|
|
25
|
+
.map(name => {
|
|
26
|
+
const job = JSON.parse(fs.readFileSync(path.join(this.directory, name), 'utf8'));
|
|
27
|
+
if (!job.id || !job.config || typeof job.text !== 'string')
|
|
28
|
+
throw new Error('An Auto submission could not be read.');
|
|
29
|
+
return job;
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
get(sessionId, workspaceId) {
|
|
33
|
+
return this.list().find(job => (sessionId && job.sessionId === sessionId) || (workspaceId && job.workspaceId === workspaceId && !job.sessionId));
|
|
34
|
+
}
|
|
35
|
+
state(sessionId, workspaceId) {
|
|
36
|
+
const job = this.get(sessionId, workspaceId);
|
|
37
|
+
return (job && { status: job.status, decision: job.status === 'cancelled' ? undefined : job.decision, error: job.error });
|
|
38
|
+
}
|
|
39
|
+
pending() {
|
|
40
|
+
return this.list()
|
|
41
|
+
.filter(pending)
|
|
42
|
+
.map(job => ({
|
|
43
|
+
workspaceId: job.workspaceId,
|
|
44
|
+
sessionId: job.sessionId,
|
|
45
|
+
text: job.text,
|
|
46
|
+
createdAt: job.createdAt,
|
|
47
|
+
status: job.status === 'failed' ? 'failed' : 'waiting',
|
|
48
|
+
attempts: job.attempts,
|
|
49
|
+
reason: job.reason,
|
|
50
|
+
error: job.error,
|
|
51
|
+
autoModel: true,
|
|
52
|
+
agent: job.decision && { model: job.decision.model, effort: job.decision.effort, fast: job.decision.fast }
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
accept(input) {
|
|
56
|
+
const existing = this.get(input.sessionId, input.workspaceId);
|
|
57
|
+
if (existing && existing.status !== 'draft' && existing.status !== 'cancelled') {
|
|
58
|
+
if (existing.text !== input.text)
|
|
59
|
+
throw new Error('This chat already has an Auto submission. Dismiss it before sending another.');
|
|
60
|
+
if (existing.status === 'failed') {
|
|
61
|
+
existing.status = 'waiting';
|
|
62
|
+
existing.attempts = 0;
|
|
63
|
+
existing.earlyAttempts = 0;
|
|
64
|
+
existing.lastAttemptAt = undefined;
|
|
65
|
+
existing.error = undefined;
|
|
66
|
+
existing.reason = 'Retrying with the saved model choice';
|
|
67
|
+
this.save(existing);
|
|
68
|
+
}
|
|
69
|
+
return existing;
|
|
70
|
+
}
|
|
71
|
+
const job = {
|
|
72
|
+
...input,
|
|
73
|
+
config: structuredClone(input.config),
|
|
74
|
+
id: existing?.id ??
|
|
75
|
+
crypto
|
|
76
|
+
.createHash('sha256')
|
|
77
|
+
.update(`${input.workspaceId}:${input.sessionId ?? 'first'}`)
|
|
78
|
+
.digest('hex'),
|
|
79
|
+
attachmentIds: input.attachmentIds ?? [],
|
|
80
|
+
sendImmediately: input.sendImmediately !== false,
|
|
81
|
+
createdAt: Math.max(this.now(), (existing?.createdAt ?? 0) + 1),
|
|
82
|
+
attempts: 0,
|
|
83
|
+
earlyAttempts: 0,
|
|
84
|
+
status: input.text ? 'waiting' : 'draft',
|
|
85
|
+
reason: 'Choosing a model…'
|
|
86
|
+
};
|
|
87
|
+
if (existing)
|
|
88
|
+
this.save(job);
|
|
89
|
+
else {
|
|
90
|
+
try {
|
|
91
|
+
fs.writeFileSync(this.file(job), JSON.stringify(job), { flag: 'wx', mode: 0o600 });
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
if (error.code === 'EEXIST')
|
|
95
|
+
return this.accept(input);
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return job;
|
|
100
|
+
}
|
|
101
|
+
cancel(sessionId, workspaceId) {
|
|
102
|
+
const job = this.get(sessionId, workspaceId);
|
|
103
|
+
if (!job || !['draft', 'selecting', 'waiting', 'failed'].includes(job.status))
|
|
104
|
+
return false;
|
|
105
|
+
const lease = this.readLease(job);
|
|
106
|
+
if (lease?.dispatching && this.alive(lease.pid))
|
|
107
|
+
throw new Error('Auto is sending this prompt now. Wait for its receipt before dismissing it.');
|
|
108
|
+
job.status = 'cancelled';
|
|
109
|
+
job.reason = 'Auto cancelled';
|
|
110
|
+
this.save(job);
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
save(job) {
|
|
114
|
+
atomicJson(this.file(job), job);
|
|
115
|
+
}
|
|
116
|
+
current(job) {
|
|
117
|
+
const saved = JSON.parse(fs.readFileSync(this.file(job), 'utf8'));
|
|
118
|
+
return saved.createdAt === job.createdAt && saved.text === job.text && pending(saved) && saved.status !== 'failed';
|
|
119
|
+
}
|
|
120
|
+
alive(pid) {
|
|
121
|
+
try {
|
|
122
|
+
process.kill(pid, 0);
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
return error.code !== 'ESRCH';
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
readLease(job) {
|
|
130
|
+
try {
|
|
131
|
+
return JSON.parse(fs.readFileSync(`${this.file(job)}.lock`, 'utf8'));
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
if (error.code !== 'ENOENT')
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
lease(job) {
|
|
139
|
+
const existing = this.readLease(job);
|
|
140
|
+
if (existing) {
|
|
141
|
+
if (this.alive(existing.pid))
|
|
142
|
+
return false;
|
|
143
|
+
fs.rmSync(`${this.file(job)}.lock`, { force: true });
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
fs.writeFileSync(`${this.file(job)}.lock`, JSON.stringify({ pid: process.pid, dispatching: false }), {
|
|
147
|
+
flag: 'wx',
|
|
148
|
+
mode: 0o600
|
|
149
|
+
});
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
if (error.code === 'EEXIST')
|
|
154
|
+
return false;
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
async tick() {
|
|
159
|
+
if (this.running)
|
|
160
|
+
return;
|
|
161
|
+
this.running = true;
|
|
162
|
+
try {
|
|
163
|
+
for (const candidate of this.list()) {
|
|
164
|
+
if (!pending(candidate) || candidate.status === 'failed' || !this.lease(candidate))
|
|
165
|
+
continue;
|
|
166
|
+
const job = JSON.parse(fs.readFileSync(this.file(candidate), 'utf8'));
|
|
167
|
+
try {
|
|
168
|
+
if (this.current(job))
|
|
169
|
+
await this.step(job);
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
if (this.current(job)) {
|
|
173
|
+
job.status = 'failed';
|
|
174
|
+
job.error = error instanceof Error ? error.message : 'Auto could not deliver the prompt.';
|
|
175
|
+
this.save(job);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
finally {
|
|
179
|
+
fs.rmSync(`${this.file(job)}.lock`, { force: true });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
finally {
|
|
184
|
+
this.running = false;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
async step(job) {
|
|
188
|
+
if (job.sessionId && job.cursor && this.deps.received(job)) {
|
|
189
|
+
job.status = 'delivered';
|
|
190
|
+
this.save(job);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (await this.deps.locked()) {
|
|
194
|
+
if (!this.current(job))
|
|
195
|
+
return;
|
|
196
|
+
job.reason = 'Waiting for the Mac to unlock';
|
|
197
|
+
job.lastAttemptAt = this.now();
|
|
198
|
+
this.save(job);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (!this.current(job))
|
|
202
|
+
return;
|
|
203
|
+
const target = this.deps.inspect(job);
|
|
204
|
+
if (target.obsolete) {
|
|
205
|
+
job.status = 'cancelled';
|
|
206
|
+
job.reason = 'The chat started before Auto could send.';
|
|
207
|
+
this.save(job);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (target.error)
|
|
211
|
+
throw new Error(target.error);
|
|
212
|
+
if (!target.sessionId || !target.worktree || (!target.ready && (!job.sendImmediately || job.earlyAttempts >= 2))) {
|
|
213
|
+
if (this.now() - (job.lastAttemptAt ?? job.createdAt) > 15 * 60_000)
|
|
214
|
+
throw new Error('The workspace did not become ready for Auto. Retry when setup finishes.');
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (job.lastAttemptAt && this.now() - job.lastAttemptAt < 5000)
|
|
218
|
+
return;
|
|
219
|
+
job.sessionId = target.sessionId;
|
|
220
|
+
this.deps.materialize(job, target.worktree);
|
|
221
|
+
job.cursor ??= this.deps.cursor(job.sessionId);
|
|
222
|
+
this.save(job);
|
|
223
|
+
if (!job.decision) {
|
|
224
|
+
job.status = 'selecting';
|
|
225
|
+
job.reason = 'Choosing a model…';
|
|
226
|
+
this.save(job);
|
|
227
|
+
const decision = await this.deps.choose(job, target.worktree);
|
|
228
|
+
if (!this.current(job))
|
|
229
|
+
return;
|
|
230
|
+
job.decision = decision;
|
|
231
|
+
job.status = 'waiting';
|
|
232
|
+
job.reason = `${decision.model} selected · ${decision.reason}`;
|
|
233
|
+
this.save(job); // Decision is durable before any UI write.
|
|
234
|
+
}
|
|
235
|
+
const result = await this.deps.deliver(job, () => this.current(job), () => {
|
|
236
|
+
atomicJson(`${this.file(job)}.lock`, { pid: process.pid, dispatching: true });
|
|
237
|
+
});
|
|
238
|
+
if (!this.current(job))
|
|
239
|
+
return;
|
|
240
|
+
if (result.cancelled)
|
|
241
|
+
job.status = 'cancelled';
|
|
242
|
+
else if (result.ok)
|
|
243
|
+
job.status = 'delivered';
|
|
244
|
+
else if (result.blocked)
|
|
245
|
+
job.reason = 'Waiting for the Mac to unlock';
|
|
246
|
+
else {
|
|
247
|
+
if (target.ready)
|
|
248
|
+
job.attempts++;
|
|
249
|
+
else
|
|
250
|
+
job.earlyAttempts++;
|
|
251
|
+
job.error = result.error ?? 'The prompt was not accepted.';
|
|
252
|
+
job.reason = job.error;
|
|
253
|
+
if (job.attempts >= 3)
|
|
254
|
+
job.status = 'failed';
|
|
255
|
+
}
|
|
256
|
+
job.lastAttemptAt = this.now();
|
|
257
|
+
this.save(job);
|
|
258
|
+
}
|
|
259
|
+
start() {
|
|
260
|
+
const tick = () => void this.tick().catch(error => console.error('[auto-model] queue failed:', error));
|
|
261
|
+
const timer = setInterval(tick, 1000);
|
|
262
|
+
timer.unref();
|
|
263
|
+
tick();
|
|
264
|
+
return () => clearInterval(timer);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -19,6 +19,7 @@ export const agentPatchSchema = z.object({
|
|
|
19
19
|
});
|
|
20
20
|
export const setAgentOptionsSchema = agentPatchSchema.extend({ workspaceId: workspaceIdSchema });
|
|
21
21
|
export const sendPromptSchema = z.object({
|
|
22
|
+
auto: z.boolean().optional().describe('Select a configured model from this pristine chat’s first message.'),
|
|
22
23
|
text: z.string({ error: 'prompt must be a string' }).trim().min(1, 'empty prompt'),
|
|
23
24
|
workspaceId: workspaceIdSchema,
|
|
24
25
|
agent: agentPatchSchema.optional(),
|
|
@@ -26,6 +27,7 @@ export const sendPromptSchema = z.object({
|
|
|
26
27
|
queue: z.boolean().default(false)
|
|
27
28
|
});
|
|
28
29
|
export const createWorkspaceSchema = agentPatchSchema.extend({
|
|
30
|
+
auto: z.boolean().optional().describe('Use Auto to select the initial model. Omit manual agent settings.'),
|
|
29
31
|
repo: z.string().trim().min(1).optional().describe('Exact name from list_repos.'),
|
|
30
32
|
prompt: optionalText.describe('First prompt for the new agent. Omit to open an empty workspace.'),
|
|
31
33
|
send: z
|
|
@@ -2,6 +2,7 @@ import http from 'node:http';
|
|
|
2
2
|
import { InputError } from "../contracts/validation.js";
|
|
3
3
|
import { UiBusyError, uiQueueDepth, withUiPriority } from "../writes/ui-lock.js";
|
|
4
4
|
import { NOT_HANDLED } from "./router-types.js";
|
|
5
|
+
import { createAutoModelRoutes } from "./routes/auto-model.js";
|
|
5
6
|
import { createCreateWorkspaceRoutes } from "./routes/create-workspace.js";
|
|
6
7
|
import { createFilesRoutes } from "./routes/files.js";
|
|
7
8
|
import { createPromptsRoutes } from "./routes/prompts.js";
|
|
@@ -14,6 +15,7 @@ import { createWorkspacesRoutes } from "./routes/workspaces.js";
|
|
|
14
15
|
export function createRelayServer(services) {
|
|
15
16
|
const { handleMcpHttp, serveStatic, authed, json, PayloadTooLargeError, workflowHttpError } = services;
|
|
16
17
|
const handlers = [
|
|
18
|
+
createAutoModelRoutes(services),
|
|
17
19
|
createStateRoutes(services),
|
|
18
20
|
createWorkflowsRoutes(services),
|
|
19
21
|
createVoiceRoutes(services),
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { autoModelIssues, decodeAutoModelConfig } from "../../agents/auto-model/config.js";
|
|
2
|
+
import { isRoute, routes } from "../../routes.js";
|
|
3
|
+
import { NOT_HANDLED } from "../router-types.js";
|
|
4
|
+
export function createAutoModelRoutes(services) {
|
|
5
|
+
const { autoModelConfig, modelCache, readBody, json } = services;
|
|
6
|
+
return async (req, res, url) => {
|
|
7
|
+
if (isRoute(routes.autoModelConfig, req.method, url.pathname)) {
|
|
8
|
+
const config = autoModelConfig.read();
|
|
9
|
+
return json(req, res, 200, { config, issues: autoModelIssues(config, modelCache.list()) });
|
|
10
|
+
}
|
|
11
|
+
if (isRoute(routes.updateAutoModelConfig, req.method, url.pathname)) {
|
|
12
|
+
try {
|
|
13
|
+
const config = decodeAutoModelConfig(JSON.parse(await readBody(req)));
|
|
14
|
+
const issues = autoModelIssues(config, modelCache.list());
|
|
15
|
+
if (issues.length)
|
|
16
|
+
return json(req, res, 400, { error: issues.join(' ') });
|
|
17
|
+
autoModelConfig.write(config);
|
|
18
|
+
return json(req, res, 200, { config, issues: [] });
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return json(req, res, 400, { error: 'Invalid Auto settings. Check the profiles, fallback, and router.' });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return NOT_HANDLED;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { freezeAutoModelConfig } from "../../agents/auto-model/config.js";
|
|
1
2
|
import { createWorkspaceSchema, hasAgentSettings } from "../../contracts/agent-inputs.js";
|
|
2
3
|
import { parseInput } from "../../contracts/validation.js";
|
|
3
4
|
import { stagedAttachments } from "../../files/staged-attachments.js";
|
|
@@ -17,6 +18,21 @@ export function createCreateWorkspaceRoutes(services) {
|
|
|
17
18
|
const body = parseInput(createWorkspaceSchema, input);
|
|
18
19
|
const { attachmentIds, model, effort, plan, fast } = body;
|
|
19
20
|
const requestedAgent = { model, effort, plan, fast };
|
|
21
|
+
if (body.auto && [body.model, body.effort, body.fast, body.plan].some(value => value !== undefined))
|
|
22
|
+
return json(req, res, 400, {
|
|
23
|
+
error: 'Auto chooses the agent settings. Omit manual model, effort, Fast, and Plan.'
|
|
24
|
+
});
|
|
25
|
+
let autoConfig;
|
|
26
|
+
if (body.auto) {
|
|
27
|
+
if (!services.autoModels || !services.autoModelConfig || !services.modelCache)
|
|
28
|
+
return json(req, res, 503, { error: 'Auto is unavailable.' });
|
|
29
|
+
try {
|
|
30
|
+
autoConfig = freezeAutoModelConfig(services.autoModelConfig.read(), services.modelCache.list());
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
return json(req, res, 409, { error: error instanceof Error ? error.message : 'Invalid Auto settings.' });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
20
36
|
const attachments = stagedAttachments(STAGED_ATTACHMENTS_DIR, attachmentIds);
|
|
21
37
|
if (!attachments)
|
|
22
38
|
return json(req, res, 409, { error: 'an attached file is no longer available; add it again' });
|
|
@@ -37,7 +53,7 @@ export function createCreateWorkspaceRoutes(services) {
|
|
|
37
53
|
return json(req, res, 404, { error: `unknown repo ${body.repo}` });
|
|
38
54
|
if (repo && !repo.root_path)
|
|
39
55
|
return json(req, res, 409, { error: `${repo.name} has no checkout path` });
|
|
40
|
-
const { result, created } = await createWorkspaceAndRead(prompt, repo?.root_path ?? null, repo?.name);
|
|
56
|
+
const { result, created } = await createWorkspaceAndRead(prompt, repo?.root_path ?? null, repo?.name, !!body.auto);
|
|
41
57
|
if (!result.ok)
|
|
42
58
|
return json(req, res, 502, result);
|
|
43
59
|
if (!created) {
|
|
@@ -47,6 +63,25 @@ export function createCreateWorkspaceRoutes(services) {
|
|
|
47
63
|
error: 'Conductor didn’t create a workspace — check it’s running and not showing a dialog.'
|
|
48
64
|
});
|
|
49
65
|
}
|
|
66
|
+
if (autoConfig && services.autoModels) {
|
|
67
|
+
const sessions = reads.listSessions(created.id);
|
|
68
|
+
services.autoModels.accept({
|
|
69
|
+
workspaceId: created.id,
|
|
70
|
+
sessionId: sessions.length === 1 ? sessions[0].id : undefined,
|
|
71
|
+
text: prompt,
|
|
72
|
+
repo: created.repo_name ?? body.repo ?? '',
|
|
73
|
+
config: autoConfig,
|
|
74
|
+
attachmentIds,
|
|
75
|
+
sendImmediately: body.sendImmediately
|
|
76
|
+
});
|
|
77
|
+
return json(req, res, 200, {
|
|
78
|
+
ok: true,
|
|
79
|
+
workspaceId: created.id,
|
|
80
|
+
workspace: created,
|
|
81
|
+
pendingPrompt: prompt || undefined,
|
|
82
|
+
sent: false
|
|
83
|
+
});
|
|
84
|
+
}
|
|
50
85
|
// Return as soon as the row exists (~2s) — waiting for delivery would block the
|
|
51
86
|
// request through Conductor's whole setup, measured at 30s+ on a real repo and
|
|
52
87
|
// past any budget a phone should hold a request open for. The queue delivers on
|