Skip to content
Draft
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
78 changes: 37 additions & 41 deletions docs/plans/deploy-v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ One file. One command. One contract.

### In

- Persona JSON schema extension: `cloud`, `useSubscription`, `integrations`, `schedules`, `sandbox`, `memory`, `traits`, `onEvent`.
- Persona JSON schema extension: `cloud`, `useSubscription`, `integrations`, `schedules`, `memory`, `onEvent`.
- New package `@agentworkforce/runtime` — thin facade exposing `handler(...)` that wraps `agent({...})` from `@agent-relay/agent` (cloud proactive-runtime M1 SDK).
- New package `@agentworkforce/deploy` — the deploy CLI logic; the existing `cli.ts` gets a `deploy` case that dispatches to it.
- Daytona sandbox launcher used in the `--sandbox` run mode.
Expand All @@ -64,20 +64,29 @@ One file. One command. One contract.

## 3. Persona JSON schema diff

All new fields are optional. A persona that does not set any of them continues to behave exactly as today — `workforce agent <id>` works unchanged. Set `cloud: true` and at least one trigger to opt into the new deploy surface.
All new fields are optional. A persona that does not set any of them continues to behave exactly as today — `workforce agent <id>` works unchanged. Set `cloud: true` and at least one listener to opt into the new deploy surface.

A persona listens for events. Three listener kinds exist in v1:

- **clock** — cron `schedules[]` (§3.3).
- **radio** — RelayFile integration event triggers, declared via `integrations.<provider>.triggers[]` (§3.2).
- **inbox** — RelayCast targeted messages. Not yet modeled in v1; reserved.

Two fields explored in earlier drafts were dropped before v1 ships:

- **`traits`** was removed in v1. Personality is handled by the persona-personality-builder tool, which is out of scope for v1. Parsers reject any persona that still declares `traits` so old specs surface loudly during migration.
- **`sandbox`** was removed in v1. Sandbox is on by default at deploy time; opt out with `workforce deploy --no-sandbox` or runtime config rather than per-persona JSON. Parsers reject any persona that still declares `sandbox` for the same reason.

### 3.1 Top-level additions

| Field | Type | Required when | Meaning |
|---|---|---|---|
| `cloud` | `boolean` | always (default `false`) | When `true`, this persona is deployable. `workforce deploy` only operates on personas where this is `true`. |
| `useSubscription` | `boolean` | optional | When `true`, inference uses the user's connected LLM subscription via `@agent-relay/cloud`'s provider link (no workforce-billed tokens). Triggers a `connectProvider` step at deploy time. |
| `integrations` | `Record<string, IntegrationConfig>` | when persona has event triggers | Declares which Relayfile providers this agent needs and what events fire its handler. See §3.2. |
| `schedules` | `Schedule[]` | when persona runs on cron | One or more cron triggers, registered with the runtime's `ctx.schedule.every(...)`. Each schedule has a `name` echoed back to the handler. See §3.3. |
| `sandbox` | `boolean \| SandboxConfig` | optional | `true` (default) means agent runs inside a Daytona sandbox. `false` means the runner process owns its own filesystem. Object form lets you tune env / timeout. See §3.4. |
| `integrations` | `Record<string, IntegrationConfig>` | when persona has event triggers | Declares which Relayfile providers this agent needs and what events fire its handler. The "radio" listener surface. See §3.2. |
| `schedules` | `Schedule[]` | when persona runs on cron | One or more cron triggers, registered with the runtime's `ctx.schedule.every(...)`. Each schedule has a `name` echoed back to the handler. The "clock" listener surface. See §3.3. |
| `memory` | `boolean \| MemoryConfig` | optional | Enables the agent-assistant memory subsystem. Scopes and TTL configurable. See §3.5. |
| `traits` | `Traits` | optional, **only meaningful for interactive agents** | Mirrors `@agent-assistant/traits`: voice, formality, proactivity, etc. Applied when the agent posts to a chat surface (Slack, Relaycast). Headless agents (paraglide-style "Linear issue → ship") may omit this. See §3.6. |
| `onEvent` | `string` | when `cloud: true` and any trigger declared | Path to a TS file (relative to the persona JSON) whose default export is the event handler. Sub-file references like `./agent.ts` and `./handlers/index.ts` are supported. See §4. |
| `onEvent` | `string` | when `cloud: true` and any listener declared | Path to a TS file (relative to the persona JSON) whose default export is the event handler. Sub-file references like `./agent.ts` and `./handlers/index.ts` are supported. See §4. |

