Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/agent/core/domain/swarm/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export type ProviderType = (typeof PROVIDER_TYPES)[number]
/**
* Query type classification for routing decisions.
*/
export type QueryType = 'creative' | 'factual' | 'personal' | 'relational' | 'temporal'
export type QueryType = 'factual' | 'personal' | 'relational' | 'temporal'

/**
* Local providers that require no network calls.
Expand Down Expand Up @@ -105,7 +105,7 @@ export type QueryRequest = {
after?: number
before?: number
}
/** Hint: factual, temporal, personal, creative, relational */
/** Hint: factual, temporal, personal, relational */
type?: QueryType
}

Expand Down Expand Up @@ -172,7 +172,7 @@ export function createDefaultCapabilities(type: ProviderType): ProviderCapabilit
semanticSearch: false,
temporalQuery: false,
userModeling: false,
writeSupported: true,
writeSupported: false,
}
}

Expand Down
15 changes: 12 additions & 3 deletions src/agent/infra/swarm/adapters/gbrain-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export function resolveGBrainBin(options: GBrainAdapterOptions): {argsPrefix: st
return {argsPrefix: [], command: options.gbrainBinPath}
}

// 2. Check PATH
// 2. Check PATH (sync probe — runs lazily on first executor access, not at construction)
try {
execFileSync('gbrain', ['--version'], {encoding: 'utf8', stdio: 'pipe', timeout: 5000})

Expand Down Expand Up @@ -148,14 +148,23 @@ export class GBrainAdapter implements IMemoryProvider {
}
public readonly id = 'gbrain'
public readonly type = 'gbrain' as const
private readonly executor: GBrainExecutor
private cachedExecutor?: GBrainExecutor
private readonly injectedExecutor?: GBrainExecutor
private readonly options: GBrainAdapterOptions
private readonly repoPath: string
Comment thread
cuongdo-byterover marked this conversation as resolved.
private readonly searchMode: 'hybrid' | 'keyword' | 'vector'

constructor(options: GBrainAdapterOptions, executor?: GBrainExecutor) {
this.repoPath = options.repoPath
this.searchMode = options.searchMode
this.executor = executor ?? createDefaultExecutor(resolveGBrainBin(options))
this.options = options
this.injectedExecutor = executor
}

private get executor(): GBrainExecutor {
Comment thread
cuongdo-byterover marked this conversation as resolved.
if (this.injectedExecutor) return this.injectedExecutor
this.cachedExecutor ??= createDefaultExecutor(resolveGBrainBin(this.options))
return this.cachedExecutor
}

public async delete(id: string): Promise<void> {
Expand Down
9 changes: 7 additions & 2 deletions src/agent/infra/swarm/adapters/local-markdown-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,15 +94,18 @@ function buildIndexSignature(files: ScannedMarkdownFile[]): string {
function resolveUniqueFilename(folderPath: string, preferredFilename: string): string {
const baseName = preferredFilename.replace(/\.md$/u, '')

const MAX_SUFFIX = 10_000
let suffix = 0
while (true) {
while (suffix <= MAX_SUFFIX) {
const candidate = suffix === 0 ? `${baseName}.md` : `${baseName}-${suffix}.md`
if (!existsSync(join(folderPath, candidate))) {
return candidate
}

suffix++
}

return `${baseName}-${Date.now()}.md`
}

type IndexedDoc = {
Expand Down Expand Up @@ -186,8 +189,10 @@ export class LocalMarkdownAdapter implements IMemoryProvider {

const maxResults = request.maxResults ?? 10

if (!this.index) return []

// T1/T2/T3: Precision-filtered search (stop words, AND-first, score floor, gap ratio)
const precisionResults = searchWithPrecision(this.index!, request.query, {maxResults})
const precisionResults = searchWithPrecision(this.index, request.query, {maxResults})
if (precisionResults.length === 0) return []

// Collect direct matches
Expand Down
11 changes: 9 additions & 2 deletions src/agent/infra/swarm/adapters/memory-wiki-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ export class MemoryWikiAdapter implements IMemoryProvider {
public async query(request: QueryRequest): Promise<QueryResult[]> {
this.ensureIndex()

// Safety net: ensureIndex() could leave this.index null if file reads fail
if (!this.index) {
return []
}
Expand Down Expand Up @@ -204,21 +205,27 @@ export class MemoryWikiAdapter implements IMemoryProvider {
const now = new Date().toISOString()

// Resolve unique filename
const MAX_SUFFIX = 10_000
let filename = `${slug}.md`
let filePath = join(dirPath, filename)
let suffix = 1
while (existsSync(filePath)) {
while (existsSync(filePath) && suffix <= MAX_SUFFIX) {
filename = `${slug}-${suffix}.md`
filePath = join(dirPath, filename)
suffix++
}

if (existsSync(filePath)) {
filename = `${slug}-${Date.now()}.md`
filePath = join(dirPath, filename)
}

const pageId = `${pageType}.swarm.${slug}`
const pageContent = [
'---',
`pageType: ${pageType}`,
Comment thread
cuongdo-byterover marked this conversation as resolved.
`id: ${pageId}`,
`title: "${title}"`,
`title: ${JSON.stringify(title)}`,
Comment thread
cuongdo-byterover marked this conversation as resolved.
'status: active',
`updatedAt: "${now}"`,
'sourceType: swarm-curate',
Expand Down
4 changes: 3 additions & 1 deletion src/agent/infra/swarm/adapters/obsidian-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,10 @@ export class ObsidianAdapter implements IMemoryProvider {

const maxResults = request.maxResults ?? 10

if (!this.index) return []

// T1/T2/T3: Precision-filtered search (stop words, AND-first, score floor, gap ratio)
const precisionResults = searchWithPrecision(this.index!, request.query, {maxResults})
const precisionResults = searchWithPrecision(this.index, request.query, {maxResults})
if (precisionResults.length === 0) return []

// Collect direct matches
Expand Down
14 changes: 12 additions & 2 deletions src/agent/infra/swarm/swarm-coordinator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,26 @@ export type BrvCurateResult = {data?: {logId?: string; taskId?: string}; error?:

const execFileAsync = promisify(execFileCb)

const MAX_ARG_LENGTH = 200_000 // ~200KB safe for most OS arg limits

async function execBrvCurate(content: string): Promise<BrvCurateResult> {
if (content.length > MAX_ARG_LENGTH) {
Comment thread
cuongdo-byterover marked this conversation as resolved.
throw new Error(`Content too large for CLI argument (${content.length} bytes, max ${MAX_ARG_LENGTH}). Use brv curate directly.`)
}

let stdout: string
try {
;({stdout} = await execFileAsync('brv', ['curate', '--detach', '--format', 'json', content], {
encoding: 'utf8',
timeout: 30_000,
}))
} catch (error) {
const err = error as {message: string; stderr?: string}
throw new Error(err.stderr?.trim() || err.message)
if (error instanceof Error) {
const stderr = (error as NodeJS.ErrnoException & {stderr?: string}).stderr?.trim()
Comment thread
cuongdo-byterover marked this conversation as resolved.
throw new Error(stderr ?? error.message)
}

throw new Error(String(error))
}

try {
Expand Down
3 changes: 1 addition & 2 deletions src/agent/infra/swarm/swarm-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,9 @@ export function classifyQuery(query: string): QueryType {
/**
Comment thread
cuongdo-byterover marked this conversation as resolved.
* Provider selection matrix per query type.
* Honcho and Hindsight are temporarily disabled — adapters coming in Phase 3.
* When re-enabled, add 'honcho' to personal/creative and 'hindsight' to temporal/relational/creative.
* When re-enabled, add 'honcho' to personal and 'hindsight' to temporal/relational.
*/
const SELECTION_MATRIX: Record<QueryType, string[]> = {
creative: ['byterover', 'obsidian', 'local-markdown', 'gbrain', 'memory-wiki'],
factual: ['byterover', 'obsidian', 'local-markdown', 'gbrain', 'memory-wiki'],
personal: ['byterover', 'obsidian', 'local-markdown'],
relational: ['byterover', 'obsidian', 'local-markdown', 'gbrain', 'memory-wiki'],
Expand Down
15 changes: 9 additions & 6 deletions src/agent/infra/swarm/validation/config-validator.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import {execFileSync} from 'node:child_process'
import {execFile} from 'node:child_process'
import {existsSync} from 'node:fs'
import {promisify} from 'node:util'

const execFileAsync = promisify(execFile)
Comment thread
cuongdo-byterover marked this conversation as resolved.
import {join} from 'node:path'

import type {SwarmConfig} from '../config/swarm-config-schema.js'
Expand Down Expand Up @@ -151,10 +154,10 @@ function validateHindsight(
/**
* Validate gbrain provider config at runtime.
*/
function validateGBrain(
async function validateGBrain(
config: NonNullable<SwarmConfig['providers']['gbrain']>,
errors: ValidationIssue[]
): void {
): Promise<void> {
if (!existsSync(config.repoPath)) {
errors.push({
field: 'repo_path',
Expand All @@ -172,7 +175,7 @@ function validateGBrain(

// Option A: gbrain globally installed
try {
execFileSync('gbrain', ['--version'], {encoding: 'utf8', stdio: 'pipe', timeout: 5000})
await execFileAsync('gbrain', ['--version'], {encoding: 'utf8', timeout: 5000})
gbrainReachable = true
} catch {
// Not in PATH
Expand All @@ -189,7 +192,7 @@ function validateGBrain(
if (scriptFound) {
// Script exists — verify bun is available to run it
try {
execFileSync('bun', ['--version'], {encoding: 'utf8', stdio: 'pipe', timeout: 5000})
await execFileAsync('bun', ['--version'], {encoding: 'utf8', timeout: 5000})
gbrainReachable = true
} catch {
errors.push({
Expand Down Expand Up @@ -440,7 +443,7 @@ export async function validateSwarmProviders(
}

if (providers.gbrain?.enabled) {
validateGBrain(providers.gbrain, errors)
await validateGBrain(providers.gbrain, errors)
}

if (providers.memoryWiki?.enabled && !existsSync(providers.memoryWiki.vaultPath)) {
Expand Down
5 changes: 3 additions & 2 deletions src/oclif/commands/swarm/curate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,11 @@ public static description = 'Store knowledge in a swarm provider (GBrain, local
this.exit(2)
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
if (isJson) {
this.log(JSON.stringify({error: (error as Error).message, success: false}))
this.log(JSON.stringify({error: message, success: false}))
} else {
this.logToStderr(`Error: ${(error as Error).message}`)
this.logToStderr(`Error: ${message}`)
this.exit(2)
}
}
Expand Down
5 changes: 3 additions & 2 deletions src/oclif/commands/swarm/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,11 @@ public static description = 'Query the memory swarm across all active providers'
this.log(formatQueryResults(result, args.query))
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
if (isJson) {
this.log(JSON.stringify({error: (error as Error).message, success: false}))
this.log(JSON.stringify({error: message, success: false}))
} else {
this.logToStderr(`Error: ${(error as Error).message}`)
this.logToStderr(`Error: ${message}`)
this.exit(2)
}
}
Expand Down
2 changes: 1 addition & 1 deletion test/unit/agent/core/domain/swarm/types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ describe('Swarm Types', () => {
expect(caps.graphTraversal).to.be.false
expect(caps.temporalQuery).to.be.false
expect(caps.userModeling).to.be.false
expect(caps.writeSupported).to.be.true
expect(caps.writeSupported).to.be.false
expect(caps.localOnly).to.be.true
expect(caps.avgLatencyMs).to.equal(50)
})
Expand Down
Loading