feat: Reginald is real — model router + agent loop + query_project (P4)
The fixture chat panel is now a working agent. Ask Reginald a question and it consults the real project through a tool loop, then answers in grounded prose. Read-only v0 — writes still go through the propose-approve controls. core (@commitea/core/agent): - chat-client: OpenAI-wire chat completions over an injected fetch (same seam as gitea). Points at any OpenAI-compatible endpoint (LM Studio/Ollama/OpenAI). - model-router: small model for prose + the read tool; big model reserved for later decomposition (pickModel). - agent-loop: runAgentTurn drives call→tool→result→call until prose (or a step budget), recording each tool step. Injected complete + execute → fully testable. - query-project: the single read tool's engine — compact focus/board/calibration/ issue/search views built from scheduler + lifecycle + calibration; unbuilt views return a notImplemented marker (never fabricated). The model reports, never computes. - agent-tools: query_project declaration + Reginald's system prompt. app: - main model bridge (model:status, model:chat) runs the loop; query_project reconciles the repo and builds the view. Model traffic stays in main (token/CSP). gitea.ts refactored to share getGiteaClient + reconcileSnapshot. - preload + global.d.ts expose the model bridge; useChat drives the panel — real agent turn when a model is configured, scripted fixture reply otherwise (so fixture e2e is unchanged). A subtle "consulted the project" activity line. Model config (env, defaults to LM Studio on :1234): COMMITEA_MODEL_URL / _SMALL (google/gemma-4-e4b) / _BIG (qwen/qwen3.6-35b-a3b). COMMITEA_E2E=1 keeps it unconfigured so the panel stays scripted. Verified: 88 core tests green (14 agent: client parse, loop tool/error/budget, all views) + a gated live integration test. Desktop typecheck clean, 14 fixture e2e green. Gated live e2e drives the real app against gitea + gemma-4-e4b: asked "what now?", Reginald called query_project and answered "focus is on issue #2" (the real scheduler pick). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
62
packages/core/src/agent/agent-live.test.ts
Normal file
62
packages/core/src/agent/agent-live.test.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { extractLabelFacts } from '../labels/label-schema.js'
|
||||
import type { FetchLike, GiteaIssue } from '../gitea/types.js'
|
||||
import { runAgentTurn } from './agent-loop.js'
|
||||
import { REGINALD_SYSTEM, REGINALD_TOOLS } from './agent-tools.js'
|
||||
import { createChatClient } from './chat-client.js'
|
||||
import { buildProjectView, type ProjectSnapshot } from './query-project.js'
|
||||
|
||||
/**
|
||||
* Opt-in (COMMITEA_MODEL_LIVE=1). Drives the real chat client + agent loop
|
||||
* against a local OpenAI-compatible server (LM Studio on :1234 by default),
|
||||
* proving the model calls query_project and narrates the real result.
|
||||
*/
|
||||
const LIVE = !!process.env.COMMITEA_MODEL_LIVE
|
||||
const BASE = process.env.COMMITEA_MODEL_URL ?? 'http://localhost:1234/v1'
|
||||
const MODEL = process.env.COMMITEA_MODEL_SMALL ?? 'google/gemma-4-e4b'
|
||||
|
||||
function issue(over: Partial<GiteaIssue>): GiteaIssue {
|
||||
const labels = over.labels ?? []
|
||||
return {
|
||||
number: 1, title: '#1', body: '', state: 'open', labels, facts: extractLabelFacts(labels),
|
||||
milestone: null, assignee: null, assignees: [], createdAt: '2026-01-05T09:00:00Z',
|
||||
updatedAt: '2026-01-05T09:00:00Z', closedAt: null, url: '', ...over,
|
||||
}
|
||||
}
|
||||
|
||||
const SNAP: ProjectSnapshot = {
|
||||
issues: [
|
||||
issue({ number: 2, title: 'ChangeSource interface + polling source', labels: ['est/3d', 'p/1'] }),
|
||||
issue({ number: 3, title: 'SQLite cache bootstrap', labels: ['est/2d', 'p/1'] }),
|
||||
],
|
||||
timelines: {},
|
||||
deps: [{ issue: 3, dependsOn: 2 }],
|
||||
}
|
||||
|
||||
describe('agent loop (live model)', () => {
|
||||
it.skipIf(!LIVE)(
|
||||
'calls query_project and narrates the real focus',
|
||||
async () => {
|
||||
const client = createChatClient({ baseUrl: BASE, model: MODEL }, globalThis.fetch as unknown as FetchLike)
|
||||
const turn = await runAgentTurn({
|
||||
complete: (m, t) => client.complete(m, t),
|
||||
messages: [
|
||||
{ role: 'system', content: REGINALD_SYSTEM },
|
||||
{ role: 'user', content: 'What should I work on right now?' },
|
||||
],
|
||||
tools: REGINALD_TOOLS,
|
||||
execute: async (name, args) =>
|
||||
name === 'query_project'
|
||||
? buildProjectView((args as { view: any }).view, (args as any).filters, SNAP, new Date())
|
||||
: { error: `unknown tool ${name}` },
|
||||
})
|
||||
|
||||
// it consulted the project, then answered in prose about the real top item (#2)
|
||||
expect(turn.steps.some((s) => s.tool === 'query_project')).toBe(true)
|
||||
expect(turn.content.trim().length).toBeGreaterThan(0)
|
||||
expect(turn.content).toMatch(/#?2\b|ChangeSource/i)
|
||||
},
|
||||
60_000,
|
||||
)
|
||||
})
|
||||
Reference in New Issue
Block a user