### 3.2 `integrations` shape

Expand Down Expand Up @@ -119,18 +128,16 @@ The act of stacking integrations is just declaring multiple keys. The act of lin
- `cron` is a standard 5-field expression. `tz` defaults to `UTC`.
- Multiple schedules are allowed. The runtime registers each with `ctx.schedule.every(cron, { tz, payload: { name } })`.

### 3.4 `sandbox` shape
### 3.4 Sandbox (deploy-time, not persona-level)

```jsonc
"sandbox": true // default
"sandbox": { "enabled": true, "timeoutSeconds": 1800, "env": { "FOO": "bar" } }
"sandbox": false // run in the runner process's fs
```
Sandbox configuration was lifted out of the persona spec in v1. The deploy
CLI runs every cloud persona inside a Daytona sandbox by default; pass
`workforce deploy --no-sandbox` (or set runtime config) to opt out. The
sandbox image, timeout, and env are decided at deploy time:

- Image is **not** user-configurable in v1. Workforce picks a standard image (`node-22` baseline) for the default Daytona sandbox. We can add `image` later if a real demand surfaces; eliminating the field keeps the v1 contract small.
- `timeoutSeconds` caps a single handler invocation. Default 1800s.
- `env` adds env vars on top of the auto-injected secrets (Relayfile connection tokens, harness inference creds, etc.).
- When `sandbox: false`, the agent's `ctx.sandbox` still exists but points at the runner's own process — useful for `--dev` iteration, **not** what we recommend for production.
- Image is **not** user-configurable in v1. Workforce picks a standard image (`node-22` baseline) for the default Daytona sandbox.
- The sandbox-client default exec timeout (~600s, see `packages/deploy/src/modes/sandbox-client.ts`) applies to every run. Per-persona handler timeouts continue to live on `harnessSettings.timeoutSeconds`.
- Auto-injected secrets (Relayfile connection tokens, harness inference creds) come from the deploy environment, not the persona JSON.

### 3.5 `memory` shape

Expand All @@ -148,25 +155,17 @@ The act of stacking integrations is just declaring multiple keys. The act of lin
- Implementation: the runtime wires `@agent-assistant/memory` with the supermemory adapter (matching sage today). API key is pulled from workforce-managed env, not declared in the persona.
- `scopes` is the only field with real semantic weight: session-only memory is wiped per handler; user-scope persists across the user's invocations of this agent; workspace persists across all users.
- `autoPromote` flips on the sage turn-recorder pattern — agent decides if session content is worth promoting.
- **No `memoryMd` file.** Memory is config, not prose. Personality goes in `traits` and `description`.
- **No `memoryMd` file.** Memory is config, not prose. Personality goes in `description` (and, in a future iteration, in the persona-personality-builder tool's output).

### 3.6 `traits` shape

Direct mapping to `@agent-assistant/traits`:

```jsonc
"traits": {
"voice": "professional-warm",
"formality": "low",
"proactivity": "medium",
"riskPosture": "conservative",
"domain": "engineering",
"vocabulary": ["PR", "diff", "CI"],
"preferMarkdown": true
}
```
### 3.6 Traits (removed in v1)

Only used when the runtime renders into a conversational surface (Slack message, Relaycast post, GitHub PR comment). Skip the field entirely for headless agents — saves the runtime a subsystem registration.
`traits` was explored in earlier drafts as a direct mapping to
`@agent-assistant/traits` (voice, formality, proactivity, risk posture,
domain vocabulary, markdown preference). It is **removed from the v1
persona spec**: personality is handled by the persona-personality-builder
tool, which is out of scope for v1. Parsers reject any persona that
still declares `traits` so old specs surface during migration. The
replacement story will be designed alongside that tool's first iteration.

### 3.7 Trigger-name registry

Expand Down Expand Up @@ -242,7 +241,7 @@ interface WorkforceCtx {
cancel(name: string): Promise<void>;
};

// Persona metadata (id, traits, harness tier defaults, etc.) — read-only
// Persona metadata (id, listener config, harness tier defaults, etc.) — read-only
persona: PersonaSpec;
}

