Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ function agentRequestContext(
prepared: PreparedMastraContext,
): ReturnType<typeof createCodeRequestContext> {
const { credential, input } = options;
const { toolCredentials, userSkillLoader, userSkills } = prepared;
const { toolCredentials, userSkillCreator, userSkillLoader, userSkills } = prepared;
const isSkillCreator = input.runIntent === "skill-creator";
const codeRuntime: CodeRuntimeContext = {
artifacts: options.artifactRuntime,
Expand Down Expand Up @@ -209,6 +209,7 @@ function agentRequestContext(
runIntent: input.runIntent,
runId: input.runId,
taskMessage: input.messageText,
...(isSkillCreator ? { userSkillCreator } : {}),
userSkillLoader,
userSkills,
});
Expand Down
79 changes: 75 additions & 4 deletions apps/agent-worker/src/durable-objects/agent-run-user-skills.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import type { UserSkillDefinition, UserSkillLoader, UserSkillRuntime } from "@cheatcode/agent-core";
import type {
UserSkillCreateInput,
UserSkillCreateResult,
UserSkillCreator,
UserSkillDefinition,
UserSkillLoader,
UserSkillRuntime,
} from "@cheatcode/agent-core";
import {
createDb,
getUserSkillByName,
Expand All @@ -9,11 +16,22 @@ import {
} from "@cheatcode/db";
import type { SandboxLike } from "@cheatcode/sandbox-contracts";
import { UserId } from "@cheatcode/types";
import { resolveUserSkillMirror, userSkillSlug, writeUserSkillMirror } from "../user-skill-files";
import { readUserSkillPackage, writeUserSkillPackageMirror } from "../user-skill-packages";
import {
resolveUserSkillMirror,
serializeUserSkillMarkdown,
userSkillSlug,
writeUserSkillMirror,
} from "../user-skill-files";
import {
collectUserSkillPackageFromSandbox,
persistUserSkillPackage,
readUserSkillPackage,
writeUserSkillPackageMirror,
} from "../user-skill-packages";
import type { AgentRunEnv } from "./agent-run-env";

export interface ResolvedUserSkillContext {
userSkillCreator: UserSkillCreator;
userSkills: UserSkillRuntime[];
userSkillLoader: UserSkillLoader;
}
Expand All @@ -34,7 +52,60 @@ export async function resolveUserSkillContext(
const userSkillLoader: UserSkillLoader = {
load: async (name) => loadUserSkill(env, userId, sandbox, name),
};
return { userSkills, userSkillLoader };
const userSkillCreator: UserSkillCreator = {
create: async (input) => persistCreatedUserSkill(env, userId, sandbox, input),
};
return { userSkillCreator, userSkills, userSkillLoader };
}

async function persistCreatedUserSkill(
env: AgentRunEnv,
userId: UserId,
sandbox: SandboxLike,
input: UserSkillCreateInput,
): Promise<UserSkillCreateResult> {
const skill = await saveUserSkillRecord(env, userId, input);
const collected = await collectUserSkillPackageFromSandbox(sandbox, skill, input.sourceSlug);
const canonicalMarkdown = await serializeUserSkillMarkdown(skill);
const files = collected.some((file) => file.path === "SKILL.md")
? collected.map((file) =>
file.path === "SKILL.md" ? { content: canonicalMarkdown, path: file.path } : file,
)
: [{ content: canonicalMarkdown, path: "SKILL.md" }, ...collected];
const packageValue = await persistUserSkillPackage(env.R2_OUTPUTS, userId, skill.id, files);
const filePath = await writeUserSkillPackageMirror(sandbox, skill, packageValue);
return {
description: skill.description,
filePath,
id: skill.id,
name: skill.name,
slug: userSkillSlug(skill.name),
};
}

async function saveUserSkillRecord(
env: AgentRunEnv,
userId: UserId,
input: UserSkillCreateInput,
): Promise<UserSkillRecord> {
const { db, close } = createDb(env.HYPERDRIVE, {
audience: "app_agent",
signingSecret: env.DATABASE_CONTEXT_SIGNING_SECRET_AGENT,
});
try {
return await withUserContext(db, userId, (tx) =>
upsertUserSkill(tx, {
body: input.body,
category: input.category,
description: input.description,
name: input.name,
tags: input.tags,
userId,
}),
);
} finally {
await close();
}
}

async function loadUserSkill(
Expand Down
32 changes: 7 additions & 25 deletions apps/agent-worker/src/durable-objects/mastra-stream-chunks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ function controlMastraChunk(chunk: ControlAgentChunk): UIMessageChunk[] {

function toolResultChunks(payload: ToolResultPayload): UIMessageChunk[] {
if (payload.toolName === "skill_create") {
const skill = skillProposedChunkFromResult(payload.result);
const skill = skillCreatedChunkFromResult(payload.result);
return skill ? [skill] : [];
}
if (isSandboxTool(payload)) {
Expand All @@ -125,40 +125,22 @@ function toolResultChunks(payload: ToolResultPayload): UIMessageChunk[] {
return [];
}

function skillProposedChunkFromResult(result: unknown): UIMessageChunk | undefined {
function skillCreatedChunkFromResult(result: unknown): UIMessageChunk | undefined {
const record = asRecord(result);
const name = stringField(record, "name");
const description = stringField(record, "description");
const body = stringField(record, "body");
const category = stringField(record, "category");
const proposalId = stringField(record, "proposalId");
const filePath = stringField(record, "filePath");
const id = stringField(record, "id");
const slug = stringField(record, "slug");
const tags = stringArrayField(record, "tags");
if (
record["proposed"] !== true ||
!name ||
!description ||
!body ||
!category ||
!proposalId ||
!slug ||
!tags
) {
if (record["created"] !== true || !name || !description || !filePath || !id || !slug) {
return undefined;
}
return {
type: "data-skill-proposed",
data: { body, category, description, name, proposalId, slug, tags, v: 1 },
type: "data-skill-created",
data: { description, filePath, id, name, slug, v: 1 },
};
}

function stringArrayField(record: Record<string, unknown>, key: string): string[] | undefined {
const value = record[key];
return Array.isArray(value) && value.every((item) => typeof item === "string")
? value
: undefined;
}

export function mastraChunkError(chunk: AgentChunkType): unknown | null {
if (chunk.type !== "error") {
return null;
Expand Down
4 changes: 2 additions & 2 deletions apps/agent-worker/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ import { AgentRunWorkflow } from "./durable-objects/agent-run-workflow";
import { ProjectSandbox } from "./durable-objects/project-sandbox";
import { formatAgentRouteError } from "./error-handling";
import { registerSandboxHttpRoutes } from "./sandbox-http-routes";
import { registerSkillProposalHttpRoutes } from "./skill-proposal-http-routes";
import { registerSkillRuntimeExecutionRoutes } from "./skill-runtime-execution-routes";
import { registerSkillRuntimeManagedRoutes } from "./skill-runtime-managed-routes";
import { registerUserSkillHttpRoutes } from "./user-skill-http-routes";

export { AgentRun, AgentRunWorkflow, ProjectSandbox };

Expand Down Expand Up @@ -80,7 +80,7 @@ registerAgentDurableObjectStorageRoute(agentApp);
registerAgentSystemHttpRoutes(agentApp);
registerAgentRunHttpRoutes(agentApp);
registerSandboxHttpRoutes(agentApp);
registerSkillProposalHttpRoutes(agentApp);
registerUserSkillHttpRoutes(agentApp);
registerSkillRuntimeManagedRoutes(agentApp);
registerSkillRuntimeExecutionRoutes(agentApp);

Expand Down
Loading