Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/agent/hosted/chat-request-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
hostedChatRequestSchema,
} from "./chat-request.ts";
import { RuntimeAgentRunInvocationSchema } from "../runtime/agent-invocation-contract.ts";
import type { RuntimeAgentMarkdownDefinition } from "../runtime/agent-definition.ts";

/** Public API contract for hosted chat request principal. */
export type HostedChatRequestPrincipal = {
Expand Down Expand Up @@ -49,6 +50,7 @@ export type ParsedHostedChatRequest = {
runtimeOverrides: ChatRuntimeOverrides | undefined;
durableRootRun: DurableRootRunDescriptor | undefined;
persistLatestUserMessageBeforeDurableRun: boolean;
agentConfig?: RuntimeAgentMarkdownDefinition;
};

/** Options accepted by parse hosted chat request. */
Expand Down Expand Up @@ -108,6 +110,7 @@ async function verifyHostedChatProjectAccess(input: {
export async function buildParsedHostedChatRequest(input: {
chatRequest: HostedChatRequest;
agentId?: string;
agentConfig?: RuntimeAgentMarkdownDefinition;
authToken: string;
userId: string;
verifyProjectAccess?: ParseHostedChatRequestOptions["verifyProjectAccess"];
Expand All @@ -125,6 +128,13 @@ export async function buildParsedHostedChatRequest(input: {
const projectSlug = chatContext.projectSlug;
const conversationId = chatContext.conversationId;

if (input.agentConfig && input.agentId && input.agentConfig.id !== input.agentId) {
return createValidationErrorResponse({
messagePrefix: "Invalid runtime agent invocation",
validationMessage: "agentConfig.id must match the requested agent id",
});
}

const accessError = await verifyHostedChatProjectAccess({
projectId,
authToken: input.authToken,
Expand Down Expand Up @@ -153,6 +163,7 @@ export async function buildParsedHostedChatRequest(input: {
runtimeOverrides,
durableRootRun,
persistLatestUserMessageBeforeDurableRun: false,
...(input.agentConfig ? { agentConfig: input.agentConfig } : {}),
};
}

Expand Down Expand Up @@ -215,6 +226,7 @@ export async function parseRuntimeAgentRunInvocationHostedChatRequestFromRequest
userId: invocation.data.run.requestedByUserId,
chatRequest: chatRequest.data,
agentId: invocation.data.run.agentId,
agentConfig: invocation.data.agentConfig,
verifyProjectAccess: options.verifyProjectAccess,
});
}
30 changes: 30 additions & 0 deletions src/agent/hosted/chat-request.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,36 @@ describe("agent/hosted-chat-request", () => {
assertEquals(parsed.validatedContext.projectSlug, "demo-project");
});

it("preserves request-scoped project agent config from runtime invocations", async () => {
const parsed = await parseRuntimeAgentRunInvocationHostedChatRequestFromRequest(
new Request("https://agent.example.com/api/runs", {
method: "POST",
body: JSON.stringify({
...createRuntimeInvocation(),
agentConfig: {
id: "builder",
name: "Builder",
description: "Builds with project skills.",
instructions: "Use project skills.",
skills: ["support-triage"],
tools: ["search_knowledge", "get_file"],
},
}),
}),
{
authenticate: () => Promise.resolve({ userId, authToken: "token_1" }),
verifyProjectAccess: () => Promise.resolve({ success: true }),
},
);

if (parsed instanceof Response) {
throw new Error("Expected parsed request");
}

assertEquals(parsed.agentConfig?.skills, ["support-triage"]);
assertEquals(parsed.agentConfig?.tools, ["search_knowledge", "get_file"]);
});

Comment on lines +301 to +302
it("returns hosted chat project-access errors as stable JSON responses", async () => {
const response = await parseHostedChatRequestFromRequest(
new Request("https://agent.example.com/api/runs", {
Expand Down
3 changes: 2 additions & 1 deletion src/agent/hosted/veryfront-cloud-agent-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -915,7 +915,8 @@ async function prepareChatExecution(

setPrepareChatExecutionStartAttributes(context, { projectId, userId });

const agentConfig = await resolveAgentConfig(context, req.agentId ?? getDefaultAgentId(context));
const requestedAgentId = req.agentId ?? getDefaultAgentId(context);
const agentConfig = req.agentConfig ?? await resolveAgentConfig(context, requestedAgentId);
const abortController = new AbortController();
const {
effectiveMessages,
Expand Down
24 changes: 24 additions & 0 deletions src/agent/runtime/agent-invocation-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,30 @@ describe("agent/runtime-agent-invocation-contract", () => {
});
});

it("preserves the selected project agent config on control-plane stream requests", () => {
const parsed = RuntimeAgentRunInvocationSchema.parse(createInvocation({
agentConfig: {
id: "builder",
name: "Builder",
description: "Builds with project skills.",
instructions: "Use project skills.",
skills: ["support-triage"],
tools: ["search_knowledge", "get_file"],
},
}));

const request = buildRuntimeAgentControlPlaneStreamRequestFromInvocation(parsed);

assertEquals(request.agentConfig, {
id: "builder",
name: "Builder",
description: "Builds with project skills.",
instructions: "Use project skills.",
skills: ["support-triage"],
tools: ["search_knowledge", "get_file"],
});
});

it("parses runtime agent invocation request bodies through the public helper", async () => {
const parsed = await parseRuntimeAgentRunInvocation(
new Request("http://localhost/api/control-plane/runs/run_1/stream", {
Expand Down
4 changes: 4 additions & 0 deletions src/agent/runtime/agent-invocation-contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { defineSchema, lazySchema } from "#veryfront/schemas/index.ts";
import type { InferSchema, RefinementCtx } from "#veryfront/extensions/schema/index.ts";
import { ensureBuiltinSchemaValidator } from "#veryfront/extensions/builtin-extensions.ts";
import { parseAgUiJsonRequestOrError } from "../ag-ui/request-shared.ts";
import { getRuntimeAgentMarkdownDefinitionSchema } from "./agent-definition.ts";

ensureBuiltinSchemaValidator();

Expand Down Expand Up @@ -313,6 +314,7 @@ export const getRuntimeAgentRunInvocationSchema = defineSchema((v) =>
{ message: "context must be less than 64 KB total" },
),
agentSource: getRuntimeAgentSourceContextSchema().optional(),
agentConfig: getRuntimeAgentMarkdownDefinitionSchema().optional(),
credentials: getRuntimeAgentCredentialsSchema().optional(),
forwardedProps: v.record(v.string(), v.unknown()).optional().refine(
(value) => value === undefined || isWithinJsonSizeLimit(value, MAX_FORWARDED_PROPS_BYTES),
Expand Down Expand Up @@ -368,6 +370,7 @@ export type RuntimeAgentControlPlaneStreamRequest = {
context: RuntimeAgentRunInvocation["context"];
credentials?: RuntimeAgentRunInvocation["credentials"];
agentSource?: RuntimeAgentRunInvocation["agentSource"];
agentConfig?: RuntimeAgentRunInvocation["agentConfig"];
forwardedProps?: RuntimeAgentRunInvocation["forwardedProps"];
};

Expand All @@ -385,6 +388,7 @@ export function buildRuntimeAgentControlPlaneStreamRequestFromInvocation(
context: input.context,
...(input.credentials ? { credentials: input.credentials } : {}),
...(input.agentSource ? { agentSource: input.agentSource } : {}),
...(input.agentConfig ? { agentConfig: input.agentConfig } : {}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Add agentConfig to the internal stream schema

When an invocation includes this new field, buildRuntimeAgentControlPlaneStreamRequestFromInvocation now emits agentConfig, but AgentStreamHandler parses that transformed object with getInternalAgentStreamRequestSchema(), whose control-plane schema in src/internal-agents/schema.ts is .strict() and has no agentConfig field. In that environment, any runtime invocation carrying a request-scoped project agent config is accepted by RuntimeAgentRunInvocationSchema and then rejected as an invalid internal agent stream request before streaming, so the new project-agent-config path cannot work through /api/control-plane/runs/.../stream until the strict schema accepts the field.

Useful? React with 👍 / 👎.

...(input.forwardedProps ? { forwardedProps: input.forwardedProps } : {}),
};
}
Expand Down