Expand All @@ -254,7 +253,7 @@ export function handler<I extends IntegrationKeys>(
Implementation notes:
- `handler(...)` reads the persona JSON adjacent to the entrypoint (workforce bundles them together). At cold-start it:
1. Calls `agent({ workspace, schedule, watch, inbox, onEvent: shim })` from `@agent-relay/agent`, mapping `persona.integrations` to `watch` and `persona.schedules` to `schedule`.
2. Builds `ctx` once per agent boot: opens Daytona handle (if `sandbox: true`), wires Relayfile-derived clients, attaches memory adapter.
2. Builds `ctx` once per agent boot: opens Daytona handle (sandbox is on by default at deploy time; opt out with `workforce deploy --no-sandbox`), wires Relayfile-derived clients, attaches memory adapter.
3. The `shim` reshapes the raw envelope from `@agent-relay/agent` into the `WorkforceEvent` discriminated union and invokes the user's `fn(ctx, event)`.
- The user never imports `@agent-relay/agent` directly. Workforce owns the ergonomics. If the underlying SDK churns, we absorb the diff here.
- The SDK doors stay open for power users: we re-export `agent` from `@agentworkforce/runtime/raw` so anyone who wants the lower-level surface can drop down. This matters for nightcto-shaped projects that outgrow the persona contract.
Expand Down Expand Up @@ -354,7 +353,6 @@ Direct port of the proactive-agents weekly-digest pattern.
"cloud": true,
"integrations": { "github": { "scope": { "repo": "AgentWorkforce/weekly-digest" } } },
"schedules": [{ "name": "weekly", "cron": "0 9 * * 6", "tz": "UTC" }],
"sandbox": true,
"memory": { "enabled": true, "scopes": ["workspace"], "ttlDays": 90 },
"onEvent": "./agent.ts",
"tiers": { ... standard codex/opencode tiers ... }
Expand Down Expand Up @@ -385,9 +383,7 @@ Direct port of the proactive-agents weekly-digest pattern.
},
"slack": { "triggers": [{ "on": "app_mention" }] }
},
"sandbox": true,
"memory": { "enabled": true, "scopes": ["session", "workspace"] },
"traits": { "voice": "professional-warm", "formality": "low", "preferMarkdown": true },
"onEvent": "./agent.ts",
"tiers": { ... }
}
Expand All @@ -409,7 +405,7 @@ workforce/
│ ├── cli/ # add `deploy`, `login` cases
│ ├── persona-kit/ # extend PersonaSpec schema (§3)
│ │ └── src/
│ │ ├── types.ts # +CloudFields, +IntegrationConfig, +Schedule, +Sandbox, +Memory, +Traits
│ │ ├── types.ts # +CloudFields, +IntegrationConfig, +Schedule, +Memory
│ │ ├── parse.ts # extend parsePersonaSpec to read new fields
│ │ └── triggers.ts # NEW — known triggers registry (§3.7)
│ ├── harness-kit/ # no changes for v1
Expand Down Expand Up @@ -494,7 +490,7 @@ If a track slips, §10's fallback applies: ship `--dev` end-to-end with `weekly-
Tasks that are mechanical, well-specified, and don't gate on my decisions — perfect for a codex agent spawned via `workforce agent code-implementer` or a similar persona:

