triflux 3.3.0-dev.3 → 3.3.0-dev.6

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.
@@ -14,6 +14,13 @@ import * as z from 'zod';
14
14
 
15
15
  import { CodexMcpWorker } from './codex-mcp.mjs';
16
16
  import { GeminiWorker } from './gemini-worker.mjs';
17
+ import {
18
+ buildPromptHint,
19
+ getCodexMcpConfig,
20
+ getGeminiAllowedServers,
21
+ resolveMcpProfile,
22
+ SUPPORTED_MCP_PROFILES,
23
+ } from '../../scripts/lib/mcp-filter.mjs';
17
24
 
18
25
  const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
19
26
  const SERVER_INFO = { name: 'triflux-delegator', version: '1.0.0' };
@@ -78,12 +85,6 @@ const REVIEW_INSTRUCTION_BY_AGENT = Object.freeze({
78
85
  'quality-reviewer': '품질 리뷰 모드로 동작하라. 로직 결함, 유지보수성 저하, 테스트 누락을 우선 식별하라.',
79
86
  });
80
87
 
81
- const IMPLEMENT_AGENT_SET = new Set(['executor', 'build-fixer', 'debugger', 'deep-executor']);
82
- const ANALYZE_AGENT_SET = new Set(['architect', 'planner', 'critic', 'analyst', 'scientist', 'scientist-deep', 'document-specialist']);
83
- const REVIEW_AGENT_SET = new Set(['code-reviewer', 'security-reviewer', 'quality-reviewer', 'verifier']);
84
- const DOCS_AGENT_SET = new Set(['designer', 'writer']);
85
- const SEARCH_TOOL_ORDER = ['brave-search', 'tavily', 'exa'];
86
-
87
88
  function cloneEnv(env = process.env) {
88
89
  return Object.fromEntries(
89
90
  Object.entries(env).filter(([, value]) => typeof value === 'string'),
@@ -125,73 +126,6 @@ function resolveRouteScript(explicitPath, cwd = process.cwd()) {
125
126
  return null;
126
127
  }
127
128
 
128
- function resolveMcpProfile(agentType, requested = 'auto') {
129
- if (requested && requested !== 'auto') return requested;
130
- if (IMPLEMENT_AGENT_SET.has(agentType)) return 'implement';
131
- if (ANALYZE_AGENT_SET.has(agentType)) return 'analyze';
132
- if (REVIEW_AGENT_SET.has(agentType)) return 'review';
133
- if (DOCS_AGENT_SET.has(agentType)) return 'docs';
134
- return 'minimal';
135
- }
136
-
137
- function resolveSearchToolOrder(searchTool, workerIndex) {
138
- const available = [...SEARCH_TOOL_ORDER];
139
- if (searchTool && available.includes(searchTool)) {
140
- return [searchTool, ...available.filter((tool) => tool !== searchTool)];
141
- }
142
-
143
- if (Number.isInteger(workerIndex) && workerIndex > 0 && available.length > 1) {
144
- const offset = (workerIndex - 1) % available.length;
145
- return available.slice(offset).concat(available.slice(0, offset));
146
- }
147
-
148
- return available;
149
- }
150
-
151
- function buildPromptHint(profile, args) {
152
- const orderedTools = resolveSearchToolOrder(args.searchTool, args.workerIndex);
153
- switch (profile) {
154
- case 'implement':
155
- return [
156
- 'context7으로 라이브러리 문서를 조회하세요.',
157
- `웹 검색은 ${orderedTools[0]}를 사용하세요.`,
158
- '검색 도구 실패 시 402, 429, 432, 433, quota 에러에서 재시도하지 말고 다음 도구로 전환하세요.',
159
- ].join(' ');
160
- case 'analyze':
161
- return [
162
- 'context7으로 관련 문서를 조회하세요.',
163
- `웹 검색 우선순위: ${orderedTools.join(', ')}. 402, 429, 432, 433, quota 에러 시 즉시 다음 도구로 전환.`,
164
- '모든 검색 실패 시 playwright로 직접 방문 (최대 3 URL).',
165
- '검색 깊이를 제한하고 결과를 빠르게 요약하세요.',
166
- ].join(' ');
167
- case 'review':
168
- return 'sequential-thinking으로 체계적으로 분석하세요.';
169
- case 'docs':
170
- return [
171
- 'context7으로 공식 문서를 참조하세요.',
172
- `추가 검색은 ${orderedTools[0]}를 사용하세요.`,
173
- '검색 결과의 출처 URL을 함께 제시하세요.',
174
- ].join(' ');
175
- default:
176
- return '';
177
- }
178
- }
179
-
180
- function resolveGeminiMcpServers(profile) {
181
- switch (profile) {
182
- case 'implement':
183
- return ['context7', 'brave-search'];
184
- case 'analyze':
185
- return ['context7', 'brave-search', 'exa', 'tavily'];
186
- case 'review':
187
- return ['sequential-thinking'];
188
- case 'docs':
189
- return ['context7', 'brave-search'];
190
- default:
191
- return [];
192
- }
193
- }
194
-
195
129
  function resolveCodexProfile(agentType) {
196
130
  return CODEX_PROFILE_BY_AGENT[agentType] || 'high';
197
131
  }
@@ -232,10 +166,16 @@ function withContext(prompt, contextFile) {
232
166
  }
233
167
 
234
168
  function withPromptHint(prompt, args) {
235
- const profile = resolveMcpProfile(args.agentType, args.mcpProfile);
236
- const hint = buildPromptHint(profile, args);
237
- if (!hint) return withContext(prompt, args.contextFile);
238
- return `${withContext(prompt, args.contextFile)}. ${hint}`;
169
+ const promptWithContext = withContext(prompt, args.contextFile);
170
+ const hint = buildPromptHint({
171
+ agentType: args.agentType,
172
+ requestedProfile: args.mcpProfile,
173
+ searchTool: args.searchTool,
174
+ workerIndex: Number.isInteger(args.workerIndex) ? args.workerIndex : undefined,
175
+ taskText: promptWithContext,
176
+ });
177
+ if (!hint) return promptWithContext;
178
+ return `${promptWithContext}. ${hint}`;
239
179
  }
240
180
 
241
181
  function joinInstructions(...values) {
@@ -287,7 +227,7 @@ const DelegateInputSchema = z.object({
287
227
  timeoutMs: z.number().int().positive().optional().describe('요청 타임아웃(ms)'),
288
228
  sessionKey: z.string().optional().describe('Codex warm session 재사용 키'),
289
229
  resetSession: z.boolean().optional().describe('기존 Codex 세션 초기화 여부'),
290
- mcpProfile: z.enum(['auto', 'implement', 'analyze', 'review', 'docs', 'minimal', 'none']).default('auto'),
230
+ mcpProfile: z.enum(SUPPORTED_MCP_PROFILES).default('auto'),
291
231
  contextFile: z.string().optional().describe('tfx-route prior_context 파일 경로'),
292
232
  searchTool: z.enum(['brave-search', 'tavily', 'exa']).optional().describe('검색 우선 도구'),
293
233
  workerIndex: z.number().int().positive().optional().describe('병렬 워커 인덱스'),
@@ -308,9 +248,16 @@ const DelegateStatusInputSchema = z.object({
308
248
  jobId: z.string().min(1).describe('조회할 비동기 job ID'),
309
249
  });
310
250
 
251
+ const DelegateReplyInputSchema = z.object({
252
+ job_id: z.string().min(1).describe('후속 응답을 보낼 기존 delegate job ID'),
253
+ reply: z.string().min(1).describe('후속 사용자 응답'),
254
+ done: z.boolean().default(false).describe('true이면 응답 처리 후 대화를 종료'),
255
+ });
256
+
311
257
  const DelegateOutputSchema = z.object({
312
258
  ok: z.boolean(),
313
259
  jobId: z.string().optional(),
260
+ job_id: z.string().optional(),
314
261
  mode: z.enum(['sync', 'async']).optional(),
315
262
  status: z.enum(['running', 'completed', 'failed']).optional(),
316
263
  error: z.string().optional(),
@@ -327,6 +274,7 @@ const DelegateOutputSchema = z.object({
327
274
  stderr: z.string().optional(),
328
275
  threadId: z.string().nullable().optional(),
329
276
  sessionKey: z.string().nullable().optional(),
277
+ conversationOpen: z.boolean().optional(),
330
278
  });
331
279
 
332
280
  function isTeamRouteRequested(args) {
@@ -343,6 +291,44 @@ function pickRouteMode(provider) {
343
291
  return provider === 'auto' ? 'auto' : provider;
344
292
  }
345
293
 
294
+ function sanitizeDelegateArgs(args = {}) {
295
+ return {
296
+ provider: args.provider || 'auto',
297
+ agentType: args.agentType || 'executor',
298
+ cwd: args.cwd || null,
299
+ timeoutMs: Number.isFinite(Number(args.timeoutMs)) ? Math.trunc(Number(args.timeoutMs)) : null,
300
+ sessionKey: args.sessionKey || null,
301
+ resetSession: Boolean(args.resetSession),
302
+ mcpProfile: args.mcpProfile || 'auto',
303
+ contextFile: args.contextFile || null,
304
+ searchTool: args.searchTool || null,
305
+ workerIndex: Number.isInteger(args.workerIndex) ? args.workerIndex : null,
306
+ model: args.model || null,
307
+ developerInstructions: args.developerInstructions || null,
308
+ compactPrompt: args.compactPrompt || null,
309
+ threadId: args.threadId || null,
310
+ codexTransport: args.codexTransport || null,
311
+ noClaudeNative: args.noClaudeNative === true,
312
+ teamName: args.teamName || null,
313
+ teamTaskId: args.teamTaskId || null,
314
+ teamAgentName: args.teamAgentName || null,
315
+ teamLeadName: args.teamLeadName || null,
316
+ hubUrl: args.hubUrl || null,
317
+ };
318
+ }
319
+
320
+ function formatConversationTranscript(turns = []) {
321
+ return turns.map((turn, index) => {
322
+ const parts = [
323
+ `Turn ${index + 1} user:\n${turn.user}`,
324
+ ];
325
+ if (typeof turn.assistant === 'string' && turn.assistant.trim()) {
326
+ parts.push(`Turn ${index + 1} assistant:\n${turn.assistant}`);
327
+ }
328
+ return parts.join('\n\n');
329
+ }).join('\n\n');
330
+ }
331
+
346
332
  async function emitProgress(extra, progress, total, message) {
347
333
  if (extra?._meta?.progressToken === undefined) return;
348
334
  await extra.sendNotification({
@@ -389,6 +375,7 @@ export class DelegatorMcpWorker {
389
375
  this.server = null;
390
376
  this.transport = null;
391
377
  this.jobs = new Map();
378
+ this.geminiConversations = new Map();
392
379
  this.routeChildren = new Set();
393
380
  this.ready = false;
394
381
  }
@@ -425,6 +412,15 @@ export class DelegatorMcpWorker {
425
412
  return createToolResponse(payload, { isError: payload.ok === false });
426
413
  });
427
414
 
415
+ server.registerTool('triflux-delegate-reply', {
416
+ description: '기존 delegate job에 후속 응답을 보내고, Gemini direct job이면 multi-turn 대화를 이어갑니다.',
417
+ inputSchema: DelegateReplyInputSchema,
418
+ outputSchema: DelegateOutputSchema,
419
+ }, async (args, extra) => {
420
+ const payload = await this.reply(args, extra);
421
+ return createToolResponse(payload, { isError: payload.ok === false });
422
+ });
423
+
428
424
  this.server = server;
429
425
  this.ready = true;
430
426
  }
@@ -453,6 +449,7 @@ export class DelegatorMcpWorker {
453
449
  job.worker = null;
454
450
  }
455
451
  }
452
+ this.geminiConversations.clear();
456
453
 
457
454
  if (this.server) {
458
455
  await this.server.close().catch(() => {});
@@ -481,7 +478,7 @@ export class DelegatorMcpWorker {
481
478
  if (args.mode === 'async') {
482
479
  return this._startAsyncJob(args, extra);
483
480
  }
484
- return this._executeDirect(args, extra);
481
+ return this._runSyncJob(args, extra);
485
482
  }
486
483
 
487
484
  async getJobStatus(jobId, extra) {
@@ -501,6 +498,85 @@ export class DelegatorMcpWorker {
501
498
  return payload;
502
499
  }
503
500
 
501
+ async reply({ job_id, reply, done = false }, extra) {
502
+ const job = this.jobs.get(job_id);
503
+ if (!job) {
504
+ return createErrorPayload(`알 수 없는 jobId: ${job_id}`, { jobId: job_id, job_id });
505
+ }
506
+ if (job.status === 'running') {
507
+ return createErrorPayload(`job ${job_id}가 아직 실행 중입니다.`, { jobId: job_id, job_id });
508
+ }
509
+ if (job.providerRequested !== 'gemini' || job.transport !== 'gemini-worker') {
510
+ return createErrorPayload('delegate-reply는 현재 direct Gemini job에만 지원됩니다.', {
511
+ jobId: job_id,
512
+ job_id,
513
+ });
514
+ }
515
+
516
+ const conversation = this.geminiConversations.get(job_id);
517
+ if (!conversation) {
518
+ return createErrorPayload(`Gemini 대화 컨텍스트가 없습니다: ${job_id}`, { jobId: job_id, job_id });
519
+ }
520
+ if (conversation.closed) {
521
+ return createErrorPayload(`이미 종료된 대화입니다: ${job_id}`, { jobId: job_id, job_id });
522
+ }
523
+
524
+ await emitProgress(extra, DIRECT_PROGRESS_START, 100, `job ${job_id} 후속 응답을 시작합니다.`);
525
+ job.status = 'running';
526
+ job.updatedAt = new Date().toISOString();
527
+
528
+ const worker = this._createGeminiWorker();
529
+ job.worker = worker;
530
+ const prompt = this._buildGeminiReplyPrompt(conversation, reply);
531
+
532
+ try {
533
+ const result = await worker.execute(prompt, {
534
+ cwd: job.requestArgs.cwd || this.cwd,
535
+ timeoutMs: resolveTimeoutMs(job.agentType, job.requestArgs.timeoutMs),
536
+ model: job.requestArgs.model || resolveGeminiModel(job.agentType),
537
+ approvalMode: 'yolo',
538
+ allowedMcpServerNames: getGeminiAllowedServers(this._getMcpPolicyOptions(job.requestArgs)),
539
+ });
540
+
541
+ conversation.turns.push({
542
+ user: reply,
543
+ assistant: result.output,
544
+ });
545
+ conversation.updatedAt = new Date().toISOString();
546
+ conversation.closed = Boolean(done);
547
+ if (done) {
548
+ this.geminiConversations.delete(job_id);
549
+ }
550
+
551
+ this._applyJobResult(job, {
552
+ ok: result.exitCode === 0,
553
+ status: result.exitCode === 0 ? 'completed' : 'failed',
554
+ providerRequested: 'gemini',
555
+ providerResolved: 'gemini',
556
+ agentType: job.agentType,
557
+ transport: 'gemini-worker',
558
+ exitCode: result.exitCode,
559
+ output: result.output,
560
+ sessionKey: result.sessionKey || job.sessionKey || null,
561
+ });
562
+ await emitProgress(extra, DIRECT_PROGRESS_DONE, 100, `job ${job_id} 후속 응답이 완료되었습니다.`);
563
+ return this._serializeJob(job);
564
+ } catch (error) {
565
+ const message = error instanceof Error ? error.message : String(error);
566
+ this._applyJobResult(job, createErrorPayload(message, {
567
+ mode: job.mode,
568
+ providerRequested: 'gemini',
569
+ providerResolved: 'gemini',
570
+ agentType: job.agentType,
571
+ transport: 'gemini-worker',
572
+ }));
573
+ return this._serializeJob(job);
574
+ } finally {
575
+ await worker.stop().catch(() => {});
576
+ job.worker = null;
577
+ }
578
+ }
579
+
504
580
  _createGeminiWorker() {
505
581
  return new GeminiWorker({
506
582
  command: this.geminiCommand,
@@ -524,13 +600,33 @@ export class DelegatorMcpWorker {
524
600
  });
525
601
  }
526
602
 
527
- _buildPromptHintInstruction(args) {
528
- return buildPromptHint(resolveMcpProfile(args.agentType, args.mcpProfile), {
603
+ _buildGeminiReplyPrompt(conversation, reply) {
604
+ const transcript = formatConversationTranscript(conversation.turns);
605
+ return [
606
+ 'Continue the conversation using the prior transcript below.',
607
+ '',
608
+ '<conversation_history>',
609
+ transcript,
610
+ '</conversation_history>',
611
+ '',
612
+ '<latest_user_reply>',
613
+ reply,
614
+ '</latest_user_reply>',
615
+ ].join('\n');
616
+ }
617
+
618
+ _getMcpPolicyOptions(args) {
619
+ return {
529
620
  agentType: args.agentType || 'executor',
530
- mcpProfile: args.mcpProfile || 'auto',
621
+ requestedProfile: args.mcpProfile || 'auto',
531
622
  searchTool: args.searchTool,
532
623
  workerIndex: Number.isInteger(args.workerIndex) ? args.workerIndex : undefined,
533
- });
624
+ taskText: withContext(String(args.prompt ?? ''), args.contextFile),
625
+ };
626
+ }
627
+
628
+ _buildPromptHintInstruction(args) {
629
+ return buildPromptHint(this._getMcpPolicyOptions(args));
534
630
  }
535
631
 
536
632
  _shouldUseRoute(args) {
@@ -578,6 +674,7 @@ export class DelegatorMcpWorker {
578
674
  this._buildPromptHintInstruction(args),
579
675
  args.developerInstructions,
580
676
  ),
677
+ config: getCodexMcpConfig(this._getMcpPolicyOptions(args)),
581
678
  compactPrompt: args.compactPrompt,
582
679
  model: args.model,
583
680
  });
@@ -599,15 +696,14 @@ export class DelegatorMcpWorker {
599
696
 
600
697
  if (args.provider === 'gemini') {
601
698
  const worker = this._createGeminiWorker();
699
+ const prompt = this._buildDirectPromptWithHint(args);
602
700
  try {
603
- const result = await worker.execute(this._buildDirectPromptWithHint(args), {
701
+ const result = await worker.execute(prompt, {
604
702
  cwd: args.cwd || this.cwd,
605
703
  timeoutMs: resolveTimeoutMs(args.agentType, args.timeoutMs),
606
704
  model: args.model || resolveGeminiModel(args.agentType),
607
705
  approvalMode: 'yolo',
608
- allowedMcpServerNames: resolveGeminiMcpServers(
609
- resolveMcpProfile(args.agentType, args.mcpProfile),
610
- ),
706
+ allowedMcpServerNames: getGeminiAllowedServers(this._getMcpPolicyOptions(args)),
611
707
  });
612
708
 
613
709
  return {
@@ -621,6 +717,7 @@ export class DelegatorMcpWorker {
621
717
  exitCode: result.exitCode,
622
718
  output: result.output,
623
719
  sessionKey: result.sessionKey,
720
+ _geminiPrompt: prompt,
624
721
  };
625
722
  } finally {
626
723
  await worker.stop().catch(() => {});
@@ -662,61 +759,27 @@ export class DelegatorMcpWorker {
662
759
  }
663
760
 
664
761
  async _startAsyncJob(args, extra) {
665
- const jobId = randomUUID();
666
- const startedAt = new Date().toISOString();
667
- const payload = {
668
- ok: true,
669
- jobId,
670
- mode: 'async',
671
- status: 'running',
672
- providerRequested: args.provider,
673
- providerResolved: null,
674
- agentType: args.agentType,
675
- transport: this._shouldUseRoute(args) ? 'route-script' : `${args.provider}-worker`,
676
- createdAt: startedAt,
677
- startedAt,
678
- };
679
-
680
- const job = {
681
- ...payload,
682
- updatedAt: startedAt,
683
- completedAt: null,
684
- output: '',
685
- stderr: '',
686
- exitCode: null,
687
- threadId: null,
688
- sessionKey: args.sessionKey || null,
689
- worker: null,
690
- child: null,
691
- };
692
- this.jobs.set(jobId, job);
762
+ const job = this._createJob(args, 'async');
763
+ this.jobs.set(job.jobId, job);
693
764
 
694
- await emitProgress(extra, DIRECT_PROGRESS_START, 100, `비동기 job ${jobId}를 시작합니다.`);
765
+ await emitProgress(extra, DIRECT_PROGRESS_START, 100, `비동기 job ${job.jobId}를 시작합니다.`);
695
766
 
696
767
  void (async () => {
697
768
  try {
698
769
  const result = this._shouldUseRoute(args)
699
770
  ? await this._spawnRoute(args, job)
700
771
  : await this._runAsyncWorker(args, job);
701
-
702
- job.ok = result.ok;
703
- job.status = result.ok ? 'completed' : 'failed';
704
- job.providerResolved = result.providerResolved || job.providerRequested;
705
- job.output = result.output || '';
706
- job.stderr = result.stderr || '';
707
- job.exitCode = result.exitCode ?? (result.ok ? 0 : 1);
708
- job.threadId = result.threadId || null;
709
- job.sessionKey = result.sessionKey || job.sessionKey || null;
710
- job.completedAt = new Date().toISOString();
711
- job.updatedAt = job.completedAt;
772
+ this._applyJobResult(job, result);
712
773
  } catch (error) {
713
- job.ok = false;
714
- job.status = 'failed';
715
- job.output = '';
716
- job.stderr = error instanceof Error ? error.message : String(error);
717
- job.exitCode = 1;
718
- job.completedAt = new Date().toISOString();
719
- job.updatedAt = job.completedAt;
774
+ this._applyJobResult(job, createErrorPayload(
775
+ error instanceof Error ? error.message : String(error),
776
+ {
777
+ mode: 'async',
778
+ providerRequested: args.provider,
779
+ agentType: args.agentType,
780
+ transport: this._shouldUseRoute(args) ? 'route-script' : `${args.provider}-worker`,
781
+ },
782
+ ));
720
783
  } finally {
721
784
  if (job.worker) {
722
785
  await job.worker.stop().catch(() => {});
@@ -726,7 +789,7 @@ export class DelegatorMcpWorker {
726
789
  }
727
790
  })();
728
791
 
729
- return payload;
792
+ return this._serializeJob(job);
730
793
  }
731
794
 
732
795
  async _runAsyncWorker(args, job) {
@@ -745,6 +808,7 @@ export class DelegatorMcpWorker {
745
808
  this._buildPromptHintInstruction(args),
746
809
  args.developerInstructions,
747
810
  ),
811
+ config: getCodexMcpConfig(this._getMcpPolicyOptions(args)),
748
812
  compactPrompt: args.compactPrompt,
749
813
  model: args.model,
750
814
  });
@@ -762,14 +826,13 @@ export class DelegatorMcpWorker {
762
826
  if (args.provider === 'gemini') {
763
827
  const worker = this._createGeminiWorker();
764
828
  job.worker = worker;
765
- const result = await worker.execute(this._buildDirectPromptWithHint(args), {
829
+ const prompt = this._buildDirectPromptWithHint(args);
830
+ const result = await worker.execute(prompt, {
766
831
  cwd: args.cwd || this.cwd,
767
832
  timeoutMs: resolveTimeoutMs(args.agentType, args.timeoutMs),
768
833
  model: args.model || resolveGeminiModel(args.agentType),
769
834
  approvalMode: 'yolo',
770
- allowedMcpServerNames: resolveGeminiMcpServers(
771
- resolveMcpProfile(args.agentType, args.mcpProfile),
772
- ),
835
+ allowedMcpServerNames: getGeminiAllowedServers(this._getMcpPolicyOptions(args)),
773
836
  });
774
837
 
775
838
  return {
@@ -778,6 +841,7 @@ export class DelegatorMcpWorker {
778
841
  output: result.output,
779
842
  exitCode: result.exitCode,
780
843
  sessionKey: result.sessionKey,
844
+ _geminiPrompt: prompt,
781
845
  };
782
846
  }
783
847
 
@@ -862,7 +926,8 @@ export class DelegatorMcpWorker {
862
926
  return {
863
927
  ok: job.ok,
864
928
  jobId: job.jobId,
865
- mode: 'async',
929
+ job_id: job.jobId,
930
+ mode: job.mode || 'async',
866
931
  status: job.status,
867
932
  providerRequested: job.providerRequested,
868
933
  providerResolved: job.providerResolved,
@@ -877,8 +942,88 @@ export class DelegatorMcpWorker {
877
942
  stderr: job.stderr,
878
943
  threadId: job.threadId,
879
944
  sessionKey: job.sessionKey,
945
+ conversationOpen: this.geminiConversations.has(job.jobId),
946
+ };
947
+ }
948
+
949
+ _createJob(args, mode) {
950
+ const jobId = randomUUID();
951
+ const now = new Date().toISOString();
952
+ return {
953
+ ok: true,
954
+ jobId,
955
+ mode,
956
+ status: 'running',
957
+ providerRequested: args.provider,
958
+ providerResolved: null,
959
+ agentType: args.agentType,
960
+ transport: this._shouldUseRoute(args) ? 'route-script' : `${args.provider}-worker`,
961
+ createdAt: now,
962
+ startedAt: now,
963
+ updatedAt: now,
964
+ completedAt: null,
965
+ output: '',
966
+ stderr: '',
967
+ exitCode: null,
968
+ threadId: null,
969
+ sessionKey: args.sessionKey || null,
970
+ worker: null,
971
+ child: null,
972
+ requestArgs: sanitizeDelegateArgs(args),
880
973
  };
881
974
  }
975
+
976
+ _applyJobResult(job, result = {}) {
977
+ job.ok = result.ok !== false;
978
+ job.status = job.ok ? 'completed' : 'failed';
979
+ job.providerResolved = result.providerResolved || job.providerRequested;
980
+ job.transport = result.transport || job.transport;
981
+ job.output = result.output || '';
982
+ job.stderr = result.stderr || result.error || '';
983
+ job.exitCode = result.exitCode ?? (job.ok ? 0 : 1);
984
+ job.threadId = result.threadId || job.threadId || null;
985
+ job.sessionKey = result.sessionKey || job.sessionKey || null;
986
+ job.completedAt = new Date().toISOString();
987
+ job.updatedAt = job.completedAt;
988
+
989
+ if (job.providerRequested === 'gemini'
990
+ && job.transport === 'gemini-worker'
991
+ && typeof result._geminiPrompt === 'string') {
992
+ this._storeGeminiConversation(job, result._geminiPrompt, result.output || '');
993
+ }
994
+ }
995
+
996
+ _storeGeminiConversation(job, userPrompt, assistantReply) {
997
+ const existing = this.geminiConversations.get(job.jobId);
998
+ if (existing) {
999
+ if (typeof assistantReply === 'string') {
1000
+ const lastTurn = existing.turns.at(-1);
1001
+ if (lastTurn && lastTurn.assistant !== assistantReply) {
1002
+ lastTurn.assistant = assistantReply;
1003
+ }
1004
+ }
1005
+ existing.updatedAt = new Date().toISOString();
1006
+ return;
1007
+ }
1008
+
1009
+ this.geminiConversations.set(job.jobId, {
1010
+ jobId: job.jobId,
1011
+ closed: false,
1012
+ updatedAt: new Date().toISOString(),
1013
+ turns: [{
1014
+ user: userPrompt,
1015
+ assistant: assistantReply,
1016
+ }],
1017
+ });
1018
+ }
1019
+
1020
+ async _runSyncJob(args, extra) {
1021
+ const job = this._createJob(args, 'sync');
1022
+ this.jobs.set(job.jobId, job);
1023
+ const result = await this._executeDirect(args, extra);
1024
+ this._applyJobResult(job, result);
1025
+ return this._serializeJob(job);
1026
+ }
882
1027
  }
883
1028
 
884
1029
  export function createDelegatorMcpWorker(options = {}) {