feat(agent): unify MCP tools and adopt Responses delivery
Move project and host queries into native MCP with host-owned OAuth/context, while retaining memory, skills, actions and publication plugins. Add nearby-model queries and explicit chat/task/learning model defaults. Adopt Responses HTTP/SSE and native steer/queue inbox delivery, preserving scoped session ownership, pending input edits and investigation communication. Disable local-provider discovery: Server /health allowed native vLLM to probe /v1/models on port 8000. A native before/after regression prevents recurrence. Validation: 170 focused Bun tests; service/plugin typechecks; native discovery, provider-discovery and input-inbox smoke checks; no paid model inference.
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { config } from "../../src/config.js";
|
||||
import { LearningOrchestrator } from "../../src/learning/orchestrator.js";
|
||||
import { SessionLearningStateStore } from "../../src/learning/sessionStateStore.js";
|
||||
import { MemoryStore } from "../../src/memory/store.js";
|
||||
import { SessionTranscriptStore } from "../../src/sessions/transcriptStore.js";
|
||||
import { SessionWorkspaceStore } from "../../src/sessions/workspace-store.js";
|
||||
import { type OpencodeRuntimeAdapter } from "../../src/runtime/opencode.js";
|
||||
import { getRuntimeSessionContext } from "../../src/runtime/sessionContext.js";
|
||||
|
||||
const owner = { actorKey: "actor-a", projectKey: "personal-actor-a", sessionId: "parent-chat" };
|
||||
const input = {
|
||||
sessionId: owner.sessionId, userMessage: "question", assistantMessage: "answer", toolCallCount: 0,
|
||||
requestContext: { ...owner, clientSessionId: owner.sessionId, traceId: "learning-test" },
|
||||
};
|
||||
const deferred = () => {
|
||||
let resolve!: () => void;
|
||||
const promise = new Promise<void>((done) => { resolve = done; });
|
||||
return { resolve, promise };
|
||||
};
|
||||
|
||||
describe("learning session lifecycle", () => {
|
||||
let base: string;
|
||||
let originalAuditPath: string;
|
||||
let originalCooldown: number;
|
||||
let records: SessionWorkspaceStore;
|
||||
let state: SessionLearningStateStore;
|
||||
beforeEach(async () => {
|
||||
base = await mkdtemp(join(tmpdir(), "tjwater-learning-lifecycle-"));
|
||||
originalAuditPath = config.LEARNING_AUDIT_LOG_PATH;
|
||||
originalCooldown = config.LEARNING_GATE_TURN_COOLDOWN;
|
||||
config.LEARNING_AUDIT_LOG_PATH = join(base, "audit.log");
|
||||
config.LEARNING_GATE_TURN_COOLDOWN = 1;
|
||||
records = new SessionWorkspaceStore(join(base, "workspaces"));
|
||||
state = new SessionLearningStateStore(join(base, "legacy-learning"), records);
|
||||
});
|
||||
afterEach(async () => {
|
||||
config.LEARNING_AUDIT_LOG_PATH = originalAuditPath;
|
||||
config.LEARNING_GATE_TURN_COOLDOWN = originalCooldown;
|
||||
await rm(base, { recursive: true, force: true });
|
||||
});
|
||||
const orchestrator = (runtime: object) => new LearningOrchestrator(
|
||||
runtime as OpencodeRuntimeAdapter, new MemoryStore(join(base, "memory"), join(base, "memory-backup")),
|
||||
new SessionTranscriptStore(join(base, "legacy-transcripts"), records), state,
|
||||
);
|
||||
|
||||
it("releases both native helper sessions after a completed review and persists progress in chat", async () => {
|
||||
let nextId = 0;
|
||||
const removed: string[] = [];
|
||||
const finished = deferred();
|
||||
const learning = orchestrator({
|
||||
createSession: async () => ({ id: `helper-${++nextId}` }), prompt: async () => undefined,
|
||||
messages: async (id: string) => [{ info: { role: "assistant" }, parts: [{ type: "text", text: JSON.stringify(
|
||||
id === "helper-1" ? { should_review: true, confidence: 1, focus: "memory", reason: "useful" } : { memories: [], skills: [], summary: "reviewed" },
|
||||
) }] }],
|
||||
removeSession: async (id: string) => { removed.push(id); if (removed.length === 2) finished.resolve(); },
|
||||
});
|
||||
await learning.onTurnCompleted(input);
|
||||
await finished.promise;
|
||||
expect(removed).toEqual(["helper-2", "helper-1"]);
|
||||
expect(getRuntimeSessionContext("helper-1")).toBeNull();
|
||||
expect(getRuntimeSessionContext("helper-2")).toBeNull();
|
||||
expect((await state.read(owner)).lastReviewedTurn).toBe(1);
|
||||
expect(records.path(owner, "learning.json")).toContain("/personal/");
|
||||
});
|
||||
|
||||
it("cancels a running gate without promoting memory or reviving deleted content", async () => {
|
||||
const started = deferred();
|
||||
const interrupted = deferred();
|
||||
const removed: string[] = [];
|
||||
const learning = orchestrator({
|
||||
createSession: async () => ({ id: "cancelled-gate" }),
|
||||
prompt: async () => { started.resolve(); await interrupted.promise; },
|
||||
messages: async () => [{ info: { role: "assistant" }, parts: [{ type: "text", text: '{"should_review":true,"confidence":1,"focus":"memory"}' }] }],
|
||||
abortSession: async () => { interrupted.resolve(); },
|
||||
removeSession: async (id: string) => { removed.push(id); },
|
||||
});
|
||||
await learning.onTurnCompleted(input);
|
||||
await started.promise;
|
||||
await records.markDeleted(owner);
|
||||
await learning.cancelSession(owner.sessionId);
|
||||
await records.purge(owner);
|
||||
expect(removed).toEqual(["cancelled-gate"]);
|
||||
expect(getRuntimeSessionContext("cancelled-gate")).toBeNull();
|
||||
expect(await records.list(owner, "learning.json")).toEqual([]);
|
||||
});
|
||||
|
||||
it("releases the helper when model inference fails", async () => {
|
||||
const finished = deferred();
|
||||
const removed: string[] = [];
|
||||
const learning = orchestrator({
|
||||
createSession: async () => ({ id: "failed-gate" }),
|
||||
prompt: async () => { throw new Error("model unavailable"); },
|
||||
removeSession: async (id: string) => { removed.push(id); finished.resolve(); },
|
||||
});
|
||||
await learning.onTurnCompleted(input);
|
||||
await finished.promise;
|
||||
expect(removed).toEqual(["failed-gate"]);
|
||||
expect(getRuntimeSessionContext("failed-gate")).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user