File size: 3,855 Bytes
3f7a9b2
 
 
 
 
 
6986878
3f7a9b2
 
 
 
6986878
3f7a9b2
 
 
 
 
 
 
 
 
 
 
 
 
6986878
 
 
 
 
3f7a9b2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
#!/usr/bin/env bun
import { readFile } from 'node:fs/promises'
import { Command, CommanderError, Option } from 'commander'
import { TerminalProgress } from './cli/progress.js'
import type { CrivoLoadOptions } from './decisions/adapters/crivo.js'
import type { RuntimeMetrics } from './decisions/adapters/runtime.js'
import { MODEL_SIZES, type ModelSize } from './decisions/models.js'
import { InvalidInputError, validateInput } from './decisions/validate.js'
import { decide } from './index.js'

interface Options {
  size: ModelSize
  device: NonNullable<CrivoLoadOptions['device']>
  dtype?: CrivoLoadOptions['dtype']
  cacheDir?: string
}

function report(error: object): void {
  process.stderr.write(`${JSON.stringify({ ok: false, error }, null, 2)}\n`)
  process.exitCode = 1
}

const program = new Command()
  .name('decide')
  .description('Run typed decisions against a shared state. Input: JSON with state and decisions.')
  .addOption(
    new Option('--size <size>', 'Crivo model size (latest upstream checkpoint)')
      .choices([...MODEL_SIZES])
      .default('150m')
  )
  .argument('<input>', 'JSON file path, or - to read JSON from stdin')
  .addOption(
    new Option('--device <device>', 'execution device')
      .choices(['auto', 'cpu', 'gpu'])
      .default('auto')
  )
  .addOption(
    new Option('--dtype <dtype>', 'model precision (default depends on device)').choices([
      'q8',
      'fp32'
    ])
  )
  .option('--cache-dir <path>', 'model cache directory', './.cache/crivo')
  .addHelpText(
    'after',
    '\nExamples:\n  bun run decide examples/basic/input.json --device cpu\n  cat request.json | bun run decide -\n'
  )
  .exitOverride()
  .action(async (path: string, options: Options) => {
    let source: string
    try {
      if (path === '-') {
        if (process.stdin.isTTY) throw new Error('Pipe JSON to stdin when using -.')
        source = await new Response(Bun.stdin.stream()).text()
      } else {
        source = await readFile(path, 'utf8')
      }
    } catch (error) {
      report({
        type: 'input-read-error',
        message: error instanceof Error ? error.message : String(error)
      })
      return
    }
    let input: unknown
    try {
      input = JSON.parse(source)
    } catch {
      report({
        type: 'invalid-json',
        message: 'Input must contain a valid JSON object with state and decisions.'
      })
      return
    }
    validateInput(input)
    const progress = new TerminalProgress()
    let metrics: RuntimeMetrics | undefined
    const { createRuntime } = await import('./decisions/adapters/runtime.js')
    const result = await decide(input, {
      requirements: {
        decisionRuntime: createRuntime(
          {
            ...options,
            onLoadStart: (info) => progress.loading(info),
            onProgress: (event) => progress.progress(event)
          },
          (value) => {
            metrics = value
          }
        )
      }
    })
      .onSuccess((answers) => ({ ok: true as const, answers }))
      .onError({ type: 'invalid-input' }, (error) => ({ ok: false as const, error }))
      .onError({ type: 'input-too-long' }, (error) => ({ ok: false as const, error }))
    if (result.ok) {
      process.stdout.write(`${JSON.stringify(result, null, 2)}\n`)
      if (metrics) progress.summary(metrics, input.decisions.length)
    } else report(result.error)
  })

try {
  await program.parseAsync()
} catch (error) {
  if (error instanceof CommanderError) {
    // Commander already printed help or the usage error.
    process.exitCode = error.exitCode
  } else if (error instanceof InvalidInputError) {
    report({ type: 'invalid-input', path: error.path, message: error.message })
  } else {
    report({
      type: 'runtime-error',
      message: error instanceof Error ? error.message : String(error)
    })
  }
}