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
3 changes: 2 additions & 1 deletion agents/myra/src/system-prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ export const ASSISTANT_SYSTEM_PROMPT =
"workbench itself. To stand up a new agent or workflow, write the " +
"package in your working tree, then reply with its files attached — " +
'a "package.json" plus a "definition.json" holding {"name", ' +
'"description", "systemPrompt"} — a one-line summary of what it ' +
'"description", "systemPrompt", and an optional five-field cron ' +
'"schedule" for a routine} — a one-line summary of what it ' +
"does, and a note to press Deploy. Workbench renders and deploys the " +
"package itself from those two files, so send exactly them and " +
"never try to deploy anything yourself. New capabilities, " +
Expand Down
8 changes: 7 additions & 1 deletion apps/web/src/agent-deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,15 @@ export function buildAgentDefinitionJson(args: {
systemPrompt: string;
triggerAddress: string;
declaredSources: readonly { readonly provider: string; readonly model: string }[];
schedule?: string;
}): unknown {
const stepId = "run";
return {
id: args.slug,
triggers: [{ type: "mail", to: args.triggerAddress }],
triggers: [
{ type: "mail", to: args.triggerAddress },
...(args.schedule !== undefined ? [{ type: "schedule", cron: args.schedule }] : []),
],
steps: {
[stepId]: {
kind: "step",
Expand Down Expand Up @@ -191,6 +195,7 @@ export function isAgentDeploySourceAssetName(name: string): boolean {
export type NewAgentInput = {
readonly name: string;
readonly systemPrompt: string;
readonly schedule?: string;
};

export type DeployedAgent = typeof WorkflowDeploymentResponse.infer;
Expand Down Expand Up @@ -240,6 +245,7 @@ export async function deployAgentSource(
systemPrompt,
triggerAddress: `${slug}@${tenant.domain}`,
declaredSources: offering.declaredSources,
...(args.input.schedule !== undefined ? { schedule: args.input.schedule } : {}),
});
const commitSha = await pushAgentSource(
args.tenantId,
Expand Down
8 changes: 8 additions & 0 deletions apps/web/src/chat/deployable-package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,16 @@ const AgentDefinition = type({
name: "string",
systemPrompt: "string",
"description?": "string",
"schedule?": "string",
});

/** A cron string this pipeline accepts: exactly five whitespace-separated
* fields. No third-party parser — the fields are validated for shape only,
* `cronSentence` (from `@corbits/workflows/client`) is the semantic check. */
export function isFiveFieldCron(schedule: string): boolean {
return schedule.trim().split(/\s+/).length === 5;
}

export type DeployablePackage = typeof AgentDefinition.infer;

export const PACKAGE_MANIFEST_NAME = "package.json";
Expand Down
21 changes: 18 additions & 3 deletions apps/web/src/chat/message-attachments.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
// agent never calls the hub; anything else is a plain file list.

import { Button } from "@corbits/react-ui";
import { cronSentence } from "@corbits/workflows/client";

import { useDeployAgentMutation } from "../agents-api";
import { chatPath } from "../chat-path";
import { Link } from "../navigation";
import { deployablePackage, type DeployablePackage } from "./deployable-package";
import { deployablePackage, isFiveFieldCron, type DeployablePackage } from "./deployable-package";
import type { MailAttachment } from "./threads-api";

function errorText(cause: unknown): string {
Expand Down Expand Up @@ -47,20 +48,34 @@ function DeployPackageCard({
}) {
const deploy = useDeployAgentMutation(tenantId);
const deployed = deploy.data;
const scheduleValid = pkg.schedule === undefined || isFiveFieldCron(pkg.schedule);
const sentence = pkg.schedule !== undefined && scheduleValid ? cronSentence(pkg.schedule) : null;
return (
<div className="chat-deploy-card">
<div className="chat-deploy-card-text">
<span className="chat-deploy-card-name">{pkg.name}</span>
{pkg.description === undefined ? null : (
<span className="chat-deploy-card-note">{pkg.description}</span>
)}
{sentence !== null ? <span className="chat-deploy-card-note">{sentence}</span> : null}
{pkg.schedule !== undefined && !scheduleValid ? (
<span className="chat-deploy-card-error">
{`This package's schedule ("${pkg.schedule}") isn't a valid five-field cron string.`}
</span>
) : null}
</div>
{deployed === undefined ? (
<Button
variant="primary"
size="sm"
disabled={deploy.isPending}
onClick={() => deploy.mutate({ name: pkg.name, systemPrompt: pkg.systemPrompt })}
disabled={deploy.isPending || !scheduleValid}
onClick={() =>
deploy.mutate({
name: pkg.name,
systemPrompt: pkg.systemPrompt,
...(pkg.schedule !== undefined ? { schedule: pkg.schedule } : {}),
})
}
>
{deploy.isPending ? "Deploying…" : `Deploy ${pkg.name}`}
</Button>
Expand Down
Loading