@lythos/agent-adapter 0.17.11 → 0.19.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/package.json +1 -1
- package/src/adapters/kimi.test.ts +128 -4
- package/src/adapters/kimi.ts +174 -39
- package/src/index.ts +3 -0
- package/src/types.ts +16 -0
package/package.json
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { describe, expect, it } from 'bun:test'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
detectKimiBinary,
|
|
4
|
+
buildKimiCommand,
|
|
5
|
+
parseKimiStreamJson,
|
|
6
|
+
parseKimiVersion,
|
|
7
|
+
classifyKimiUpstream,
|
|
8
|
+
satisfiesVersionRange,
|
|
9
|
+
detectKimiProtocolMismatch,
|
|
10
|
+
KIMI_VERSION_RANGE,
|
|
11
|
+
} from './kimi'
|
|
3
12
|
|
|
4
13
|
describe('detectKimiBinary', () => {
|
|
5
14
|
it('returns a non-empty string when a kimi binary is available', () => {
|
|
@@ -19,12 +28,18 @@ describe('buildKimiCommand', () => {
|
|
|
19
28
|
expect(cmd).toContain('stream-json')
|
|
20
29
|
})
|
|
21
30
|
|
|
22
|
-
it('builds kimi
|
|
23
|
-
const cmd = buildKimiCommand(undefined, 'kimi')
|
|
31
|
+
it('builds kimi-code args for kimi binary (never --print)', () => {
|
|
32
|
+
const cmd = buildKimiCommand(undefined, 'kimi', 'kimi-code', 'do the thing')
|
|
24
33
|
expect(cmd[0]).toBe('kimi')
|
|
25
|
-
expect(cmd).toContain('--
|
|
34
|
+
expect(cmd).toContain('--prompt')
|
|
35
|
+
expect(cmd).toContain('do the thing')
|
|
26
36
|
expect(cmd).toContain('--output-format')
|
|
27
37
|
expect(cmd).toContain('stream-json')
|
|
38
|
+
expect(cmd).not.toContain('--print')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('kimi-code requires a prompt argument', () => {
|
|
42
|
+
expect(() => buildKimiCommand(undefined, 'kimi', 'kimi-code')).toThrow('--prompt')
|
|
28
43
|
})
|
|
29
44
|
|
|
30
45
|
it('does not use shell or redirect (injection-safe)', () => {
|
|
@@ -116,3 +131,112 @@ describe('parseKimiStreamJson', () => {
|
|
|
116
131
|
expect(result.checkpoints[1].stdout_summary).toBe('agent-stdout.txt\nreport.docx')
|
|
117
132
|
})
|
|
118
133
|
})
|
|
134
|
+
|
|
135
|
+
describe('parseKimiVersion', () => {
|
|
136
|
+
it('parses kimi-code bare version output', () => {
|
|
137
|
+
expect(parseKimiVersion('0.38.0')).toEqual({ version: '0.38.0', major: 0 })
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('parses kimi-cli self-report ("kimi, version 1.45.0")', () => {
|
|
141
|
+
expect(parseKimiVersion('kimi, version 1.45.0')).toEqual({ version: '1.45.0', major: 1 })
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('returns null for unparseable output', () => {
|
|
145
|
+
expect(parseKimiVersion('garbage')).toBeNull()
|
|
146
|
+
expect(parseKimiVersion('')).toBeNull()
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
describe('classifyKimiUpstream', () => {
|
|
151
|
+
it('major 0 → kimi-code', () => {
|
|
152
|
+
expect(classifyKimiUpstream(0)).toBe('kimi-code')
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('major 1 → kimi-cli', () => {
|
|
156
|
+
expect(classifyKimiUpstream(1)).toBe('kimi-cli')
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('unknown majors fail closed', () => {
|
|
160
|
+
expect(classifyKimiUpstream(2)).toBeNull()
|
|
161
|
+
expect(classifyKimiUpstream(9)).toBeNull()
|
|
162
|
+
})
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
describe('satisfiesVersionRange', () => {
|
|
166
|
+
it('accepts in-range kimi-code and kimi-cli versions', () => {
|
|
167
|
+
expect(satisfiesVersionRange('0.38.0', KIMI_VERSION_RANGE)).toBe(true)
|
|
168
|
+
expect(satisfiesVersionRange('1.45.0', KIMI_VERSION_RANGE)).toBe(true)
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('rejects out-of-range versions', () => {
|
|
172
|
+
expect(satisfiesVersionRange('2.0.0', KIMI_VERSION_RANGE)).toBe(false)
|
|
173
|
+
expect(satisfiesVersionRange('0.10.0', KIMI_VERSION_RANGE)).toBe(false)
|
|
174
|
+
expect(satisfiesVersionRange('9.9.9', KIMI_VERSION_RANGE)).toBe(false)
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('boundary values follow comparator semantics', () => {
|
|
178
|
+
expect(satisfiesVersionRange('0.30.0', KIMI_VERSION_RANGE)).toBe(true) // >= inclusive
|
|
179
|
+
expect(satisfiesVersionRange('2.0.0', '>=0.30.0 <2.0.0')).toBe(false) // < exclusive
|
|
180
|
+
expect(satisfiesVersionRange('1.0.0', '>1.0.0')).toBe(false)
|
|
181
|
+
expect(satisfiesVersionRange('1.0.1', '>1.0.0')).toBe(true)
|
|
182
|
+
expect(satisfiesVersionRange('1.0.0', '<=1.0.0')).toBe(true)
|
|
183
|
+
})
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
describe('parseKimiStreamJson — real kimi-code capture', () => {
|
|
187
|
+
// Live capture from kimi-code 0.38.0: `kimi -p "Reply with exactly: PROBE_OK" --output-format stream-json`
|
|
188
|
+
const kimiCodeCapture = [
|
|
189
|
+
'{"role":"meta","type":"system.version","version":"0.38.0"}',
|
|
190
|
+
'{"role":"assistant","content":"PROBE_OK"}',
|
|
191
|
+
'{"role":"meta","type":"session.resume_hint","session_id":"session_x","command":"kimi -r session_x","content":"To resume this session: kimi -r session_x"}',
|
|
192
|
+
].join('\n')
|
|
193
|
+
|
|
194
|
+
it('extracts assistant text and skips meta events', () => {
|
|
195
|
+
const result = parseKimiStreamJson(kimiCodeCapture)
|
|
196
|
+
expect(result.text).toBe('PROBE_OK')
|
|
197
|
+
expect(result.events).toBe(3)
|
|
198
|
+
expect(result.checkpoints).toEqual([])
|
|
199
|
+
})
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
describe('detectKimiProtocolMismatch', () => {
|
|
203
|
+
it('regression: kimi-code invoked with --print must NOT pass silently (the live bug)', () => {
|
|
204
|
+
// Exact live capture: `kimi --print --output-format stream-json` → exit 1, empty stdout
|
|
205
|
+
const msg = detectKimiProtocolMismatch({
|
|
206
|
+
code: 1,
|
|
207
|
+
rawStdout: '',
|
|
208
|
+
stderr: "error: unknown option '--print'\n(Did you mean --prompt?)",
|
|
209
|
+
events: 0,
|
|
210
|
+
})
|
|
211
|
+
expect(msg).not.toBeNull()
|
|
212
|
+
expect(msg).toContain('code 1')
|
|
213
|
+
expect(msg).toContain("unknown option '--print'")
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
it('exit 0 with non-JSON stdout → protocol mismatch', () => {
|
|
217
|
+
const msg = detectKimiProtocolMismatch({
|
|
218
|
+
code: 0,
|
|
219
|
+
rawStdout: 'plain text that is not stream-json',
|
|
220
|
+
stderr: '',
|
|
221
|
+
events: 0,
|
|
222
|
+
})
|
|
223
|
+
expect(msg).not.toBeNull()
|
|
224
|
+
expect(msg).toContain('no parseable stream-json events')
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
it('exit 0 with empty stdout → no-output error', () => {
|
|
228
|
+
const msg = detectKimiProtocolMismatch({ code: 0, rawStdout: '', stderr: '', events: 0 })
|
|
229
|
+
expect(msg).not.toBeNull()
|
|
230
|
+
expect(msg).toContain('no output at all')
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
it('exit 0 with parsed events → OK', () => {
|
|
234
|
+
const msg = detectKimiProtocolMismatch({
|
|
235
|
+
code: 0,
|
|
236
|
+
rawStdout: '{"role":"assistant","content":"hi"}',
|
|
237
|
+
stderr: '',
|
|
238
|
+
events: 1,
|
|
239
|
+
})
|
|
240
|
+
expect(msg).toBeNull()
|
|
241
|
+
})
|
|
242
|
+
})
|
package/src/adapters/kimi.ts
CHANGED
|
@@ -7,28 +7,80 @@ import { registerAgent } from '../registry'
|
|
|
7
7
|
|
|
8
8
|
// ── Pure functions (testable without CLI) ────────────────────────────────────
|
|
9
9
|
|
|
10
|
-
/**
|
|
11
|
-
export
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
10
|
+
/** kimi-cli = legacy Python CLI (v1.x, binary `kimi-cli`); kimi-code = TS CLI (v0.x, binary `kimi`). */
|
|
11
|
+
export type KimiUpstream = 'kimi-cli' | 'kimi-code'
|
|
12
|
+
|
|
13
|
+
/** Supported range across both upstreams (ADR-20260828004129233 Option B). */
|
|
14
|
+
export const KIMI_VERSION_RANGE = '>=0.30.0 <2.0.0'
|
|
15
|
+
|
|
16
|
+
/** Detect which kimi binary to use, in the declared preference order (default: kimi-cli legacy over kimi kimi-code). */
|
|
17
|
+
export function detectKimiBinary(binaries: string[] = ['kimi-cli', 'kimi']): string {
|
|
18
|
+
for (const b of binaries) {
|
|
19
|
+
if (Bun.which(b)) return b
|
|
19
20
|
}
|
|
20
21
|
return ''
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* Parse the first semver triple from `<binary> --version` output.
|
|
26
|
+
* Both upstreams self-report as "kimi" — kimi-code prints "0.38.0",
|
|
27
|
+
* kimi-cli prints "kimi, version 1.45.0" — so discriminate on the
|
|
28
|
+
* version, never on the product name.
|
|
29
|
+
*/
|
|
30
|
+
export function parseKimiVersion(output: string): { version: string; major: number } | null {
|
|
31
|
+
const m = output.match(/(\d+)\.(\d+)\.(\d+)/)
|
|
32
|
+
if (!m) return null
|
|
33
|
+
return { version: m[0], major: parseInt(m[1], 10) }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** major 0 = kimi-code, major 1 = kimi-cli, anything else = unknown (fail closed). */
|
|
37
|
+
export function classifyKimiUpstream(major: number): KimiUpstream | null {
|
|
38
|
+
if (major === 0) return 'kimi-code'
|
|
39
|
+
if (major === 1) return 'kimi-cli'
|
|
40
|
+
return null
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Minimal semver-triple check against space-separated comparators (>=, >, <=, <). No semver dep. */
|
|
44
|
+
export function satisfiesVersionRange(version: string, range: string): boolean {
|
|
45
|
+
const v = version.split('.').map(n => parseInt(n, 10))
|
|
46
|
+
for (const cmp of range.trim().split(/\s+/)) {
|
|
47
|
+
const m = cmp.match(/^(>=|<=|>|<)(\d+\.\d+\.\d+)$/)
|
|
48
|
+
if (!m) return false
|
|
49
|
+
const t = m[2].split('.').map(n => parseInt(n, 10))
|
|
50
|
+
let d = 0
|
|
51
|
+
for (let i = 0; i < 3; i++) {
|
|
52
|
+
d = (v[i] ?? 0) - (t[i] ?? 0)
|
|
53
|
+
if (d !== 0) break
|
|
54
|
+
}
|
|
55
|
+
if (m[1] === '>=' && d < 0) return false
|
|
56
|
+
if (m[1] === '>' && d <= 0) return false
|
|
57
|
+
if (m[1] === '<=' && d > 0) return false
|
|
58
|
+
if (m[1] === '<' && d >= 0) return false
|
|
59
|
+
}
|
|
60
|
+
return true
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Build kimi CLI command args (no shell wrapper — safe from injection).
|
|
65
|
+
* kimi-cli speaks `--print` (prompt on stdin). kimi-code speaks `--prompt <prompt>`
|
|
66
|
+
* (its `--output-format` only works in prompt mode; `--print` never existed and
|
|
67
|
+
* exits 1 with "unknown option").
|
|
68
|
+
*/
|
|
24
69
|
export function buildKimiCommand(
|
|
25
70
|
_modelTier?: 'fast' | 'balanced' | 'deep',
|
|
26
71
|
binary = detectKimiBinary(),
|
|
72
|
+
upstream: KimiUpstream = 'kimi-cli',
|
|
73
|
+
prompt?: string,
|
|
27
74
|
): string[] {
|
|
28
75
|
if (!binary) {
|
|
29
76
|
throw new Error('No kimi binary found in PATH. Install: https://github.com/MoonshotAI/kimi-cli')
|
|
30
77
|
}
|
|
31
|
-
|
|
78
|
+
if (upstream === 'kimi-code') {
|
|
79
|
+
if (!prompt) {
|
|
80
|
+
throw new Error('kimi-code requires the prompt as a --prompt argument (no stdin prompt mode)')
|
|
81
|
+
}
|
|
82
|
+
return [binary, '--prompt', prompt, '--output-format', 'stream-json']
|
|
83
|
+
}
|
|
32
84
|
return [binary, '--print', '--output-format', 'stream-json']
|
|
33
85
|
}
|
|
34
86
|
|
|
@@ -36,16 +88,20 @@ export function buildKimiCommand(
|
|
|
36
88
|
* Parse kimi stream-json output into plain text + checkpoint trace.
|
|
37
89
|
* Each line is a JSON event; extracts text from assistant role messages
|
|
38
90
|
* and tool_calls/tool messages into CheckpointEntry for white-box replay.
|
|
91
|
+
* `events` counts successfully parsed JSON lines — zero events on non-empty
|
|
92
|
+
* stdout means the upstream is speaking a different protocol.
|
|
39
93
|
*/
|
|
40
|
-
export function parseKimiStreamJson(raw: string): { text: string; checkpoints: CheckpointEntry[] } {
|
|
94
|
+
export function parseKimiStreamJson(raw: string): { text: string; checkpoints: CheckpointEntry[]; events: number } {
|
|
41
95
|
const textLines: string[] = []
|
|
42
96
|
const checkpoints: CheckpointEntry[] = []
|
|
43
97
|
const pendingTools = new Map<string, { name: string; args: string }>()
|
|
98
|
+
let events = 0
|
|
44
99
|
|
|
45
100
|
for (const line of raw.split('\n')) {
|
|
46
101
|
if (!line.trim()) continue
|
|
47
102
|
try {
|
|
48
103
|
const event = JSON.parse(line)
|
|
104
|
+
events++
|
|
49
105
|
if (event.role === 'assistant') {
|
|
50
106
|
const content = event.content
|
|
51
107
|
if (typeof content === 'string') {
|
|
@@ -85,24 +141,103 @@ export function parseKimiStreamJson(raw: string): { text: string; checkpoints: C
|
|
|
85
141
|
} catch { /* skip malformed lines */ }
|
|
86
142
|
}
|
|
87
143
|
|
|
88
|
-
return { text: textLines.join('\n'), checkpoints }
|
|
144
|
+
return { text: textLines.join('\n'), checkpoints, events }
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Fail-loud check on a finished kimi run. Returns a HATEOAS-style error
|
|
149
|
+
* message, or null when the run produced parseable stream-json output.
|
|
150
|
+
* Replaces the old never-throw passthrough that returned empty stdout as success.
|
|
151
|
+
*/
|
|
152
|
+
export function detectKimiProtocolMismatch(result: {
|
|
153
|
+
code: number
|
|
154
|
+
rawStdout: string
|
|
155
|
+
stderr: string
|
|
156
|
+
events: number
|
|
157
|
+
}): string | null {
|
|
158
|
+
const { code, rawStdout, stderr, events } = result
|
|
159
|
+
if (code !== 0) {
|
|
160
|
+
return [
|
|
161
|
+
`kimi exited with code ${code} and produced no usable stream-json output.`,
|
|
162
|
+
stderr.trim() ? `stderr: ${stderr.trim().slice(0, 200)}` : '',
|
|
163
|
+
`Likely cause: flag/protocol mismatch between this adapter and the installed kimi upstream.`,
|
|
164
|
+
`Fix: upgrade kimi-cli (https://github.com/MoonshotAI/kimi-cli) or kimi-code, or pick another --player.`,
|
|
165
|
+
].filter(Boolean).join('\n ')
|
|
166
|
+
}
|
|
167
|
+
if (!rawStdout.trim()) {
|
|
168
|
+
return [
|
|
169
|
+
`kimi exited 0 but produced no output at all.`,
|
|
170
|
+
`Likely cause: protocol mismatch with the installed kimi upstream.`,
|
|
171
|
+
`Fix: upgrade kimi-cli/kimi-code, or pick another --player.`,
|
|
172
|
+
].join('\n ')
|
|
173
|
+
}
|
|
174
|
+
if (events === 0) {
|
|
175
|
+
return [
|
|
176
|
+
`kimi exited 0 but stdout had no parseable stream-json events.`,
|
|
177
|
+
`stdout (first 200 chars): ${rawStdout.trim().slice(0, 200)}`,
|
|
178
|
+
`Likely cause: the upstream is speaking a different output protocol.`,
|
|
179
|
+
`Fix: upgrade kimi-cli/kimi-code, or pick another --player.`,
|
|
180
|
+
].join('\n ')
|
|
181
|
+
}
|
|
182
|
+
return null
|
|
89
183
|
}
|
|
90
184
|
|
|
91
185
|
// ── Spawn wrapper (IO, tested via BDD / arena integration) ──────────────────
|
|
92
186
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
187
|
+
/** Probe `<binary> <probeArgs>`, classify the upstream, enforce the declared version range. Fail closed. */
|
|
188
|
+
function probeKimiUpstream(
|
|
189
|
+
binary: string,
|
|
190
|
+
probeArgs: string[],
|
|
191
|
+
versionRange: string,
|
|
192
|
+
): { upstream: KimiUpstream; version: string } {
|
|
193
|
+
const probe = Bun.spawnSync([binary, ...probeArgs])
|
|
194
|
+
const output = `${probe.stdout.toString()}\n${probe.stderr.toString()}`.trim()
|
|
195
|
+
const parsed = parseKimiVersion(output)
|
|
196
|
+
const upstream = parsed ? classifyKimiUpstream(parsed.major) : null
|
|
197
|
+
if (!parsed || !upstream || !satisfiesVersionRange(parsed.version, versionRange)) {
|
|
198
|
+
throw new Error([
|
|
199
|
+
`kimi upstream probe failed: "${binary} ${probeArgs.join(' ')}" returned "${output.slice(0, 120) || '(no output)'}"`,
|
|
200
|
+
`Detected: ${parsed ? `version ${parsed.version}` : 'unparseable version output'}`,
|
|
201
|
+
`Supported: kimi-cli 1.x (legacy) / kimi-code 0.x — declared range: ${versionRange}`,
|
|
202
|
+
`Fix: install/upgrade kimi-cli (https://github.com/MoonshotAI/kimi-cli) or kimi-code, or pick another --player.`,
|
|
203
|
+
].join('\n '))
|
|
204
|
+
}
|
|
205
|
+
return { upstream, version: parsed.version }
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function spawnKimi(
|
|
209
|
+
opts: {
|
|
210
|
+
brief: string
|
|
211
|
+
cwd: string
|
|
212
|
+
timeoutMs?: number
|
|
213
|
+
},
|
|
214
|
+
declared?: AgentAdapter['upstream'],
|
|
215
|
+
): Promise<AgentRunResult> {
|
|
216
|
+
const binary = detectKimiBinary(declared?.binaries)
|
|
217
|
+
if (!binary) {
|
|
218
|
+
throw new Error('kimi not found in PATH. Install: https://github.com/MoonshotAI/kimi-cli')
|
|
219
|
+
}
|
|
220
|
+
// Contract semantics: adapters without an `upstream` declaration stay unprobed (legacy behavior).
|
|
221
|
+
const probed = declared ? probeKimiUpstream(binary, declared.probeArgs, declared.versionRange) : null
|
|
222
|
+
const upstream: KimiUpstream = probed?.upstream ?? 'kimi-cli'
|
|
223
|
+
if (probed) console.error(`ℹ️ kimi upstream: ${probed.upstream} ${probed.version}`)
|
|
224
|
+
|
|
225
|
+
// kimi-cli reads the prompt from stdin (temp file); kimi-code takes it as --prompt argv.
|
|
226
|
+
let promptFile: string | null = null
|
|
227
|
+
let cmd: string[]
|
|
228
|
+
if (upstream === 'kimi-code') {
|
|
229
|
+
cmd = buildKimiCommand(undefined, binary, upstream, opts.brief)
|
|
230
|
+
} else {
|
|
231
|
+
promptFile = join(tmpdir(), `kimi-prompt-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.txt`)
|
|
232
|
+
writeFileSync(promptFile, opts.brief, 'utf-8')
|
|
233
|
+
cmd = buildKimiCommand(undefined, binary, upstream)
|
|
234
|
+
}
|
|
100
235
|
|
|
101
236
|
const start = Date.now()
|
|
102
237
|
|
|
103
|
-
const proc = Bun.spawn(
|
|
238
|
+
const proc = Bun.spawn(cmd, {
|
|
104
239
|
cwd: opts.cwd,
|
|
105
|
-
stdin: Bun.file(promptFile),
|
|
240
|
+
stdin: promptFile ? Bun.file(promptFile) : 'ignore',
|
|
106
241
|
stdout: 'pipe',
|
|
107
242
|
stderr: 'pipe',
|
|
108
243
|
})
|
|
@@ -111,22 +246,20 @@ async function spawnKimi(opts: {
|
|
|
111
246
|
await proc.exited
|
|
112
247
|
clearTimeout(timeout)
|
|
113
248
|
|
|
114
|
-
|
|
249
|
+
if (promptFile) {
|
|
250
|
+
try { unlinkSync(promptFile) } catch {}
|
|
251
|
+
}
|
|
115
252
|
|
|
116
253
|
const durationMs = Date.now() - start
|
|
117
254
|
const rawStdout = await new Response(proc.stdout).text()
|
|
118
255
|
const stderr = await new Response(proc.stderr).text()
|
|
119
256
|
const code = proc.exitCode ?? 1
|
|
120
257
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
streamCheckpoints = parsed.checkpoints
|
|
127
|
-
} catch {
|
|
128
|
-
text = rawStdout
|
|
129
|
-
streamCheckpoints = []
|
|
258
|
+
// parseKimiStreamJson never throws; protocol mismatch is detected explicitly.
|
|
259
|
+
const parsed = parseKimiStreamJson(rawStdout)
|
|
260
|
+
const mismatch = detectKimiProtocolMismatch({ code, rawStdout, stderr, events: parsed.events })
|
|
261
|
+
if (mismatch) {
|
|
262
|
+
throw new Error(mismatch)
|
|
130
263
|
}
|
|
131
264
|
|
|
132
265
|
// Preserve raw JSONL for post-hoc white-box analysis
|
|
@@ -135,20 +268,22 @@ async function spawnKimi(opts: {
|
|
|
135
268
|
} catch { /* ignore write failures */ }
|
|
136
269
|
|
|
137
270
|
const fileCheckpoints = readCheckpoints(opts.cwd)
|
|
138
|
-
const checkpoints = [...
|
|
271
|
+
const checkpoints = [...parsed.checkpoints, ...fileCheckpoints]
|
|
139
272
|
|
|
140
|
-
return { stdout: text.trim(), stderr, code, durationMs, checkpoints }
|
|
273
|
+
return { stdout: parsed.text.trim(), stderr, code, durationMs, checkpoints }
|
|
141
274
|
}
|
|
142
275
|
|
|
143
276
|
const kimiAdapter: AgentAdapter = {
|
|
144
277
|
name: 'kimi',
|
|
145
278
|
|
|
279
|
+
upstream: {
|
|
280
|
+
binaries: ['kimi-cli', 'kimi'],
|
|
281
|
+
versionRange: KIMI_VERSION_RANGE,
|
|
282
|
+
probeArgs: ['--version'],
|
|
283
|
+
},
|
|
284
|
+
|
|
146
285
|
async spawn(opts): Promise<AgentRunResult> {
|
|
147
|
-
|
|
148
|
-
if (!binary) {
|
|
149
|
-
throw new Error('kimi not found in PATH. Install: https://github.com/MoonshotAI/kimi-cli')
|
|
150
|
-
}
|
|
151
|
-
return spawnKimi(opts)
|
|
286
|
+
return spawnKimi(opts, kimiAdapter.upstream)
|
|
152
287
|
},
|
|
153
288
|
|
|
154
289
|
async invokeTool(_opts): Promise<unknown> {
|
package/src/index.ts
CHANGED
|
@@ -37,6 +37,9 @@ export { readCheckpoints } from './checkpoint'
|
|
|
37
37
|
// Built-in adapters — lightweight CLI wrappers only.
|
|
38
38
|
// Heavy adapters live in separate packages (see above).
|
|
39
39
|
export { kimiAdapter } from './adapters/kimi'
|
|
40
|
+
// Shared version-range check (ADR-20260828004129233 Option B) — used by adapter
|
|
41
|
+
// packages (e.g. agent-adapter-deepseek-harness) that declare an upstream range.
|
|
42
|
+
export { satisfiesVersionRange } from './adapters/kimi'
|
|
40
43
|
export {
|
|
41
44
|
claudeCliAdapter,
|
|
42
45
|
buildClaudeCommand,
|
package/src/types.ts
CHANGED
|
@@ -33,6 +33,22 @@ export interface ToolDefinition {
|
|
|
33
33
|
|
|
34
34
|
export interface AgentAdapter {
|
|
35
35
|
name: string
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Optional upstream declaration (ADR-20260828004129233 Option B): which upstream
|
|
39
|
+
* binary(-ies) this adapter drives, the supported version range, and how to probe.
|
|
40
|
+
* Adapters that declare it probe at spawn time and fail closed (loud HATEOAS error)
|
|
41
|
+
* on unknown/out-of-range upstreams. Adapters without it are unprobed (unchanged).
|
|
42
|
+
*/
|
|
43
|
+
upstream?: {
|
|
44
|
+
/** Candidate binary names in preference order (e.g. ['kimi-cli', 'kimi']). */
|
|
45
|
+
binaries: string[]
|
|
46
|
+
/** Space-separated comparators, e.g. '>=0.30.0 <2.0.0'. */
|
|
47
|
+
versionRange: string
|
|
48
|
+
/** Args that print the version, e.g. ['--version']. */
|
|
49
|
+
probeArgs: string[]
|
|
50
|
+
}
|
|
51
|
+
|
|
36
52
|
spawn(opts: {
|
|
37
53
|
cwd: string
|
|
38
54
|
brief: string
|