@hunsu/codex-runner 0.1.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/LICENSE +157 -0
- package/dist/index.d.ts +471 -0
- package/dist/index.js +1780 -0
- package/package.json +34 -0
- package/src/index.ts +2263 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1780 @@
|
|
|
1
|
+
import { createDefaultManagerConfig, createDefaultHarness, createDefaultMemberConfig, DEFAULT_TEAM_PROMPT, getHarnessMember, getHarnessMemberConfig, harnessSnapshotForTeam, memberConfigFromMemberEntity, renderPromptTemplate } from "@hunsu/protocol";
|
|
2
|
+
import { delimiter, dirname, join, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { createInterface } from "node:readline";
|
|
6
|
+
import { Buffer } from "node:buffer";
|
|
7
|
+
export const DEFAULT_TEAM_INSTRUCTION = DEFAULT_TEAM_PROMPT;
|
|
8
|
+
const GOAL_ASSIGNEE_OUTPUT_SCHEMA = {
|
|
9
|
+
type: "object",
|
|
10
|
+
properties: {
|
|
11
|
+
executorId: { type: "string" },
|
|
12
|
+
goal: { type: "string" }
|
|
13
|
+
},
|
|
14
|
+
required: ["executorId", "goal"],
|
|
15
|
+
additionalProperties: false
|
|
16
|
+
};
|
|
17
|
+
const GOAL_EVALUATOR_OUTPUT_SCHEMA = {
|
|
18
|
+
type: ["object", "null"],
|
|
19
|
+
properties: {
|
|
20
|
+
executorId: { type: "string" },
|
|
21
|
+
prompt: { type: "string" }
|
|
22
|
+
},
|
|
23
|
+
required: ["executorId", "prompt"],
|
|
24
|
+
additionalProperties: false
|
|
25
|
+
};
|
|
26
|
+
const PATH_REQUIRES_OUTPUT_SCHEMA = {
|
|
27
|
+
type: ["string", "array"],
|
|
28
|
+
items: { type: "string" }
|
|
29
|
+
};
|
|
30
|
+
const GOAL_EXECUTION_PLAN_OUTPUT_SCHEMA = {
|
|
31
|
+
type: "object",
|
|
32
|
+
properties: {
|
|
33
|
+
kind: { type: "string", enum: ["goal"] },
|
|
34
|
+
stage: { type: "string", enum: ["needs_evaluation"] },
|
|
35
|
+
id: { type: "string" },
|
|
36
|
+
assignee: GOAL_ASSIGNEE_OUTPUT_SCHEMA,
|
|
37
|
+
evaluator: GOAL_EVALUATOR_OUTPUT_SCHEMA,
|
|
38
|
+
remainingAttempts: {
|
|
39
|
+
type: "integer",
|
|
40
|
+
minimum: 0
|
|
41
|
+
},
|
|
42
|
+
requires: PATH_REQUIRES_OUTPUT_SCHEMA
|
|
43
|
+
},
|
|
44
|
+
required: ["kind", "stage", "id", "assignee", "evaluator", "remainingAttempts", "requires"],
|
|
45
|
+
additionalProperties: false
|
|
46
|
+
};
|
|
47
|
+
export const TEAM_EXECUTION_PLAN_SCHEMA = {
|
|
48
|
+
type: "object",
|
|
49
|
+
properties: {
|
|
50
|
+
kind: { type: "string", enum: ["queue"] },
|
|
51
|
+
id: { type: "string" },
|
|
52
|
+
items: {
|
|
53
|
+
type: "array",
|
|
54
|
+
items: GOAL_EXECUTION_PLAN_OUTPUT_SCHEMA
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
required: ["kind", "id", "items"],
|
|
58
|
+
additionalProperties: false
|
|
59
|
+
};
|
|
60
|
+
export const GOAL_EVALUATION_SCHEMA = {
|
|
61
|
+
type: "object",
|
|
62
|
+
properties: {
|
|
63
|
+
type: { type: "string", enum: ["pass", "fail"] },
|
|
64
|
+
summary: { type: "string" },
|
|
65
|
+
reason: { type: "string" },
|
|
66
|
+
feedback: { type: "string" },
|
|
67
|
+
nextGoal: { type: "string" },
|
|
68
|
+
evidence: { type: "array", items: { type: "string" } }
|
|
69
|
+
},
|
|
70
|
+
required: ["type", "summary", "reason", "feedback", "nextGoal", "evidence"],
|
|
71
|
+
additionalProperties: false
|
|
72
|
+
};
|
|
73
|
+
export const DEFAULT_CODEX_THREAD_OPTIONS = {
|
|
74
|
+
sandboxMode: "workspace-write",
|
|
75
|
+
approvalPolicy: "never",
|
|
76
|
+
approvalsReviewer: "user",
|
|
77
|
+
networkAccessEnabled: false
|
|
78
|
+
};
|
|
79
|
+
class RunnerEventBus {
|
|
80
|
+
subscribers = new Map();
|
|
81
|
+
push(event) {
|
|
82
|
+
const subscribers = this.subscribers.get(event.runId);
|
|
83
|
+
if (!subscribers) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
for (const subscriber of subscribers) {
|
|
87
|
+
if (subscriber.closed) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
subscriber.queue.push(event);
|
|
91
|
+
subscriber.resolve?.();
|
|
92
|
+
subscriber.resolve = undefined;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
close(runId) {
|
|
96
|
+
const subscribers = this.subscribers.get(runId);
|
|
97
|
+
if (!subscribers) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
for (const subscriber of subscribers) {
|
|
101
|
+
subscriber.closed = true;
|
|
102
|
+
subscriber.resolve?.();
|
|
103
|
+
subscriber.resolve = undefined;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async *events(runId) {
|
|
107
|
+
const subscriber = { queue: [], closed: false };
|
|
108
|
+
let subscribers = this.subscribers.get(runId);
|
|
109
|
+
if (!subscribers) {
|
|
110
|
+
subscribers = new Set();
|
|
111
|
+
this.subscribers.set(runId, subscribers);
|
|
112
|
+
}
|
|
113
|
+
subscribers.add(subscriber);
|
|
114
|
+
try {
|
|
115
|
+
while (true) {
|
|
116
|
+
const event = subscriber.queue.shift();
|
|
117
|
+
if (event) {
|
|
118
|
+
yield event;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (subscriber.closed) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
await new Promise(resolve => {
|
|
125
|
+
subscriber.resolve = resolve;
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
subscribers.delete(subscriber);
|
|
131
|
+
if (subscribers.size === 0) {
|
|
132
|
+
this.subscribers.delete(runId);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
class CodexAppServerRequestError extends Error {
|
|
138
|
+
code;
|
|
139
|
+
data;
|
|
140
|
+
constructor(code, message, data) {
|
|
141
|
+
super(message);
|
|
142
|
+
this.code = code;
|
|
143
|
+
this.data = data;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
class CodexAppServerProcessTransport {
|
|
147
|
+
child;
|
|
148
|
+
stdout;
|
|
149
|
+
messageHandlers = new Set();
|
|
150
|
+
errorHandlers = new Set();
|
|
151
|
+
closeHandlers = new Set();
|
|
152
|
+
constructor(options) {
|
|
153
|
+
this.child = spawn(options.command, options.args, {
|
|
154
|
+
cwd: options.cwd,
|
|
155
|
+
env: createCodexEnvironment(options.environment)
|
|
156
|
+
});
|
|
157
|
+
this.stdout = createInterface({ input: this.child.stdout });
|
|
158
|
+
this.stdout.on("line", line => this.handleLine(line));
|
|
159
|
+
this.child.stderr.on("data", () => undefined);
|
|
160
|
+
this.child.once("error", error => this.emitError(error));
|
|
161
|
+
this.child.once("close", () => this.emitClose());
|
|
162
|
+
}
|
|
163
|
+
send(message) {
|
|
164
|
+
if (this.child.stdin.destroyed || !this.child.stdin.writable) {
|
|
165
|
+
throw new Error("Codex app-server stdin is closed");
|
|
166
|
+
}
|
|
167
|
+
this.child.stdin.write(`${JSON.stringify(message)}\n`);
|
|
168
|
+
}
|
|
169
|
+
close() {
|
|
170
|
+
this.stdout.close();
|
|
171
|
+
if (!this.child.killed) {
|
|
172
|
+
this.child.kill("SIGTERM");
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
onMessage(handler) {
|
|
176
|
+
this.messageHandlers.add(handler);
|
|
177
|
+
return () => this.messageHandlers.delete(handler);
|
|
178
|
+
}
|
|
179
|
+
onError(handler) {
|
|
180
|
+
this.errorHandlers.add(handler);
|
|
181
|
+
return () => this.errorHandlers.delete(handler);
|
|
182
|
+
}
|
|
183
|
+
onClose(handler) {
|
|
184
|
+
this.closeHandlers.add(handler);
|
|
185
|
+
return () => this.closeHandlers.delete(handler);
|
|
186
|
+
}
|
|
187
|
+
handleLine(line) {
|
|
188
|
+
if (!line.trim()) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
try {
|
|
192
|
+
const message = JSON.parse(line);
|
|
193
|
+
for (const handler of this.messageHandlers) {
|
|
194
|
+
handler(message);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
this.emitError(new Error(`Invalid Codex app-server JSON-RPC message: ${error instanceof Error ? error.message : String(error)}`));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
emitError(error) {
|
|
202
|
+
for (const handler of this.errorHandlers) {
|
|
203
|
+
handler(error);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
emitClose() {
|
|
207
|
+
for (const handler of this.closeHandlers) {
|
|
208
|
+
handler();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
export class CodexAppServerClient {
|
|
213
|
+
requestTimeoutMs;
|
|
214
|
+
transportFactory;
|
|
215
|
+
clientInfo;
|
|
216
|
+
serverRequestHandler;
|
|
217
|
+
transport;
|
|
218
|
+
startPromise;
|
|
219
|
+
initialized;
|
|
220
|
+
nextId = 1;
|
|
221
|
+
pending = new Map();
|
|
222
|
+
notificationHandlers = new Set();
|
|
223
|
+
constructor(options = {}) {
|
|
224
|
+
const environment = options.environment ?? {};
|
|
225
|
+
const command = options.command ?? "codex";
|
|
226
|
+
const args = options.args ?? ["app-server", "--stdio"];
|
|
227
|
+
this.requestTimeoutMs = options.requestTimeoutMs ?? 60_000;
|
|
228
|
+
this.clientInfo = options.clientInfo ?? { name: "hunsu-studio", title: "Hunsu Studio", version: "0.1.0" };
|
|
229
|
+
this.transportFactory = options.transportFactory ?? (() => new CodexAppServerProcessTransport({
|
|
230
|
+
command,
|
|
231
|
+
args,
|
|
232
|
+
environment,
|
|
233
|
+
cwd: options.cwd
|
|
234
|
+
}));
|
|
235
|
+
this.serverRequestHandler = options.handleServerRequest ?? defaultAppServerRequestHandler;
|
|
236
|
+
}
|
|
237
|
+
setServerRequestHandler(handler) {
|
|
238
|
+
this.serverRequestHandler = handler;
|
|
239
|
+
}
|
|
240
|
+
async initialize() {
|
|
241
|
+
await this.ensureStarted();
|
|
242
|
+
return this.initialized;
|
|
243
|
+
}
|
|
244
|
+
async request(method, params, options = {}) {
|
|
245
|
+
await this.ensureStarted();
|
|
246
|
+
return this.requestRaw(method, params, options.timeoutMs);
|
|
247
|
+
}
|
|
248
|
+
notify(method, params) {
|
|
249
|
+
this.transport?.send(params === undefined ? { method } : { method, params });
|
|
250
|
+
}
|
|
251
|
+
onNotification(handler) {
|
|
252
|
+
this.notificationHandlers.add(handler);
|
|
253
|
+
return () => this.notificationHandlers.delete(handler);
|
|
254
|
+
}
|
|
255
|
+
close() {
|
|
256
|
+
this.disposeTransport(new Error("Codex app-server client closed"), true);
|
|
257
|
+
}
|
|
258
|
+
async ensureStarted() {
|
|
259
|
+
if (this.transport && this.initialized) {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (this.startPromise) {
|
|
263
|
+
return this.startPromise;
|
|
264
|
+
}
|
|
265
|
+
this.startPromise = this.start();
|
|
266
|
+
try {
|
|
267
|
+
await this.startPromise;
|
|
268
|
+
}
|
|
269
|
+
finally {
|
|
270
|
+
this.startPromise = undefined;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
async start() {
|
|
274
|
+
const transport = this.transportFactory();
|
|
275
|
+
this.transport = transport;
|
|
276
|
+
transport.onMessage(message => this.handleMessage(message));
|
|
277
|
+
transport.onError(error => this.disposeTransport(error, true));
|
|
278
|
+
transport.onClose(() => {
|
|
279
|
+
this.disposeTransport(new Error("Codex app-server process exited"), false);
|
|
280
|
+
});
|
|
281
|
+
this.initialized = await this.requestRaw("initialize", {
|
|
282
|
+
clientInfo: this.clientInfo,
|
|
283
|
+
capabilities: null
|
|
284
|
+
}, this.requestTimeoutMs);
|
|
285
|
+
this.notify("initialized");
|
|
286
|
+
}
|
|
287
|
+
requestRaw(method, params, timeoutMs = this.requestTimeoutMs) {
|
|
288
|
+
const transport = this.transport;
|
|
289
|
+
if (!transport) {
|
|
290
|
+
throw new Error("Codex app-server transport is not started");
|
|
291
|
+
}
|
|
292
|
+
const id = `hunsu-${this.nextId++}`;
|
|
293
|
+
return new Promise((resolve, reject) => {
|
|
294
|
+
const timer = setTimeout(() => {
|
|
295
|
+
this.pending.delete(id);
|
|
296
|
+
const error = new Error(`Codex app-server request timed out: ${method}`);
|
|
297
|
+
reject(error);
|
|
298
|
+
this.disposeTransport(error, true);
|
|
299
|
+
}, timeoutMs);
|
|
300
|
+
this.pending.set(id, { method, resolve, reject, timer });
|
|
301
|
+
try {
|
|
302
|
+
transport.send(params === undefined ? { id, method } : { id, method, params });
|
|
303
|
+
}
|
|
304
|
+
catch (error) {
|
|
305
|
+
clearTimeout(timer);
|
|
306
|
+
this.pending.delete(id);
|
|
307
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
handleMessage(message) {
|
|
312
|
+
if (message.id !== undefined && (message.result !== undefined || message.error !== undefined) && !message.method) {
|
|
313
|
+
this.handleResponse(message);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
if (message.id !== undefined && typeof message.method === "string") {
|
|
317
|
+
void this.handleServerRequest(message);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (typeof message.method === "string") {
|
|
321
|
+
for (const handler of this.notificationHandlers) {
|
|
322
|
+
handler(message);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
handleResponse(message) {
|
|
327
|
+
const pending = this.pending.get(message.id);
|
|
328
|
+
if (!pending) {
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
clearTimeout(pending.timer);
|
|
332
|
+
this.pending.delete(message.id);
|
|
333
|
+
if (message.error) {
|
|
334
|
+
pending.reject(new Error(`Codex app-server ${pending.method} failed: ${message.error.message}`));
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
pending.resolve(message.result);
|
|
338
|
+
}
|
|
339
|
+
async handleServerRequest(message) {
|
|
340
|
+
try {
|
|
341
|
+
const result = await this.serverRequestHandler(message);
|
|
342
|
+
this.transport?.send({ id: message.id, result });
|
|
343
|
+
}
|
|
344
|
+
catch (error) {
|
|
345
|
+
const requestError = error instanceof CodexAppServerRequestError
|
|
346
|
+
? error
|
|
347
|
+
: new CodexAppServerRequestError(-32603, error instanceof Error ? error.message : String(error));
|
|
348
|
+
this.transport?.send({
|
|
349
|
+
id: message.id,
|
|
350
|
+
error: {
|
|
351
|
+
code: requestError.code,
|
|
352
|
+
message: requestError.message,
|
|
353
|
+
data: requestError.data
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
rejectPending(error) {
|
|
359
|
+
for (const [id, pending] of this.pending) {
|
|
360
|
+
clearTimeout(pending.timer);
|
|
361
|
+
pending.reject(error);
|
|
362
|
+
this.pending.delete(id);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
disposeTransport(error, closeTransport) {
|
|
366
|
+
const transport = this.transport;
|
|
367
|
+
this.transport = undefined;
|
|
368
|
+
this.initialized = undefined;
|
|
369
|
+
this.rejectPending(error);
|
|
370
|
+
if (closeTransport) {
|
|
371
|
+
transport?.close();
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
export class CodexAppServerRunner {
|
|
376
|
+
eventBus = new RunnerEventBus();
|
|
377
|
+
client;
|
|
378
|
+
threadOptions;
|
|
379
|
+
activeByRunId = new Map();
|
|
380
|
+
activeByThreadId = new Map();
|
|
381
|
+
activeByTurnId = new Map();
|
|
382
|
+
constructor(options = {}) {
|
|
383
|
+
this.threadOptions = {
|
|
384
|
+
...DEFAULT_CODEX_THREAD_OPTIONS,
|
|
385
|
+
...options.threadOptions
|
|
386
|
+
};
|
|
387
|
+
this.client = options.client ?? new CodexAppServerClient({
|
|
388
|
+
...options.clientOptions,
|
|
389
|
+
environment: options.clientOptions?.environment ?? options.environment ?? {}
|
|
390
|
+
});
|
|
391
|
+
this.client.setServerRequestHandler(message => this.handleServerRequest(message));
|
|
392
|
+
this.client.onNotification(message => this.handleNotification(message));
|
|
393
|
+
}
|
|
394
|
+
async runTeamPlanning(input) {
|
|
395
|
+
const config = teamPlanningConfigFor(input);
|
|
396
|
+
return this.executeTurn(input, config, buildTeamPlanningPrompt(input), undefined, true, TEAM_EXECUTION_PLAN_SCHEMA);
|
|
397
|
+
}
|
|
398
|
+
async runMemberPath(input) {
|
|
399
|
+
const member = memberConfigFor(input, input.memberPath.executorId);
|
|
400
|
+
return this.executeTurn(input, member, buildMemberPathPrompt(input), undefined, false, input.outputSchema);
|
|
401
|
+
}
|
|
402
|
+
async runMoveFinalizer(input) {
|
|
403
|
+
const config = moveFinalizerConfigFor();
|
|
404
|
+
return this.executeTurn(input, config, buildMoveFinalizerPrompt(input), undefined, true);
|
|
405
|
+
}
|
|
406
|
+
async runHunsuDraftTurn(input) {
|
|
407
|
+
const config = hunsuDraftConfigFor(input.manager);
|
|
408
|
+
return this.executeTurn(input, config, buildHunsuDraftPrompt(input), input.providerThreadId, false, undefined, { reusePreparedThread: true });
|
|
409
|
+
}
|
|
410
|
+
async prepareHunsuDraftSession(input) {
|
|
411
|
+
const config = hunsuDraftConfigFor(input.manager);
|
|
412
|
+
const providerThreadId = await this.prepareThread(input, config, input.providerThreadId, false);
|
|
413
|
+
return {
|
|
414
|
+
runId: input.runId,
|
|
415
|
+
provider: "codex",
|
|
416
|
+
providerThreadId
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
async resumeRun(input) {
|
|
420
|
+
const config = teamPlanningConfigFor(input);
|
|
421
|
+
return this.executeTurn(input, config, buildTeamPlanningPrompt(input), input.providerThreadId, true, TEAM_EXECUTION_PLAN_SCHEMA);
|
|
422
|
+
}
|
|
423
|
+
async pauseRun(runId) {
|
|
424
|
+
this.eventBus.push({ type: "runner.status.changed", runId, phase: "waiting", headline: "Pause requested", detail: "Codex app-server will stop after the current turn boundary." });
|
|
425
|
+
}
|
|
426
|
+
async stopRun(runId) {
|
|
427
|
+
const active = this.activeByRunId.get(runId);
|
|
428
|
+
this.eventBus.push({ type: "runner.status.changed", runId, phase: "waiting", headline: "Stop requested" });
|
|
429
|
+
if (active?.threadId && active.turnId) {
|
|
430
|
+
try {
|
|
431
|
+
await this.client.request("turn/interrupt", { threadId: active.threadId, turnId: active.turnId }, { timeoutMs: 10_000 });
|
|
432
|
+
}
|
|
433
|
+
catch (error) {
|
|
434
|
+
this.eventBus.push({ type: "runner.error", runId, error: `Codex app-server interrupt failed: ${error instanceof Error ? error.message : String(error)}` });
|
|
435
|
+
}
|
|
436
|
+
active.completed = true;
|
|
437
|
+
active.reject(new Error("Codex turn interrupted by Studio"));
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
if (active) {
|
|
441
|
+
active.completed = true;
|
|
442
|
+
this.client.close();
|
|
443
|
+
active.reject(new Error("Codex turn interrupted by Studio before app-server returned a turn id"));
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
this.eventBus.close(runId);
|
|
447
|
+
}
|
|
448
|
+
async providerStatus() {
|
|
449
|
+
try {
|
|
450
|
+
const initialized = await this.client.initialize();
|
|
451
|
+
const status = { backend: "app-server", available: true, initialized };
|
|
452
|
+
try {
|
|
453
|
+
status.account = await this.client.request("account/read", { refreshToken: false });
|
|
454
|
+
}
|
|
455
|
+
catch (error) {
|
|
456
|
+
status.accountError = error instanceof Error ? error.message : String(error);
|
|
457
|
+
}
|
|
458
|
+
try {
|
|
459
|
+
status.rateLimits = await this.client.request("account/rateLimits/read");
|
|
460
|
+
}
|
|
461
|
+
catch (error) {
|
|
462
|
+
status.rateLimitsError = error instanceof Error ? error.message : String(error);
|
|
463
|
+
}
|
|
464
|
+
const errors = [status.accountError, status.rateLimitsError].filter(Boolean);
|
|
465
|
+
if (errors.length > 0) {
|
|
466
|
+
status.error = errors.join("; ");
|
|
467
|
+
}
|
|
468
|
+
return status;
|
|
469
|
+
}
|
|
470
|
+
catch (error) {
|
|
471
|
+
return { backend: "app-server", available: false, error: error instanceof Error ? error.message : String(error) };
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
events(runId) {
|
|
475
|
+
return this.eventBus.events(runId);
|
|
476
|
+
}
|
|
477
|
+
async executeTurn(input, config, prompt, providerThreadId, readOnly = false, outputSchema, options = {}) {
|
|
478
|
+
const shouldReusePreparedThread = options.reusePreparedThread === true && providerThreadId !== undefined && !outputSchema;
|
|
479
|
+
try {
|
|
480
|
+
const threadId = shouldReusePreparedThread
|
|
481
|
+
? providerThreadId
|
|
482
|
+
: await this.prepareThread(input, config, providerThreadId, readOnly, outputSchema);
|
|
483
|
+
if (shouldReusePreparedThread) {
|
|
484
|
+
this.eventBus.push({
|
|
485
|
+
type: "runner.status.changed",
|
|
486
|
+
runId: input.runId,
|
|
487
|
+
phase: "working",
|
|
488
|
+
headline: "Prepared thread reused",
|
|
489
|
+
detail: threadId,
|
|
490
|
+
providerThreadId: threadId
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
try {
|
|
494
|
+
return await this.startTurn(input, config, threadId, prompt, readOnly, outputSchema);
|
|
495
|
+
}
|
|
496
|
+
catch (error) {
|
|
497
|
+
if (!shouldReusePreparedThread) {
|
|
498
|
+
throw error;
|
|
499
|
+
}
|
|
500
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
501
|
+
this.eventBus.push({
|
|
502
|
+
type: "runner.status.changed",
|
|
503
|
+
runId: input.runId,
|
|
504
|
+
phase: "working",
|
|
505
|
+
headline: "Prepared thread reuse failed",
|
|
506
|
+
detail: message,
|
|
507
|
+
providerThreadId
|
|
508
|
+
});
|
|
509
|
+
const resumedThreadId = await this.prepareThread(input, config, providerThreadId, readOnly, outputSchema);
|
|
510
|
+
return await this.startTurn(input, config, resumedThreadId, prompt, readOnly, outputSchema);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
catch (error) {
|
|
514
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
515
|
+
this.eventBus.push({ type: "runner.error", runId: input.runId, error: message });
|
|
516
|
+
throw error;
|
|
517
|
+
}
|
|
518
|
+
finally {
|
|
519
|
+
this.eventBus.close(input.runId);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
async startTurn(input, config, threadId, prompt, readOnly, outputSchema) {
|
|
523
|
+
let active;
|
|
524
|
+
try {
|
|
525
|
+
return await new Promise((resolve, reject) => {
|
|
526
|
+
active = {
|
|
527
|
+
runId: input.runId,
|
|
528
|
+
threadId,
|
|
529
|
+
rawEvents: [],
|
|
530
|
+
activeCommandExecutionIds: new Set(),
|
|
531
|
+
backgroundTerminalCleanupRequested: false,
|
|
532
|
+
finalEmitted: false,
|
|
533
|
+
completed: false,
|
|
534
|
+
resolve,
|
|
535
|
+
reject
|
|
536
|
+
};
|
|
537
|
+
this.trackActiveTurn(active);
|
|
538
|
+
this.client.request("turn/start", this.turnStartParamsFor(input, config, threadId, prompt, readOnly, outputSchema), { timeoutMs: 60_000 })
|
|
539
|
+
.then(turnResponse => {
|
|
540
|
+
const turn = readRecord(turnResponse, "turn");
|
|
541
|
+
if (!active || active.completed) {
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
const turnId = readString(turn, "id");
|
|
545
|
+
if (turnId) {
|
|
546
|
+
this.setActiveTurnId(active, turnId);
|
|
547
|
+
}
|
|
548
|
+
if (turn && readString(turn, "status") !== "inProgress") {
|
|
549
|
+
this.completeActiveTurn(active, turn);
|
|
550
|
+
}
|
|
551
|
+
})
|
|
552
|
+
.catch(error => {
|
|
553
|
+
if (!active?.completed) {
|
|
554
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
finally {
|
|
560
|
+
if (active) {
|
|
561
|
+
this.untrackActiveTurn(active);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
async prepareThread(input, config, providerThreadId, readOnly, outputSchema) {
|
|
566
|
+
const threadResponse = providerThreadId
|
|
567
|
+
? await this.client.request("thread/resume", this.threadResumeParamsFor(input, config, providerThreadId, readOnly))
|
|
568
|
+
: await this.client.request("thread/start", this.threadStartParamsFor(input, config, readOnly));
|
|
569
|
+
const threadId = readNestedString(threadResponse, ["thread", "id"]) ?? providerThreadId;
|
|
570
|
+
if (!threadId) {
|
|
571
|
+
throw new Error("Codex app-server did not return a thread id");
|
|
572
|
+
}
|
|
573
|
+
this.eventBus.push({
|
|
574
|
+
type: "runner.status.changed",
|
|
575
|
+
runId: input.runId,
|
|
576
|
+
phase: "working",
|
|
577
|
+
headline: "Thread ready",
|
|
578
|
+
detail: threadId,
|
|
579
|
+
providerThreadId: threadId
|
|
580
|
+
});
|
|
581
|
+
if (outputSchema) {
|
|
582
|
+
await this.client.request("thread/goal/clear", { threadId }, { timeoutMs: 60_000 });
|
|
583
|
+
this.eventBus.push({
|
|
584
|
+
type: "runner.status.changed",
|
|
585
|
+
runId: input.runId,
|
|
586
|
+
phase: "working",
|
|
587
|
+
headline: "Provider goal cleared",
|
|
588
|
+
providerThreadId: threadId
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
else {
|
|
592
|
+
await this.client.request("thread/goal/set", this.threadGoalSetParamsFor(input, config, threadId), { timeoutMs: 60_000 });
|
|
593
|
+
this.eventBus.push({
|
|
594
|
+
type: "runner.status.changed",
|
|
595
|
+
runId: input.runId,
|
|
596
|
+
phase: "working",
|
|
597
|
+
headline: "Provider goal set",
|
|
598
|
+
providerThreadId: threadId
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
return threadId;
|
|
602
|
+
}
|
|
603
|
+
threadStartParamsFor(input, config, readOnly) {
|
|
604
|
+
const options = this.effectiveThreadOptions(input, config, readOnly);
|
|
605
|
+
return {
|
|
606
|
+
cwd: input.repositoryPath,
|
|
607
|
+
approvalPolicy: options.approvalPolicy,
|
|
608
|
+
approvalsReviewer: options.approvalsReviewer ?? "user",
|
|
609
|
+
sandbox: options.sandboxMode,
|
|
610
|
+
model: options.model ?? null,
|
|
611
|
+
serviceTier: config.serviceTier === "fast" ? "fast" : null,
|
|
612
|
+
ephemeral: false,
|
|
613
|
+
threadSource: "user"
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
threadResumeParamsFor(input, config, providerThreadId, readOnly) {
|
|
617
|
+
const options = this.effectiveThreadOptions(input, config, readOnly);
|
|
618
|
+
return {
|
|
619
|
+
threadId: providerThreadId,
|
|
620
|
+
cwd: input.repositoryPath,
|
|
621
|
+
approvalPolicy: options.approvalPolicy,
|
|
622
|
+
approvalsReviewer: options.approvalsReviewer ?? "user",
|
|
623
|
+
sandbox: options.sandboxMode,
|
|
624
|
+
model: options.model ?? null,
|
|
625
|
+
serviceTier: config.serviceTier === "fast" ? "fast" : null
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
threadGoalSetParamsFor(input, config, threadId) {
|
|
629
|
+
return {
|
|
630
|
+
threadId,
|
|
631
|
+
objective: renderProviderGoal(input, config)
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
turnStartParamsFor(input, config, threadId, prompt, readOnly, outputSchema) {
|
|
635
|
+
const options = this.effectiveThreadOptions(input, config, readOnly);
|
|
636
|
+
return {
|
|
637
|
+
threadId,
|
|
638
|
+
input: [{ type: "text", text: prompt, text_elements: [] }],
|
|
639
|
+
cwd: input.repositoryPath,
|
|
640
|
+
approvalPolicy: options.approvalPolicy,
|
|
641
|
+
approvalsReviewer: options.approvalsReviewer ?? "user",
|
|
642
|
+
sandboxPolicy: appServerSandboxPolicyFor(input.repositoryPath, options, readOnly),
|
|
643
|
+
model: options.model ?? null,
|
|
644
|
+
serviceTier: config.serviceTier === "fast" ? "fast" : null,
|
|
645
|
+
effort: options.modelReasoningEffort ?? null,
|
|
646
|
+
outputSchema: outputSchema ?? null
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
effectiveThreadOptions(input, config, readOnly) {
|
|
650
|
+
const memberOptions = threadOptionsForMember(config);
|
|
651
|
+
const options = {
|
|
652
|
+
...this.threadOptions,
|
|
653
|
+
...input.codexThreadOptions,
|
|
654
|
+
...memberOptions
|
|
655
|
+
};
|
|
656
|
+
if (readOnly) {
|
|
657
|
+
options.networkAccessEnabled = input.codexThreadOptions?.networkAccessEnabled ?? this.threadOptions.networkAccessEnabled ?? memberOptions.networkAccessEnabled;
|
|
658
|
+
options.sandboxMode = "read-only";
|
|
659
|
+
options.approvalPolicy = "never";
|
|
660
|
+
options.approvalsReviewer = "user";
|
|
661
|
+
}
|
|
662
|
+
return options;
|
|
663
|
+
}
|
|
664
|
+
handleNotification(message) {
|
|
665
|
+
const active = activeTurnForMessage(message, this.activeByThreadId, this.activeByTurnId);
|
|
666
|
+
if (!active) {
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
active.rawEvents.push(rawCodexEvent(message));
|
|
670
|
+
const params = isRecord(message.params) ? message.params : {};
|
|
671
|
+
const messageTurnId = readString(params, "turnId") ?? readNestedString(params, ["turn", "id"]);
|
|
672
|
+
this.eventBus.push({
|
|
673
|
+
type: "runner.appServer.message",
|
|
674
|
+
runId: active.runId,
|
|
675
|
+
direction: "server-notification",
|
|
676
|
+
providerThreadId: active.threadId,
|
|
677
|
+
providerTurnId: messageTurnId ?? active.turnId,
|
|
678
|
+
method: message.method,
|
|
679
|
+
message
|
|
680
|
+
});
|
|
681
|
+
if (messageTurnId && messageTurnId !== active.turnId) {
|
|
682
|
+
this.setActiveTurnId(active, messageTurnId);
|
|
683
|
+
}
|
|
684
|
+
if (message.method === "item/started" || message.method === "item/completed") {
|
|
685
|
+
this.trackCommandExecutionItem(active, message.method, readRecord(params, "item"));
|
|
686
|
+
}
|
|
687
|
+
const effects = interpretAppServerNotification({
|
|
688
|
+
runId: active.runId,
|
|
689
|
+
providerThreadId: active.threadId,
|
|
690
|
+
providerTurnId: messageTurnId ?? active.turnId
|
|
691
|
+
}, message);
|
|
692
|
+
for (const effect of effects) {
|
|
693
|
+
switch (effect.kind) {
|
|
694
|
+
case "setTurnId":
|
|
695
|
+
this.setActiveTurnId(active, effect.turnId);
|
|
696
|
+
break;
|
|
697
|
+
case "usage":
|
|
698
|
+
active.usage = effect.usage;
|
|
699
|
+
break;
|
|
700
|
+
case "teamEvent":
|
|
701
|
+
this.eventBus.push(effect.event);
|
|
702
|
+
break;
|
|
703
|
+
case "finalResponse":
|
|
704
|
+
active.finalResponse = effect.finalResponse;
|
|
705
|
+
this.requestBackgroundTerminalCleanupIfNeeded(active);
|
|
706
|
+
break;
|
|
707
|
+
case "completeTurn":
|
|
708
|
+
this.completeActiveTurn(active, effect.turn);
|
|
709
|
+
break;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
trackCommandExecutionItem(active, method, item) {
|
|
714
|
+
if (readString(item, "type") !== "commandExecution") {
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
const itemId = appServerItemId(item);
|
|
718
|
+
if (!itemId) {
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
const status = readString(item, "status");
|
|
722
|
+
if (method === "item/completed" || status === "completed" || status === "failed" || status === "declined") {
|
|
723
|
+
active.activeCommandExecutionIds.delete(itemId);
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
active.activeCommandExecutionIds.add(itemId);
|
|
727
|
+
}
|
|
728
|
+
requestBackgroundTerminalCleanupIfNeeded(active) {
|
|
729
|
+
if (active.backgroundTerminalCleanupRequested || active.activeCommandExecutionIds.size === 0) {
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
active.backgroundTerminalCleanupRequested = true;
|
|
733
|
+
void this.client.request("thread/backgroundTerminals/clean", { threadId: active.threadId }, { timeoutMs: 10_000 }).catch(() => undefined);
|
|
734
|
+
}
|
|
735
|
+
completeActiveTurn(active, turn) {
|
|
736
|
+
if (active.completed) {
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
active.completed = true;
|
|
740
|
+
const turnId = readString(turn, "id") ?? active.turnId;
|
|
741
|
+
if (turnId) {
|
|
742
|
+
this.setActiveTurnId(active, turnId);
|
|
743
|
+
}
|
|
744
|
+
const status = readString(turn, "status");
|
|
745
|
+
if (status === "failed") {
|
|
746
|
+
const error = readRecord(turn, "error");
|
|
747
|
+
const message = readString(error, "message") ?? "Codex app-server turn failed";
|
|
748
|
+
this.eventBus.push({ type: "runner.error", runId: active.runId, error: message });
|
|
749
|
+
active.reject(new Error(message));
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
if (status === "interrupted") {
|
|
753
|
+
active.reject(new Error("Codex app-server turn interrupted"));
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
const finalResponse = active.finalResponse ?? finalResponseFromAppServerTurn(turn);
|
|
757
|
+
if (finalResponse && !active.finalEmitted) {
|
|
758
|
+
active.finalEmitted = true;
|
|
759
|
+
this.eventBus.push({ type: "runner.final", runId: active.runId, finalResponse });
|
|
760
|
+
}
|
|
761
|
+
active.resolve({
|
|
762
|
+
runId: active.runId,
|
|
763
|
+
provider: "codex",
|
|
764
|
+
providerThreadId: active.threadId,
|
|
765
|
+
providerTurnId: active.turnId,
|
|
766
|
+
finalResponse,
|
|
767
|
+
rawResult: {
|
|
768
|
+
rawEvents: active.rawEvents,
|
|
769
|
+
finalResponse,
|
|
770
|
+
usage: active.usage,
|
|
771
|
+
turn
|
|
772
|
+
}
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
trackActiveTurn(active) {
|
|
776
|
+
this.activeByRunId.set(active.runId, active);
|
|
777
|
+
this.activeByThreadId.set(active.threadId, active);
|
|
778
|
+
if (active.turnId) {
|
|
779
|
+
this.activeByTurnId.set(active.turnId, active);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
setActiveTurnId(active, turnId) {
|
|
783
|
+
if (active.turnId && active.turnId !== turnId) {
|
|
784
|
+
this.activeByTurnId.delete(active.turnId);
|
|
785
|
+
}
|
|
786
|
+
active.turnId = turnId;
|
|
787
|
+
this.activeByTurnId.set(turnId, active);
|
|
788
|
+
}
|
|
789
|
+
untrackActiveTurn(active) {
|
|
790
|
+
this.activeByRunId.delete(active.runId);
|
|
791
|
+
this.activeByThreadId.delete(active.threadId);
|
|
792
|
+
if (active.turnId) {
|
|
793
|
+
this.activeByTurnId.delete(active.turnId);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
async handleServerRequest(message) {
|
|
797
|
+
const params = isRecord(message.params) ? message.params : {};
|
|
798
|
+
const active = activeTurnForMessage(message, this.activeByThreadId, this.activeByTurnId);
|
|
799
|
+
active?.rawEvents.push(rawCodexEvent(message));
|
|
800
|
+
const runId = active?.runId;
|
|
801
|
+
if (runId) {
|
|
802
|
+
this.eventBus.push({
|
|
803
|
+
type: "runner.appServer.message",
|
|
804
|
+
runId,
|
|
805
|
+
direction: "server-request",
|
|
806
|
+
providerThreadId: active?.threadId,
|
|
807
|
+
providerTurnId: active?.turnId,
|
|
808
|
+
method: message.method,
|
|
809
|
+
message
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
const approvalSummary = appServerApprovalSummary(message.method, params);
|
|
813
|
+
if (runId && approvalSummary) {
|
|
814
|
+
this.eventBus.push({ type: "runner.status.changed", runId, phase: "waiting", headline: "Approval requested", detail: approvalSummary, providerThreadId: active?.threadId, providerTurnId: active?.turnId });
|
|
815
|
+
}
|
|
816
|
+
if (message.method === "item/permissions/requestApproval") {
|
|
817
|
+
if (active?.threadId && active.turnId) {
|
|
818
|
+
void this.client.request("turn/interrupt", { threadId: active.threadId, turnId: active.turnId }, { timeoutMs: 10_000 }).catch(() => undefined);
|
|
819
|
+
}
|
|
820
|
+
if (runId) {
|
|
821
|
+
this.eventBus.push({ type: "runner.error", runId, error: "Codex app-server requested additional permissions; Hunsu declined and interrupted the turn." });
|
|
822
|
+
}
|
|
823
|
+
if (active && !active.completed) {
|
|
824
|
+
active.completed = true;
|
|
825
|
+
active.reject(new Error("Codex app-server requested additional permissions; Hunsu declined and interrupted the turn."));
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
return defaultAppServerRequestHandler(message);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
export function createDefaultCodexRunner(options = {}) {
|
|
832
|
+
return new CodexAppServerRunner(options);
|
|
833
|
+
}
|
|
834
|
+
function defaultAppServerRequestHandler(message) {
|
|
835
|
+
switch (message.method) {
|
|
836
|
+
case "item/commandExecution/requestApproval":
|
|
837
|
+
return { decision: "decline" };
|
|
838
|
+
case "item/fileChange/requestApproval":
|
|
839
|
+
return { decision: "decline" };
|
|
840
|
+
case "execCommandApproval":
|
|
841
|
+
case "applyPatchApproval":
|
|
842
|
+
return { decision: "denied" };
|
|
843
|
+
case "item/permissions/requestApproval":
|
|
844
|
+
return { permissions: {}, scope: "turn", strictAutoReview: true };
|
|
845
|
+
default:
|
|
846
|
+
throw new CodexAppServerRequestError(-32601, `Unsupported Codex app-server request: ${message.method ?? "unknown"}`);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
function appServerSandboxPolicyFor(repositoryPath, options, readOnly) {
|
|
850
|
+
const networkAccess = options.networkAccessEnabled === true;
|
|
851
|
+
if (readOnly || options.sandboxMode === "read-only") {
|
|
852
|
+
return { type: "readOnly", networkAccess };
|
|
853
|
+
}
|
|
854
|
+
if (options.sandboxMode === "danger-full-access") {
|
|
855
|
+
return { type: "dangerFullAccess", networkAccess };
|
|
856
|
+
}
|
|
857
|
+
return {
|
|
858
|
+
type: "workspaceWrite",
|
|
859
|
+
writableRoots: [repositoryPath],
|
|
860
|
+
networkAccess,
|
|
861
|
+
excludeTmpdirEnvVar: false,
|
|
862
|
+
excludeSlashTmp: false
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
function activeTurnForMessage(message, byThreadId, byTurnId) {
|
|
866
|
+
const params = isRecord(message.params) ? message.params : {};
|
|
867
|
+
const turnId = readString(params, "turnId") ?? readNestedString(params, ["turn", "id"]);
|
|
868
|
+
if (turnId) {
|
|
869
|
+
const active = byTurnId.get(turnId);
|
|
870
|
+
if (active) {
|
|
871
|
+
return active;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
const threadId = readString(params, "threadId") ?? readNestedString(params, ["thread", "id"]);
|
|
875
|
+
return threadId ? byThreadId.get(threadId) : undefined;
|
|
876
|
+
}
|
|
877
|
+
function rawCodexEvent(message) {
|
|
878
|
+
return { source: "codex.app-server", message };
|
|
879
|
+
}
|
|
880
|
+
function interpretAppServerNotification(context, message) {
|
|
881
|
+
const params = isRecord(message.params) ? message.params : {};
|
|
882
|
+
switch (message.method) {
|
|
883
|
+
case "turn/started": {
|
|
884
|
+
const turn = readRecord(params, "turn");
|
|
885
|
+
const turnId = readString(turn, "id") ?? readString(params, "turnId") ?? context.providerTurnId ?? "turn";
|
|
886
|
+
return [
|
|
887
|
+
...(turnId ? [{ kind: "setTurnId", turnId }] : []),
|
|
888
|
+
{
|
|
889
|
+
kind: "teamEvent",
|
|
890
|
+
event: {
|
|
891
|
+
type: "runner.turn.started",
|
|
892
|
+
runId: context.runId,
|
|
893
|
+
providerThreadId: context.providerThreadId,
|
|
894
|
+
providerTurnId: turnId,
|
|
895
|
+
turn,
|
|
896
|
+
startedAtMs: readNumber(params, "startedAtMs")
|
|
897
|
+
}
|
|
898
|
+
},
|
|
899
|
+
{
|
|
900
|
+
kind: "teamEvent",
|
|
901
|
+
event: {
|
|
902
|
+
type: "runner.status.changed",
|
|
903
|
+
runId: context.runId,
|
|
904
|
+
phase: "working",
|
|
905
|
+
headline: "Working",
|
|
906
|
+
detail: turnId,
|
|
907
|
+
providerThreadId: context.providerThreadId,
|
|
908
|
+
providerTurnId: turnId
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
];
|
|
912
|
+
}
|
|
913
|
+
case "turn/completed": {
|
|
914
|
+
const turn = readRecord(params, "turn");
|
|
915
|
+
const turnId = readString(turn, "id") ?? readString(params, "turnId") ?? context.providerTurnId;
|
|
916
|
+
return [
|
|
917
|
+
{
|
|
918
|
+
kind: "teamEvent",
|
|
919
|
+
event: {
|
|
920
|
+
type: "runner.turn.completed",
|
|
921
|
+
runId: context.runId,
|
|
922
|
+
providerThreadId: context.providerThreadId,
|
|
923
|
+
providerTurnId: turnId,
|
|
924
|
+
turn,
|
|
925
|
+
completedAtMs: readNumber(params, "completedAtMs")
|
|
926
|
+
}
|
|
927
|
+
},
|
|
928
|
+
{ kind: "completeTurn", turn }
|
|
929
|
+
];
|
|
930
|
+
}
|
|
931
|
+
case "thread/tokenUsage/updated": {
|
|
932
|
+
const usage = appServerUsageFromNotification(params);
|
|
933
|
+
return [
|
|
934
|
+
{ kind: "usage", usage },
|
|
935
|
+
{
|
|
936
|
+
kind: "teamEvent",
|
|
937
|
+
event: {
|
|
938
|
+
type: "runner.status.changed",
|
|
939
|
+
runId: context.runId,
|
|
940
|
+
phase: "working",
|
|
941
|
+
headline: "Working",
|
|
942
|
+
detail: formatAppServerUsage(usage),
|
|
943
|
+
providerThreadId: context.providerThreadId,
|
|
944
|
+
providerTurnId: context.providerTurnId
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
];
|
|
948
|
+
}
|
|
949
|
+
case "item/started":
|
|
950
|
+
case "item/completed":
|
|
951
|
+
return mapAppServerThreadItemEvent(context, message.method, readRecord(params, "item"));
|
|
952
|
+
case "item/agentMessage/delta":
|
|
953
|
+
case "item/agentMessage/textDelta":
|
|
954
|
+
case "item/agentMessage/outputDelta":
|
|
955
|
+
case "item/assistantMessage/delta":
|
|
956
|
+
case "item/assistantMessage/textDelta":
|
|
957
|
+
case "item/assistantMessage/outputDelta": {
|
|
958
|
+
const delta = readDeltaText(params);
|
|
959
|
+
if (!delta) {
|
|
960
|
+
return [];
|
|
961
|
+
}
|
|
962
|
+
const itemId = appServerItemId(params) ?? "agentMessage";
|
|
963
|
+
const providerTurnId = readString(params, "turnId") ?? context.providerTurnId;
|
|
964
|
+
return [{
|
|
965
|
+
kind: "teamEvent",
|
|
966
|
+
event: {
|
|
967
|
+
type: "runner.item.delta",
|
|
968
|
+
runId: context.runId,
|
|
969
|
+
providerThreadId: context.providerThreadId,
|
|
970
|
+
providerTurnId,
|
|
971
|
+
itemId,
|
|
972
|
+
deltaKind: "agentMessage",
|
|
973
|
+
delta
|
|
974
|
+
}
|
|
975
|
+
}];
|
|
976
|
+
}
|
|
977
|
+
case "item/commandExecution/outputDelta":
|
|
978
|
+
case "item/fileChange/outputDelta":
|
|
979
|
+
case "item/plan/delta":
|
|
980
|
+
case "item/reasoning/textDelta":
|
|
981
|
+
case "item/reasoning/summaryTextDelta": {
|
|
982
|
+
const delta = readDeltaText(params);
|
|
983
|
+
if (!delta) {
|
|
984
|
+
return [];
|
|
985
|
+
}
|
|
986
|
+
const deltaKind = appServerDeltaKind(message.method);
|
|
987
|
+
const itemId = appServerItemId(params) ?? deltaKind;
|
|
988
|
+
const providerTurnId = readString(params, "turnId") ?? context.providerTurnId;
|
|
989
|
+
return [{
|
|
990
|
+
kind: "teamEvent",
|
|
991
|
+
event: {
|
|
992
|
+
type: "runner.item.delta",
|
|
993
|
+
runId: context.runId,
|
|
994
|
+
providerThreadId: context.providerThreadId,
|
|
995
|
+
providerTurnId,
|
|
996
|
+
itemId,
|
|
997
|
+
deltaKind,
|
|
998
|
+
delta,
|
|
999
|
+
contentIndex: readNumber(params, "contentIndex")
|
|
1000
|
+
}
|
|
1001
|
+
}];
|
|
1002
|
+
}
|
|
1003
|
+
case "turn/plan/updated": {
|
|
1004
|
+
const explanation = readString(params, "explanation");
|
|
1005
|
+
return [{
|
|
1006
|
+
kind: "teamEvent",
|
|
1007
|
+
event: {
|
|
1008
|
+
type: "runner.status.changed",
|
|
1009
|
+
runId: context.runId,
|
|
1010
|
+
phase: "thinking",
|
|
1011
|
+
headline: "Planning",
|
|
1012
|
+
detail: explanation ? truncateText(explanation, 500) : "Codex plan updated.",
|
|
1013
|
+
providerThreadId: context.providerThreadId,
|
|
1014
|
+
providerTurnId: context.providerTurnId
|
|
1015
|
+
}
|
|
1016
|
+
}];
|
|
1017
|
+
}
|
|
1018
|
+
case "error": {
|
|
1019
|
+
const error = readRecord(params, "error");
|
|
1020
|
+
return [{
|
|
1021
|
+
kind: "teamEvent",
|
|
1022
|
+
event: { type: "runner.error", runId: context.runId, error: readString(error, "message") ?? "Codex app-server error" }
|
|
1023
|
+
}];
|
|
1024
|
+
}
|
|
1025
|
+
default:
|
|
1026
|
+
return [];
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
function appServerUsageFromNotification(params) {
|
|
1030
|
+
const usage = readRecord(params, "tokenUsage");
|
|
1031
|
+
const last = readRecord(usage, "last");
|
|
1032
|
+
if (!last) {
|
|
1033
|
+
return undefined;
|
|
1034
|
+
}
|
|
1035
|
+
return {
|
|
1036
|
+
input_tokens: readNumber(last, "inputTokens") ?? 0,
|
|
1037
|
+
cached_input_tokens: readNumber(last, "cachedInputTokens") ?? 0,
|
|
1038
|
+
output_tokens: readNumber(last, "outputTokens") ?? 0,
|
|
1039
|
+
reasoning_output_tokens: readNumber(last, "reasoningOutputTokens") ?? 0
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
function formatAppServerUsage(usage) {
|
|
1043
|
+
if (!usage) {
|
|
1044
|
+
return "Codex app-server usage updated.";
|
|
1045
|
+
}
|
|
1046
|
+
return `Codex turn usage. Input ${usage.input_tokens}, output ${usage.output_tokens}, reasoning ${usage.reasoning_output_tokens}.`;
|
|
1047
|
+
}
|
|
1048
|
+
function appServerApprovalSummary(method, params) {
|
|
1049
|
+
switch (method) {
|
|
1050
|
+
case "item/commandExecution/requestApproval":
|
|
1051
|
+
return `Codex app-server requested command approval; Hunsu declined: ${readString(params, "command") ?? "unknown command"}`;
|
|
1052
|
+
case "item/fileChange/requestApproval":
|
|
1053
|
+
return `Codex app-server requested file-change approval; Hunsu declined${readString(params, "grantRoot") ? ` for ${readString(params, "grantRoot")}` : ""}.`;
|
|
1054
|
+
case "item/permissions/requestApproval":
|
|
1055
|
+
return "Codex app-server requested additional permissions; Hunsu declined.";
|
|
1056
|
+
case "execCommandApproval":
|
|
1057
|
+
return `Codex app-server requested command approval; Hunsu declined: ${params.command?.join(" ") ?? "unknown command"}`;
|
|
1058
|
+
case "applyPatchApproval":
|
|
1059
|
+
return "Codex app-server requested patch approval; Hunsu declined.";
|
|
1060
|
+
default:
|
|
1061
|
+
return undefined;
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
function mapAppServerThreadItemEvent(context, method, item) {
|
|
1065
|
+
const phase = method === "item/started" ? "started" : "completed";
|
|
1066
|
+
const normalized = normalizeAppServerThreadItem(item);
|
|
1067
|
+
if (!normalized) {
|
|
1068
|
+
return [];
|
|
1069
|
+
}
|
|
1070
|
+
const itemId = normalized.id ?? appServerItemId(item) ?? normalized.type;
|
|
1071
|
+
const lifecycleEffect = {
|
|
1072
|
+
kind: "teamEvent",
|
|
1073
|
+
event: phase === "started"
|
|
1074
|
+
? {
|
|
1075
|
+
type: "runner.item.started",
|
|
1076
|
+
runId: context.runId,
|
|
1077
|
+
providerThreadId: context.providerThreadId,
|
|
1078
|
+
providerTurnId: context.providerTurnId,
|
|
1079
|
+
itemId,
|
|
1080
|
+
item: normalized
|
|
1081
|
+
}
|
|
1082
|
+
: {
|
|
1083
|
+
type: "runner.item.completed",
|
|
1084
|
+
runId: context.runId,
|
|
1085
|
+
providerThreadId: context.providerThreadId,
|
|
1086
|
+
providerTurnId: context.providerTurnId,
|
|
1087
|
+
itemId,
|
|
1088
|
+
item: normalized
|
|
1089
|
+
}
|
|
1090
|
+
};
|
|
1091
|
+
switch (normalized.type) {
|
|
1092
|
+
case "agentMessage": {
|
|
1093
|
+
const text = normalized.text;
|
|
1094
|
+
if (phase !== "completed" || !text) {
|
|
1095
|
+
return [lifecycleEffect];
|
|
1096
|
+
}
|
|
1097
|
+
return [
|
|
1098
|
+
lifecycleEffect,
|
|
1099
|
+
{ kind: "finalResponse", finalResponse: text }
|
|
1100
|
+
];
|
|
1101
|
+
}
|
|
1102
|
+
case "reasoning": {
|
|
1103
|
+
return [lifecycleEffect];
|
|
1104
|
+
}
|
|
1105
|
+
case "plan": {
|
|
1106
|
+
return [lifecycleEffect];
|
|
1107
|
+
}
|
|
1108
|
+
case "commandExecution": {
|
|
1109
|
+
return [lifecycleEffect];
|
|
1110
|
+
}
|
|
1111
|
+
case "fileChange": {
|
|
1112
|
+
return [lifecycleEffect];
|
|
1113
|
+
}
|
|
1114
|
+
case "mcpToolCall": {
|
|
1115
|
+
return [lifecycleEffect];
|
|
1116
|
+
}
|
|
1117
|
+
case "webSearch": {
|
|
1118
|
+
return [lifecycleEffect];
|
|
1119
|
+
}
|
|
1120
|
+
default:
|
|
1121
|
+
return [lifecycleEffect];
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
function appServerDeltaKind(method) {
|
|
1125
|
+
switch (method) {
|
|
1126
|
+
case "item/agentMessage/delta":
|
|
1127
|
+
case "item/agentMessage/textDelta":
|
|
1128
|
+
case "item/agentMessage/outputDelta":
|
|
1129
|
+
case "item/assistantMessage/delta":
|
|
1130
|
+
case "item/assistantMessage/textDelta":
|
|
1131
|
+
case "item/assistantMessage/outputDelta":
|
|
1132
|
+
return "agentMessage";
|
|
1133
|
+
case "item/plan/delta":
|
|
1134
|
+
return "plan";
|
|
1135
|
+
case "item/reasoning/textDelta":
|
|
1136
|
+
return "reasoningText";
|
|
1137
|
+
case "item/reasoning/summaryTextDelta":
|
|
1138
|
+
return "reasoningSummary";
|
|
1139
|
+
case "item/fileChange/outputDelta":
|
|
1140
|
+
return "fileChangeOutput";
|
|
1141
|
+
case "item/commandExecution/outputDelta":
|
|
1142
|
+
default:
|
|
1143
|
+
return "commandOutput";
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
function normalizeAppServerThreadItem(item) {
|
|
1147
|
+
const type = readString(item, "type");
|
|
1148
|
+
if (!item || !type) {
|
|
1149
|
+
return undefined;
|
|
1150
|
+
}
|
|
1151
|
+
const normalized = {
|
|
1152
|
+
id: appServerItemId(item),
|
|
1153
|
+
type,
|
|
1154
|
+
raw: item
|
|
1155
|
+
};
|
|
1156
|
+
const text = readString(item, "text");
|
|
1157
|
+
if (text !== undefined) {
|
|
1158
|
+
normalized.text = text;
|
|
1159
|
+
}
|
|
1160
|
+
else {
|
|
1161
|
+
const contentText = readAppServerContentText(item);
|
|
1162
|
+
if (contentText !== undefined) {
|
|
1163
|
+
normalized.text = contentText;
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
const summary = readAppServerTextParts(item.summary);
|
|
1167
|
+
if (summary.length > 0) {
|
|
1168
|
+
normalized.summary = summary;
|
|
1169
|
+
}
|
|
1170
|
+
const content = readAppServerTextParts(item.content);
|
|
1171
|
+
if (content.length > 0) {
|
|
1172
|
+
normalized.content = content;
|
|
1173
|
+
}
|
|
1174
|
+
const command = readString(item, "command");
|
|
1175
|
+
if (command !== undefined) {
|
|
1176
|
+
normalized.command = command;
|
|
1177
|
+
}
|
|
1178
|
+
const cwd = readString(item, "cwd");
|
|
1179
|
+
if (cwd !== undefined) {
|
|
1180
|
+
normalized.cwd = cwd;
|
|
1181
|
+
}
|
|
1182
|
+
const status = readString(item, "status");
|
|
1183
|
+
if (status !== undefined) {
|
|
1184
|
+
normalized.status = status;
|
|
1185
|
+
}
|
|
1186
|
+
const commandActions = readAppServerCommandActions(item);
|
|
1187
|
+
if (commandActions.length > 0) {
|
|
1188
|
+
normalized.commandActions = commandActions;
|
|
1189
|
+
}
|
|
1190
|
+
const aggregatedOutput = readString(item, "aggregatedOutput");
|
|
1191
|
+
if (aggregatedOutput !== undefined) {
|
|
1192
|
+
normalized.aggregatedOutput = aggregatedOutput;
|
|
1193
|
+
}
|
|
1194
|
+
const exitCode = readNumber(item, "exitCode");
|
|
1195
|
+
if (exitCode !== undefined) {
|
|
1196
|
+
normalized.exitCode = exitCode;
|
|
1197
|
+
}
|
|
1198
|
+
const durationMs = readNumber(item, "durationMs");
|
|
1199
|
+
if (durationMs !== undefined) {
|
|
1200
|
+
normalized.durationMs = durationMs;
|
|
1201
|
+
}
|
|
1202
|
+
if ("changes" in item) {
|
|
1203
|
+
normalized.changes = item.changes;
|
|
1204
|
+
}
|
|
1205
|
+
const server = readString(item, "server");
|
|
1206
|
+
if (server !== undefined) {
|
|
1207
|
+
normalized.server = server;
|
|
1208
|
+
}
|
|
1209
|
+
const tool = readString(item, "tool");
|
|
1210
|
+
if (tool !== undefined) {
|
|
1211
|
+
normalized.tool = tool;
|
|
1212
|
+
}
|
|
1213
|
+
const query = readString(item, "query");
|
|
1214
|
+
if (query !== undefined) {
|
|
1215
|
+
normalized.query = query;
|
|
1216
|
+
}
|
|
1217
|
+
return normalized;
|
|
1218
|
+
}
|
|
1219
|
+
function readAppServerContentText(item) {
|
|
1220
|
+
const content = readAppServerTextParts(item.content);
|
|
1221
|
+
if (content.length === 0) {
|
|
1222
|
+
return undefined;
|
|
1223
|
+
}
|
|
1224
|
+
return content.join("\n\n");
|
|
1225
|
+
}
|
|
1226
|
+
function readAppServerTextParts(value) {
|
|
1227
|
+
if (!Array.isArray(value)) {
|
|
1228
|
+
return [];
|
|
1229
|
+
}
|
|
1230
|
+
return value.flatMap((part) => {
|
|
1231
|
+
if (typeof part === "string") {
|
|
1232
|
+
return part ? [part] : [];
|
|
1233
|
+
}
|
|
1234
|
+
if (!isRecord(part)) {
|
|
1235
|
+
return [];
|
|
1236
|
+
}
|
|
1237
|
+
const text = readString(part, "text") ?? readString(part, "content") ?? readString(part, "summary");
|
|
1238
|
+
return text ? [text] : [];
|
|
1239
|
+
});
|
|
1240
|
+
}
|
|
1241
|
+
function readAppServerCommandActions(item) {
|
|
1242
|
+
const value = item.commandActions;
|
|
1243
|
+
if (!Array.isArray(value)) {
|
|
1244
|
+
return [];
|
|
1245
|
+
}
|
|
1246
|
+
return value.flatMap((action) => {
|
|
1247
|
+
if (!isRecord(action)) {
|
|
1248
|
+
return [];
|
|
1249
|
+
}
|
|
1250
|
+
const type = readString(action, "type");
|
|
1251
|
+
switch (type) {
|
|
1252
|
+
case "read":
|
|
1253
|
+
return [{ type, command: readString(action, "command"), name: readString(action, "name"), path: readString(action, "path") }];
|
|
1254
|
+
case "listFiles":
|
|
1255
|
+
return [{ type, command: readString(action, "command"), path: readString(action, "path") }];
|
|
1256
|
+
case "search":
|
|
1257
|
+
return [{ type, command: readString(action, "command"), query: readString(action, "query"), path: readString(action, "path") }];
|
|
1258
|
+
case "unknown":
|
|
1259
|
+
return [{ type, command: readString(action, "command") }];
|
|
1260
|
+
default:
|
|
1261
|
+
return [];
|
|
1262
|
+
}
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
function finalResponseFromAppServerTurn(turn) {
|
|
1266
|
+
const items = Array.isArray(turn?.items) ? turn.items : [];
|
|
1267
|
+
for (const item of [...items].reverse()) {
|
|
1268
|
+
if (isRecord(item) && item.type === "agentMessage" && typeof item.text === "string") {
|
|
1269
|
+
return item.text;
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
return undefined;
|
|
1273
|
+
}
|
|
1274
|
+
function appServerItemId(value) {
|
|
1275
|
+
if (!isRecord(value)) {
|
|
1276
|
+
return undefined;
|
|
1277
|
+
}
|
|
1278
|
+
return readString(value, "itemId") ?? readString(value, "id");
|
|
1279
|
+
}
|
|
1280
|
+
function readRecord(value, key) {
|
|
1281
|
+
if (!isRecord(value)) {
|
|
1282
|
+
return undefined;
|
|
1283
|
+
}
|
|
1284
|
+
const child = value[key];
|
|
1285
|
+
return isRecord(child) ? child : undefined;
|
|
1286
|
+
}
|
|
1287
|
+
function readString(value, key) {
|
|
1288
|
+
if (!isRecord(value)) {
|
|
1289
|
+
return undefined;
|
|
1290
|
+
}
|
|
1291
|
+
const child = value[key];
|
|
1292
|
+
return typeof child === "string" ? child : undefined;
|
|
1293
|
+
}
|
|
1294
|
+
function readDeltaText(value) {
|
|
1295
|
+
const delta = readString(value, "delta");
|
|
1296
|
+
if (delta !== undefined) {
|
|
1297
|
+
return delta;
|
|
1298
|
+
}
|
|
1299
|
+
const deltaBase64 = readString(value, "deltaBase64");
|
|
1300
|
+
if (!deltaBase64) {
|
|
1301
|
+
return undefined;
|
|
1302
|
+
}
|
|
1303
|
+
try {
|
|
1304
|
+
return Buffer.from(deltaBase64, deltaBase64.includes("-") || deltaBase64.includes("_") ? "base64url" : "base64").toString("utf8");
|
|
1305
|
+
}
|
|
1306
|
+
catch {
|
|
1307
|
+
return undefined;
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
function readNumber(value, key) {
|
|
1311
|
+
if (!isRecord(value)) {
|
|
1312
|
+
return undefined;
|
|
1313
|
+
}
|
|
1314
|
+
const child = value[key];
|
|
1315
|
+
return typeof child === "number" ? child : undefined;
|
|
1316
|
+
}
|
|
1317
|
+
function readStringArray(value, key) {
|
|
1318
|
+
if (!isRecord(value) || !Array.isArray(value[key])) {
|
|
1319
|
+
return [];
|
|
1320
|
+
}
|
|
1321
|
+
return value[key].filter((item) => typeof item === "string");
|
|
1322
|
+
}
|
|
1323
|
+
function readNestedString(value, keys) {
|
|
1324
|
+
let current = value;
|
|
1325
|
+
for (const key of keys) {
|
|
1326
|
+
if (!isRecord(current)) {
|
|
1327
|
+
return undefined;
|
|
1328
|
+
}
|
|
1329
|
+
current = current[key];
|
|
1330
|
+
}
|
|
1331
|
+
return typeof current === "string" ? current : undefined;
|
|
1332
|
+
}
|
|
1333
|
+
function isRecord(value) {
|
|
1334
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
1335
|
+
}
|
|
1336
|
+
export function buildTeamPlanningPrompt(input) {
|
|
1337
|
+
const protocol = teamHarnessForInput(input);
|
|
1338
|
+
return [
|
|
1339
|
+
"<role>",
|
|
1340
|
+
" <purpose>",
|
|
1341
|
+
" Plan a closure-free executable ExecutionPlan.",
|
|
1342
|
+
" </purpose>",
|
|
1343
|
+
"",
|
|
1344
|
+
" <instructions>",
|
|
1345
|
+
indentBlock(teamInstructionFor(input, protocol), 4),
|
|
1346
|
+
" </instructions>",
|
|
1347
|
+
"</role>",
|
|
1348
|
+
"",
|
|
1349
|
+
"<goal>",
|
|
1350
|
+
indentBlock(formatTeamGoal(input), 2),
|
|
1351
|
+
"</goal>",
|
|
1352
|
+
"",
|
|
1353
|
+
"<member_profiles>",
|
|
1354
|
+
formatMemberProfiles(protocol.members),
|
|
1355
|
+
"</member_profiles>",
|
|
1356
|
+
"",
|
|
1357
|
+
"<rules>",
|
|
1358
|
+
" Return one structured ExecutionPlan object with root kind queue.",
|
|
1359
|
+
" Put each executable step in the queue items array as kind goal with stage needs_evaluation.",
|
|
1360
|
+
" Use each goal item to delegate to one assignee Executor and optionally one evaluator Executor.",
|
|
1361
|
+
" A goal assignee must name executorId and a standalone goal.",
|
|
1362
|
+
" A goal evaluator, when present, must name executorId and a prompt that can return pass or fail with type, summary, reason, feedback, nextGoal, and evidence.",
|
|
1363
|
+
` Set remainingAttempts to ${normalizePositiveInteger(input.maxAttemptCount) || maxAttemptCountForPrompt(protocol)} for each goal unless a smaller bound is clearly enough.`,
|
|
1364
|
+
" Set requires to PrevMove unless this goal intentionally depends on earlier Path ids.",
|
|
1365
|
+
"</rules>"
|
|
1366
|
+
].join("\n");
|
|
1367
|
+
}
|
|
1368
|
+
function maxAttemptCountForPrompt(protocol) {
|
|
1369
|
+
switch (protocol.kind) {
|
|
1370
|
+
case "team_execution_plan":
|
|
1371
|
+
return protocol.maxAttemptCount;
|
|
1372
|
+
case "role_squad":
|
|
1373
|
+
case "council_vote":
|
|
1374
|
+
case "court_debate":
|
|
1375
|
+
return protocol.maxRoundCount;
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
function normalizePositiveInteger(value) {
|
|
1379
|
+
return Number.isInteger(value) && value && value > 0 ? value : undefined;
|
|
1380
|
+
}
|
|
1381
|
+
export function buildMemberPathPrompt(input) {
|
|
1382
|
+
const member = memberConfigFor(input, input.memberPath.executorId);
|
|
1383
|
+
const renderedMemberPrompt = renderPromptTemplate(member.promptTemplate, memberPromptTemplateContext(input));
|
|
1384
|
+
return [
|
|
1385
|
+
"<member>",
|
|
1386
|
+
renderedMemberPrompt.trim() || "No additional Member prompt.",
|
|
1387
|
+
"</member>",
|
|
1388
|
+
"",
|
|
1389
|
+
"<goal>",
|
|
1390
|
+
input.memberPath.goal.trim(),
|
|
1391
|
+
"</goal>",
|
|
1392
|
+
"",
|
|
1393
|
+
"<constraints>",
|
|
1394
|
+
"Do not read .hunsu files.",
|
|
1395
|
+
"Do not create commits.",
|
|
1396
|
+
"Use bounded commands for verification; do not run preview servers, dev servers, watchers, or other long-running processes in the foreground.",
|
|
1397
|
+
"If you start a preview server, dev server, watcher, or any other long-running command, stop it before your final response.",
|
|
1398
|
+
"Do not leave running command sessions, local listeners, or background processes open at the end of the turn.",
|
|
1399
|
+
"</constraints>"
|
|
1400
|
+
].join("\n");
|
|
1401
|
+
}
|
|
1402
|
+
export function buildMoveFinalizerPrompt(input) {
|
|
1403
|
+
const selectedDestination = selectedDestinationForPrompt(input);
|
|
1404
|
+
const pathOutputs = input.pathOutputs.map(output => [
|
|
1405
|
+
`- ${output.pathId} (${output.executorId})`,
|
|
1406
|
+
` goal: ${output.goal}`,
|
|
1407
|
+
output.commit ? ` commit: ${output.commit}` : undefined,
|
|
1408
|
+
output.finalResponse ? ` output: ${truncateText(output.finalResponse, 2000)}` : undefined
|
|
1409
|
+
].filter(Boolean).join("\n")).join("\n");
|
|
1410
|
+
return [
|
|
1411
|
+
"You are the MOVE finalizer for a completed Hunsu Execute.",
|
|
1412
|
+
"Write the Git commit message for MOVE N+1 from the source MOVE to terminal Path diff.",
|
|
1413
|
+
"Do not edit files. Do not create Git commits. Return only the commit message content.",
|
|
1414
|
+
"",
|
|
1415
|
+
`MOVE id: ${input.moveId}`,
|
|
1416
|
+
`Source MOVE commit: ${input.sourceMoveCommit}`,
|
|
1417
|
+
`Terminal Path commit: ${input.terminalPathCommit}`,
|
|
1418
|
+
input.terminalMemberPathId ? `Terminal Path id: ${input.terminalMemberPathId}` : undefined,
|
|
1419
|
+
"",
|
|
1420
|
+
"Destination:",
|
|
1421
|
+
selectedDestination ? formatDestinationForPrompt(selectedDestination) : `- ${input.requestGoal}`,
|
|
1422
|
+
"",
|
|
1423
|
+
"Completion summary:",
|
|
1424
|
+
input.completionSummary || "Not provided.",
|
|
1425
|
+
"",
|
|
1426
|
+
"Path commits:",
|
|
1427
|
+
Object.entries(input.pathCommits).map(([pathId, commit]) => `- ${pathId}: ${commit}`).join("\n") || "- none",
|
|
1428
|
+
"",
|
|
1429
|
+
"Path outputs:",
|
|
1430
|
+
pathOutputs || "- none",
|
|
1431
|
+
"",
|
|
1432
|
+
"Diff stat:",
|
|
1433
|
+
input.diffStat?.trim() || "Not provided.",
|
|
1434
|
+
"",
|
|
1435
|
+
"Diff name-status:",
|
|
1436
|
+
input.diffNameStatus?.trim() || "Not provided.",
|
|
1437
|
+
"",
|
|
1438
|
+
"Diff patch:",
|
|
1439
|
+
input.diffPatch?.trim() || "Not provided.",
|
|
1440
|
+
"",
|
|
1441
|
+
"Commit message requirements:",
|
|
1442
|
+
"- First line: concise imperative subject for the MOVE result.",
|
|
1443
|
+
"- Body: summarize the product changes and relevant Path outputs.",
|
|
1444
|
+
"- Do not include Hunsu trailers; Studio appends system trailers."
|
|
1445
|
+
].filter((line) => line !== undefined).join("\n");
|
|
1446
|
+
}
|
|
1447
|
+
export function buildHunsuDraftPrompt(input) {
|
|
1448
|
+
const managerInstructions = renderPromptTemplate(input.manager.promptTemplate, providerPromptTemplateContext(input)).trim();
|
|
1449
|
+
return [
|
|
1450
|
+
"<role>",
|
|
1451
|
+
"You are the Hunsu Draft agent for a local Hunsu Studio session.",
|
|
1452
|
+
"Help the user refine a proposed HUNSU intervention conversationally.",
|
|
1453
|
+
"You may edit decoded draft request files under .hunsu-request/ in the supplied Route worktree.",
|
|
1454
|
+
"Do not edit .hunsu/ encoded runtime files, .hunsu-prev/, product files, or create commits.",
|
|
1455
|
+
"Available request runtime files are .hunsu-request/destinations.json, .hunsu-request/harness.json, .hunsu-request/executors.json, .hunsu-request/resources.json, and .hunsu-request/artifact-actions.json.",
|
|
1456
|
+
"Edit only the files required by the user's explicit request.",
|
|
1457
|
+
"Use destinations.json for Destination/TODO changes and artifact-actions.json for Artifact Action changes.",
|
|
1458
|
+
"For a simple TODO/Destination addition, edit only .hunsu-request/destinations.json.",
|
|
1459
|
+
".hunsu-request/executors.json is the source of truth for Member definitions.",
|
|
1460
|
+
".hunsu-request/resources.json is the source of truth for Skill, Plugin, and Resource bindings.",
|
|
1461
|
+
".hunsu-request/harness.json stores Harness policy only; it must not contain a members field.",
|
|
1462
|
+
"Do not edit .hunsu-request/harness.json unless the user explicitly asks to change root Team or Harness policy.",
|
|
1463
|
+
"Do not edit .hunsu-request/executors.json unless the user explicitly asks to change Member definitions or Member config.",
|
|
1464
|
+
"Do not edit .hunsu-request/resources.json unless the user explicitly asks to change Resource bindings or requirements.",
|
|
1465
|
+
"Do not create, copy, or update Resource bindings or assignment entries for newly added Destinations; Team-planner routing and binding are handled outside Destination edits.",
|
|
1466
|
+
"After editing .hunsu-request files, run the supplied Draft check command. It creates a DiffArtifact in Hunsu Local.",
|
|
1467
|
+
"If the DiffArtifact passes, include this exact marker in your final reply: ::hunsu-diff{draftSessionId=\"<draftSessionId>\" diffArtifactId=\"<diffArtifactId>\" status=\"pass\"}",
|
|
1468
|
+
"If the DiffArtifact fails, include the same marker with status=\"failed\" and explain the validation error briefly.",
|
|
1469
|
+
"Confirmation is handled by Hunsu Local from the DiffArtifact id.",
|
|
1470
|
+
"</role>",
|
|
1471
|
+
"",
|
|
1472
|
+
"<manager>",
|
|
1473
|
+
`id: ${input.manager.id}`,
|
|
1474
|
+
"instructions:",
|
|
1475
|
+
indentBlock(managerInstructions || "No Manager-specific instructions.", 2),
|
|
1476
|
+
"</manager>",
|
|
1477
|
+
"",
|
|
1478
|
+
"<draft_check_command>",
|
|
1479
|
+
indentBlock(input.draftCheckCommand, 2),
|
|
1480
|
+
"</draft_check_command>",
|
|
1481
|
+
"",
|
|
1482
|
+
"<source_snapshot>",
|
|
1483
|
+
indentBlock(formatHunsuSourceSnapshot(input.sourceSnapshot), 2),
|
|
1484
|
+
"</source_snapshot>",
|
|
1485
|
+
"",
|
|
1486
|
+
"<request_goal>",
|
|
1487
|
+
indentBlock(input.requestGoal, 2),
|
|
1488
|
+
"</request_goal>",
|
|
1489
|
+
"",
|
|
1490
|
+
"<conversation>",
|
|
1491
|
+
indentBlock(formatHunsuDraftConversation(input.messages), 2),
|
|
1492
|
+
"</conversation>",
|
|
1493
|
+
"",
|
|
1494
|
+
"<latest_user_message>",
|
|
1495
|
+
indentBlock(input.userMessage, 2),
|
|
1496
|
+
"</latest_user_message>",
|
|
1497
|
+
"",
|
|
1498
|
+
"<response_rules>",
|
|
1499
|
+
"Answer naturally and briefly.",
|
|
1500
|
+
"If you edited .hunsu-request files, summarize the changed files and the intended runtime change after running the Draft check command.",
|
|
1501
|
+
"If the requested change cannot be represented in the provided .hunsu-request runtime files, say which runtime file or schema is missing.",
|
|
1502
|
+
"If more detail is needed, ask a focused follow-up question.",
|
|
1503
|
+
"Do not output JSON unless the user explicitly asks for an example.",
|
|
1504
|
+
"</response_rules>"
|
|
1505
|
+
].join("\n");
|
|
1506
|
+
}
|
|
1507
|
+
function renderProviderGoal(input, config) {
|
|
1508
|
+
const renderedPrompt = renderPromptTemplate(config.promptTemplate, providerPromptTemplateContext(input));
|
|
1509
|
+
if (isHunsuDraftInput(input)) {
|
|
1510
|
+
return truncateText([
|
|
1511
|
+
"Objective: discuss a HUNSU draft with the user.",
|
|
1512
|
+
`Source: ${input.sourceSnapshot.teamName ?? "Team"} ${input.sourceSnapshot.moveOrdinal !== undefined ? `M${String(input.sourceSnapshot.moveOrdinal).padStart(4, "0")}` : input.sourceSnapshot.sourceNodeId}`,
|
|
1513
|
+
isHunsuDraftTurnInput(input) ? `User request: ${input.userMessage}` : "Status: preparing the Draft chat session before the first user message.",
|
|
1514
|
+
renderedPrompt.trim() ? `Instructions:\n${renderedPrompt.trim()}` : undefined
|
|
1515
|
+
].filter(Boolean).join("\n\n"), 4000);
|
|
1516
|
+
}
|
|
1517
|
+
if (isMemberPathRunInput(input)) {
|
|
1518
|
+
const parts = [
|
|
1519
|
+
`Objective: ${input.memberPath.goal}`,
|
|
1520
|
+
renderedPrompt.trim() ? `Member:\n${renderedPrompt.trim()}` : undefined
|
|
1521
|
+
].filter(Boolean);
|
|
1522
|
+
return truncateText(parts.join("\n\n"), 4000);
|
|
1523
|
+
}
|
|
1524
|
+
const parts = [
|
|
1525
|
+
`Objective:\n${formatTeamGoal(input)}`,
|
|
1526
|
+
`Instructions:\n${renderedPrompt.trim() || DEFAULT_TEAM_INSTRUCTION}`
|
|
1527
|
+
].filter(Boolean);
|
|
1528
|
+
return truncateText(parts.join("\n\n"), 4000);
|
|
1529
|
+
}
|
|
1530
|
+
function teamInstructionFor(input, protocol) {
|
|
1531
|
+
const prompt = renderPromptTemplate(protocol.team.promptTemplate, teamPromptTemplateContext(input, protocol)).trim();
|
|
1532
|
+
if (!prompt) {
|
|
1533
|
+
return DEFAULT_TEAM_INSTRUCTION;
|
|
1534
|
+
}
|
|
1535
|
+
return prompt;
|
|
1536
|
+
}
|
|
1537
|
+
function teamPromptTemplateContext(input, protocol) {
|
|
1538
|
+
const selectedDestinations = selectedDestinationsForPrompt(input);
|
|
1539
|
+
return {
|
|
1540
|
+
requestGoal: input.requestGoal,
|
|
1541
|
+
currentDestination: selectedDestinations[0] ?? selectedDestinationForPrompt(input),
|
|
1542
|
+
currentDestinations: selectedDestinations,
|
|
1543
|
+
selectedDestinations,
|
|
1544
|
+
activeDestinations: input.activeDestinations,
|
|
1545
|
+
constraints: futureConstraintsForPrompt(input),
|
|
1546
|
+
members: protocol.members.map(member => ({
|
|
1547
|
+
id: member.id,
|
|
1548
|
+
promptTemplate: member.promptTemplate.template,
|
|
1549
|
+
model: member.model,
|
|
1550
|
+
reasoningEffort: member.reasoningEffort,
|
|
1551
|
+
serviceTier: member.serviceTier,
|
|
1552
|
+
skills: member.skills.map(skill => skill.name),
|
|
1553
|
+
plugins: member.plugins.map(plugin => plugin.id)
|
|
1554
|
+
}))
|
|
1555
|
+
};
|
|
1556
|
+
}
|
|
1557
|
+
function memberPromptTemplateContext(input) {
|
|
1558
|
+
const selectedDestinations = selectedDestinationsForPrompt(input);
|
|
1559
|
+
return {
|
|
1560
|
+
requestGoal: input.requestGoal,
|
|
1561
|
+
pathGoal: input.memberPath.goal,
|
|
1562
|
+
memberPath: input.memberPath,
|
|
1563
|
+
dependencyOutputs: input.dependencyOutputs ?? [],
|
|
1564
|
+
worktreeStatus: input.worktreeStatus,
|
|
1565
|
+
worktreeDiff: input.worktreeDiff,
|
|
1566
|
+
attemptTranscript: input.attemptTranscript,
|
|
1567
|
+
evaluationFeedback: input.attemptTranscript,
|
|
1568
|
+
currentDestination: selectedDestinations[0] ?? selectedDestinationForPrompt(input),
|
|
1569
|
+
currentDestinations: selectedDestinations,
|
|
1570
|
+
selectedDestinations,
|
|
1571
|
+
activeDestinations: input.activeDestinations,
|
|
1572
|
+
constraints: futureConstraintsForPrompt(input),
|
|
1573
|
+
sourceMoveId: input.sourceMoveId,
|
|
1574
|
+
targetMoveOrdinal: input.targetMoveOrdinal
|
|
1575
|
+
};
|
|
1576
|
+
}
|
|
1577
|
+
function providerPromptTemplateContext(input) {
|
|
1578
|
+
if (isMemberPathRunInput(input)) {
|
|
1579
|
+
return memberPromptTemplateContext(input);
|
|
1580
|
+
}
|
|
1581
|
+
if (isHunsuDraftInput(input)) {
|
|
1582
|
+
return {
|
|
1583
|
+
requestGoal: input.requestGoal,
|
|
1584
|
+
sourceSnapshot: input.sourceSnapshot,
|
|
1585
|
+
userMessage: isHunsuDraftTurnInput(input) ? input.userMessage : undefined,
|
|
1586
|
+
messages: isHunsuDraftTurnInput(input) ? input.messages : []
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1589
|
+
return teamPromptTemplateContext(input, teamHarnessForInput(input));
|
|
1590
|
+
}
|
|
1591
|
+
function formatTeamGoal(input) {
|
|
1592
|
+
const selectedDestination = selectedDestinationForPrompt(input);
|
|
1593
|
+
const constraints = futureConstraintsForPrompt(input)
|
|
1594
|
+
.map(item => `- ${item}`)
|
|
1595
|
+
.join("\n");
|
|
1596
|
+
return [
|
|
1597
|
+
selectedDestination ? formatDestinationGoalForPrompt(selectedDestination) : `- ${input.requestGoal}`,
|
|
1598
|
+
constraints ? `Relevant constraints:\n${constraints}` : undefined
|
|
1599
|
+
].filter((line) => line !== undefined).join("\n\n");
|
|
1600
|
+
}
|
|
1601
|
+
function selectedDestinationForPrompt(input) {
|
|
1602
|
+
const selectedId = input.selectedDestinationIds?.[0];
|
|
1603
|
+
if (selectedId) {
|
|
1604
|
+
return input.activeDestinations.find(destination => destination.id === selectedId);
|
|
1605
|
+
}
|
|
1606
|
+
return [...input.activeDestinations].sort((left, right) => (right.priority ?? 0) - (left.priority ?? 0))[0];
|
|
1607
|
+
}
|
|
1608
|
+
function selectedDestinationsForPrompt(input) {
|
|
1609
|
+
const selectedIds = input.selectedDestinationIds ?? [];
|
|
1610
|
+
if (selectedIds.length === 0) {
|
|
1611
|
+
const selected = selectedDestinationForPrompt(input);
|
|
1612
|
+
return selected ? [selected] : [];
|
|
1613
|
+
}
|
|
1614
|
+
const selected = new Set(selectedIds);
|
|
1615
|
+
return input.activeDestinations.filter(destination => selected.has(destination.id));
|
|
1616
|
+
}
|
|
1617
|
+
function futureConstraintsForPrompt(input) {
|
|
1618
|
+
return input.futureConstraints && input.futureConstraints.length > 0
|
|
1619
|
+
? input.futureConstraints
|
|
1620
|
+
: input.board.futureConstraints.map(constraint => constraint.constraint);
|
|
1621
|
+
}
|
|
1622
|
+
function formatDestinationGoalForPrompt(destination) {
|
|
1623
|
+
const parts = [`- ${destination.title}`];
|
|
1624
|
+
if (destination.acceptanceCriteria && destination.acceptanceCriteria.length > 0) {
|
|
1625
|
+
parts.push(` Acceptance: ${destination.acceptanceCriteria.join("; ")}`);
|
|
1626
|
+
}
|
|
1627
|
+
if (destination.constraints && destination.constraints.length > 0) {
|
|
1628
|
+
parts.push(` Constraints: ${destination.constraints.join("; ")}`);
|
|
1629
|
+
}
|
|
1630
|
+
if (destination.notes?.trim()) {
|
|
1631
|
+
parts.push(` Notes: ${destination.notes.trim()}`);
|
|
1632
|
+
}
|
|
1633
|
+
return parts.join("\n");
|
|
1634
|
+
}
|
|
1635
|
+
function formatMemberProfiles(members) {
|
|
1636
|
+
return members.map(member => [
|
|
1637
|
+
` <member id="${xmlAttribute(member.id)}">`,
|
|
1638
|
+
" <profile>",
|
|
1639
|
+
indentBlock(member.promptTemplate.template.trim() || "No profile.", 6),
|
|
1640
|
+
" </profile>",
|
|
1641
|
+
" </member>"
|
|
1642
|
+
].join("\n")).join("\n\n") || " <member id=\"default\">\n <profile>\n No profile.\n </profile>\n </member>";
|
|
1643
|
+
}
|
|
1644
|
+
function indentBlock(text, spaces) {
|
|
1645
|
+
const prefix = " ".repeat(spaces);
|
|
1646
|
+
return text.split("\n").map(line => line ? `${prefix}${line}` : "").join("\n");
|
|
1647
|
+
}
|
|
1648
|
+
function xmlAttribute(value) {
|
|
1649
|
+
return value
|
|
1650
|
+
.replaceAll("&", "&")
|
|
1651
|
+
.replaceAll("\"", """)
|
|
1652
|
+
.replaceAll("<", "<")
|
|
1653
|
+
.replaceAll(">", ">");
|
|
1654
|
+
}
|
|
1655
|
+
function formatDestinationForPrompt(destination) {
|
|
1656
|
+
const parts = [`- ${destination.title}`];
|
|
1657
|
+
if (destination.acceptanceCriteria && destination.acceptanceCriteria.length > 0) {
|
|
1658
|
+
parts.push(` Acceptance: ${destination.acceptanceCriteria.join("; ")}`);
|
|
1659
|
+
}
|
|
1660
|
+
if (destination.constraints && destination.constraints.length > 0) {
|
|
1661
|
+
parts.push(` Constraints: ${destination.constraints.join("; ")}`);
|
|
1662
|
+
}
|
|
1663
|
+
if (destination.notes?.trim()) {
|
|
1664
|
+
parts.push(` Notes: ${destination.notes.trim()}`);
|
|
1665
|
+
}
|
|
1666
|
+
return parts.join("\n");
|
|
1667
|
+
}
|
|
1668
|
+
function formatHunsuSourceSnapshot(snapshot) {
|
|
1669
|
+
return [
|
|
1670
|
+
`draftSessionId source line: ${snapshot.sourceLineId}`,
|
|
1671
|
+
`source node: ${snapshot.sourceNodeId}`,
|
|
1672
|
+
snapshot.sourceMoveId ? `source move: ${snapshot.sourceMoveId}` : undefined,
|
|
1673
|
+
snapshot.moveOrdinal !== undefined ? `position: M${String(snapshot.moveOrdinal).padStart(4, "0")}` : undefined,
|
|
1674
|
+
snapshot.teamName ? `team: ${snapshot.teamName}` : undefined,
|
|
1675
|
+
snapshot.summary ? `summary: ${snapshot.summary}` : undefined,
|
|
1676
|
+
"destinations:",
|
|
1677
|
+
snapshot.destinationSummaries.length > 0
|
|
1678
|
+
? snapshot.destinationSummaries.map(destination => `- ${destination.id} [${destination.status}] ${destination.title}`).join("\n")
|
|
1679
|
+
: "- none"
|
|
1680
|
+
].filter((line) => line !== undefined).join("\n");
|
|
1681
|
+
}
|
|
1682
|
+
function formatHunsuDraftConversation(messages) {
|
|
1683
|
+
if (messages.length === 0) {
|
|
1684
|
+
return "No prior draft conversation.";
|
|
1685
|
+
}
|
|
1686
|
+
return messages.map(message => {
|
|
1687
|
+
const label = message.role === "draft-agent" ? "Draft Agent" : message.role === "user" ? "User" : "System";
|
|
1688
|
+
return `${label}: ${truncateText(message.text, 1200)}`;
|
|
1689
|
+
}).join("\n\n");
|
|
1690
|
+
}
|
|
1691
|
+
function teamPlanningConfigFor(input) {
|
|
1692
|
+
const protocol = teamHarnessForInput(input);
|
|
1693
|
+
return createDefaultMemberConfig("team", teamInstructionFor(input, protocol), []);
|
|
1694
|
+
}
|
|
1695
|
+
function moveFinalizerConfigFor() {
|
|
1696
|
+
return createDefaultMemberConfig("move-finalizer", "Write a precise final MOVE commit message from the completed Execute diff and evidence.", []);
|
|
1697
|
+
}
|
|
1698
|
+
function hunsuDraftConfigFor(manager = createDefaultManagerConfig()) {
|
|
1699
|
+
const config = createDefaultMemberConfig(String(manager.id), manager.promptTemplate.template, manager.skills);
|
|
1700
|
+
return {
|
|
1701
|
+
...config,
|
|
1702
|
+
promptTemplate: { ...manager.promptTemplate },
|
|
1703
|
+
plugins: manager.plugins.map(plugin => ({ ...plugin })),
|
|
1704
|
+
execution: { kind: "worktree_write", network: "enabled" },
|
|
1705
|
+
approval: { policy: "never" }
|
|
1706
|
+
};
|
|
1707
|
+
}
|
|
1708
|
+
function memberConfigFor(input, executorId) {
|
|
1709
|
+
if (input.harnessGraph) {
|
|
1710
|
+
const member = getHarnessMember(input.harnessGraph, executorId);
|
|
1711
|
+
if (!member) {
|
|
1712
|
+
throw new Error(`Harness does not define required Member ${executorId}`);
|
|
1713
|
+
}
|
|
1714
|
+
return memberConfigFromMemberEntity(member);
|
|
1715
|
+
}
|
|
1716
|
+
const protocol = teamHarnessForInput(input);
|
|
1717
|
+
const member = getHarnessMemberConfig(protocol, executorId);
|
|
1718
|
+
if (!member) {
|
|
1719
|
+
throw new Error(`Harness ${protocol.kind} does not define required Member ${executorId}`);
|
|
1720
|
+
}
|
|
1721
|
+
return member;
|
|
1722
|
+
}
|
|
1723
|
+
function teamHarnessForInput(input) {
|
|
1724
|
+
if (input.harnessGraph) {
|
|
1725
|
+
return harnessSnapshotForTeam(input.harnessGraph, input.teamScopeId ?? input.harnessGraph.rootTeamId);
|
|
1726
|
+
}
|
|
1727
|
+
return input.harness ?? createDefaultHarness();
|
|
1728
|
+
}
|
|
1729
|
+
function threadOptionsForMember(member) {
|
|
1730
|
+
const options = {
|
|
1731
|
+
approvalPolicy: member.approval.policy === "on_request" ? "on-request" : "never",
|
|
1732
|
+
approvalsReviewer: member.approval.policy === "on_request" ? member.approval.reviewer : "user",
|
|
1733
|
+
networkAccessEnabled: member.execution.network === "enabled"
|
|
1734
|
+
};
|
|
1735
|
+
switch (member.execution.kind) {
|
|
1736
|
+
case "read_only":
|
|
1737
|
+
options.sandboxMode = "read-only";
|
|
1738
|
+
break;
|
|
1739
|
+
case "worktree_write":
|
|
1740
|
+
options.sandboxMode = "workspace-write";
|
|
1741
|
+
break;
|
|
1742
|
+
case "unrestricted":
|
|
1743
|
+
options.sandboxMode = "danger-full-access";
|
|
1744
|
+
break;
|
|
1745
|
+
}
|
|
1746
|
+
if (member.model && member.model !== "codex-default") {
|
|
1747
|
+
options.model = member.model;
|
|
1748
|
+
}
|
|
1749
|
+
if (member.reasoningEffort && member.reasoningEffort !== "default") {
|
|
1750
|
+
options.modelReasoningEffort = member.reasoningEffort;
|
|
1751
|
+
}
|
|
1752
|
+
return options;
|
|
1753
|
+
}
|
|
1754
|
+
function isMemberPathRunInput(input) {
|
|
1755
|
+
return "memberPath" in input;
|
|
1756
|
+
}
|
|
1757
|
+
function isHunsuDraftTurnInput(input) {
|
|
1758
|
+
return "draftSessionId" in input && "sourceSnapshot" in input && "userMessage" in input;
|
|
1759
|
+
}
|
|
1760
|
+
function isHunsuDraftInput(input) {
|
|
1761
|
+
return "draftSessionId" in input && "sourceSnapshot" in input;
|
|
1762
|
+
}
|
|
1763
|
+
function createCodexEnvironment(env) {
|
|
1764
|
+
const next = {};
|
|
1765
|
+
for (const [key, value] of Object.entries(env)) {
|
|
1766
|
+
if (value !== undefined) {
|
|
1767
|
+
next[key] = value;
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
const workspaceRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../../..");
|
|
1771
|
+
const workspaceBin = join(workspaceRoot, "node_modules", ".bin");
|
|
1772
|
+
next.PATH = [workspaceBin, env.PATH].filter(Boolean).join(delimiter);
|
|
1773
|
+
return next;
|
|
1774
|
+
}
|
|
1775
|
+
function truncateText(text, maxLength = 4000) {
|
|
1776
|
+
if (text.length <= maxLength) {
|
|
1777
|
+
return text;
|
|
1778
|
+
}
|
|
1779
|
+
return `${text.slice(0, maxLength - 1)}...`;
|
|
1780
|
+
}
|