@goodandready/dsh-moa 0.2.6 → 0.2.7

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/lib/index.js CHANGED
@@ -16,7 +16,7 @@ import os from 'node:os'
16
16
  import {
17
17
  parseMoACommand,
18
18
  runMoAPipeline,
19
- MoaRunnerAdapter,
19
+ streamMoATurn,
20
20
  } from './moa-runner.js'
21
21
 
22
22
  import {
@@ -450,44 +450,9 @@ export function apply(ctx, config) {
450
450
  })
451
451
  }, 'dsh-moa: run pipeline route')
452
452
 
453
- // Register virtual LLM adapter for live-streaming MoA turns
454
- if (ctx.llm && typeof ctx.llm.registerAdapter === 'function') {
455
- ctx.effect(() => {
456
- const adapter = new MoaRunnerAdapter((options) => {
457
- const msgs = options.messages || []
458
- const lastUser = [...msgs].reverse().find((m) => m && m.role === 'user')
459
- const userText = typeof lastUser?.content === 'string'
460
- ? lastUser.content
461
- : (Array.isArray(lastUser?.content)
462
- ? lastUser.content.filter((p) => p.type === 'text').map((p) => p.text).join('\n')
463
- : '')
464
-
465
- const cfg = live()
466
- const parsed = parseMoACommand(userText, cfg.presets || []) || { prompt: userText, presetName: 'default' }
467
- const targetPreset = (cfg.presets || []).find((p) => p.name === parsed.presetName) || cfg.presets?.[0]
468
-
469
- const extraCtx = options.signal ? moaPendingContexts.get(options.signal) : null
470
- const cwd = options.cwd || extraCtx?.cwd || process.cwd()
471
-
472
- return {
473
- targetPreset,
474
- userPrompt: parsed.prompt || userText,
475
- messages: msgs.slice(0, -1),
476
- callLlm,
477
- cwd,
478
- }
479
- })
480
-
481
- const dispose = ctx.llm.registerAdapter(['moa-runner'], adapter)
482
- return () => {
483
- if (typeof dispose === 'function') dispose()
484
- }
485
- }, 'dsh-moa: llm adapter')
486
- }
487
-
488
- // Session turn interceptor: flag MoA turns and route to moa-runner adapter
489
- const moaPendingSignals = new WeakSet()
490
- const moaPendingContexts = new WeakMap()
453
+ // Session turn interceptor: route turn to real aggregator model & stream MoA synthesis
454
+ const moaPendingTurns = new WeakMap()
455
+ const moaSessionTurns = new Map()
491
456
 
