@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/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("&", "&amp;")
1651
+ .replaceAll("\"", "&quot;")
1652
+ .replaceAll("<", "&lt;")
1653
+ .replaceAll(">", "&gt;");
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
+ }