Initial commit: omp-edit DSH plugin (oh-my-pi port) with docs and tests
This commit is contained in:
commit
71953ffb8f
19 files changed
+2412
No files matched your search
@@ -0,0 +1,132 @@
|
||||
// Minimal DSH-like context for integration tests: real @deepseek-ai/dsh-fs-local backend,
|
||||
// the fs-observation-policy rules, a scoped tool registry, and fake agents.
|
||||
import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
import fs from 'node:fs'
|
||||
|
||||
const DSH = '/usr/lib/node_modules/@deepseek-ai/dsh/node_modules/@deepseek-ai'
|
||||
|
||||
export async function makeHarness() {
|
||||
const { default: LocalFileSystem } = await import(`${DSH}/dsh-fs-local/lib/index.js`)
|
||||
const { FsError } = await import(`${DSH}/dsh-fs/lib/index.js`)
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'omp-edit-'))
|
||||
const listeners = new Map()
|
||||
const on = (name, fn, opts) => {
|
||||
const list = listeners.get(name) ?? []
|
||||
if (opts?.prepend) list.unshift(fn); else list.push(fn)
|
||||
listeners.set(name, list)
|
||||
return () => listeners.set(name, (listeners.get(name) ?? []).filter((f) => f !== fn))
|
||||
}
|
||||
const fsCtx = { config: { cwd: dir }, effect() {}, on() {}, get() {}, provide() {}, emit() {} }
|
||||
// Construct without a Cordis context: borrow the prototype and the instance fields.
|
||||
const fsImpl = Object.create(LocalFileSystem.prototype)
|
||||
Object.assign(fsImpl, { config: { cwd: dir, diffBasisMaxBytes: 10 * 1024 * 1024 }, internals: {}, locks: new Map() })
|
||||
void fsCtx
|
||||
|
||||
// observation policy (copy of DSH's rules)
|
||||
const observed = new WeakMap()
|
||||
const own = (actor) => actor?.agent?.session
|
||||
on('fs/write-intent', async (target, actor) => {
|
||||
const p = own(actor) && observed.get(own(actor))?.get(target.targetKey)
|
||||
return p?.kind === 'present' ? { kind: 'replaceIfVersion', version: p.version } : { kind: 'createIfAbsent' }
|
||||
})
|
||||
on('fs/edit-intent', async (target, actor) => {
|
||||
const o = own(actor); const p = o && observed.get(o)?.get(target.targetKey)
|
||||
if (!o || p === undefined) throw new FsError(`edit requires reading "${target.displayPath}" first`, 'FS_NOT_OBSERVED')
|
||||
if (p.kind === 'absent') throw new FsError(`cannot edit "${target.displayPath}": not found`, 'FS_NOT_FOUND')
|
||||
return { version: p.version }
|
||||
})
|
||||
on('fs/observed', (target, obs, actor) => {
|
||||
const o = own(actor); if (!o) return
|
||||
if (!observed.has(o)) observed.set(o, new Map())
|
||||
observed.get(o).set(target.targetKey, obs)
|
||||
})
|
||||
|
||||
const global = new Map()
|
||||
const scoped = new WeakMap()
|
||||
const makeTools = (scope) => ({
|
||||
register(def) {
|
||||
const m = scope ? (scoped.get(scope) ?? scoped.set(scope, new Map()).get(scope)) : global
|
||||
if (m.has(def.name)) throw new Error(`duplicate tool ${def.name}`)
|
||||
m.set(def.name, def)
|
||||
return () => m.delete(def.name)
|
||||
},
|
||||
get(name, s) { return (s && scoped.get(s)?.get(name)) ?? global.get(name) },
|
||||
})
|
||||
const agents = []
|
||||
const sp = { section: () => () => {}, getSectionOrder: () => 1300 }
|
||||
const ctx = {
|
||||
fs: fsImpl,
|
||||
tools: makeTools(undefined),
|
||||
agents: { list: () => agents },
|
||||
systemPrompt: sp,
|
||||
on,
|
||||
emit(name, ...args) { for (const f of listeners.get(name) ?? []) f(...args) },
|
||||
async waterfall(name, ...args) {
|
||||
const inner = args.pop()
|
||||
const cbs = [...(listeners.get(name) ?? [])]
|
||||
const next = () => (cbs.shift() ?? inner)(...args, next)
|
||||
return next()
|
||||
},
|
||||
effect(fn) { const d = fn(); return d },
|
||||
get() { return undefined },
|
||||
}
|
||||
function newAgent(model = 'anthropic/claude-sonnet') {
|
||||
const agent = { options: { model }, session: { header: { cwd: dir } } }
|
||||
agent.ctx = { tools: makeTools(agent), on, systemPrompt: sp }
|
||||
agents.push(agent)
|
||||
for (const f of listeners.get('agent/created') ?? []) f({ agent })
|
||||
return agent
|
||||
}
|
||||
const call = async (agent, name, args) => {
|
||||
const t = ctx.tools.get(name, agent)
|
||||
if (!t) throw new Error(`no tool ${name}`)
|
||||
const exec = { agent, name, arguments: args, signal: new AbortController().signal }
|
||||
const value = await t.execute(args, exec)
|
||||
const content = t.output.render(args, value)
|
||||
const meta = t.output.presentationMeta?.(args, value)
|
||||
return { value, text: content.map((c) => c.text).join('\n'), meta }
|
||||
}
|
||||
const write = (rel, text) => fs.writeFileSync(path.join(dir, rel), text)
|
||||
const readF = (rel) => fs.readFileSync(path.join(dir, rel), 'utf8')
|
||||
return { ctx, dir, newAgent, call, write, read: readF, listeners, cleanup: () => fs.rmSync(dir, { recursive: true, force: true }) }
|
||||
}
|
||||
|
||||
/** Fake built-in read/grep with DSH's output shapes. */
|
||||
export function registerBuiltins(h) {
|
||||
h.ctx.tools.register({
|
||||
name: 'read',
|
||||
description: 'Read a UTF-8 text file and return line-numbered content.',
|
||||
parameters: { type: 'object', properties: { file_path: { type: 'string' } }, required: ['file_path'] },
|
||||
output: {
|
||||
schema: { type: 'object', additionalProperties: false, properties: { path: { type: 'string' }, offset: { type: 'integer' }, lines: { type: 'array' }, totalLines: { type: 'integer' } } },
|
||||
render: (_a, v) => [{ type: 'text', text: `<path>${v.path}</path>\n<type>file</type>\n<content>\n${v.lines.map((l) => `${l.number}: ${l.text}`).join('\n')}\n\n(End of file - total ${v.totalLines} lines)\n</content>` }],
|
||||
},
|
||||
async execute(args, exec) {
|
||||
const t = await h.ctx.fs.resolve(args.file_path, { cwd: h.dir })
|
||||
const info = await h.ctx.fs.stat(t)
|
||||
const text = await h.ctx.fs.readText(t)
|
||||
const lines = text.replace(/\n$/, '').split('\n').map((x, i) => ({ number: i + 1, text: x }))
|
||||
h.ctx.emit('fs/observed', t, { kind: 'present', version: info.version }, exec)
|
||||
return { path: t.displayPath, offset: 1, lines, totalLines: lines.length }
|
||||
},
|
||||
})
|
||||
h.ctx.tools.register({
|
||||
name: 'grep',
|
||||
description: 'Search file contents.',
|
||||
parameters: { type: 'object', properties: { pattern: { type: 'string' } }, required: ['pattern'] },
|
||||
output: {
|
||||
schema: { type: 'object', additionalProperties: false, properties: { matches: { type: 'array' } } },
|
||||
render: (_a, v) => [{ type: 'text', text: v.matches.map((m) => `${m.path}:${m.lineNumber}: ${m.line}`).join('\n') || 'No matches' }],
|
||||
},
|
||||
async execute(args) {
|
||||
const out = []
|
||||
for (const f of fs.readdirSync(h.dir)) {
|
||||
const p = path.join(h.dir, f)
|
||||
if (!fs.statSync(p).isFile()) continue
|
||||
fs.readFileSync(p, 'utf8').split('\n').forEach((l, i) => { if (new RegExp(args.pattern).test(l)) out.push({ path: f, lineNumber: i + 1, line: l }) })
|
||||
}
|
||||
return { matches: out }
|
||||
},
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user