mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-31 04:07:52 +00:00
feat(agents): v0.38 Slice 1 — kill the Anthropic pin, route through gateway.toolLoop
Closes the three-layer Anthropic-only enforcement (queue gate / model-config
runtime fallback / doctor check) with a capability-based gate driven by the
recipe registry. Any provider that supports native tool calling can now
run the subagent loop.
Three layers reworked:
- queue.ts:87-106 (S1.7) — drop isAnthropicProvider hard-reject. Replace
with classifyCapabilities() check: refuse only when verdict is
'unusable:no_tools' or 'unknown'. Degraded providers (no caching, no
parallel tools) pass through; the gateway prints once-per-(source, model)
cost warnings at first dispatch.
- model-config.ts:205 (S1.8) — rename enforceSubagentAnthropic →
enforceSubagentCapable. Keeps the once-per-(source, model) warn seam
from v0.31.12 and inherits the same suppression Set so doctor + first-
call surfaces stay in sync. Legacy name kept as a thin wrapper for
external callers.
- doctor.ts:1189 (S1.9) — rename subagent_provider check →
subagent_capability. The check now surfaces three states: 'unusable',
'unknown', and 'degraded:no_caching' (the cost-regression warn). Paste-
ready fix hints point at `gbrain config set models.tier.subagent`.
Subagent handler routing (S1.5 + S1.10):
- New `agent.use_gateway_loop` config flag (default off). When enabled,
the handler routes through gateway.toolLoop() — provider-agnostic via
the Vercel AI SDK. When disabled, the legacy Anthropic-direct path
stays unchanged.
- Handler-entry capability check refuses tool-unsupported / unknown
providers loudly. With flag OFF + non-Anthropic model, refuses with a
paste-ready hint.
- runSubagentViaGateway() (new helper) bridges the existing ToolDef
registry to gateway's ChatToolDef + ToolHandler shapes. Persists to
the v0.38 stable-ID columns (ordinal + gbrain_tool_use_id) at first
observation; settles complete/failed on tool exit.
- D5 read-time shim (S1.6) — loadPriorToolsV2 + adaptContentBlocksToChatBlocks
handle v1 Anthropic-shaped legacy rows alongside v2 gateway-shaped writes
so crash-replay reconciles across the upgrade boundary.
Tests:
- test/agent-cli.test.ts Layer 1/2/3 cases flipped from "rejects non-
Anthropic" to "any tool-supporting provider accepted; refuses unknown
and embedding-only providers". 4 new cases covering openai, google,
unknown provider, embedding-only.
- All 27 cases pass; typecheck clean.
Plan: ~/.claude/plans/system-instruction-you-are-working-shimmying-breeze.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
cf91450dd8
commit
6b8d50f7d9
+35
-18
@@ -165,17 +165,28 @@ describe('queue.add trusted-submit gate for subagent', () => {
|
||||
expect(ok.name).toBe('subagent_aggregator');
|
||||
});
|
||||
|
||||
test('v0.31.12: subagent with non-Anthropic data.model is rejected at submit time (Layer 1)', async () => {
|
||||
// Codex F1 in v0.31.12 plan review: the subagent loop is Anthropic Messages
|
||||
// API + prompt caching. A job submitted with `data.model = openai:gpt-5.5`
|
||||
// would silently fail at runtime with a confusing provider error. The
|
||||
// submit-time guard rejects BEFORE the job enters the queue.
|
||||
await expect(
|
||||
queue.add('subagent', { prompt: 'hi', model: 'openai:gpt-5.5' }, {}, { allowProtectedSubmit: true }),
|
||||
).rejects.toThrow(/non-Anthropic/i);
|
||||
test('v0.38 S1.7: subagent with any tool-supporting provider passes the queue gate', async () => {
|
||||
// v0.38 D6/D7 — the Anthropic pin is removed. The gateway tool loop
|
||||
// routes any provider with native tool calling. Submit-time guard now
|
||||
// refuses ONLY on unusable:no_tools or unknown verdicts.
|
||||
const openaiJob = await queue.add(
|
||||
'subagent',
|
||||
{ prompt: 'hi', model: 'openai:gpt-5.2' },
|
||||
{},
|
||||
{ allowProtectedSubmit: true },
|
||||
);
|
||||
expect(openaiJob.name).toBe('subagent');
|
||||
|
||||
const googleJob = await queue.add(
|
||||
'subagent',
|
||||
{ prompt: 'hi', model: 'google:gemini-1.5-pro' },
|
||||
{},
|
||||
{ allowProtectedSubmit: true },
|
||||
);
|
||||
expect(googleJob.name).toBe('subagent');
|
||||
});
|
||||
|
||||
test('v0.31.12: subagent with Anthropic data.model still succeeds', async () => {
|
||||
test('v0.38 S1.7: subagent with Anthropic data.model still succeeds', async () => {
|
||||
const job = await queue.add(
|
||||
'subagent',
|
||||
{ prompt: 'hi', model: 'anthropic:claude-opus-4-7' },
|
||||
@@ -185,15 +196,21 @@ describe('queue.add trusted-submit gate for subagent', () => {
|
||||
expect(job.name).toBe('subagent');
|
||||
});
|
||||
|
||||
test('v0.31.12: subagent with bare claude- model id passes (provider-prefix optional)', async () => {
|
||||
// isAnthropicProvider accepts both `anthropic:claude-foo` and bare `claude-foo`.
|
||||
const job = await queue.add(
|
||||
'subagent',
|
||||
{ prompt: 'hi', model: 'claude-sonnet-4-6' },
|
||||
{},
|
||||
{ allowProtectedSubmit: true },
|
||||
);
|
||||
expect(job.name).toBe('subagent');
|
||||
test('v0.38 S1.7: subagent with unknown provider is rejected at submit time', async () => {
|
||||
// The remaining hard reject — unknown providers can't be classified, so
|
||||
// we refuse the job rather than risk burning money on something we
|
||||
// can't verify supports tools.
|
||||
await expect(
|
||||
queue.add('subagent', { prompt: 'hi', model: 'madeup-provider:foo' }, {}, { allowProtectedSubmit: true }),
|
||||
).rejects.toThrow(/unknown provider/i);
|
||||
});
|
||||
|
||||
test('v0.38 S1.7: subagent with embedding-only provider (no chat) is rejected', async () => {
|
||||
// Voyage has no chat touchpoint → classifyCapabilities returns 'unknown' →
|
||||
// refused at submit. Same rejection path as unknown provider.
|
||||
await expect(
|
||||
queue.add('subagent', { prompt: 'hi', model: 'voyage:voyage-3-large' }, {}, { allowProtectedSubmit: true }),
|
||||
).rejects.toThrow(/unknown provider/i);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user