492
457
  ctx.on('agent/pre-step', async (event, next) => {
493
458
  const messages = event.messages || []
@@ -501,10 +466,26 @@ export function apply(ctx, config) {
501
466
  if (userText.startsWith('/moa')) {
502
467
  const cfg = live()
503
468
  if (cfg.enabled !== false) {
469
+ const parsed = parseMoACommand(userText, cfg.presets || []) || { prompt: userText, presetName: cfg.default_preset || 'default' }
470
+ const targetPreset = (cfg.presets || []).find((p) => p.name === parsed.presetName) || cfg.presets?.[0]
471
+ const aggProvider = targetPreset?.aggregator?.provider || 'codex'
472
+ const aggModel = targetPreset?.aggregator?.model || 'gpt-5.6-sol'
473
+ const sessCwd = event.cwd || event.session?.cwd || (event.session?.path ? path.dirname(event.session.path) : undefined) || process.cwd()
474
+
475
+ const turnContext = {
476
+ targetPreset,
477
+ userPrompt: parsed.prompt || userText,
478
+ messages: messages.slice(0, -1),
479
+ cwd: sessCwd,
480
+ aggregator: { provider: aggProvider, model: aggModel },
481
+ callLlm,
482
+ }
483
+
504
484
  if (event.signal) {
505
- moaPendingSignals.add(event.signal)
506
- const sessCwd = event.cwd || event.session?.cwd || (event.session?.path ? path.dirname(event.session.path) : undefined)
507
- moaPendingContexts.set(event.signal, { cwd: sessCwd })
485
+ moaPendingTurns.set(event.signal, turnContext)
486
+ }
487
+ if (event.session?.id) {
488
+ moaSessionTurns.set(event.session.id, turnContext)
508
489
  }
509
490
  }
510
491
  }
@@ -514,14 +495,32 @@ export function apply(ctx, config) {
514
495
 
515
496
  ctx.on('agent/request', async (payload, next) => {
516
497
  const resolved = await next()
517
- if (payload.signal && moaPendingSignals.has(payload.signal)) {
518
- moaPendingSignals.delete(payload.signal)
498
+ const turnContext = (payload.signal ? moaPendingTurns.get(payload.signal) : null)
499
+ || (payload.session?.id ? moaSessionTurns.get(payload.session.id) : null)
500
+
501
+ if (turnContext) {
519
502
  return {
520
503
  ...resolved,
521
- provider: 'moa-runner',
522
- model: 'ensemble',
504
+ provider: turnContext.aggregator.provider,
505
+ model: turnContext.aggregator.model,
523
506
  }
524
507
  }
525
508
  return resolved
526
509
  })
510
+
511
+ ctx.effect(() => {
512
+ return ctx.on('llm/stream', (options, next) => {
513
+ const turnContext = (options.signal ? moaPendingTurns.get(options.signal) : null)
514
+ || (options.session?.id ? moaSessionTurns.get(options.session.id) : null)
515
+
516
+ if (turnContext) {
517
+ if (options.signal) moaPendingTurns.delete(options.signal)
518
+ if (options.session?.id) moaSessionTurns.delete(options.session.id)
519
+
520
+ return streamMoATurn(turnContext, options)
521
+ }
522
+
523
+ return next(options)
524
+ })
525
+ }, 'dsh-moa: llm stream interceptor')
527
526
  }
package/lib/moa-runner.js CHANGED
@@ -231,7 +231,7 @@ export function parseMoACommand(text, presets = []) {
231
231
  }
232
232
  }
233
233
 
234
- const matchPreset = /^--preset=([a-zA-Z0-9_-]+)\s*(.*)/s.exec(remainder)
234
+ const matchPreset = /^--preset(?:=|\s+)([a-zA-Z0-9_-]+)\s*(.*)/s.exec(remainder)
235
235
  if (matchPreset) {
236
236
  return {
237
237
  presetName: matchPreset[1],
@@ -684,118 +684,80 @@ export function formatMoAResponse({ moaResult, presetName }) {
684
684
  return parts.join('\n')
685
685
  }
686
686
 
687
- export class MoaRunnerAdapter {
688
- constructor(getMoaContext) {
689
- this.getMoaContext = getMoaContext
690
- }
691
-
692
- providerInfo(provider) {
693
- return { id: provider, name: 'Mixture of Agents Runner' }
694
- }
687
+ export async function* streamMoATurn({ targetPreset, userPrompt, messages, callLlm, cwd }, options = {}) {
688
+ const signal = options?.signal
689
+ if (signal?.aborted) return
695
690
 
696
- providerRetryPolicy(_provider) {
697
- return undefined
698
- }
699
-
700
- imageRequestPricing(_provider, _model) {
701
- return undefined
702
- }
691
+ yield { type: 'block-start', index: 0, blockType: 'text' }
692
+ yield { type: 'text-delta', index: 0, text: '🧠 *Mixture of Agents запущен...*\n\n' }
703
693
 
704
- async listModels(provider) {
705
- const prov = typeof provider === 'string' && provider.length > 0 ? provider : 'moa-runner'
706
- return [{ provider: prov, id: 'ensemble', name: 'MoA Ensemble' }]
707
- }
708
-
709
- async resolveModel(provider, model) {
710
- return { provider, id: model, name: 'MoA Ensemble' }
711
- }
712
-
713
- async prepareCall(providerOrConfig, modelOrSignal, _signal) {
714
- const provider = typeof providerOrConfig === 'object' && providerOrConfig !== null ? providerOrConfig.provider : providerOrConfig
715
- const model = typeof providerOrConfig === 'object' && providerOrConfig !== null ? providerOrConfig.model : modelOrSignal
716
- return {
717
- model: { provider, id: model, name: 'MoA Ensemble' },
718
- stream: (options) => this.stream(options),
719
- }
694
+ // Queue for streaming progress updates from within runMoAPipeline
695
+ const progressChunks = []
696
+ const onProgress = (text) => {
697
+ progressChunks.push(text)
720
698
  }
721
699
 
722
- async *stream(options) {
723
- const signal = options.signal
724
- if (signal?.aborted) return
725
-
726
- yield { type: 'block-start', index: 0, blockType: 'text' }
727
- yield { type: 'text-delta', index: 0, text: '🧠 *Mixture of Agents запущен...*\n\n' }
728
-
729
- const { targetPreset, userPrompt, messages, callLlm, cwd } = this.getMoaContext(options)
730
-
731
- // Queue for streaming progress updates from within runMoAPipeline
732
- const progressChunks = []
733
- const onProgress = (text) => {
734
- progressChunks.push(text)
735
- }
736
-
737
- try {
738
- const pipelinePromise = runMoAPipeline({
739
- userPrompt,
740
- messages,
741
- preset: targetPreset,
742
- callLlm,
743
- cwd,
744
- onProgress,
745
- })
746
-
747
- const startTime = Date.now()
748
- let lastYieldTime = Date.now()
749
-
750
- // Yield progressive updates while pipeline runs
751
- while (true) {
752
- if (signal?.aborted) return
700
+ try {
701
+ const pipelinePromise = runMoAPipeline({
702
+ userPrompt,
703
+ messages,
704
+ preset: targetPreset,
705
+ callLlm,
706
+ cwd,
707
+ onProgress,
708
+ })
753
709
 
754
- let emittedAny = false
755
- while (progressChunks.length > 0) {
756
- const chunk = progressChunks.shift()
757
- yield { type: 'text-delta', index: 0, text: chunk }
758
- emittedAny = true
759
- lastYieldTime = Date.now()
760
- }
710
+ const startTime = Date.now()
711
+ let lastYieldTime = Date.now()
761
712
 
762
- const done = await Promise.race([
763
- pipelinePromise.then(() => true),
764
- new Promise((r) => { const t = setTimeout(() => r(false), 500); t.unref?.(); }),
765
- ])
766
- if (done) break
767
-
768
- const elapsedSec = Math.floor((Date.now() - startTime) / 1000)
769
- // If 3 seconds passed without any progress message, send a live tick
770
- if (!emittedAny && Date.now() - lastYieldTime >= 3000) {
771
- yield { type: 'text-delta', index: 0, text: `⏳ *[${elapsedSec}с] Идёт обработка...*\n` }
772
- lastYieldTime = Date.now()
773
- }
774
- }
713
+ // Yield progressive updates while pipeline runs
714
+ while (true) {
715
+ if (signal?.aborted) return
775
716
 
717
+ let emittedAny = false
776
718
  while (progressChunks.length > 0) {
777
719
  const chunk = progressChunks.shift()
778
720
  yield { type: 'text-delta', index: 0, text: chunk }
721
+ emittedAny = true
722
+ lastYieldTime = Date.now()
779
723
  }
780
724
 
781
- const moaResult = await pipelinePromise
782
- if (signal?.aborted) return
783
-
784
- const formatted = formatMoAResponse({
785
- moaResult,
786
- presetName: targetPreset?.name || 'default',
787
- })
725
+ const done = await Promise.race([
726
+ pipelinePromise.then(() => true),
727
+ new Promise((r) => { const t = setTimeout(() => r(false), 500); t.unref?.(); }),
728
+ ])
729
+ if (done) break
730
+
731
+ const elapsedSec = Math.floor((Date.now() - startTime) / 1000)
732
+ // If 3 seconds passed without any progress message, send a live tick
733
+ if (!emittedAny && Date.now() - lastYieldTime >= 3000) {
734
+ yield { type: 'text-delta', index: 0, text: `⏳ *[${elapsedSec}с] Идёт обработка...*\n` }
735
+ lastYieldTime = Date.now()
736
+ }
737
+ }
788
738
 
789
- yield { type: 'text-delta', index: 0, text: '\n---\n\n' + formatted }
790
- yield { type: 'block-end', index: 0, block: { type: 'text', text: formatted } }
791
- yield { type: 'usage', usage: { inputTokens: moaResult?.usage?.totalTokens || 100, outputTokens: formatted.length } }
792
- yield { type: 'finish', reason: { kind: 'stop' } }
793
- } catch (err) {
794
- if (signal?.aborted) return
795
- const errText = '\n\n⚠️ **Ошибка Mixture of Agents**: ' + (err?.message || String(err))
796
- yield { type: 'text-delta', index: 0, text: errText }
797
- yield { type: 'block-end', index: 0, block: { type: 'text', text: errText } }
798
- yield { type: 'finish', reason: { kind: 'stop' } }
739
+ while (progressChunks.length > 0) {
740
+ const chunk = progressChunks.shift()
741
+ yield { type: 'text-delta', index: 0, text: chunk }
799
742
  }
743
+
744
+ const moaResult = await pipelinePromise
745
+ if (signal?.aborted) return
746
+
747
+ const formatted = formatMoAResponse({
748
+ moaResult,
749
+ presetName: targetPreset?.name || 'default',
750
+ })
751
+
752
+ yield { type: 'text-delta', index: 0, text: '\n---\n\n' + formatted }
753
+ yield { type: 'block-end', index: 0, block: { type: 'text', text: formatted } }
754
+ yield { type: 'usage', usage: { inputTokens: moaResult?.usage?.totalTokens || 100, outputTokens: formatted.length } }
755
+ yield { type: 'finish', reason: { kind: 'stop' } }
756
+ } catch (err) {
757
+ if (signal?.aborted) return
758
+ const errText = '\n\n⚠️ **Ошибка Mixture of Agents**: ' + (err?.message || String(err))
759
+ yield { type: 'text-delta', index: 0, text: errText }
760
+ yield { type: 'block-end', index: 0, block: { type: 'text', text: errText } }
761
+ yield { type: 'finish', reason: { kind: 'stop' } }
800
762
  }
801
763
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-moa",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "description": "Mixture of Agents (MoA) plugin for DeepSeek Harness with /moa slash command",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",