Unify referenced result validation
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,223 @@
|
||||
import { config } from "../config.js";
|
||||
import { readJsonFile } from "../utils/fileStore.js";
|
||||
import {
|
||||
type ResultReferenceKind,
|
||||
type ResultReferenceRecord,
|
||||
type ResultReferenceSource,
|
||||
type RetrievalContext,
|
||||
RESULT_REFERENCE_KIND,
|
||||
type ResultReferenceStore,
|
||||
} from "./store.js";
|
||||
|
||||
type ResolveOptions = {
|
||||
expectedKind?: ResultReferenceKind;
|
||||
maxItems?: number;
|
||||
};
|
||||
|
||||
type RegisterResultReferenceInput = {
|
||||
actorKey: string;
|
||||
clientSessionId: string;
|
||||
data: unknown;
|
||||
kind: ResultReferenceKind;
|
||||
projectId?: string;
|
||||
projectKey: string;
|
||||
schemaVersion: number;
|
||||
sessionId: string;
|
||||
source: ResultReferenceSource;
|
||||
traceId: string;
|
||||
};
|
||||
|
||||
export type RenderJunctionPayload = {
|
||||
node_area_map: Record<string, string>;
|
||||
area_ids?: string[];
|
||||
area_colors?: Record<string, string>;
|
||||
};
|
||||
|
||||
export class ResultReferenceResolver {
|
||||
constructor(private readonly store: ResultReferenceStore) {}
|
||||
|
||||
async register(input: RegisterResultReferenceInput) {
|
||||
const normalizedData = normalizeDataForKind(
|
||||
input.kind,
|
||||
input.data,
|
||||
input.schemaVersion,
|
||||
);
|
||||
if (!normalizedData) {
|
||||
throw new Error(`invalid payload for result ref kind '${input.kind}'`);
|
||||
}
|
||||
return this.store.store({
|
||||
actorKey: input.actorKey,
|
||||
clientSessionId: input.clientSessionId,
|
||||
data: normalizedData,
|
||||
kind: input.kind,
|
||||
projectId: input.projectId,
|
||||
projectKey: input.projectKey,
|
||||
schemaVersion: input.schemaVersion,
|
||||
sessionId: input.sessionId,
|
||||
source: input.source,
|
||||
traceId: input.traceId,
|
||||
});
|
||||
}
|
||||
|
||||
async registerRenderPayloadFile(
|
||||
filePath: string,
|
||||
input: Omit<RegisterResultReferenceInput, "data" | "kind" | "schemaVersion">,
|
||||
) {
|
||||
const raw = await readJsonFile<unknown>(filePath);
|
||||
if (raw === null) {
|
||||
throw new Error(`render payload file not found: ${filePath}`);
|
||||
}
|
||||
|
||||
const payload = extractRenderJunctionPayload(raw);
|
||||
if (!payload) {
|
||||
throw new Error("render payload file does not contain a valid junction render payload");
|
||||
}
|
||||
|
||||
return this.register({
|
||||
...input,
|
||||
data: payload,
|
||||
kind: RESULT_REFERENCE_KIND.renderJunctionsPayload,
|
||||
schemaVersion: 1,
|
||||
});
|
||||
}
|
||||
|
||||
async getAuthorized(resultRef: string, context: RetrievalContext, options: ResolveOptions = {}) {
|
||||
const record = await this.getResolvedRecord(resultRef, context, options);
|
||||
if (!record) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
result_ref: record.resultRef,
|
||||
result_size_bytes: record.sizeBytes,
|
||||
stored_at: record.createdAt,
|
||||
data: projectData(record.data, options.maxItems ?? config.RESULT_REF_MAX_RETRIEVAL_ITEMS),
|
||||
preview: record.preview,
|
||||
kind: record.kind,
|
||||
schema_version: record.schemaVersion,
|
||||
source: record.source,
|
||||
};
|
||||
}
|
||||
|
||||
async getFullAuthorized(
|
||||
resultRef: string,
|
||||
context: RetrievalContext,
|
||||
options: ResolveOptions = {},
|
||||
) {
|
||||
const record = await this.getResolvedRecord(resultRef, context, options);
|
||||
if (!record) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
result_ref: record.resultRef,
|
||||
result_size_bytes: record.sizeBytes,
|
||||
stored_at: record.createdAt,
|
||||
data: record.data,
|
||||
preview: record.preview,
|
||||
kind: record.kind,
|
||||
schema_version: record.schemaVersion,
|
||||
source: record.source,
|
||||
};
|
||||
}
|
||||
|
||||
private async getResolvedRecord(
|
||||
resultRef: string,
|
||||
context: RetrievalContext,
|
||||
options: ResolveOptions,
|
||||
): Promise<ResultReferenceRecord | null> {
|
||||
const record = await this.store.getAuthorizedRecord(resultRef, context);
|
||||
if (!record) {
|
||||
return null;
|
||||
}
|
||||
if (options.expectedKind && record.kind !== options.expectedKind) {
|
||||
return null;
|
||||
}
|
||||
const normalizedData = normalizeDataForKind(
|
||||
record.kind,
|
||||
record.data,
|
||||
record.schemaVersion,
|
||||
);
|
||||
if (!normalizedData) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
...record,
|
||||
data: normalizedData,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const extractRenderJunctionPayload = (
|
||||
value: unknown,
|
||||
): RenderJunctionPayload | null => {
|
||||
const candidate = unwrapReferencePayload(value);
|
||||
if (!candidate || !isRecord(candidate.node_area_map)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const nodeAreaMap = normalizeStringRecord(candidate.node_area_map);
|
||||
if (Object.keys(nodeAreaMap).length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const areaIds = Array.isArray(candidate.area_ids)
|
||||
? candidate.area_ids.map((entry) => String(entry).trim()).filter(Boolean)
|
||||
: undefined;
|
||||
|
||||
const areaColors = isRecord(candidate.area_colors)
|
||||
? normalizeStringRecord(candidate.area_colors)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
node_area_map: nodeAreaMap,
|
||||
...(areaIds && areaIds.length > 0 ? { area_ids: areaIds } : {}),
|
||||
...(areaColors && Object.keys(areaColors).length > 0
|
||||
? { area_colors: areaColors }
|
||||
: {}),
|
||||
};
|
||||
};
|
||||
|
||||
const normalizeDataForKind = (
|
||||
kind: ResultReferenceKind,
|
||||
data: unknown,
|
||||
schemaVersion: number,
|
||||
): unknown | null => {
|
||||
if (!Number.isInteger(schemaVersion) || schemaVersion < 1) {
|
||||
return null;
|
||||
}
|
||||
if (kind === RESULT_REFERENCE_KIND.renderJunctionsPayload) {
|
||||
return extractRenderJunctionPayload(data);
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
const unwrapReferencePayload = (value: unknown): Record<string, unknown> | null => {
|
||||
if (!isRecord(value)) {
|
||||
return null;
|
||||
}
|
||||
if ("data" in value && value.data !== undefined && value.data !== null) {
|
||||
return isRecord(value.data) ? value.data : null;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
const normalizeStringRecord = (value: Record<string, unknown>) =>
|
||||
Object.fromEntries(
|
||||
Object.entries(value)
|
||||
.map(([key, entry]) => [String(key), String(entry ?? "").trim()])
|
||||
.filter(([, entry]) => entry.length > 0),
|
||||
);
|
||||
|
||||
const projectData = (data: unknown, maxItems: number) => {
|
||||
if (Array.isArray(data)) {
|
||||
return data.slice(0, maxItems);
|
||||
}
|
||||
if (isRecord(data)) {
|
||||
return Object.fromEntries(Object.entries(data).slice(0, maxItems));
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
+176
-73
@@ -12,19 +12,25 @@ import {
|
||||
removeFileIfExists,
|
||||
} from "../utils/fileStore.js";
|
||||
|
||||
export type ResultReferenceRecord = {
|
||||
resultRef: string;
|
||||
actorKey: string;
|
||||
clientSessionId: string;
|
||||
createdAt: string;
|
||||
data: unknown;
|
||||
preview: ResultPreview;
|
||||
projectId?: string;
|
||||
projectKey: string;
|
||||
sessionId: string;
|
||||
sizeBytes: number;
|
||||
traceId: string;
|
||||
};
|
||||
export const RESULT_REF_PATTERN = /^res-[a-f0-9-]{16}$/;
|
||||
|
||||
export const RESULT_REFERENCE_KIND = {
|
||||
dynamicHttpResult: "dynamic-http-result",
|
||||
renderJunctionsPayload: "render-junctions-payload",
|
||||
} as const;
|
||||
|
||||
export const RESULT_REFERENCE_SOURCE = {
|
||||
dynamicHttp: "dynamic_http",
|
||||
agentGenerated: "agent_generated",
|
||||
legacy: "legacy",
|
||||
migration: "migration",
|
||||
} as const;
|
||||
|
||||
export type ResultReferenceKind =
|
||||
(typeof RESULT_REFERENCE_KIND)[keyof typeof RESULT_REFERENCE_KIND];
|
||||
|
||||
export type ResultReferenceSource =
|
||||
(typeof RESULT_REFERENCE_SOURCE)[keyof typeof RESULT_REFERENCE_SOURCE];
|
||||
|
||||
export type ResultPreview = {
|
||||
count: number;
|
||||
@@ -33,29 +39,51 @@ export type ResultPreview = {
|
||||
summary: string;
|
||||
};
|
||||
|
||||
export type ResultReferenceRecord = {
|
||||
resultRef: string;
|
||||
actorKey: string;
|
||||
clientSessionId: string;
|
||||
createdAt: string;
|
||||
data: unknown;
|
||||
kind: ResultReferenceKind;
|
||||
preview: ResultPreview;
|
||||
projectId?: string;
|
||||
projectKey: string;
|
||||
schemaVersion: number;
|
||||
sessionId: string;
|
||||
sizeBytes: number;
|
||||
source: ResultReferenceSource;
|
||||
traceId: string;
|
||||
};
|
||||
|
||||
export type StoreResultInput = {
|
||||
actorKey: string;
|
||||
clientSessionId: string;
|
||||
data: unknown;
|
||||
kind: ResultReferenceKind;
|
||||
projectId?: string;
|
||||
projectKey: string;
|
||||
schemaVersion: number;
|
||||
sessionId: string;
|
||||
source: ResultReferenceSource;
|
||||
traceId: string;
|
||||
};
|
||||
|
||||
export type RetrievalContext = {
|
||||
actorKey: string;
|
||||
clientSessionId?: string;
|
||||
maxItems?: number;
|
||||
projectId?: string;
|
||||
};
|
||||
|
||||
export type ResultReferencePeek = {
|
||||
resultRef: string;
|
||||
kind: ResultReferenceKind;
|
||||
preview: ResultPreview;
|
||||
storedAt: string;
|
||||
};
|
||||
|
||||
type PartialRecord = Partial<ResultReferenceRecord> & { data?: unknown };
|
||||
|
||||
export class ResultReferenceStore {
|
||||
private cleanupTimer: NodeJS.Timeout | null = null;
|
||||
|
||||
@@ -95,55 +123,56 @@ export class ResultReferenceStore {
|
||||
clientSessionId: input.clientSessionId,
|
||||
createdAt: new Date().toISOString(),
|
||||
data: input.data,
|
||||
kind: input.kind,
|
||||
preview: buildPreview(input.data),
|
||||
projectId: input.projectId,
|
||||
projectKey: input.projectKey,
|
||||
schemaVersion: input.schemaVersion,
|
||||
sessionId: input.sessionId,
|
||||
sizeBytes: estimateBytes(input.data),
|
||||
source: input.source,
|
||||
traceId: input.traceId,
|
||||
};
|
||||
await atomicWriteJson(this.filePath(resultRef), record);
|
||||
return record;
|
||||
}
|
||||
|
||||
async getAuthorized(resultRef: string, context: RetrievalContext) {
|
||||
const record = await this.readAuthorizedRecord(resultRef, context);
|
||||
if (!record) {
|
||||
return null;
|
||||
}
|
||||
const data = projectData(record.data, context.maxItems ?? config.RESULT_REF_MAX_RETRIEVAL_ITEMS);
|
||||
return {
|
||||
ok: true,
|
||||
result_ref: record.resultRef,
|
||||
result_size_bytes: record.sizeBytes,
|
||||
stored_at: record.createdAt,
|
||||
data,
|
||||
preview: record.preview,
|
||||
};
|
||||
}
|
||||
async getAuthorizedRecord(resultRef: string, context: RetrievalContext) {
|
||||
if (!RESULT_REF_PATTERN.test(resultRef)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
async getFullAuthorized(resultRef: string, context: RetrievalContext) {
|
||||
const record = await this.readAuthorizedRecord(resultRef, context);
|
||||
const rawRecord = await readJsonFile<unknown>(this.filePath(resultRef));
|
||||
const record = normalizeResultReferenceRecord(rawRecord);
|
||||
if (!record) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
result_ref: record.resultRef,
|
||||
result_size_bytes: record.sizeBytes,
|
||||
stored_at: record.createdAt,
|
||||
data: record.data,
|
||||
preview: record.preview,
|
||||
};
|
||||
if (record.actorKey !== context.actorKey) {
|
||||
return null;
|
||||
}
|
||||
if ((record.projectId ?? "") !== (context.projectId ?? "")) {
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
context.clientSessionId &&
|
||||
record.clientSessionId !== context.clientSessionId
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return record;
|
||||
}
|
||||
|
||||
async peekAuthorized(resultRef: string, context: RetrievalContext): Promise<ResultReferencePeek | null> {
|
||||
const record = await this.readAuthorizedRecord(resultRef, context);
|
||||
async peekAuthorized(
|
||||
resultRef: string,
|
||||
context: RetrievalContext,
|
||||
): Promise<ResultReferencePeek | null> {
|
||||
const record = await this.getAuthorizedRecord(resultRef, context);
|
||||
if (!record) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
resultRef: record.resultRef,
|
||||
kind: record.kind,
|
||||
preview: record.preview,
|
||||
storedAt: record.createdAt,
|
||||
};
|
||||
@@ -152,7 +181,9 @@ export class ResultReferenceStore {
|
||||
async listBySession(sessionId: string) {
|
||||
const files = await listJsonFiles(this.baseDir);
|
||||
const records = await Promise.all(
|
||||
files.map(async (filePath) => readJsonFile<ResultReferenceRecord>(filePath)),
|
||||
files.map(async (filePath) =>
|
||||
normalizeResultReferenceRecord(await readJsonFile<unknown>(filePath)),
|
||||
),
|
||||
);
|
||||
return records
|
||||
.filter((record): record is ResultReferenceRecord => Boolean(record))
|
||||
@@ -177,28 +208,108 @@ export class ResultReferenceStore {
|
||||
private filePath(resultRef: string) {
|
||||
return join(this.baseDir, `${resultRef}.json`);
|
||||
}
|
||||
|
||||
private async readAuthorizedRecord(resultRef: string, context: RetrievalContext) {
|
||||
const record = await readJsonFile<ResultReferenceRecord>(this.filePath(resultRef));
|
||||
if (!record) {
|
||||
return null;
|
||||
}
|
||||
if (record.actorKey !== context.actorKey) {
|
||||
return null;
|
||||
}
|
||||
if ((record.projectId ?? "") !== (context.projectId ?? "")) {
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
context.clientSessionId &&
|
||||
record.clientSessionId !== context.clientSessionId
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return record;
|
||||
}
|
||||
}
|
||||
|
||||
export const normalizeResultReferenceRecord = (
|
||||
value: unknown,
|
||||
): ResultReferenceRecord | null => {
|
||||
if (!isRecord(value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const partial = value as PartialRecord;
|
||||
if (
|
||||
!isValidResultRef(partial.resultRef) ||
|
||||
typeof partial.actorKey !== "string" ||
|
||||
typeof partial.clientSessionId !== "string" ||
|
||||
typeof partial.createdAt !== "string" ||
|
||||
!("data" in partial) ||
|
||||
!isResultPreview(partial.preview) ||
|
||||
typeof partial.projectKey !== "string" ||
|
||||
typeof partial.sessionId !== "string" ||
|
||||
typeof partial.sizeBytes !== "number" ||
|
||||
!Number.isFinite(partial.sizeBytes) ||
|
||||
typeof partial.traceId !== "string"
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const kind = normalizeResultReferenceKind(partial.kind);
|
||||
const source = normalizeResultReferenceSource(partial.source);
|
||||
const schemaVersion =
|
||||
typeof partial.schemaVersion === "number" &&
|
||||
Number.isInteger(partial.schemaVersion) &&
|
||||
partial.schemaVersion > 0
|
||||
? partial.schemaVersion
|
||||
: 1;
|
||||
|
||||
if (!kind || !source) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
partial.projectId !== undefined &&
|
||||
typeof partial.projectId !== "string"
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
resultRef: partial.resultRef,
|
||||
actorKey: partial.actorKey,
|
||||
clientSessionId: partial.clientSessionId,
|
||||
createdAt: partial.createdAt,
|
||||
data: partial.data,
|
||||
kind,
|
||||
preview: partial.preview,
|
||||
projectId: partial.projectId,
|
||||
projectKey: partial.projectKey,
|
||||
schemaVersion,
|
||||
sessionId: partial.sessionId,
|
||||
sizeBytes: partial.sizeBytes,
|
||||
source,
|
||||
traceId: partial.traceId,
|
||||
};
|
||||
};
|
||||
|
||||
const normalizeResultReferenceKind = (
|
||||
value: unknown,
|
||||
): ResultReferenceKind | null => {
|
||||
if (value === undefined) {
|
||||
return RESULT_REFERENCE_KIND.dynamicHttpResult;
|
||||
}
|
||||
return Object.values(RESULT_REFERENCE_KIND).includes(
|
||||
value as ResultReferenceKind,
|
||||
)
|
||||
? (value as ResultReferenceKind)
|
||||
: null;
|
||||
};
|
||||
|
||||
const normalizeResultReferenceSource = (
|
||||
value: unknown,
|
||||
): ResultReferenceSource | null => {
|
||||
if (value === undefined) {
|
||||
return RESULT_REFERENCE_SOURCE.legacy;
|
||||
}
|
||||
return Object.values(RESULT_REFERENCE_SOURCE).includes(
|
||||
value as ResultReferenceSource,
|
||||
)
|
||||
? (value as ResultReferenceSource)
|
||||
: null;
|
||||
};
|
||||
|
||||
const isValidResultRef = (value: unknown): value is string =>
|
||||
typeof value === "string" && RESULT_REF_PATTERN.test(value);
|
||||
|
||||
const isResultPreview = (value: unknown): value is ResultPreview =>
|
||||
isRecord(value) &&
|
||||
typeof value.count === "number" &&
|
||||
Number.isFinite(value.count) &&
|
||||
Array.isArray(value.fields) &&
|
||||
value.fields.every((field) => typeof field === "string") &&
|
||||
typeof value.summary === "string" &&
|
||||
"sample" in value;
|
||||
|
||||
const estimateBytes = (data: unknown) => Buffer.byteLength(JSON.stringify(data));
|
||||
|
||||
const buildPreview = (data: unknown): ResultPreview => {
|
||||
@@ -219,7 +330,9 @@ const buildPreview = (data: unknown): ResultPreview => {
|
||||
if (isRecord(data)) {
|
||||
const fields = Object.keys(data).slice(0, 30);
|
||||
const sample = Object.fromEntries(
|
||||
fields.slice(0, config.MAX_PREVIEW_SAMPLE_ITEMS).map((field) => [field, data[field]]),
|
||||
fields
|
||||
.slice(0, config.MAX_PREVIEW_SAMPLE_ITEMS)
|
||||
.map((field) => [field, data[field]]),
|
||||
);
|
||||
return {
|
||||
count: fields.length,
|
||||
@@ -237,15 +350,5 @@ const buildPreview = (data: unknown): ResultPreview => {
|
||||
};
|
||||
};
|
||||
|
||||
const projectData = (data: unknown, maxItems: number) => {
|
||||
if (Array.isArray(data)) {
|
||||
return data.slice(0, maxItems);
|
||||
}
|
||||
if (isRecord(data)) {
|
||||
return Object.fromEntries(Object.entries(data).slice(0, maxItems));
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
|
||||
+14
-7
@@ -4,7 +4,8 @@ import { z } from "zod";
|
||||
import { type LearningOrchestrator } from "../learning/orchestrator.js";
|
||||
import { logger } from "../logger.js";
|
||||
import { MemoryStore } from "../memory/store.js";
|
||||
import { type ResultReferenceStore } from "../results/store.js";
|
||||
import { type ResultReferenceResolver } from "../results/resolver.js";
|
||||
import { RESULT_REFERENCE_KIND } from "../results/store.js";
|
||||
import { type OpencodeRuntimeAdapter } from "../runtime/opencode.js";
|
||||
import { type ChatSessionBridge } from "../chat/sessionBridge.js";
|
||||
import { toActorKey } from "../utils/fileStore.js";
|
||||
@@ -40,7 +41,7 @@ export const buildChatRouter = (
|
||||
runtime: OpencodeRuntimeAdapter,
|
||||
memoryStore: MemoryStore,
|
||||
learningOrchestrator: LearningOrchestrator,
|
||||
resultReferenceStore: ResultReferenceStore,
|
||||
resultReferenceResolver: ResultReferenceResolver,
|
||||
) => {
|
||||
const chatRouter = Router();
|
||||
|
||||
@@ -67,11 +68,17 @@ export const buildChatRouter = (
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await resultReferenceStore.getFullAuthorized(renderRef, {
|
||||
actorKey: toActorKey(userId),
|
||||
clientSessionId,
|
||||
projectId,
|
||||
});
|
||||
const result = await resultReferenceResolver.getFullAuthorized(
|
||||
renderRef,
|
||||
{
|
||||
actorKey: toActorKey(userId),
|
||||
clientSessionId,
|
||||
projectId,
|
||||
},
|
||||
{
|
||||
expectedKind: RESULT_REFERENCE_KIND.renderJunctionsPayload,
|
||||
},
|
||||
);
|
||||
|
||||
if (!result) {
|
||||
res.status(404).json({ message: "render_ref not found" });
|
||||
|
||||
+64
-8
@@ -8,6 +8,7 @@ import { config } from "./config.js";
|
||||
import { logger } from "./logger.js";
|
||||
import { LearningOrchestrator } from "./learning/orchestrator.js";
|
||||
import { MemoryStore } from "./memory/store.js";
|
||||
import { ResultReferenceResolver } from "./results/resolver.js";
|
||||
import { ResultReferenceStore } from "./results/store.js";
|
||||
import { buildChatRouter } from "./routes/chat.js";
|
||||
import { opencodeRuntime } from "./runtime/opencode.js";
|
||||
@@ -27,10 +28,11 @@ const learningOrchestrator = new LearningOrchestrator(
|
||||
sessionHistoryStore,
|
||||
);
|
||||
const resultReferenceStore = new ResultReferenceStore();
|
||||
const resultReferenceResolver = new ResultReferenceResolver(resultReferenceStore);
|
||||
const dynamicHttpExecutor = new DynamicHttpExecutor(resultReferenceStore);
|
||||
const internalToken = config.AGENT_INTERNAL_TOKEN ?? randomUUID();
|
||||
|
||||
// 这个 token 只用于仍需服务端上下文的工具桥(dynamic_http_call / fetch_result_ref)。
|
||||
// 这个 token 只用于仍需服务端上下文的工具桥(dynamic_http_call / fetch_result_ref / store_render_ref)。
|
||||
process.env.TJWATER_AGENT_INTERNAL_TOKEN = internalToken;
|
||||
|
||||
app.use(cors());
|
||||
@@ -121,12 +123,17 @@ app.post("/internal/tools/fetch-result-ref", async (req, res) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await resultReferenceStore.getAuthorized(resultRef, {
|
||||
actorKey: context.actorKey,
|
||||
maxItems:
|
||||
typeof req.body?.max_items === "number" ? req.body.max_items : undefined,
|
||||
projectId: context.projectId,
|
||||
});
|
||||
const result = await resultReferenceResolver.getAuthorized(
|
||||
resultRef,
|
||||
{
|
||||
actorKey: context.actorKey,
|
||||
projectId: context.projectId,
|
||||
},
|
||||
{
|
||||
maxItems:
|
||||
typeof req.body?.max_items === "number" ? req.body.max_items : undefined,
|
||||
},
|
||||
);
|
||||
|
||||
if (!result) {
|
||||
res.status(404).json({ message: "result_ref not found" });
|
||||
@@ -136,6 +143,55 @@ app.post("/internal/tools/fetch-result-ref", async (req, res) => {
|
||||
res.json(result);
|
||||
});
|
||||
|
||||
app.post("/internal/tools/store-render-ref", async (req, res) => {
|
||||
if (req.header("x-agent-internal-token") !== internalToken) {
|
||||
res.status(403).json({ message: "forbidden" });
|
||||
return;
|
||||
}
|
||||
|
||||
const sessionId = typeof req.body?.sessionId === "string" ? req.body.sessionId : "";
|
||||
const filePath = typeof req.body?.file_path === "string" ? req.body.file_path.trim() : "";
|
||||
const context = sessionBridge.getSessionContext(sessionId);
|
||||
if (!context) {
|
||||
res.status(404).json({
|
||||
message: "session context not found",
|
||||
detail: sessionId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!filePath) {
|
||||
res.status(400).json({ message: "file_path is required" });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const record = await resultReferenceResolver.registerRenderPayloadFile(filePath, {
|
||||
actorKey: context.actorKey,
|
||||
clientSessionId: context.clientSessionId,
|
||||
projectId: context.projectId,
|
||||
projectKey: context.projectKey,
|
||||
sessionId,
|
||||
source: "migration",
|
||||
traceId: context.traceId,
|
||||
});
|
||||
res.json({
|
||||
ok: true,
|
||||
render_ref: record.resultRef,
|
||||
stored_at: record.createdAt,
|
||||
preview: record.preview,
|
||||
kind: record.kind,
|
||||
schema_version: record.schemaVersion,
|
||||
source: record.source,
|
||||
});
|
||||
} catch (error) {
|
||||
const detail = error instanceof Error ? error.message : String(error);
|
||||
res.status(400).json({
|
||||
message: "store render ref failed",
|
||||
detail,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/internal/tools/session-search", async (req, res) => {
|
||||
if (req.header("x-agent-internal-token") !== internalToken) {
|
||||
res.status(403).json({ message: "forbidden" });
|
||||
@@ -177,7 +233,7 @@ app.use(
|
||||
opencodeRuntime,
|
||||
memoryStore,
|
||||
learningOrchestrator,
|
||||
resultReferenceStore,
|
||||
resultReferenceResolver,
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { config } from "../config.js";
|
||||
import { logger } from "../logger.js";
|
||||
import { RESULT_REFERENCE_KIND, RESULT_REFERENCE_SOURCE } from "../results/store.js";
|
||||
import { ResultReferenceStore } from "../results/store.js";
|
||||
|
||||
export type DynamicHttpInput = {
|
||||
@@ -146,9 +147,12 @@ const normalizeSuccessResult = async (
|
||||
actorKey: context.actorKey,
|
||||
clientSessionId: context.clientSessionId,
|
||||
data,
|
||||
kind: RESULT_REFERENCE_KIND.dynamicHttpResult,
|
||||
projectId: context.projectId,
|
||||
projectKey: context.projectKey,
|
||||
schemaVersion: 1,
|
||||
sessionId: context.sessionId,
|
||||
source: RESULT_REFERENCE_SOURCE.dynamicHttp,
|
||||
traceId: context.traceId,
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user