diff --git a/web/app/agents/AgentsGallery.tsx b/web/app/agents/AgentsGallery.tsx index 5331a3c..b053797 100644 --- a/web/app/agents/AgentsGallery.tsx +++ b/web/app/agents/AgentsGallery.tsx @@ -4,9 +4,9 @@ import Link from 'next/link'; import { Box, Database, Zap } from 'lucide-react'; import { FadeIn } from '../../components/FadeIn'; -import { AgentArt } from '../../components/agents/AgentArt'; import { BuildYourOwn } from '../../components/agents/BuildYourOwn'; -import { AGENTS, INTEGRATION_LABELS } from '../../lib/agents'; +import { IntegrationLogos } from '../../components/agents/IntegrationLogos'; +import { AGENTS } from '../../lib/agents'; import s from './agents.module.css'; const CAPABILITIES = [ @@ -89,31 +89,45 @@ export function AgentsGallery() {
-
+ +
+
diff --git a/web/app/agents/[slug]/AgentDetail.tsx b/web/app/agents/[slug]/AgentDetail.tsx index 4ebc951..b734613 100644 --- a/web/app/agents/[slug]/AgentDetail.tsx +++ b/web/app/agents/[slug]/AgentDetail.tsx @@ -5,7 +5,8 @@ import Link from 'next/link'; import { FadeIn } from '../../../components/FadeIn'; import { AgentArt } from '../../../components/agents/AgentArt'; import { ForkAgentButton } from '../../../components/agents/ForkAgentButton'; -import { INTEGRATION_LABELS, sourceUrl, type Agent } from '../../../lib/agents'; +import { IntegrationLogos } from '../../../components/agents/IntegrationLogos'; +import { sourceUrl, type Agent } from '../../../lib/agents'; import s from '../agents.module.css'; export function AgentDetail({ agent }: { agent: Agent }) { @@ -42,13 +43,13 @@ export function AgentDetail({ agent }: { agent: Agent }) {

{agent.tagline}

-
- {agent.integrations.map((integration) => ( - - {INTEGRATION_LABELS[integration]} - - ))} -
+ @@ -98,13 +99,13 @@ export function AgentDetail({ agent }: { agent: Agent }) {
Integrations -
- {agent.integrations.map((integration) => ( - - {INTEGRATION_LABELS[integration]} - - ))} -
+
{agent.inputs.length > 0 && (
diff --git a/web/app/agents/agents.module.css b/web/app/agents/agents.module.css index 6261b1c..9ead1b5 100644 --- a/web/app/agents/agents.module.css +++ b/web/app/agents/agents.module.css @@ -203,104 +203,244 @@ } } -/* ── Gallery grid ── */ +/* ── Gallery list ── */ .gallery { - max-width: 1280px; + max-width: 1180px; margin: 0 auto; padding: 24px 40px 96px; } -.grid { +/* Column headers. Mirrors the .rowLink grid; hidden once the row stacks. */ +.listHead { display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 24px; -} - -.col { - display: flex; + grid-template-columns: minmax(0, 1fr) 190px 20px; + gap: 20px; + align-items: center; + padding: 0 18px 10px; + font-size: 0.7rem; + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--fg-subtle, var(--fg-muted)); } -.card { - display: flex; - flex-direction: column; - min-height: 100%; - width: 100%; - border-radius: 18px; +.list { + list-style: none; + margin: 0; + padding: 0; border: 1px solid var(--line); + border-radius: 14px; background: var(--card-bg); overflow: hidden; - text-decoration: none; - color: inherit; - box-shadow: 0 8px 24px color-mix(in srgb, var(--primary) 4%, transparent); - transition: - border-color 0.2s, - box-shadow 0.2s, - transform 0.2s; } -.card:hover { - border-color: var(--card-hover-border); - box-shadow: 0 14px 36px color-mix(in srgb, var(--primary) 12%, transparent); - transform: translateY(-3px); +.row { + display: block; } -.cardMedia { - position: relative; - aspect-ratio: 16 / 9; - width: 100%; - overflow: hidden; - border-bottom: 1px solid var(--line); +.row + .row { + border-top: 1px solid var(--line); } -.cardMedia img { - width: 100%; - height: 100%; - object-fit: cover; - display: block; +.rowLink { + display: grid; + grid-template-columns: minmax(0, 1fr) 190px 20px; + gap: 20px; + align-items: center; + padding: 13px 18px; + text-decoration: none; + color: inherit; + transition: background 0.15s; +} + +.rowLink:hover { + background: color-mix(in srgb, var(--primary) 5%, transparent); } -.cardBody { +.rowMain { display: flex; flex-direction: column; - gap: 10px; - padding: 20px 22px 22px; - flex: 1; + gap: 3px; + min-width: 0; } -.cardName { +.rowName { font-family: var(--font-heading), sans-serif; - font-size: 1.18rem; + font-size: 1rem; font-weight: 600; color: var(--fg); - margin: 0; } -.cardTagline { - font-size: 0.94rem; - line-height: 1.55; +.rowTagline { + font-size: 0.86rem; + line-height: 1.45; color: var(--fg-muted); - margin: 0; - flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } -.cardFooter { +/* Occupies a permanently reserved line under the tagline, so fading it in on + hover/focus neither shifts the row nor hides the description. */ +.rowTrigger { display: flex; align-items: center; - justify-content: space-between; - gap: 12px; - margin-top: 4px; + gap: 8px; + min-width: 0; + font-size: 0.78rem; + line-height: 1.45; + color: var(--fg-muted); + opacity: 0; + transition: opacity 0.15s; +} + +.rowLink:hover .rowTrigger, +.rowLink:focus-visible .rowTrigger { + opacity: 1; } -.cardArrow { - font-size: 0.9rem; +.rowTriggerText { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* No hover on touch — keep the trigger visible rather than unreachable. */ +@media (hover: none) { + .rowTrigger { + opacity: 1; + } +} + +.kindSchedule, +.kindEvent { + flex: none; + padding: 2px 8px; + border-radius: 100px; + font-size: 0.68rem; font-weight: 600; + letter-spacing: 0.02em; + text-transform: uppercase; +} + +.kindSchedule { + border: 1px solid color-mix(in srgb, var(--primary) 22%, transparent); + background: color-mix(in srgb, var(--primary) 9%, transparent); color: var(--primary); - white-space: nowrap; +} + +.kindEvent { + border: 1px solid color-mix(in srgb, var(--fg-muted) 26%, transparent); + background: color-mix(in srgb, var(--fg-muted) 9%, transparent); + color: var(--fg-muted); +} + +.rowLogos { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 6px; +} + +/* Provider marks are drawn for light surfaces and several are solid black + (github, granola, notion), so they get their own light chip rather than + sitting directly on the row — otherwise they vanish in dark mode. + Height-constrained with auto width so wordmarks keep their aspect ratio. */ +.rowLogo { + height: 16px; + width: auto; + max-width: 64px; + object-fit: contain; + display: block; + box-sizing: content-box; + padding: 3px 5px; + border-radius: 5px; + background: #fff; + border: 1px solid color-mix(in srgb, var(--fg) 10%, transparent); +} + +.rowArrow { + justify-self: end; + font-size: 0.95rem; + color: var(--fg-muted); + transition: transform 0.15s, color 0.15s; +} + +.rowLink:hover .rowArrow { + color: var(--primary); + transform: translateX(2px); +} + +@media (max-width: 900px) { + .gallery { + padding: 24px 20px 72px; + } + + .listHead { + display: none; + } + + .rowLink { + grid-template-columns: minmax(0, 1fr) auto; + grid-template-areas: + 'main arrow' + 'logos arrow'; + gap: 10px 16px; + align-items: start; + padding: 16px 18px; + } + + .rowMain { + grid-area: main; + } + + .rowLogos { + grid-area: logos; + } + + .rowArrow { + grid-area: arrow; + align-self: center; + } } /* ── Integration / meta chips ── */ +/* Labeled provider chip used on the detail page — mark plus name, where + there's room to spell it out. */ +.logoChip { + display: inline-flex; + align-items: center; + gap: 7px; + padding: 4px 11px 4px 5px; + border-radius: 100px; + border: 1px solid color-mix(in srgb, var(--primary) 16%, transparent); + background: color-mix(in srgb, var(--primary) 7%, transparent); + font-size: 0.78rem; + font-weight: 600; + color: var(--primary); +} + +/* Same light chip as the gallery marks — several provider logos are solid + black and would disappear on the dark surface. */ +.logoChipIcon { + height: 14px; + width: auto; + max-width: 52px; + object-fit: contain; + display: block; + box-sizing: content-box; + padding: 3px 4px; + border-radius: 100px; + background: #fff; +} + +/* A chip with no logo loses the icon's left inset, so even it back out. */ +.logoChip:not(:has(img)) { + padding-left: 11px; +} + .chips { display: flex; flex-wrap: wrap; diff --git a/web/components/agents/IntegrationLogos.tsx b/web/components/agents/IntegrationLogos.tsx new file mode 100644 index 0000000..57be732 --- /dev/null +++ b/web/components/agents/IntegrationLogos.tsx @@ -0,0 +1,61 @@ +import { INTEGRATION_LABELS, integrationLogo, type Integration } from '../../lib/agents'; + +/** + * Provider logos for an agent. + * + * Bare (gallery rows): just the marks, for density. + * `withLabel` (detail page): mark + provider name, where there's room to spell + * it out. Providers Nango has no logo for fall back to a text-only chip so the + * set still reads correctly either way. + * + * Plain (not next/image) to match AgentArt — the SST/OpenNext optimizer + * 500s without sharp. + */ +export function IntegrationLogos({ + integrations, + withLabel = false, + className, + chipClassName, + logoClassName, +}: { + integrations: Integration[]; + /** Render the provider name next to its mark. */ + withLabel?: boolean; + className?: string; + /** Text-only chip: the labeled variant, and the no-logo fallback. */ + chipClassName?: string; + logoClassName?: string; +}) { + return ( + + {integrations.map((integration) => { + const label = INTEGRATION_LABELS[integration]; + const src = integrationLogo(integration); + + if (withLabel) { + return ( + + {src ? : null} + {label} + + ); + } + + return src ? ( + {label} + ) : ( + + {label} + + ); + })} + + ); +} diff --git a/web/content/docs/agents/build.mdx b/web/content/docs/agents/build.mdx index 0db2a95..9071121 100644 --- a/web/content/docs/agents/build.mdx +++ b/web/content/docs/agents/build.mdx @@ -1,47 +1,99 @@ --- title: 'Build your own' -description: 'Define a custom agent by making the trigger, context, permissions, and handoff path explicit.' +description: 'Author a cloud persona: persona.json for deploy wiring, agent.ts for behavior.' --- -Start with the contract. The implementation can change, but the contract keeps the agent understandable. - -## Define the job - -Write the job in one sentence: +Start with the job in one sentence: ```text When happens, inspect , take , and report . ``` -If the sentence needs a paragraph, split the agent into smaller agents. +If that needs a paragraph, split it into smaller agents. + +## Use the skill + +Don't hand-write the shape from memory — it moves. Install the skill and let your coding agent do the authoring: + +```bash +# with prpm +npx prpm install @agent-relay/creating-cloud-persona + +# with skills.sh +npx skills add https://github.com/agentworkforce/skills --skill creating-cloud-persona +``` + +Then ask it to "create a cloud persona for <job>". The skill covers the current persona shape, the event model, integration scopes and adapter config, inputs, memory, sandbox modes, and the production traps that break deploys. + +The rest of this page is what the skill encodes — worth reading so you can tell whether what it wrote is right. + +## The two files + +A cloud persona is always a pair: -## Give it a persona +- **`persona.json`** — deploy and runtime wiring: the integrations it requires and their scopes, inputs, model and harness, memory, sandbox mode, and the system prompt that defines its role. +- **`agent.ts`** — the behavior. `defineAgent(...)` declares the triggers, schedules, and watch rules; the handler branches on `event.type` and does the work. -The persona is not a brand voice. It is the operating policy: +Wakeups are declarative, behavior is imperative. Every provider your `agent.ts` listens on must also be declared in `persona.json` — an undeclared integration never mounts, and its triggers never fire. -- What the agent optimizes for -- What it must never do without approval -- Which files, tools, providers, and channels it can use -- What a good final report looks like +## Complexity stays declarative + +Complex agents aren't more code — they're more declarations. The [`review`](https://github.com/AgentWorkforce/agents/tree/main/review) agent reviews PRs, pushes fixes, and merges on approval. Its entire wakeup surface: + +```ts +export default defineAgent({ + triggers: { + github: [ + { on: 'pull_request.opened' }, + { on: 'pull_request.synchronize' }, + { on: 'pull_request_review.submitted' }, + { on: 'pull_request_review_comment.created' }, + { on: 'check_run.completed' }, + { on: 'issue_comment.created' } + ], + slack: [{ on: 'message.created', paths: ['/slack/channels/${SLACK_CHANNEL}/**'] }] + }, + handler: async (ctx, event) => { + const data = (await event.expand('full')).data; + if (event.type.startsWith('slack.')) return handleSlackMergeRequest(ctx, data); + // ...route the github events + } +}); +``` + +Seven event sources across two providers, and `event` is fully narrowed to exactly those types — an unhandled case is a type error, not a 3am surprise. + +The heavy lifting delegates to a coding harness in one call: + +```ts +const run = await ctx.harness.run({ cwd: ctx.sandbox.cwd, prompt: reviewHarnessPrompt(pr) }); +``` + +The PR's repo is already materialized at `ctx.sandbox.cwd`; the agent edits files there and cloud commits and pushes after the harness exits. No git, no `gh`, no checkout logic in your code. ## Keep context structured -Prefer durable references over pasted prose: +Prefer durable references over pasted prose: Relay channels and threads, Relayfile paths, PR URLs and commit SHAs, Linear issue IDs. Structured context lets the agent resume, verify, and explain itself. -- Relay channels and threads for coordination -- Relayfile paths for provider-backed records -- GitHub PR URLs and commit SHAs for code review -- Linear issue IDs for product work +## Verify before deploying + +```bash +npx agentworkforce persona compile ./my-agent/persona.ts # if typed +npx agentworkforce invoke ./my-agent/persona.json --schedule daily +npx agentworkforce deploy ./my-agent/persona.json --mode cloud --dry-run +``` -Structured context lets the agent resume, verify, and explain itself without reconstructing the world from a prompt. +`invoke` runs the handler locally against a fixture or a named schedule and prints a run record. The dry-run validates the deploy without side effects. -## Build from the repo + + Every command takes the `persona.json` **file**, not the directory — a directory path fails with `EISDIR`. If you author a typed `persona.ts`, `persona compile` writes the JSON next to it. + - Fork the examples and keep your agent's persona, trigger, and implementation together. + Fork the examples — persona, trigger, and implementation live together. - - Use Relay for the messaging, workspace, and handoff layer. + + Ship it, then keep it observable. diff --git a/web/content/docs/agents/cli.mdx b/web/content/docs/agents/cli.mdx new file mode 100644 index 0000000..b5169d9 --- /dev/null +++ b/web/content/docs/agents/cli.mdx @@ -0,0 +1,63 @@ +--- +title: 'CLI' +description: 'The agentworkforce command surface — deploy, run locally, trigger, inspect, and tear down.' +--- + +Everything an agent needs after it's written. Anything in the [gallery](/agents) can also be deployed from the web with **Launch agent** — same result, no terminal. + +```bash +npx agentworkforce login # browser OAuth, writes the workspace token +npx agentworkforce logout +``` + +## Deploy + +```bash +npx agentworkforce deploy [flags] +``` + +| Flag | What it does | +| --- | --- | +| `--mode dev \| sandbox \| cloud` | Local run, Daytona sandbox, or managed cloud. | +| `--dry-run` | Validate with no side effects. Prints integration and schedule counts. | +| `--on-exists update` | Redeploy over an existing persona. **Default is `cancel`** — a silent no-op. | +| `--input KEY=value` | Override a declared input. Repeatable. | +| `--reconnect ` | Force a fresh provider connect. | +| `--no-connect` | Fail instead of prompting for an unconnected provider. | +| `--workspace ` | Target a specific workspace. | +| `--detach` | Background the runner. | +| `--bundle-out ` | Write the deploy bundle to disk. | + +Deploy opens a connect flow for any declared provider that isn't connected. An unconnected provider means its triggers never fire. + +## Run and test locally + +```bash +npx agentworkforce agent # interactive local session +npx agentworkforce invoke --schedule daily # fire a named schedule +npx agentworkforce invoke --fixture ev.json +npx agentworkforce invoke --scaffold slack.message.created +npx agentworkforce local-surface # real webhooks, local handler +``` + +`invoke` emits a run record. `--reads fixtures|live` and `--model stub|fixture|live` control how much of the real world it touches; `--case ` runs a YAML case with assertions. + +## Operate + +```bash +npx agentworkforce deployments list # what's running +npx agentworkforce deployments logs # --tail defaults to 50 +npx agentworkforce trigger # fire an active agent now +npx agentworkforce runs export # replay fixture for a cloud run +npx agentworkforce destroy # cancel schedules, mark destroyed +``` + +`deployments list` accepts `--status`, `--persona `, and `--json`. `deployments logs` with no agent lists the available workspace log files. + +## Promote slowly + +Start report-only and graduate one permission at a time: comment, label, open a PR, merge, deploy. An unattended agent should leave enough in Relay that a teammate can answer why it woke up, what it inspected, what it changed, and what needs review. + + + Tighten the persona contract before increasing autonomy. + diff --git a/web/content/docs/agents/deploy.mdx b/web/content/docs/agents/deploy.mdx deleted file mode 100644 index 9d6ce43..0000000 --- a/web/content/docs/agents/deploy.mdx +++ /dev/null @@ -1,33 +0,0 @@ ---- -title: 'Deploy and operate' -description: 'Run proactive agents with clear triggers, scoped credentials, observable output, and human approval where it matters.' ---- - -Deployment is where an agent becomes part of the team. Treat it like a small service with a narrower blast radius and a louder paper trail. - -## Before enabling a trigger - -- Scope credentials to the paths, repos, channels, and providers the agent actually needs. -- Set a clear reporting channel or thread. -- Decide which actions require approval. -- Put a spending or runtime limit around open-ended work. -- Test with a real event and confirm the final report is useful. - -## Operating loop - -An unattended agent should leave enough evidence that a teammate can answer: - -1. Why did it wake up? -2. What context did it inspect? -3. What did it change? -4. What does a human need to review? - -If those answers are not visible in Relay, the agent is too quiet. - -## Promote slowly - -Start in report-only mode. Let the agent recommend the action, then graduate one permission at a time: comment, label, open a PR, merge, deploy. - - - Design the agent contract before increasing autonomy. - diff --git a/web/content/docs/agents/introduction.mdx b/web/content/docs/agents/introduction.mdx index 8f5a8d2..1f065fd 100644 --- a/web/content/docs/agents/introduction.mdx +++ b/web/content/docs/agents/introduction.mdx @@ -1,11 +1,89 @@ --- title: 'Agents' -description: 'Proactive workers that watch the systems your team already uses, decide when action is needed, and report back through Relay.' +description: 'Proactive workers that watch the systems your team already uses, decide when action is needed, and report back.' --- -Agents turn Relay from a coordination layer into an operating system for work. They can watch channels, files, provider events, schedules, and pull requests, then take a bounded action without waiting for someone to paste context into a chat box. +Relay agents are your team of proactive agents. Built on top of [file](/docs/file) they can react to any signal and take action. That signal can be any third party event, a schedule, or a message from another agent. -The important part is not autonomy for its own sake. A useful agent has a small job, clear inputs, visible output, and a human-friendly trail of what it did. +A relay agent is specialized and focused agent that has a task or a few small tasks and that it does very well. They are completely customizable +and live in your codebase and deployable by a simple CLI command or a one click deploy on the web. You can deploy several +relay agents and each agent on the relay can communicate with each other enabling as complex of workflows as you want with +full traceability and control in straightforward and concise TypeScript code. + +## A whole agent + +This is [`granola-prospect`](https://github.com/AgentWorkforce/agents/tree/main/granola). A meeting recording syncs in; it decides whether the call contained a real feature ask, files a Linear issue, and hands the implementation to a coding agent that opens the PR. + +The persona declares what it can reach: + +```ts +export default definePersona({ + id: 'granola-prospect', + description: 'Detects prospect calls, files a Linear issue with the ask, and opens a PR implementing it.', + cloud: true, + useSubscription: true, + integrations: { + granola: {}, + linear: { scope: { issues: '/linear/issues/**', teams: '/linear/teams/**' } }, + github: { scope: { repo: 'your-org/your-repo' } } + }, + harness: 'claude', + model: 'claude-sonnet-4-6', + systemPrompt: 'Turn prospect asks from meeting transcripts into a Linear issue and a small implementing PR.', + harnessSettings: { reasoning: 'high', timeoutSeconds: 1800, sandboxMode: 'workspace-write' }, + onEvent: './agent.ts' +}); +``` + +And the handler decides what to do: + +```ts +export default defineAgent({ + triggers: { granola: [{ on: 'file.created' }] }, + handler: async (ctx, event) => { + const notePath = readNotePath((await event.expand('full')).data); + const transcript = await readNote(ctx, notePath); + if (!transcript) return; + + // The judgment call: was this a prospect asking for something? + const ask = await classify(ctx, transcript); + if (!ask.isProspect) return; + + const linear = relayClient('linear'); + const issue = await linear.write('issues', {}, { + teamId: await resolveTeamId(ctx), + title: ask.title, + description: ask.summary + }); + + // Hand the actual work to a coding agent. The repo is already checked out. + const run = await ctx.harness.run({ + cwd: ctx.sandbox.cwd, + prompt: `A prospect asked for the following. Implement it, then open a pull request. + Put the PR URL on the last line.\n\n${ask.summary}` + }); + + const prUrl = run.output.match(/https?:\/\/\S*\/pull\/\d+/g)?.pop(); + if (prUrl) { + await linear.write('comments', { issueId: issue.receipt?.id }, { body: `:rocket: ${prUrl}` }); + } + } +}); +``` + +`classify` is a model call, not a rule: + +```ts +const ask = await ctx.llm.complete(` + Read this meeting transcript. Decide if it is a sales/prospect call where the + prospect asked for a feature or change. Reply with JSON only: + {"isProspect": boolean, "title": "short issue title", "summary": "what they asked for"} + + ${transcript.slice(0, 8000)} +`, { maxTokens: 400 }); +``` + +The agent makes decisions based on the inputs and allows the code to lean but accomplish complicated functionality. ## What an agent needs @@ -14,19 +92,17 @@ The important part is not autonomy for its own sake. A useful agent has a small - **A policy**: what the agent may read, write, spend, and deploy. - **A reporting path**: where the agent posts progress, findings, and handoffs. -Relay gives that work a shared substrate: messages for coordination, Relayfile for provider-backed state, and profiles for repeatable behavior. - ## Start here - Launch a small repo-review agent and see how it reports through Relay. - - - Pick the shape that matches the job: reviewer, monitor, triager, or worker pair. + Get an agent from the gallery running in one click. - Define the trigger, behavior, permissions, and handoff path for a custom agent. + Author a cloud persona: `persona.json` for wiring, `agent.ts` for behavior. + + + Deploy, test locally, trigger, read logs, and tear down. diff --git a/web/content/docs/agents/patterns.mdx b/web/content/docs/agents/patterns.mdx deleted file mode 100644 index 7a9dcd9..0000000 --- a/web/content/docs/agents/patterns.mdx +++ /dev/null @@ -1,34 +0,0 @@ ---- -title: 'Agent patterns' -description: 'Use a small set of repeatable patterns for proactive agents instead of designing every workflow from scratch.' ---- - -Good agents are boring in the best way. They have one reason to wake up, one definition of done, and a clear path back to the team. - -## Reviewer - -Reviews pull requests, migrations, release notes, or generated work. The agent should cite files, produce concrete findings, and hand back a short pass/fail summary. - -Use it when quality gates are frequent and repetitive. - -## Monitor - -Watches outside signals: package releases, vendor changelogs, HN keywords, inbound emails, or provider webhooks. It should dedupe aggressively and only post when the signal clears a threshold. - -Use it when the job is mostly "notice the important thing." - -## Triager - -Turns unstructured input into routed work. It can classify Linear issues, summarize customer requests, open follow-up tickets, or assign an owner. - -Use it when the next step is known, but humans spend time sorting the queue. - -## Worker pair - -Splits a job between an implementer and a reviewer. One agent changes the system, the other checks the result and asks for corrections before a human sees it. - -Use it when the task benefits from independent review before handoff. - - - See concrete proactive-agent use cases. - diff --git a/web/content/docs/agents/quickstart.mdx b/web/content/docs/agents/quickstart.mdx index 76207e2..92c5127 100644 --- a/web/content/docs/agents/quickstart.mdx +++ b/web/content/docs/agents/quickstart.mdx @@ -1,50 +1,35 @@ --- title: 'Quickstart' -description: 'Fork an existing proactive agent, connect it to a Relay workspace, and run it against a real event.' +description: 'Get a proactive agent running in one click.' --- -The fastest way to start is to fork an existing agent and keep the first job narrow. Review a PR. Triage one Linear project. Watch one package. Send one digest. +Start with someone else's agent. Pick one from the [gallery](/agents) and hit **Launch agent**. -## 1. Pick an agent +The deploy wizard opens with that agent's persona already loaded. Pick a workspace, connect the providers it asks for, choose how it gets model access, fill in its inputs — Slack channel, repo, whatever it declared — and deploy. -Start from the open-source agents repo: +That's it. Its triggers and schedules are registered and it's live. Sandboxes spin up on the first trigger fire, so an idle agent costs nothing. -```bash -gh repo clone AgentWorkforce/agents -cd agents -``` - -Choose a small agent directory as the reference. Each agent should make the trigger, persona, and implementation obvious from the files in that folder. - -## 2. Connect Relay - -Install the Relay skill in the coding harness you use for setup work: +## Prefer the terminal? ```bash -npx @agent-relay@latest skills add +gh repo clone AgentWorkforce/agents && cd agents +npx agentworkforce login +npx agentworkforce deploy ./hn-monitor/persona.json --mode cloud ``` -Then create or select the workspace where the agent should report progress. The same workspace can host the agent's channel, human handoffs, and durable context. - -## 3. Run the first event - -Use a real event, but keep the permission surface small: +Then fire it without waiting for its schedule: ```bash -agent-relay spawn repo-reviewer \ - --channel dev \ - --task "Review the latest pull request and report findings." +npx agentworkforce trigger hn-monitor ``` -The first successful run should prove three things: the agent got the right context, stayed inside its policy, and reported its work somewhere humans already look. - ## Next - - Use a known shape instead of inventing every workflow from scratch. + + Write a persona from scratch — or have your coding agent write it. - - Add schedules, secrets, limits, and reporting before leaving it unattended. + + Deploy, test locally, trigger, read logs, and tear down. diff --git a/web/lib/agents.ts b/web/lib/agents.ts index d48636b..4068169 100644 --- a/web/lib/agents.ts +++ b/web/lib/agents.ts @@ -199,9 +199,11 @@ export const AGENTS: Agent[] = [ summary: 'Twice a day · 9am & 5pm ET', detail: '0 9,17 * * *', }, - integrations: ['slack', 'hacker-news'], + // slack and telegram are both optional transports gated by + // enabledByInput (SLACK_CHANNEL / TELEGRAM_CHAT) — set either or both. + integrations: ['slack', 'telegram', 'hacker-news'], runtime: 'Claude · claude-haiku-4-5', - inputs: ['TOPICS', 'SLACK_CHANNEL'], + inputs: ['TOPICS', 'SLACK_CHANNEL', 'TELEGRAM_CHAT'], accent: '#c1674b', hasCustomArt: true, }, @@ -249,9 +251,11 @@ export const AGENTS: Agent[] = [ summary: 'Every day · 10am ET', detail: '0 10 * * *', }, - integrations: ['spotify', 'slack'], + // Delivery over Slack DM, Telegram, or both — each optional and gated by + // enabledByInput (SLACK_USER / TELEGRAM_CHAT). + integrations: ['spotify', 'slack', 'telegram'], runtime: 'Default harness', - inputs: ['SLACK_USER', 'SPOTIFY_TOKEN'], + inputs: ['SLACK_USER', 'TELEGRAM_CHAT', 'SPOTIFY_TOKEN'], accent: '#1db954', hasCustomArt: true, }, @@ -610,6 +614,32 @@ export function agentAsset(slug: string, asset: 'banner' | 'card' | 'card-sm'): return `/agent-art/${slug}/${asset}.png`; } +/** + * Provider logos vendored from Nango's template logos by + * scripts/sync-integration-logos.sh — the same source pear resolves icons from. + * Nango has no logo for every provider we support (it answers 200 with its app + * shell for unknown slugs), so this maps only the ones that resolve to a real + * image. Anything absent renders as a text chip instead; re-run the sync script + * after adding an integration and add it here only if a file lands. + */ +const INTEGRATION_LOGO_FILES: Partial> = { + cloudflare: 'cloudflare.svg', + github: 'github.svg', + 'google-mail': 'google-mail.svg', + granola: 'granola.png', + linear: 'linear.svg', + notion: 'notion.svg', + slack: 'slack.svg', + spotify: 'spotify.svg', + telegram: 'telegram.svg', +}; + +/** Logo URL for an integration, or undefined when no vendored logo exists. */ +export function integrationLogo(integration: Integration): string | undefined { + const file = INTEGRATION_LOGO_FILES[integration]; + return file ? `/integration-logos/${file}` : undefined; +} + function agentRepo(agent: Agent): string { return agent.repo ?? DEFAULT_REPO; } diff --git a/web/lib/product-docs-nav.ts b/web/lib/product-docs-nav.ts index 0d2991c..3b01858 100644 --- a/web/lib/product-docs-nav.ts +++ b/web/lib/product-docs-nav.ts @@ -151,14 +151,11 @@ export const agentsSection: ProductDocSection = { }, { title: 'Design', - items: [ - { title: 'Agent patterns', slug: 'patterns' }, - { title: 'Build your own', slug: 'build' }, - ], + items: [{ title: 'Build your own', slug: 'build' }], }, { title: 'Run', - items: [{ title: 'Deploy and operate', slug: 'deploy' }], + items: [{ title: 'CLI', slug: 'cli' }], }, ], }; diff --git a/web/next.config.mjs b/web/next.config.mjs index 2be9a29..08fe1c1 100644 --- a/web/next.config.mjs +++ b/web/next.config.mjs @@ -81,6 +81,11 @@ const nextConfig = { { source: '/relaycast/:path*', destination: '/primitives#message', permanent: true }, { source: '/docs/reference-sdk', destination: '/docs/typescript-sdk', permanent: true }, { source: '/docs/reference-sdk-py', destination: '/docs/typescript-sdk', permanent: true }, + // The agents docs lost these two pages: 'Deploy and operate' became the + // CLI reference, and 'Agent patterns' was dropped without a successor + // (the build guide is the nearest thing that still covers agent shape). + { source: '/docs/agents/deploy', destination: '/docs/agents/cli', permanent: true }, + { source: '/docs/agents/patterns', destination: '/docs/agents/build', permanent: true }, ]; }, }; diff --git a/web/public/integration-logos/cloudflare.svg b/web/public/integration-logos/cloudflare.svg new file mode 100644 index 0000000..2c84b33 --- /dev/null +++ b/web/public/integration-logos/cloudflare.svg @@ -0,0 +1 @@ + diff --git a/web/public/integration-logos/github.svg b/web/public/integration-logos/github.svg new file mode 100644 index 0000000..b7c3ee7 --- /dev/null +++ b/web/public/integration-logos/github.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/public/integration-logos/google-mail.svg b/web/public/integration-logos/google-mail.svg new file mode 100644 index 0000000..2136fcd --- /dev/null +++ b/web/public/integration-logos/google-mail.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/public/integration-logos/granola.png b/web/public/integration-logos/granola.png new file mode 100644 index 0000000..6216a09 Binary files /dev/null and b/web/public/integration-logos/granola.png differ diff --git a/web/public/integration-logos/linear.svg b/web/public/integration-logos/linear.svg new file mode 100644 index 0000000..c89d91c --- /dev/null +++ b/web/public/integration-logos/linear.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/public/integration-logos/notion.svg b/web/public/integration-logos/notion.svg new file mode 100644 index 0000000..b685add --- /dev/null +++ b/web/public/integration-logos/notion.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/public/integration-logos/slack.svg b/web/public/integration-logos/slack.svg new file mode 100644 index 0000000..ae95ae0 --- /dev/null +++ b/web/public/integration-logos/slack.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/public/integration-logos/spotify.svg b/web/public/integration-logos/spotify.svg new file mode 100644 index 0000000..097076b --- /dev/null +++ b/web/public/integration-logos/spotify.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/public/integration-logos/telegram.svg b/web/public/integration-logos/telegram.svg new file mode 100644 index 0000000..d2ecb2c --- /dev/null +++ b/web/public/integration-logos/telegram.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/scripts/sync-integration-logos.sh b/web/scripts/sync-integration-logos.sh new file mode 100755 index 0000000..de04751 --- /dev/null +++ b/web/scripts/sync-integration-logos.sh @@ -0,0 +1,98 @@ +#!/usr/bin/env bash +# +# Vendor the integration provider logos into public/integration-logos/ so the +# /agents gallery and agent detail pages serve them as static assets instead of +# hotlinking Nango on every render. +# +# Source of truth: Nango's template logos, the same convention pear uses to +# resolve a provider icon (see pear/src/main/integrations.ts nangoTemplateLogoUrl). +# +# Two things this script has to defend against: +# +# 1. Nango answers 200 with its SPA HTML shell for slugs it has no logo for, +# so a status check is not enough — we sniff the body for an SVG root and +# skip anything else. As of this writing daytona, gcp, hacker-news, neon, +# and npm have no logo and intentionally fall through to a text chip in +# the UI (see INTEGRATION_LOGO_FILES in lib/agents.ts). +# 2. granola's logo is a ~525KB SVG wrapping a base64 PNG — absurd for a 16px +# mark — so it is unwrapped and downscaled to a small PNG instead. +# +# Re-running is safe and idempotent. After adding an integration, run this and +# then add it to INTEGRATION_LOGO_FILES only if a file actually lands here. +# +# NOTE: destination is public/integration-logos, NOT public/integrations — +# top-level public/ folders become CloudFront → S3 behaviors under SST/OpenNext +# and would shadow any same-named page route (see sync-agent-assets.sh). +# +# Usage: web/scripts/sync-integration-logos.sh + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +WEB_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +DEST="$WEB_DIR/public/integration-logos" +BASE="https://app.nango.dev/images/template-logos" +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT + +# Every provider in the `Integration` union in lib/agents.ts. Slugs that Nango +# has no logo for are listed anyway so the skip is visible in the output. +SLUGS=( + cloudflare + daytona + gcp + github + google-mail + granola + hacker-news + linear + neon + notion + npm + slack + spotify + telegram +) + +mkdir -p "$DEST" +echo "Syncing integration logos from $BASE -> $DEST" + +for slug in "${SLUGS[@]}"; do + raw="$TMP/$slug.svg" + + if ! curl -sfL "$BASE/$slug.svg" -o "$raw"; then + echo " skip $slug (fetch failed)" + continue + fi + + # Nango serves its app shell for unknown slugs — only keep real SVGs. + if ! head -c 512 "$raw" | grep -qi '/dev/null 2>&1 + rm -f "$DEST/granola.svg" + echo " granola.png (unwrapped from svg)" + continue + fi + echo " WARN: granola svg had no embedded raster; keeping svg as-is" >&2 + fi + + cp "$raw" "$DEST/$slug.svg" + echo " $slug.svg" +done + +echo "Done."