1. **Trigger registry expansion** — fill out `packages/persona-kit/src/triggers.ts` with the full set of known trigger names per Tier-1 provider (Linear, GitHub, Slack, Notion, Jira) by reading the Relayfile provider docs in `/Users/khaliqgant/Projects/AgentWorkforce/relayfile/docs/`.
2. **Test fixtures** — generate sample `persona.json` files exercising every optional combination (with/without traits, sandbox false, multi-schedule, etc.) into `packages/persona-kit/src/__fixtures__/`.
2. **Test fixtures** — generate sample `persona.json` files exercising every optional combination (multi-schedule, with/without memory, multi-provider integrations, etc.) into `packages/persona-kit/src/__fixtures__/`.
3. **JSON Schema export** — emit a JSON Schema from the extended `PersonaSpec` for editor autocomplete. New script: `packages/persona-kit/scripts/emit-schema.mjs`. Wire to `pnpm run build` so it ships with the package.
4. **Example expansion** — write a third example, `examples/linear-shipper/` (the paraglide pattern: Linear issue created → drive to PR), purely against the runtime substrate I land in §9.1.
5. **README polish** — once the deploy command is real, codex agent rewrites the workforce README to lead with the deploy story.
Expand Down
1 change: 0 additions & 1 deletion examples/weekly-digest/persona.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@
"tz": "UTC"
}
],
"sandbox": true,
"memory": {
"enabled": true,
"scopes": [
Expand Down
15 changes: 6 additions & 9 deletions packages/deploy/src/modes/sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,12 @@ export const sandboxLauncher: ModeLauncher = {
throw err;
}

const sandboxTimeoutSeconds = resolveTimeoutSeconds(input.persona.sandbox);
// Per-persona sandbox tuning (`persona.sandbox.timeoutSeconds`) was
// removed in deploy-v1; the sandbox-client default (600s, see
// sandbox-client.ts) applies to every run. Persona-level harness
// timeouts (`harnessSettings.timeoutSeconds`) are honored by the
// harness inside the sandbox, not by the sandbox `exec` envelope.
const sandboxTimeoutSeconds: number | undefined = undefined;

let stopping = false;
const stop = async (): Promise<void> => {
Expand Down Expand Up @@ -149,14 +154,6 @@ export function resolveSandboxClient(
});
}

function resolveTimeoutSeconds(sandbox: ModeLaunchInput['persona']['sandbox']): number | undefined {
if (sandbox === undefined || sandbox === true || sandbox === false) return undefined;
if (typeof sandbox.timeoutSeconds === 'number' && sandbox.timeoutSeconds > 0) {
return sandbox.timeoutSeconds;
}
return undefined;
}

// Re-exported for tests + power users wanting to compose the client manually.
export {
SANDBOX_BUNDLE_DIR,
Expand Down
5 changes: 0 additions & 5 deletions packages/persona-kit/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,11 @@ export type {
PersonaMemoryScope,
PersonaMount,
PersonaPermissions,
PersonaSandbox,
PersonaSandboxConfig,
PersonaSchedule,
PersonaSelection,
PersonaSkill,
PersonaSpec,
PersonaTag,
PersonaTraits,
SidecarMdMode,
SkillInstall,
SkillMaterializationOptions,
Expand Down Expand Up @@ -69,13 +66,11 @@ export {
parseOnEvent,
parsePermissions,
parsePersonaSpec,
parseSandbox,
parseSchedules,
parseSkills,
parseStringList,
parseStringMap,
parseTags,
parseTraits,
resolveSidecar,
sidecarSelectionFields
} from './parse.js';
Expand Down
Loading
Loading