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:
2026-10-19 18:46:58 +08:00
parent f5c3cde2c2
commit 6a49d2209e
164 changed files with 12286 additions and 2368 deletions
+42 -15
View File
@@ -1,6 +1,9 @@
import { join } from "node:path";
import { basename, join } from "node:path";
import { lstat } from "node:fs/promises";
import { config } from "../config.js";
import { type SessionWorkspaceContext } from "../runtime/conversationWorkspace.js";
import { type SessionWorkspaceStore } from "../sessions/workspace-store.js";
import {
atomicWriteJson,
ensureDirectory,
@@ -15,15 +18,31 @@ export type SessionLearningState = {
};
export class SessionLearningStateStore {
constructor(private readonly baseDir = config.SESSION_LEARNING_STATE_STORAGE_DIR) {}
constructor(private readonly baseDir = config.SESSION_LEARNING_STATE_STORAGE_DIR, private readonly workspaces?: SessionWorkspaceStore) {}
async initialize() {
await ensureDirectory(this.baseDir);
if (!this.workspaces) await ensureDirectory(this.baseDir);
}
async read(sessionId: string): Promise<SessionLearningState> {
const existing = await readJsonFile<SessionLearningState>(this.filePath(sessionId));
async read(context: SessionWorkspaceContext): Promise<SessionLearningState> {
const sessionId = context.sessionId;
if (this.workspaces) {
await this.workspaces.ensure(context);
await this.workspaces.validate(context, "learning.json");
}
let existing = await readJsonFile<SessionLearningState>(this.filePath(context));
if (!existing && this.workspaces) {
const legacy = this.legacyPath(sessionId);
try {
if ((await lstat(legacy)).isSymbolicLink()) throw new Error("legacy learning state must not be a symbolic link");
existing = await readJsonFile<SessionLearningState>(legacy);
if (existing?.sessionId !== sessionId) throw new Error("learning state session mismatch");
await this.workspaces.writeRecord(context, "learning.json", existing);
await this.workspaces.archiveLegacyRecord(context, "learning.json", legacy);
} catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; }
}
if (existing) {
if (existing.sessionId !== sessionId) throw new Error("learning state session mismatch");
return {
lastGatedTurn: existing.lastGatedTurn,
lastReviewedTurn: existing.lastReviewedTurn,
@@ -39,31 +58,39 @@ export class SessionLearningStateStore {
};
}
async write(state: SessionLearningState) {
await atomicWriteJson(this.filePath(state.sessionId), {
async write(context: SessionWorkspaceContext, state: SessionLearningState) {
if (state.sessionId !== context.sessionId) throw new Error("learning state session mismatch");
const next = {
...state,
updatedAt: new Date().toISOString(),
});
};
if (this.workspaces) await this.workspaces.writeRecord(context, "learning.json", next);
else await atomicWriteJson(this.filePath(context), next);
}
async completeReview(sessionId: string, reviewedTurnCount: number) {
const current = await this.read(sessionId);
await this.write({
async completeReview(context: SessionWorkspaceContext, reviewedTurnCount: number) {
const current = await this.read(context);
await this.write(context, {
...current,
lastGatedTurn: Math.max(current.lastGatedTurn, reviewedTurnCount),
lastReviewedTurn: reviewedTurnCount,
});
}
async completeGate(sessionId: string, gatedTurnCount: number) {
const current = await this.read(sessionId);
await this.write({
async completeGate(context: SessionWorkspaceContext, gatedTurnCount: number) {
const current = await this.read(context);
await this.write(context, {
...current,
lastGatedTurn: gatedTurnCount,
});
}
private filePath(sessionId: string) {
private filePath(context: SessionWorkspaceContext) {
return this.workspaces ? this.workspaces.path(context, "learning.json") : this.legacyPath(context.sessionId);
}
private legacyPath(sessionId: string) {
if (basename(`${sessionId}.json`) !== `${sessionId}.json` || sessionId.includes("\\")) throw new Error("invalid learning session id");
return join(this.baseDir, `${sessionId}.json`);
}
}