mirror of
https://github.com/garrytan/gbrain.git
synced 2026-08-17 10:22:34 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
453c480989 |
+15
-33
@@ -107,14 +107,6 @@ export interface EmbedOpts {
|
||||
* runs lock every source in sorted order. dryRun skips it.
|
||||
*/
|
||||
singleFlight?: boolean;
|
||||
/**
|
||||
* #394: suppress human stdout summaries (the `[dry-run] Would embed ...` /
|
||||
* `Embedded N chunks ...` slog lines). Set by structured-output callers —
|
||||
* the cycle's embed phase (dream --json must keep stdout JSON-clean per
|
||||
* docs/progress-events.md) reports counts via its own PhaseResult instead.
|
||||
* Errors/warnings still go to stderr regardless.
|
||||
*/
|
||||
quiet?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -261,7 +253,7 @@ export async function runEmbedCore(engine: BrainEngine, opts: EmbedOpts): Promis
|
||||
for (const s of opts.slugs) {
|
||||
if (isAborted(opts.signal)) break; // #1737: stop the per-slug loop on abort
|
||||
try {
|
||||
await embedPage(engine, s, !!opts.dryRun, result, opts.sourceId, opts.signal, opts.quiet);
|
||||
await embedPage(engine, s, !!opts.dryRun, result, opts.sourceId, opts.signal);
|
||||
} catch (e: unknown) {
|
||||
serr(` Error embedding ${s}: ${e instanceof Error ? e.message : e}`);
|
||||
}
|
||||
@@ -355,7 +347,6 @@ export async function runEmbedCore(engine: BrainEngine, opts: EmbedOpts): Promis
|
||||
catchUp: opts.catchUp,
|
||||
pacer,
|
||||
paceMaxConcurrency,
|
||||
quiet: opts.quiet,
|
||||
}, opts.signal);
|
||||
} finally {
|
||||
// E1: surface pacing telemetry (human + structured) when pacing was on.
|
||||
@@ -385,7 +376,7 @@ export async function runEmbedCore(engine: BrainEngine, opts: EmbedOpts): Promis
|
||||
return result;
|
||||
}
|
||||
if (opts.slug) {
|
||||
await embedPage(engine, opts.slug, !!opts.dryRun, result, opts.sourceId, opts.signal, opts.quiet);
|
||||
await embedPage(engine, opts.slug, !!opts.dryRun, result, opts.sourceId, opts.signal);
|
||||
return result;
|
||||
}
|
||||
throw new Error('No embed target specified. Pass { slug }, { slugs }, { all }, or { stale }.');
|
||||
@@ -530,7 +521,6 @@ async function embedPage(
|
||||
result: EmbedResult,
|
||||
sourceId?: string,
|
||||
signal?: AbortSignal,
|
||||
quiet?: boolean,
|
||||
) {
|
||||
const opts = sourceId ? { sourceId } : undefined;
|
||||
const page = await engine.getPage(slug, opts);
|
||||
@@ -575,7 +565,7 @@ async function embedPage(
|
||||
result.skipped += chunks.length - toEmbed.length;
|
||||
|
||||
if (toEmbed.length === 0) {
|
||||
if (!quiet) slog(`${slug}: all ${chunks.length} chunks already embedded`);
|
||||
slog(`${slug}: all ${chunks.length} chunks already embedded`);
|
||||
result.pages_processed++;
|
||||
return;
|
||||
}
|
||||
@@ -612,7 +602,7 @@ async function embedPage(
|
||||
}
|
||||
result.embedded += toEmbed.length;
|
||||
result.pages_processed++;
|
||||
if (!quiet) slog(`${slug}: embedded ${toEmbed.length} chunks`);
|
||||
slog(`${slug}: embedded ${toEmbed.length} chunks`);
|
||||
}
|
||||
|
||||
async function embedAll(
|
||||
@@ -630,8 +620,6 @@ async function embedAll(
|
||||
pacer?: DbPacer;
|
||||
/** Resolved concurrency cap (E-1: the worker count, no separate permit). */
|
||||
paceMaxConcurrency?: number;
|
||||
/** #394: suppress human stdout summaries (structured-output callers). */
|
||||
quiet?: boolean;
|
||||
},
|
||||
signal?: AbortSignal,
|
||||
) {
|
||||
@@ -775,12 +763,10 @@ async function embedAll(
|
||||
});
|
||||
|
||||
// Stdout summary preserved for scripts/tests that grep for counts.
|
||||
if (!staleOpts?.quiet) {
|
||||
if (dryRun) {
|
||||
slog(`[dry-run] Would embed ${result.would_embed} chunks across ${pages.length} pages`);
|
||||
} else {
|
||||
slog(`Embedded ${result.embedded} chunks across ${pages.length} pages`);
|
||||
}
|
||||
if (dryRun) {
|
||||
slog(`[dry-run] Would embed ${result.would_embed} chunks across ${pages.length} pages`);
|
||||
} else {
|
||||
slog(`Embedded ${result.embedded} chunks across ${pages.length} pages`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -816,8 +802,6 @@ async function embedAllStale(
|
||||
pacer?: DbPacer;
|
||||
/** Resolved concurrency cap (E-1: the worker count, no separate permit). */
|
||||
paceMaxConcurrency?: number;
|
||||
/** #394: suppress human stdout summaries (structured-output callers). */
|
||||
quiet?: boolean;
|
||||
},
|
||||
signature?: string,
|
||||
externalSignal?: AbortSignal,
|
||||
@@ -835,7 +819,7 @@ async function embedAllStale(
|
||||
signature,
|
||||
...(sourceId && { sourceId }),
|
||||
});
|
||||
if (invalidated > 0 && !staleOpts?.quiet) {
|
||||
if (invalidated > 0) {
|
||||
slog(`[embed] invalidated ${invalidated} chunk(s) embedded under a prior model signature`);
|
||||
}
|
||||
}
|
||||
@@ -846,12 +830,10 @@ async function embedAllStale(
|
||||
dryRun && signature ? { ...sourceOpt, signature } : sourceOpt,
|
||||
);
|
||||
if (staleCount === 0) {
|
||||
if (!staleOpts?.quiet) {
|
||||
if (dryRun) {
|
||||
slog('[dry-run] Would embed 0 chunks (0 stale found)');
|
||||
} else {
|
||||
slog('Embedded 0 chunks (0 stale found)');
|
||||
}
|
||||
if (dryRun) {
|
||||
slog('[dry-run] Would embed 0 chunks (0 stale found)');
|
||||
} else {
|
||||
slog('Embedded 0 chunks (0 stale found)');
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -860,7 +842,7 @@ async function embedAllStale(
|
||||
result.would_embed += staleCount;
|
||||
result.total_chunks += staleCount;
|
||||
if (onProgress) onProgress(1, 1, 0);
|
||||
if (!staleOpts?.quiet) slog(`[dry-run] Would embed ${staleCount} stale chunks`);
|
||||
slog(`[dry-run] Would embed ${staleCount} stale chunks`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1100,7 +1082,7 @@ async function embedAllStale(
|
||||
if (budgetTimer) clearTimeout(budgetTimer);
|
||||
}
|
||||
|
||||
if (!staleOpts?.quiet) slog(`Embedded ${result.embedded} chunks across ${totalProcessedPages} pages`);
|
||||
slog(`Embedded ${result.embedded} chunks across ${totalProcessedPages} pages`);
|
||||
|
||||
// #1946 (OV2a): a catch-up pass that completed without being aborted but left
|
||||
// chunks unembedded means those chunks are stuck (a non-transient embed
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
*/
|
||||
|
||||
import { resolveRecipe } from './model-resolver.ts';
|
||||
import { listRecipes } from './recipes/index.ts';
|
||||
import { AIConfigError } from './errors.ts';
|
||||
|
||||
export interface ProviderCapabilities {
|
||||
@@ -77,7 +78,10 @@ export function getProviderCapabilities(modelString: string): ProviderCapabiliti
|
||||
if (!chat) {
|
||||
throw new AIConfigError(
|
||||
`Provider "${recipe.id}" does not offer a chat touchpoint.`,
|
||||
`Known providers with chat: openai, anthropic, google, openrouter, litellm-proxy, deepseek, groq, together, azure-openai, dashscope, minimax, zhipu, ollama, llama-server. Pick one for models.tier.subagent.`,
|
||||
// Computed from the registry so the hint can't drift into listing
|
||||
// chat-less providers (the pre-fix list falsely included embedding-only
|
||||
// recipes, sending users in circles — #1157).
|
||||
`Known providers with chat: ${listRecipes().filter(r => r.touchpoints.chat).map(r => r.id).join(', ')}. Pick one for models.tier.subagent.`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { Recipe } from '../types.ts';
|
||||
|
||||
/**
|
||||
* Zhipu AI (智谱AI) BigModel Open Platform. OpenAI-compatible /embeddings
|
||||
* endpoint at open.bigmodel.cn. Hosts embedding-2 (1024d) and embedding-3
|
||||
* (Matryoshka up to 2048d).
|
||||
* Zhipu AI (智谱AI) BigModel Open Platform. OpenAI-compatible /embeddings and
|
||||
* /chat/completions endpoints at open.bigmodel.cn. Hosts embedding-2 (1024d),
|
||||
* embedding-3 (Matryoshka up to 2048d), and the GLM chat family (glm-5.1 etc.)
|
||||
* with native tool calling — usable for models.tier.subagent (#1157).
|
||||
*
|
||||
* embedding-3 at 2048 dims exceeds pgvector's HNSW cap of 2000 — those
|
||||
* brains fall back to exact vector scans (see
|
||||
@@ -25,6 +26,20 @@ export const zhipu: Recipe = {
|
||||
setup_url: 'https://open.bigmodel.cn/',
|
||||
},
|
||||
touchpoints: {
|
||||
chat: {
|
||||
// Informational list (openai-compat tier: assertTouchpoint doesn't
|
||||
// enforce it), so newer GLM ids pass without a recipe edit.
|
||||
models: ['glm-5.1', 'glm-4.6', 'glm-4.5'],
|
||||
supports_tools: true,
|
||||
// gbrain-side stable tool ids (v0.38 D11) decoupled the loop from
|
||||
// Anthropic response formats; GLM tool calling is stable through the
|
||||
// OpenAI-compat path, same as deepseek/groq.
|
||||
supports_subagent_loop: true,
|
||||
// Anthropic-style cache_control markers are not honored on the
|
||||
// OpenAI-compat path — the loop runs hot (degraded:no_caching warn).
|
||||
supports_prompt_cache: false,
|
||||
max_context_tokens: 128000,
|
||||
},
|
||||
embedding: {
|
||||
models: ['embedding-3', 'embedding-2'],
|
||||
default_dims: 1024,
|
||||
@@ -36,5 +51,5 @@ export const zhipu: Recipe = {
|
||||
},
|
||||
},
|
||||
setup_hint:
|
||||
'Get an API key at https://open.bigmodel.cn/, then `export ZHIPUAI_API_KEY=...`',
|
||||
'Get an API key at https://open.bigmodel.cn/, then `export ZHIPUAI_API_KEY=...`. Chat/subagent: use `zhipu:glm-5.1`.',
|
||||
};
|
||||
|
||||
+1
-3
@@ -1214,9 +1214,7 @@ async function runPhaseEmbed(engine: BrainEngine, dryRun: boolean, signal?: Abor
|
||||
// 10-15 min one) bails within a batch instead of running to completion
|
||||
// after the job was killed — which left gbrain_cycle_locks held and
|
||||
// wedged every subsequent autopilot cycle.
|
||||
// #394: quiet — the cycle reports embed counts via its own PhaseResult;
|
||||
// raw `[dry-run] Would embed ...` stdout lines would corrupt `dream --json`.
|
||||
const result = await runEmbedCore(engine, { stale: true, dryRun, signal, quiet: true });
|
||||
const result = await runEmbedCore(engine, { stale: true, dryRun, signal });
|
||||
const embeddedCount = dryRun ? result.would_embed : result.embedded;
|
||||
return {
|
||||
phase: 'embed',
|
||||
|
||||
@@ -69,6 +69,45 @@ describe('recipe: zhipu', () => {
|
||||
expect(sql.toLowerCase()).toContain('hnsw');
|
||||
});
|
||||
|
||||
test('chat touchpoint declares GLM models with tool + subagent-loop support (#1157)', () => {
|
||||
const r = getRecipe('zhipu')!;
|
||||
expect(r.touchpoints.chat).toBeDefined();
|
||||
expect(r.touchpoints.chat!.models).toContain('glm-5.1');
|
||||
expect(r.touchpoints.chat!.supports_tools).toBe(true);
|
||||
expect(r.touchpoints.chat!.supports_subagent_loop).toBe(true);
|
||||
expect(r.touchpoints.chat!.supports_prompt_cache).toBe(false);
|
||||
});
|
||||
|
||||
test('zhipu:glm-5.1 passes the subagent capability gate (degraded:no_caching, not refused)', async () => {
|
||||
// Pre-fix: getProviderCapabilities threw "does not offer a chat touchpoint"
|
||||
// and classifyCapabilities returned 'unknown' → subagent submit refused.
|
||||
const { getProviderCapabilities, classifyCapabilities } =
|
||||
await import('../../src/core/ai/capabilities.ts');
|
||||
const caps = getProviderCapabilities('zhipu:glm-5.1');
|
||||
expect(caps.supportsToolCalling).toBe(true);
|
||||
expect(classifyCapabilities('zhipu:glm-5.1')).toBe('degraded:no_caching');
|
||||
});
|
||||
|
||||
test('no-chat-touchpoint error hint lists only providers that actually have chat', async () => {
|
||||
// The hint is computed from the registry; every provider it names must
|
||||
// really carry a chat touchpoint (pre-fix it hardcoded zhipu/dashscope/
|
||||
// minimax, all embedding-only at the time).
|
||||
const { getProviderCapabilities } = await import('../../src/core/ai/capabilities.ts');
|
||||
const { listRecipes } = await import('../../src/core/ai/recipes/index.ts');
|
||||
let hint = '';
|
||||
try {
|
||||
getProviderCapabilities('voyage:voyage-3');
|
||||
throw new Error('expected AIConfigError for embedding-only provider');
|
||||
} catch (e) {
|
||||
hint = (e as { fix?: string }).fix ?? String(e);
|
||||
}
|
||||
const listed = hint.match(/chat: ([^.]+)\./)?.[1]?.split(', ') ?? [];
|
||||
expect(listed.length).toBeGreaterThan(0);
|
||||
const withChat = new Set(listRecipes().filter(r => r.touchpoints.chat).map(r => r.id));
|
||||
for (const id of listed) expect(withChat.has(id)).toBe(true);
|
||||
expect(listed).toContain('zhipu');
|
||||
});
|
||||
|
||||
test('dimsProviderOptions threads dimensions for embedding-3 (Matryoshka)', async () => {
|
||||
// Codex finding #1: Zhipu embedding-3 is Matryoshka 256-2048. Without
|
||||
// `dimensions` on the wire, user-selected non-default dims are
|
||||
|
||||
@@ -292,33 +292,6 @@ describe('runDream — output format', () => {
|
||||
expect(parsed).toHaveProperty('totals');
|
||||
});
|
||||
|
||||
// #394 / takeover of #854: the embed phase's `[dry-run] Would embed ...`
|
||||
// summary must not leak onto stdout ahead of the JSON CycleReport.
|
||||
test('--dry-run --json emits only JSON even when embed has stale chunks', async () => {
|
||||
await engine.putPage('concepts/testing', {
|
||||
type: 'concept',
|
||||
title: 'Testing',
|
||||
compiled_truth: 'Testing keeps JSON contracts honest.',
|
||||
timeline: '',
|
||||
});
|
||||
await engine.upsertChunks('concepts/testing', [
|
||||
{ chunk_index: 0, chunk_text: 'Testing keeps JSON contracts honest.', chunk_source: 'compiled_truth' },
|
||||
]);
|
||||
|
||||
const lines: string[] = [];
|
||||
const logSpy = spyOn(console, 'log').mockImplementation((msg: string) => { lines.push(String(msg)); });
|
||||
await runDream(engine, ['--dir', repo, '--phase', 'embed', '--dry-run', '--json']);
|
||||
logSpy.mockRestore();
|
||||
|
||||
const output = lines.join('\n');
|
||||
expect(output.trimStart().startsWith('{')).toBe(true);
|
||||
const parsed = JSON.parse(output);
|
||||
expect(parsed.schema_version).toBe('1');
|
||||
expect(parsed.phases[0].phase).toBe('embed');
|
||||
// The stale chunk was still counted in the structured report.
|
||||
expect(parsed.phases[0].details.would_embed).toBe(1);
|
||||
});
|
||||
|
||||
test('human output for clean status mentions "Brain is healthy"', async () => {
|
||||
const lines: string[] = [];
|
||||
const logSpy = spyOn(console, 'log').mockImplementation((msg: string) => { lines.push(String(msg)); });
|
||||
|
||||
Reference in New Issue
Block a user