diff --git a/docs/plans/deploy-v1.md b/docs/plans/deploy-v1.md index a11ab43..e60aecc 100644 --- a/docs/plans/deploy-v1.md +++ b/docs/plans/deploy-v1.md @@ -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. @@ -64,7 +64,18 @@ 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 ` 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 ` 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..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 @@ -72,12 +83,10 @@ All new fields are optional. A persona that does not set any of them continues t |---|---|---|---| | `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` | 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` | 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 @@ -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 @@ -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 @@ -242,7 +241,7 @@ interface WorkforceCtx { cancel(name: string): Promise; }; - // Persona metadata (id, traits, harness tier defaults, etc.) — read-only + // Persona metadata (id, listener config, harness tier defaults, etc.) — read-only persona: PersonaSpec; } @@ -254,7 +253,7 @@ export function handler( 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. @@ -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 ... } @@ -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": { ... } } @@ -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 @@ -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. diff --git a/examples/weekly-digest/persona.json b/examples/weekly-digest/persona.json index 8f028f7..a127b25 100644 --- a/examples/weekly-digest/persona.json +++ b/examples/weekly-digest/persona.json @@ -35,7 +35,6 @@ "tz": "UTC" } ], - "sandbox": true, "memory": { "enabled": true, "scopes": [ diff --git a/packages/deploy/src/modes/sandbox.ts b/packages/deploy/src/modes/sandbox.ts index 56d7f47..3c193a4 100644 --- a/packages/deploy/src/modes/sandbox.ts +++ b/packages/deploy/src/modes/sandbox.ts @@ -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 => { @@ -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, diff --git a/packages/persona-kit/src/index.ts b/packages/persona-kit/src/index.ts index 9730154..f2ca1e3 100644 --- a/packages/persona-kit/src/index.ts +++ b/packages/persona-kit/src/index.ts @@ -32,14 +32,11 @@ export type { PersonaMemoryScope, PersonaMount, PersonaPermissions, - PersonaSandbox, - PersonaSandboxConfig, PersonaSchedule, PersonaSelection, PersonaSkill, PersonaSpec, PersonaTag, - PersonaTraits, SidecarMdMode, SkillInstall, SkillMaterializationOptions, @@ -69,13 +66,11 @@ export { parseOnEvent, parsePermissions, parsePersonaSpec, - parseSandbox, parseSchedules, parseSkills, parseStringList, parseStringMap, parseTags, - parseTraits, resolveSidecar, sidecarSelectionFields } from './parse.js'; diff --git a/packages/persona-kit/src/parse.test.ts b/packages/persona-kit/src/parse.test.ts index e201c44..a328443 100644 --- a/packages/persona-kit/src/parse.test.ts +++ b/packages/persona-kit/src/parse.test.ts @@ -13,13 +13,11 @@ import { parseOnEvent, parsePermissions, parsePersonaSpec, - parseSandbox, parseSchedules, parseSkills, parseStringList, parseStringMap, - parseTags, - parseTraits + parseTags } from './parse.js'; function validSpec(over: Record = {}): Record { @@ -66,9 +64,7 @@ test('parsePersonaSpec accepts deploy-v1 optional fields', () => { } }, schedules: [{ name: 'weekly', cron: '0 9 * * 6', tz: 'UTC' }], - sandbox: { enabled: true, timeoutSeconds: 1800, env: { NODE_ENV: 'production' } }, memory: { enabled: true, scopes: ['workspace'], ttlDays: 30 }, - traits: { voice: 'professional-warm', preferMarkdown: true }, onEvent: './agent.ts' }), 'documentation' @@ -77,16 +73,28 @@ test('parsePersonaSpec accepts deploy-v1 optional fields', () => { assert.equal(spec.cloud, true); assert.equal(spec.integrations?.github.triggers?.[0].on, 'pull_request.opened'); assert.equal(spec.schedules?.[0].name, 'weekly'); - assert.deepEqual(spec.sandbox, { - enabled: true, - timeoutSeconds: 1800, - env: { NODE_ENV: 'production' } - }); assert.deepEqual(spec.memory, { enabled: true, scopes: ['workspace'], ttlDays: 30 }); - assert.equal(spec.traits?.preferMarkdown, true); assert.equal(spec.onEvent, './agent.ts'); }); +test('parsePersonaSpec rejects traits with the v1 migration error', () => { + assert.throws( + () => parsePersonaSpec(validSpec({ traits: { voice: 'concise' } }), 'documentation'), + /traits was removed in v1; personality is handled by the persona-personality-builder tool \(out of scope for v1\)\. See docs\/plans\/deploy-v1\.md/ + ); +}); + +test('parsePersonaSpec rejects sandbox with the v1 migration error', () => { + assert.throws( + () => parsePersonaSpec(validSpec({ sandbox: true }), 'documentation'), + /sandbox was removed in v1; sandbox is on by default at deploy time\. Use 'workforce deploy --no-sandbox' or runtime config to opt out\. See docs\/plans\/deploy-v1\.md/ + ); + assert.throws( + () => parsePersonaSpec(validSpec({ sandbox: { enabled: true } }), 'documentation'), + /sandbox was removed in v1/ + ); +}); + test('parsePersonaSpec throws when intent does not match the expected intent', () => { assert.throws( () => parsePersonaSpec(validSpec({ intent: 'review' }), 'documentation'), @@ -347,33 +355,6 @@ test('parsePersonaSpec rejects a non-object spec', () => { // --- deploy-v1 schema additions ---------------------------------------------- -test('parseSandbox accepts boolean shorthand and round-trips both forms', () => { - assert.equal(parseSandbox(true, 'sandbox'), true); - assert.equal(parseSandbox(false, 'sandbox'), false); - assert.equal(parseSandbox(undefined, 'sandbox'), undefined); - const obj = parseSandbox( - { enabled: true, timeoutSeconds: 600, env: { FOO: 'bar' } }, - 'sandbox' - ); - assert.deepEqual(obj, { enabled: true, timeoutSeconds: 600, env: { FOO: 'bar' } }); -}); - -test('parseSandbox rejects malformed objects with field-pointed errors', () => { - assert.throws(() => parseSandbox('on', 'sandbox'), /sandbox must be a boolean or an object/); - assert.throws( - () => parseSandbox({ enabled: 'yes' }, 'sandbox'), - /sandbox\.enabled must be a boolean/ - ); - assert.throws( - () => parseSandbox({ timeoutSeconds: -1 }, 'sandbox'), - /sandbox\.timeoutSeconds must be a positive number/ - ); - assert.throws( - () => parseSandbox({ timeoutSeconds: Number.POSITIVE_INFINITY }, 'sandbox'), - /sandbox\.timeoutSeconds must be a positive number/ - ); -}); - test('parseMemory accepts boolean + object forms and validates scopes', () => { assert.equal(parseMemory(true, 'memory'), true); assert.equal(parseMemory(false, 'memory'), false); @@ -402,40 +383,6 @@ test('parseMemory rejects unknown scopes and non-positive ttl', () => { assert.throws(() => parseMemory({ dedupMs: -1 }, 'memory'), /dedupMs must be a non-negative number/); }); -test('parseTraits keeps only supplied fields and validates enums', () => { - assert.equal(parseTraits(undefined, 'traits'), undefined); - assert.equal(parseTraits({}, 'traits'), undefined); // empty object collapses to undefined - const t = parseTraits( - { - voice: 'concise', - formality: 'low', - proactivity: 'high', - riskPosture: 'balanced', - domain: 'engineering', - vocabulary: ['PR', 'diff'], - preferMarkdown: true - }, - 'traits' - ); - assert.deepEqual(t, { - voice: 'concise', - formality: 'low', - proactivity: 'high', - riskPosture: 'balanced', - domain: 'engineering', - vocabulary: ['PR', 'diff'], - preferMarkdown: true - }); - assert.throws( - () => parseTraits({ formality: 'extreme' }, 'traits'), - /traits\.formality must be one of: low, medium, high/ - ); - assert.throws( - () => parseTraits({ riskPosture: 'wild' }, 'traits'), - /traits\.riskPosture must be one of: conservative, balanced, aggressive/ - ); -}); - test('parseSchedules validates cron, requires unique names, preserves tz when set', () => { const s = parseSchedules( [ @@ -568,11 +515,10 @@ test('parsePersonaSpec rejects non-boolean cloud / useSubscription', () => { ); }); -test('parsePersonaSpec keeps boolean shorthand sandbox / memory through round-trip', () => { +test('parsePersonaSpec keeps boolean shorthand memory through round-trip', () => { const spec = parsePersonaSpec( - validSpec({ cloud: true, sandbox: true, memory: false }), + validSpec({ cloud: true, memory: false }), 'documentation' ); - assert.equal(spec.sandbox, true); assert.equal(spec.memory, false); }); diff --git a/packages/persona-kit/src/parse.ts b/packages/persona-kit/src/parse.ts index d5c1209..a56c745 100644 --- a/packages/persona-kit/src/parse.ts +++ b/packages/persona-kit/src/parse.ts @@ -23,14 +23,11 @@ import type { PersonaMemoryScope, PersonaMount, PersonaPermissions, - PersonaSandbox, - PersonaSandboxConfig, PersonaSchedule, PersonaSelection, PersonaSkill, PersonaSpec, PersonaTag, - PersonaTraits, SidecarMdMode } from './types.js'; @@ -395,9 +392,6 @@ const MEMORY_SCOPE_VALUES: readonly PersonaMemoryScope[] = [ 'object' ]; -const TRAIT_LEVEL_VALUES = ['low', 'medium', 'high'] as const; -const TRAIT_RISK_VALUES = ['conservative', 'balanced', 'aggressive'] as const; - const ONEVENT_EXT_RE = /\.(?:ts|tsx|mts|cts|js|mjs|cjs)$/i; // Standard 5-field cron: minute hour day-of-month month day-of-week. Each @@ -572,39 +566,6 @@ export function parseSchedules( return out; } -export function parseSandbox(value: unknown, context: string): PersonaSandbox | undefined { - if (value === undefined) return undefined; - if (typeof value === 'boolean') return value; - if (!isObject(value)) { - throw new Error(`${context} must be a boolean or an object if provided`); - } - const { enabled, timeoutSeconds, env } = value; - const out: PersonaSandboxConfig = {}; - if (enabled !== undefined) { - if (typeof enabled !== 'boolean') { - throw new Error(`${context}.enabled must be a boolean if provided`); - } - out.enabled = enabled; - } - if (timeoutSeconds !== undefined) { - if ( - typeof timeoutSeconds !== 'number' || - !Number.isFinite(timeoutSeconds) || - timeoutSeconds <= 0 - ) { - throw new Error(`${context}.timeoutSeconds must be a positive number if provided`); - } - out.timeoutSeconds = timeoutSeconds; - } - if (env !== undefined) { - const parsedEnv = parseStringMap(env, `${context}.env`); - if (parsedEnv && Object.keys(parsedEnv).length > 0) { - out.env = parsedEnv; - } - } - return out; -} - export function parseMemory(value: unknown, context: string): PersonaMemory | undefined { if (value === undefined) return undefined; if (typeof value === 'boolean') return value; @@ -658,56 +619,6 @@ export function parseMemory(value: unknown, context: string): PersonaMemory | un return out; } -export function parseTraits(value: unknown, context: string): PersonaTraits | undefined { - if (value === undefined) return undefined; - if (!isObject(value)) { - throw new Error(`${context} must be an object if provided`); - } - const { voice, formality, proactivity, riskPosture, domain, vocabulary, preferMarkdown } = value; - const out: PersonaTraits = {}; - if (voice !== undefined) { - if (typeof voice !== 'string' || !voice.trim()) { - throw new Error(`${context}.voice must be a non-empty string if provided`); - } - out.voice = voice; - } - if (formality !== undefined) { - if (typeof formality !== 'string' || !TRAIT_LEVEL_VALUES.includes(formality as 'low')) { - throw new Error(`${context}.formality must be one of: ${TRAIT_LEVEL_VALUES.join(', ')}`); - } - out.formality = formality as PersonaTraits['formality']; - } - if (proactivity !== undefined) { - if (typeof proactivity !== 'string' || !TRAIT_LEVEL_VALUES.includes(proactivity as 'low')) { - throw new Error(`${context}.proactivity must be one of: ${TRAIT_LEVEL_VALUES.join(', ')}`); - } - out.proactivity = proactivity as PersonaTraits['proactivity']; - } - if (riskPosture !== undefined) { - if (typeof riskPosture !== 'string' || !TRAIT_RISK_VALUES.includes(riskPosture as 'balanced')) { - throw new Error(`${context}.riskPosture must be one of: ${TRAIT_RISK_VALUES.join(', ')}`); - } - out.riskPosture = riskPosture as PersonaTraits['riskPosture']; - } - if (domain !== undefined) { - if (typeof domain !== 'string' || !domain.trim()) { - throw new Error(`${context}.domain must be a non-empty string if provided`); - } - out.domain = domain; - } - if (vocabulary !== undefined) { - const parsed = parseStringList(vocabulary, `${context}.vocabulary`); - if (parsed) out.vocabulary = parsed; - } - if (preferMarkdown !== undefined) { - if (typeof preferMarkdown !== 'boolean') { - throw new Error(`${context}.preferMarkdown must be a boolean if provided`); - } - out.preferMarkdown = preferMarkdown; - } - return Object.keys(out).length > 0 ? out : undefined; -} - export function parseOnEvent(value: unknown, context: string): string | undefined { if (value === undefined) return undefined; return assertOnEventPath(value, context); @@ -718,6 +629,19 @@ export function parsePersonaSpec(value: unknown, expectedIntent: PersonaIntent): throw new Error(`persona[${expectedIntent}] must be an object`); } + // `traits` and `sandbox` were removed from the persona spec in deploy-v1. + // Personas declaring them must be migrated before they can parse. + if ('traits' in value) { + throw new Error( + `persona[${expectedIntent}].traits was removed in v1; personality is handled by the persona-personality-builder tool (out of scope for v1). See docs/plans/deploy-v1.md` + ); + } + if ('sandbox' in value) { + throw new Error( + `persona[${expectedIntent}].sandbox was removed in v1; sandbox is on by default at deploy time. Use 'workforce deploy --no-sandbox' or runtime config to opt out. See docs/plans/deploy-v1.md` + ); + } + const { id, intent, @@ -743,9 +667,7 @@ export function parsePersonaSpec(value: unknown, expectedIntent: PersonaIntent): useSubscription, integrations, schedules, - sandbox, memory, - traits, onEvent } = value; @@ -826,9 +748,7 @@ export function parsePersonaSpec(value: unknown, expectedIntent: PersonaIntent): `persona[${expectedIntent}].integrations` ); const parsedSchedules = parseSchedules(schedules, `persona[${expectedIntent}].schedules`); - const parsedSandbox = parseSandbox(sandbox, `persona[${expectedIntent}].sandbox`); const parsedMemory = parseMemory(memory, `persona[${expectedIntent}].memory`); - const parsedTraits = parseTraits(traits, `persona[${expectedIntent}].traits`); const parsedOnEvent = parseOnEvent(onEvent, `persona[${expectedIntent}].onEvent`); return { @@ -856,9 +776,7 @@ export function parsePersonaSpec(value: unknown, expectedIntent: PersonaIntent): ...(typeof useSubscription === 'boolean' ? { useSubscription } : {}), ...(parsedIntegrations ? { integrations: parsedIntegrations } : {}), ...(parsedSchedules ? { schedules: parsedSchedules } : {}), - ...(parsedSandbox !== undefined ? { sandbox: parsedSandbox } : {}), ...(parsedMemory !== undefined ? { memory: parsedMemory } : {}), - ...(parsedTraits ? { traits: parsedTraits } : {}), ...(parsedOnEvent !== undefined ? { onEvent: parsedOnEvent } : {}) }; } diff --git a/packages/persona-kit/src/types.ts b/packages/persona-kit/src/types.ts index fff25a5..5d0048e 100644 --- a/packages/persona-kit/src/types.ts +++ b/packages/persona-kit/src/types.ts @@ -153,12 +153,13 @@ export interface PersonaIntegrationTrigger { } /** - * Per-provider integration configuration. The map key is the Relayfile - * provider slug (`github`, `linear`, `slack`, `notion`, `jira`). `scope` - * is provider-specific filter metadata (e.g. `{ repo: "org/repo" }` for - * github, `{ database: "" }` for notion). `triggers` are flat — all - * trigger events for this provider fan into the same `onEvent` handler, - * which discriminates on `event.source` + `event.type`. + * Per-provider integration configuration — the "radio" listener part of a + * persona's listener surface. The map key is the Relayfile provider slug + * (`github`, `linear`, `slack`, `notion`, `jira`). `scope` is provider- + * specific filter metadata (e.g. `{ repo: "org/repo" }` for github, + * `{ database: "" }` for notion). `triggers` are flat — all trigger + * events for this provider fan into the same `onEvent` handler, which + * discriminates on `event.source` + `event.type`. */ export interface PersonaIntegrationConfig { scope?: Record; @@ -166,8 +167,9 @@ export interface PersonaIntegrationConfig { } /** - * A cron-style schedule. `name` is unique within the persona and surfaces - * to the handler as `event.name`. `cron` is a standard 5-field expression. + * A cron-style schedule — the "clock" listener part of a persona's + * listener surface. `name` is unique within the persona and surfaces to + * the handler as `event.name`. `cron` is a standard 5-field expression. * `tz` defaults to `UTC` at the runtime layer (the parser keeps it * optional so the spec stays close to what the author wrote). */ @@ -177,29 +179,6 @@ export interface PersonaSchedule { tz?: string; } -/** - * Long-form sandbox configuration. `enabled` defaults to true when the - * object form is present; supply the boolean shorthand `sandbox: false` - * to opt out entirely. `timeoutSeconds` caps a single handler invocation - * (default 1800s in the runtime). `env` is merged on top of auto-injected - * secrets at sandbox-create time. - * - * Image selection is intentionally not user-configurable in v1 — workforce - * picks a standard image. Add `image` later if a real demand surfaces. - */ -export interface PersonaSandboxConfig { - enabled?: boolean; - timeoutSeconds?: number; - env?: Record; -} - -/** - * Sandbox can be specified as `true` / `false` shorthand or as the full - * config object. The parser preserves whichever form the author wrote so - * round-trips stay lossless; consumers normalize when reading. - */ -export type PersonaSandbox = boolean | PersonaSandboxConfig; - /** Memory scope semantics, mirroring @agent-assistant/memory. */ export type PersonaMemoryScope = 'session' | 'user' | 'workspace' | 'org' | 'object'; @@ -219,21 +198,19 @@ export interface PersonaMemoryConfig { export type PersonaMemory = boolean | PersonaMemoryConfig; /** - * Conversational traits, applied only when the agent posts to a chat - * surface (Slack, Relaycast, GitHub PR comment). Headless agents — the - * paraglide "Linear issue → PR" pattern — should omit this field. Mirrors - * the trait shape in `@agent-assistant/traits`. + * A persona listens for events. Three listener kinds: + * - **clock** (cron schedules — {@link PersonaSchedule}, surfaced as + * `schedules[]`). + * - **radio** (RelayFile integration events — see + * {@link PersonaIntegrationConfig}, surfaced as `integrations..triggers[]`). + * - **inbox** (RelayCast targeted messages — not yet modeled in v1). + * + * The current top-level shape predates the listeners framing; semantics + * are equivalent. `traits` and `sandbox` were removed in v1 — personality + * is handled by the persona-personality-builder tool (out of scope for + * v1), and sandbox is on by default at deploy time (opt out with + * `workforce deploy --no-sandbox` or runtime config). */ -export interface PersonaTraits { - voice?: string; - formality?: 'low' | 'medium' | 'high'; - proactivity?: 'low' | 'medium' | 'high'; - riskPosture?: 'conservative' | 'balanced' | 'aggressive'; - domain?: string; - vocabulary?: string[]; - preferMarkdown?: boolean; -} - export interface PersonaSpec { id: string; intent: string; @@ -333,25 +310,14 @@ export interface PersonaSpec { * for each provider not yet connected to the active workspace. */ integrations?: Record; - /** Cron-style schedules. Each `name` is unique within the persona. */ + /** Cron-style schedules. Each `name` is unique within the persona. The "clock" listener surface. */ schedules?: PersonaSchedule[]; - /** - * Sandbox preference. `true` (default for cloud personas) means the - * agent runs inside a Daytona sandbox at deploy time; `false` runs it in - * the runner process. The object form lets the author tune timeout / env. - */ - sandbox?: PersonaSandbox; /** * Memory subsystem opt-in. Wires the agent-assistant memory adapter at * runtime; the persona spec only declares intent, not implementation * details (api keys, adapter type, etc. come from workforce env). */ memory?: PersonaMemory; - /** - * Conversational traits, applied only when the agent posts to a chat - * surface. Omit for headless agents. - */ - traits?: PersonaTraits; /** * Relative POSIX path to the TypeScript (or compiled .js / .mjs) file * whose default export is the deploy-time event handler. Resolved diff --git a/packages/runtime/src/index.ts b/packages/runtime/src/index.ts index b4473cb..eb2df1c 100644 --- a/packages/runtime/src/index.ts +++ b/packages/runtime/src/index.ts @@ -53,6 +53,5 @@ export type { PersonaIntegrationTrigger, PersonaMemoryScope, PersonaSchedule, - PersonaSpec, - PersonaTraits + PersonaSpec } from '@agentworkforce/persona-kit'; diff --git a/packages/runtime/src/types.ts b/packages/runtime/src/types.ts index b4c7cd8..88bb94e 100644 --- a/packages/runtime/src/types.ts +++ b/packages/runtime/src/types.ts @@ -175,7 +175,7 @@ export interface IntegrationClients { * integration fields undefined. */ export interface WorkforceCtx extends IntegrationClients { - /** Read-only persona metadata, useful for branching on traits. */ + /** Read-only persona metadata. */ readonly persona: PersonaSpec; /** Workspace the agent is deployed into. */ readonly workspaceId: string;