@goodandready/dsh-moa 0.2.5 → 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/client.js CHANGED
@@ -888,14 +888,45 @@ window.__ModuleLoader__.load({
888
888
 
889
889
  function useMoASettings(props) {
890
890
  const t = makeT(rootCtx, props)
891
+ const effectiveCtx = (props && props.ctx) || rootCtx
892
+
893
+ // Reactive binding to settingsScope if available
894
+ const scope = React.useMemo(() => {
895
+ if (!effectiveCtx) return undefined
896
+ const s = effectiveCtx.get ? effectiveCtx.get('settingsScope') : effectiveCtx.settingsScope
897
+ return s && typeof s.bind === 'function' ? s.bind({ namespace: NS }) : undefined
898
+ }, [effectiveCtx])
899
+
900
+ const snapshot = React.useSyncExternalStore
901
+ ? React.useSyncExternalStore(
902
+ React.useMemo(() => (cb) => (scope ? scope.subscribe(cb) : () => {}), [scope]),
903
+ React.useCallback(() => (scope ? scope.getSnapshot() : { status: 'ready' }), [scope]),
904
+ React.useCallback(() => ({ status: 'loading' }), [])
905
+ )
906
+ : null
907
+
891
908
  const [status, setStatus] = React.useState('loading')
892
909
  const [presets, setPresets] = React.useState([])
893
910
  const [defaultPreset, setDefaultPreset] = React.useState('default')
894
911
  const [availableModels, setAvailableModels] = React.useState([])
895
912
  const [saveStatus, setSaveStatus] = React.useState('')
896
913
 
914
+ // Sync from settingsScope snapshot when available
915
+ React.useEffect(() => {
916
+ if (!scope || !snapshot) return
917
+ const snapStatus = snapshot.status || 'ready'
918
+ if (snapStatus === 'ready') {
919
+ const val = snapshot.value || {}
920
+ if (Array.isArray(val.presets)) setPresets(val.presets)
921
+ if (val.default_preset) setDefaultPreset(val.default_preset)
922
+ setStatus('ready')
923
+ } else if (snapStatus === 'unavailable') {
924
+ setStatus('unavailable')
925
+ }
926
+ }, [scope, snapshot])
927
+
897
928
  const reload = React.useCallback(() => {
898
- setStatus('loading')
929
+ if (!scope) setStatus('loading')
899
930
 
900
931
  fetch('/dsh-moa/presets', { cache: 'no-store' })
901
932
  .then((r) => {
@@ -907,13 +938,13 @@ window.__ModuleLoader__.load({
907
938
  setPresets(data.presets || [])
908
939
  setDefaultPreset(data.defaultPreset || 'default')
909
940
  setStatus('ready')
910
- } else {
941
+ } else if (!scope) {
911
942
  setStatus('unavailable')
912
943
  }
913
944
  })
914
945
  .catch((err) => {
915
946
  console.warn('[dsh-moa] Presets fetch failed:', err)
916
- setStatus('unavailable')
947
+ if (!scope) setStatus('unavailable')
917
948
  })
918
949
 
919
950
  fetch('/dsh-moa/models', { cache: 'no-store' })
@@ -926,7 +957,7 @@ window.__ModuleLoader__.load({
926
957
  .catch((err) => {
927
958
  console.warn('[dsh-moa] Models fetch failed:', err)
928
959
  })
929
- }, [])
960
+ }, [scope])
930
961
 
931
962
  React.useEffect(() => {
932
963
  reload()
@@ -934,6 +965,27 @@ window.__ModuleLoader__.load({
934
965
 
935
966
  const handleSave = () => {
936
967
  setSaveStatus(t('loading'))
968
+
969
+ if (scope && typeof scope.set === 'function') {
970
+ Promise.all([
971
+ scope.set('presets', presets),
972
+ scope.set('default_preset', defaultPreset),
973
+ ])
974
+ .then(() => {
975
+ setSaveStatus(t('saved'))
976
+ setTimeout(() => setSaveStatus(''), 2500)
977
+ })
978
+ .catch((err) => {
979
+ console.warn('[dsh-moa] scope.set failed, trying REST fallback:', err)
980
+ doRestSave()
981
+ })
982
+ return
983
+ }
984
+
985
+ doRestSave()
986
+ }
987
+
988
+ const doRestSave = () => {
937
989
  fetch('/dsh-moa/presets', {
938
990
  method: 'POST',
939
991
  headers: { 'Content-Type': 'application/json' },
@@ -1111,7 +1163,7 @@ window.__ModuleLoader__.load({
1111
1163
  )
1112
1164
  }
1113
1165
 
1114
- exports.inject = ['slots', 'locale', 'inputTriggers']
1166
+ exports.inject = ['slots', 'locale', 'inputTriggers', 'settingsScope']
1115
1167
  exports.apply = function apply(ctx) {
1116
1168
  rootCtx = ctx
1117
1169
  const t = makeT(ctx)
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.5",
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",
@@ -58,4 +58,4 @@
58
58
  "@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
59
59
  "@deepseek-ai/schemastery": "^3.18.1"
60
60
  }
61
- }
61
+ }