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:
@@ -1,5 +1,6 @@
|
||||
import { bindSessionCredential, releaseSessionCredential, requestWithSessionCredential } from "./credential-refresh.js";
|
||||
import { taskReportSchema } from "./investigation-report.js";
|
||||
import { taskReportSchema, taskProgressSchema, type TaskProgress } from "./investigation-report.js";
|
||||
import { taskPresentationCommandSchema, taskPresentationResultSchema, type TaskPresentationCommand } from "./task-presentation.js";
|
||||
export { taskReportSchema } from "./investigation-report.js";
|
||||
import { z } from "zod";
|
||||
|
||||
@@ -71,33 +72,11 @@ const agentTaskContextSchema = z.object({
|
||||
}).passthrough().nullish(),
|
||||
});
|
||||
|
||||
const receivedEvidenceSchema = z.object({
|
||||
evidence_id: z.string().min(1),
|
||||
kind: z.enum(["system_data", "external_feedback", "human_input", "synthetic"]),
|
||||
summary: z.string().min(1),
|
||||
source_label: z.string().min(1),
|
||||
source_ref: z.string().min(1).nullable().optional(),
|
||||
observed_at: z.string().datetime({ offset: true }),
|
||||
}).passthrough();
|
||||
|
||||
const taskDataSnapshotSchema = z.object({
|
||||
source: taskDataSourceSchema,
|
||||
source_ref: z.string().min(1),
|
||||
queried_at: z.string().datetime({ offset: true }),
|
||||
origin: z.enum(["system_data", "synthetic"]),
|
||||
data: z.unknown(),
|
||||
truncated: z.boolean(),
|
||||
});
|
||||
|
||||
const taskDataQueryResultSchema = z.object({
|
||||
evidence: receivedEvidenceSchema,
|
||||
snapshot: taskDataSnapshotSchema,
|
||||
});
|
||||
|
||||
const taskArtifactsResultSchema = z.object({
|
||||
task: operationalTaskSummarySchema,
|
||||
plan_revision: z.number().int().positive().nullable(),
|
||||
report_revision: z.number().int().positive().nullable(),
|
||||
progress_revision: z.number().int().positive().nullable().optional(),
|
||||
});
|
||||
|
||||
export const taskEvidenceSchema = z.object({
|
||||
@@ -109,31 +88,6 @@ export const taskEvidenceSchema = z.object({
|
||||
observed_at: z.string().datetime({ offset: true }),
|
||||
});
|
||||
|
||||
export const operationalTaskDataQuerySchema = z.object({
|
||||
source: taskDataSourceSchema,
|
||||
device_ids: z.array(z.string().trim().min(1).max(160)).max(20).optional(),
|
||||
point_id: z.string().trim().min(1).max(160).optional(),
|
||||
start: z.string().datetime({ offset: true }).optional(),
|
||||
end: z.string().datetime({ offset: true }).optional(),
|
||||
}).superRefine((query, context) => {
|
||||
const isSeries = query.source === "scada_readings" || query.source === "peripheral_observations";
|
||||
if (isSeries && (!query.start || !query.end)) {
|
||||
context.addIssue({ code: "custom", message: "time series queries require start and end" });
|
||||
}
|
||||
if (query.source === "scada_readings" && !query.device_ids?.length) {
|
||||
context.addIssue({ code: "custom", message: "SCADA readings require device_ids" });
|
||||
}
|
||||
if (query.source === "peripheral_observations" && !query.point_id) {
|
||||
context.addIssue({ code: "custom", message: "peripheral observations require point_id" });
|
||||
}
|
||||
if (query.start && query.end) {
|
||||
const duration = Date.parse(query.end) - Date.parse(query.start);
|
||||
if (duration <= 0 || duration > 86_400_000) {
|
||||
context.addIssue({ code: "custom", message: "query window must be positive and at most 24 hours" });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export const humanTaskEvidenceCommandSchema = z.object({
|
||||
request_id: z.string().uuid(),
|
||||
evidence: z.object({
|
||||
@@ -148,7 +102,6 @@ export const humanTaskEvidenceCommandSchema = z.object({
|
||||
|
||||
export type TaskEvidence = z.infer<typeof taskEvidenceSchema>;
|
||||
export type TaskReport = z.infer<typeof taskReportSchema>;
|
||||
export type OperationalTaskDataQuery = z.infer<typeof operationalTaskDataQuerySchema>;
|
||||
export const expressAccessRequestSchema = z.object({
|
||||
requestId: z.string().uuid(),
|
||||
nodeId: z.string().min(1),
|
||||
@@ -165,6 +118,7 @@ type TaskArtifactInput = {
|
||||
evidence?: TaskEvidence[];
|
||||
executionPlan?: Record<string, unknown>;
|
||||
report?: TaskReport;
|
||||
progress?: TaskProgress;
|
||||
};
|
||||
|
||||
export const taskArtifactBridgeSchema = z.object({
|
||||
@@ -172,13 +126,15 @@ export const taskArtifactBridgeSchema = z.object({
|
||||
evidence: z.array(taskEvidenceSchema).max(40).optional(),
|
||||
execution_plan: jsonRecordSchema.optional(),
|
||||
report: taskReportSchema.optional(),
|
||||
progress: taskProgressSchema.optional(),
|
||||
}).refine(
|
||||
(value) => value.evidence !== undefined || value.execution_plan !== undefined || value.report !== undefined,
|
||||
(value) => value.evidence !== undefined || value.execution_plan !== undefined || value.report !== undefined || value.progress !== undefined,
|
||||
{ message: "at least one task artifact is required" },
|
||||
);
|
||||
|
||||
const taskRunStates = new Map<string, { runId: string; revision: number }>();
|
||||
const taskRunMutationQueues = new Map<string, Promise<void>>();
|
||||
export const hasOperationalTaskRun = (runtimeSessionId: string) => taskRunStates.has(runtimeSessionId);
|
||||
|
||||
export const bindOperationalTaskCredential = (
|
||||
runtimeSessionId: string,
|
||||
@@ -267,6 +223,7 @@ export class OperationalTaskClient {
|
||||
...(input.evidence ? { evidence: input.evidence } : {}),
|
||||
...(input.executionPlan ? { plan: toServerPlan(input.executionPlan) } : {}),
|
||||
...(input.report ? { report: input.report } : {}),
|
||||
...(input.progress ? { progress: taskProgressSchema.parse(input.progress) } : {}),
|
||||
}),
|
||||
});
|
||||
const result = taskArtifactsResultSchema.parse(raw);
|
||||
@@ -279,6 +236,25 @@ export class OperationalTaskClient {
|
||||
return this.finishTaskRun(runtimeSessionId, taskId, runId, "complete", sourceSessionId);
|
||||
}
|
||||
|
||||
async publishPresentation(runtimeSessionId: string, taskId: string, input: {
|
||||
requestId: string; sourceSessionId: string; command: TaskPresentationCommand;
|
||||
}) {
|
||||
return this.mutateRun(runtimeSessionId, async () => {
|
||||
const state = taskRunStates.get(runtimeSessionId);
|
||||
if (!state) throw new OperationalTaskClientError(409, "task run context unavailable");
|
||||
const raw = await this.request(runtimeSessionId, `/api/v1/operational-tasks/${encodeURIComponent(taskId)}/presentation-events`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
request_id: input.requestId,
|
||||
run_id: state.runId,
|
||||
source_session_id: input.sourceSessionId,
|
||||
command: taskPresentationCommandSchema.parse(input.command),
|
||||
}),
|
||||
});
|
||||
return taskPresentationResultSchema.parse(raw);
|
||||
});
|
||||
}
|
||||
|
||||
async failTaskRun(
|
||||
runtimeSessionId: string,
|
||||
taskId: string,
|
||||
@@ -337,29 +313,8 @@ export class OperationalTaskClient {
|
||||
}
|
||||
}
|
||||
|
||||
async queryData(runtimeSessionId: string, taskId: string, query: OperationalTaskDataQuery) {
|
||||
const raw = await this.request(runtimeSessionId, `/api/v1/operational-tasks/${encodeURIComponent(taskId)}/data-query`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(operationalTaskDataQuerySchema.parse(query)),
|
||||
});
|
||||
const combined = taskDataQueryResultSchema.safeParse(raw);
|
||||
if (combined.success) {
|
||||
return combined.data;
|
||||
}
|
||||
const evidence = receivedEvidenceSchema.parse(raw);
|
||||
if (typeof evidence.evidence_id !== "string" || !evidence.evidence_id.trim()) {
|
||||
throw new OperationalTaskClientError(502, "task data query returned no evidence id");
|
||||
}
|
||||
const snapshot = taskDataSnapshotSchema.parse(await this.request(
|
||||
runtimeSessionId,
|
||||
`/api/v1/operational-tasks/${encodeURIComponent(taskId)}/evidence/${encodeURIComponent(evidence.evidence_id)}/snapshot`,
|
||||
{ method: "GET" },
|
||||
));
|
||||
return { evidence, snapshot };
|
||||
}
|
||||
|
||||
async requestDataAccess(runtimeSessionId: string, taskId: string, input: {
|
||||
requestId: string; sourceSessionId: string; nodeId: string; scopes: OperationalTaskDataQuery["source"][]; reason: string;
|
||||
requestId: string; sourceSessionId: string; nodeId: string; scopes: z.infer<typeof taskDataSourceSchema>[]; reason: string;
|
||||
}) {
|
||||
return this.request(runtimeSessionId, `/api/v1/operational-tasks/${encodeURIComponent(taskId)}/access-requests`, {
|
||||
method: "POST",
|
||||
@@ -479,6 +434,8 @@ const rememberLeaseRevision = (runtimeSessionId: string, value: Record<string, u
|
||||
runId: parsed.data.run_id,
|
||||
revision: parsed.data.revision,
|
||||
});
|
||||
} else if (Object.hasOwn(task, "active_run") && activeRun === null) {
|
||||
taskRunStates.delete(runtimeSessionId);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user