From c0a1f79e961964ed8de3e7b63eb5737cbf3940ea Mon Sep 17 00:00:00 2001 From: Ricky Schema Cascade Date: Thu, 4 Jun 2026 23:07:35 +0200 Subject: [PATCH 1/2] fix(deploy): allow dispatcher-launched eventless agents --- packages/deploy/CHANGELOG.md | 4 ++++ packages/deploy/src/bundle.test.ts | 1 + packages/deploy/src/bundle.ts | 2 ++ packages/deploy/src/deploy.test.ts | 21 +++++++++++++++++++ packages/deploy/src/extract-agent.ts | 9 +++++++- packages/deploy/src/modes/cloud.test.ts | 8 +++++-- packages/deploy/src/preflight.ts | 6 ++++-- packages/persona-kit/CHANGELOG.md | 4 ++++ .../persona-kit/schemas/agent.schema.json | 7 ++++++- packages/persona-kit/src/emit-schema.test.ts | 5 ++++- packages/persona-kit/src/parse.test.ts | 8 ++++++- packages/persona-kit/src/parse.ts | 9 +++++++- packages/persona-kit/src/types.ts | 9 +++++++- packages/runtime/CHANGELOG.md | 4 ++++ packages/runtime/src/define-agent.test.ts | 4 +++- packages/runtime/src/define-agent.ts | 10 +++++++++ 16 files changed, 100 insertions(+), 11 deletions(-) diff --git a/packages/deploy/CHANGELOG.md b/packages/deploy/CHANGELOG.md index d8e77538..0e343473 100644 --- a/packages/deploy/CHANGELOG.md +++ b/packages/deploy/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Allow listener-free agents only when marked `launchedBy: "team-dispatcher"`. + ## [3.0.46] - 2026-06-04 ### Fixed diff --git a/packages/deploy/src/bundle.test.ts b/packages/deploy/src/bundle.test.ts index 774f2b49..053517ed 100644 --- a/packages/deploy/src/bundle.test.ts +++ b/packages/deploy/src/bundle.test.ts @@ -71,6 +71,7 @@ test('bundleStager produces an executable, importable bundle from a real onEvent assert.match(runnerSource, /import \* as userModule from '\.\/agent\.bundle\.mjs'/); assert.match(runnerSource, /WORKFORCE_AGENT_CONTEXT/); assert.match(runnerSource, /WORKFORCE_DEPLOYMENT_CONTEXT/); + assert.match(runnerSource, /exported\.launchedBy/); assert.match(runnerSource, /await startRunner\({ persona, agent, deployment, handler/); // bundle output is ES module shape and references the runtime as external diff --git a/packages/deploy/src/bundle.ts b/packages/deploy/src/bundle.ts index 7b93042e..dbc38592 100644 --- a/packages/deploy/src/bundle.ts +++ b/packages/deploy/src/bundle.ts @@ -135,6 +135,7 @@ let agentSpec; if (exported && exported.__workforceAgent) { candidate = exported.handler; agentSpec = { + ...(exported.launchedBy ? { launchedBy: exported.launchedBy } : {}), ...(exported.triggers ? { triggers: exported.triggers } : {}), ...(exported.schedules ? { schedules: exported.schedules } : {}), ...(exported.watch ? { watch: exported.watch } : {}) @@ -142,6 +143,7 @@ if (exported && exported.__workforceAgent) { } else if (exported && typeof exported.handler === 'function') { candidate = exported.handler; agentSpec = { + ...(exported.launchedBy ? { launchedBy: exported.launchedBy } : {}), ...(exported.triggers ? { triggers: exported.triggers } : {}), ...(exported.schedules ? { schedules: exported.schedules } : {}), ...(exported.watch ? { watch: exported.watch } : {}) diff --git a/packages/deploy/src/deploy.test.ts b/packages/deploy/src/deploy.test.ts index 21cc75cf..af1c377e 100644 --- a/packages/deploy/src/deploy.test.ts +++ b/packages/deploy/src/deploy.test.ts @@ -47,6 +47,13 @@ const NO_LISTENER_AGENT_SRC = `import { defineAgent } from '@agentworkforce/runt export default defineAgent({ handler: async () => {} }); `; +const TEAM_DISPATCHER_AGENT_SRC = `import { defineAgent } from '@agentworkforce/runtime'; +export default defineAgent({ + launchedBy: 'team-dispatcher', + handler: async () => {} +}); +`; + const MISSING_HANDLER_AGENT_SRC = `import { defineAgent } from '@agentworkforce/runtime'; export default defineAgent({ schedules: [{ name: 'weekly', cron: '0 9 * * 6' }] @@ -251,6 +258,20 @@ test('preflightPersona refuses when the agent declares no listeners', async () = } }); +test('preflightPersona accepts listener-free agents launched by the team dispatcher', async () => { + const { personaPath, cleanup } = await withTempPersona( + basePersonaJson({ integrations: undefined }), + TEAM_DISPATCHER_AGENT_SRC + ); + try { + const pre = await preflightPersona(personaPath); + assert.deepEqual(pre.schedules, []); + assert.equal(pre.agent.launchedBy, 'team-dispatcher'); + } finally { + await cleanup(); + } +}); + test('preflightPersona refuses when defineAgent omits handler', async () => { const { personaPath, cleanup } = await withTempPersona(basePersonaJson(), MISSING_HANDLER_AGENT_SRC); try { diff --git a/packages/deploy/src/extract-agent.ts b/packages/deploy/src/extract-agent.ts index b340b7c0..05a90189 100644 --- a/packages/deploy/src/extract-agent.ts +++ b/packages/deploy/src/extract-agent.ts @@ -35,6 +35,7 @@ import { const RUNTIME_STUB = ` function defineAgent(input) { const out = {}; + if (input && input.launchedBy) out.launchedBy = input.launchedBy; if (input && input.triggers) out.triggers = input.triggers; if (input && input.schedules) out.schedules = input.schedules; if (input && input.watch) out.watch = input.watch; @@ -122,8 +123,14 @@ export async function extractAgentSpec(onEventPath: string): Promise { + const dispatcherAgentSpec: import('@agentworkforce/persona-kit').AgentSpec = { + ...agentSpec, + launchedBy: 'team-dispatcher' + }; const { handle, calls } = await launch({ env: { WORKFORCE_DEPLOY_CLOUD_URL: 'https://cloud.example.test' }, - input: { inputs: { topic: 'AI' } }, + input: { inputs: { topic: 'AI' }, agent: dispatcherAgentSpec }, fetch(url, init) { if (init?.method === 'GET' && url.endsWith('/deployments')) { return okJson({ agents: [] }); @@ -181,7 +185,7 @@ test('cloud launcher POSTs a deploy bundle and returns the cloud handle', async const body = JSON.parse(String(init?.body)) as Record; assert.equal((body.persona as { id: string }).id, 'demo'); // Listeners travel as the top-level `agent` block, not on the persona. - assert.deepEqual(body.agent, agentSpec); + assert.deepEqual(body.agent, dispatcherAgentSpec); assert.equal((body.persona as { schedules?: unknown }).schedules, undefined); assert.deepEqual(body.inputs, { topic: 'AI' }); assert.deepEqual((body.bundle as { packageJson: unknown }).packageJson, { type: 'module' }); diff --git a/packages/deploy/src/preflight.ts b/packages/deploy/src/preflight.ts index b1190c57..2f47e97a 100644 --- a/packages/deploy/src/preflight.ts +++ b/packages/deploy/src/preflight.ts @@ -81,9 +81,11 @@ export async function preflightPersona(personaPath: string): Promise (t?.length ?? 0) > 0); const hasSchedules = (agent.schedules?.length ?? 0) > 0; const hasWatch = (agent.watch?.length ?? 0) > 0; - if (!hasTriggers && !hasSchedules && !hasWatch) { + const hasDispatcherLaunch = agent.launchedBy === 'team-dispatcher'; + if (!hasTriggers && !hasSchedules && !hasWatch && !hasDispatcherLaunch) { throw new Error( - `agent "${persona.id}" (${persona.onEvent}) declares no listeners — add at least one trigger, schedule, or watch rule to defineAgent({...})` + `agent "${persona.id}" (${persona.onEvent}) declares no listeners — add at least one trigger, schedule, or watch rule, ` + + `or set launchedBy: "team-dispatcher" for dispatcher-launched team members` ); } diff --git a/packages/persona-kit/CHANGELOG.md b/packages/persona-kit/CHANGELOG.md index a948d664..dfea3983 100644 --- a/packages/persona-kit/CHANGELOG.md +++ b/packages/persona-kit/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add `launchedBy: "team-dispatcher"` to `AgentSpec` for dispatcher-launched team members. + ## [3.0.42] - 2026-06-03 ### Fixed diff --git a/packages/persona-kit/schemas/agent.schema.json b/packages/persona-kit/schemas/agent.schema.json index d9c35ccb..eca01204 100644 --- a/packages/persona-kit/schemas/agent.schema.json +++ b/packages/persona-kit/schemas/agent.schema.json @@ -5,6 +5,11 @@ "AgentSpec": { "type": "object", "properties": { + "launchedBy": { + "type": "string", + "const": "team-dispatcher", + "description": "Declares that this agent is launched by another runtime path, not direct listeners." + }, "triggers": { "type": "object", "additionalProperties": { @@ -30,7 +35,7 @@ "description": "Relayfile-change listeners for proactive cloud agents." } }, - "description": "Runtime-parsed shape of an **agent** (`agent.ts`) — the \"when/how it fires\" half of a deployed agent, authored via `defineAgent(...)` in the runtime package and extracted from the bundle by the deploy CLI. The persona owns \"what the agent is\" (model, harness, skills, mcp, integration *connections*); the agent owns the listeners.\n\nThree listener kinds: - **radio** — {@link triggers } : a provider-keyed map of {@link PersonaIntegrationTrigger } arrays. Keys mirror `persona.integrations` so the deploy CLI can join events→connection. - **clock** — {@link schedules } : cron {@link PersonaSchedule } s. - **relayfile** — {@link watch } : {@link WatchRule } s.\n\nAt least one listener is required for a cloud agent (enforced at deploy)." + "description": "Runtime-parsed shape of an **agent** (`agent.ts`) — the \"when/how it fires\" half of a deployed agent, authored via `defineAgent(...)` in the runtime package and extracted from the bundle by the deploy CLI. The persona owns \"what the agent is\" (model, harness, skills, mcp, integration *connections*); the agent owns the listeners.\n\n`launchedBy: \"team-dispatcher\"` is the one listener-free exception: team member agents are not event-listeners themselves; they are launched by a dispatcher agent as part of a team run.\n\nThree listener kinds: - **radio** — {@link triggers } : a provider-keyed map of {@link PersonaIntegrationTrigger } arrays. Keys mirror `persona.integrations` so the deploy CLI can join events→connection. - **clock** — {@link schedules } : cron {@link PersonaSchedule } s. - **relayfile** — {@link watch } : {@link WatchRule } s.\n\nAt least one listener is required for a cloud agent unless `launchedBy` names a supported dispatcher launch mode (enforced at deploy)." }, "PersonaIntegrationTrigger": { "type": "object", diff --git a/packages/persona-kit/src/emit-schema.test.ts b/packages/persona-kit/src/emit-schema.test.ts index 2a5a8f4b..ff39004f 100644 --- a/packages/persona-kit/src/emit-schema.test.ts +++ b/packages/persona-kit/src/emit-schema.test.ts @@ -117,12 +117,15 @@ test('persona schema keeps mount.enabled but drops the moved listener fields', a : undefined, 'boolean'); }); -test('agent schema exposes triggers/schedules/watch', async () => { +test('agent schema exposes launchedBy/triggers/schedules/watch', async () => { const schema = JSON.parse(await readFile(agentSchemaPath, 'utf8')) as SchemaNode; const definitions = schema.definitions as Record; const agentSpec = definitions.AgentSpec; const watchRule = definitions.WatchRule; + assert.equal(agentSpec.properties?.launchedBy && agentSpec.properties.launchedBy !== true + ? agentSpec.properties.launchedBy.const + : undefined, 'team-dispatcher'); assert.equal(agentSpec.properties?.triggers && agentSpec.properties.triggers !== true ? agentSpec.properties.triggers.type : undefined, 'object'); diff --git a/packages/persona-kit/src/parse.test.ts b/packages/persona-kit/src/parse.test.ts index 51dabbc0..9ff7d1ef 100644 --- a/packages/persona-kit/src/parse.test.ts +++ b/packages/persona-kit/src/parse.test.ts @@ -663,8 +663,9 @@ test('parseIntegrations preserves scope (connection-only); rejects persona-level ); }); -test('parseAgentSpec validates a provider-keyed triggers map + schedules + watch', () => { +test('parseAgentSpec validates launchedBy plus provider-keyed triggers, schedules, and watch', () => { const agent = parseAgentSpec({ + launchedBy: 'team-dispatcher', triggers: { github: [ { on: 'pull_request.opened' }, @@ -680,9 +681,14 @@ test('parseAgentSpec validates a provider-keyed triggers map + schedules + watch assert.equal(agent.triggers?.slack[0].on, 'app_mention'); assert.equal(agent.schedules?.[0].name, 'nightly'); assert.equal(agent.watch?.[0].paths[0], '/github/x.json'); + assert.equal(agent.launchedBy, 'team-dispatcher'); }); test('parseAgentSpec rejects malformed triggers maps with precise field paths', () => { + assert.throws( + () => parseAgentSpec({ launchedBy: 'cron' }), + /launchedBy must be one of: team-dispatcher/ + ); assert.throws(() => parseAgentSpec({ triggers: [] }), /triggers must be an object keyed by provider/); assert.throws(() => parseAgentSpec({ triggers: { github: [] } }), /triggers\.github must be a non-empty array/); assert.throws( diff --git a/packages/persona-kit/src/parse.ts b/packages/persona-kit/src/parse.ts index 61eabba9..19e2910f 100644 --- a/packages/persona-kit/src/parse.ts +++ b/packages/persona-kit/src/parse.ts @@ -810,9 +810,16 @@ export function parseAgentSpec(value: unknown, context = 'agent'): AgentSpec { if (!isObject(value)) { throw new Error(`${context} must be an object`); } - const { triggers, schedules, watch } = value; + const { launchedBy, triggers, schedules, watch } = value; const out: AgentSpec = {}; + if (launchedBy !== undefined) { + if (launchedBy !== 'team-dispatcher') { + throw new Error(`${context}.launchedBy must be one of: team-dispatcher`); + } + out.launchedBy = launchedBy; + } + if (triggers !== undefined) { if (!isObject(triggers) || Array.isArray(triggers)) { throw new Error( diff --git a/packages/persona-kit/src/types.ts b/packages/persona-kit/src/types.ts index 25ed9b63..9c782c06 100644 --- a/packages/persona-kit/src/types.ts +++ b/packages/persona-kit/src/types.ts @@ -277,6 +277,10 @@ export interface WatchRule { * "what the agent is" (model, harness, skills, mcp, integration *connections*); * the agent owns the listeners. * + * `launchedBy: "team-dispatcher"` is the one listener-free exception: team + * member agents are not event-listeners themselves; they are launched by a + * dispatcher agent as part of a team run. + * * Three listener kinds: * - **radio** — {@link triggers}: a provider-keyed map of * {@link PersonaIntegrationTrigger} arrays. Keys mirror @@ -284,9 +288,12 @@ export interface WatchRule { * - **clock** — {@link schedules}: cron {@link PersonaSchedule}s. * - **relayfile** — {@link watch}: {@link WatchRule}s. * - * At least one listener is required for a cloud agent (enforced at deploy). + * At least one listener is required for a cloud agent unless `launchedBy` + * names a supported dispatcher launch mode (enforced at deploy). */ export interface AgentSpec { + /** Declares that this agent is launched by another runtime path, not direct listeners. */ + launchedBy?: 'team-dispatcher'; /** Radio listeners keyed by provider slug (`github`, `linear`, …). */ triggers?: Record; /** Cron-style clock listeners. Each `name` is unique within the agent. */ diff --git a/packages/runtime/CHANGELOG.md b/packages/runtime/CHANGELOG.md index 45a01eed..f39913c5 100644 --- a/packages/runtime/CHANGELOG.md +++ b/packages/runtime/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Preserve `launchedBy: "team-dispatcher"` from `defineAgent(...)` exports. + ## [3.0.46] - 2026-06-04 ### Added diff --git a/packages/runtime/src/define-agent.test.ts b/packages/runtime/src/define-agent.test.ts index ffea0157..a11b4409 100644 --- a/packages/runtime/src/define-agent.test.ts +++ b/packages/runtime/src/define-agent.test.ts @@ -9,6 +9,7 @@ import type { test('defineAgent brands the object and wraps the handler', () => { const agent = defineAgent({ + launchedBy: 'team-dispatcher', triggers: { github: [{ on: 'pull_request.opened' }, { on: 'issue_comment.created', match: '@mention' }], slack: [{ on: 'app_mention' }] @@ -24,8 +25,9 @@ test('defineAgent brands the object and wraps the handler', () => { assert.equal(agent.triggers?.github?.length, 2); assert.equal(agent.triggers?.slack?.[0]?.on, 'app_mention'); assert.equal(agent.schedules?.[0]?.name, 'nightly'); + assert.equal(agent.launchedBy, 'team-dispatcher'); // __workforceAgent is non-enumerable so the listener declarations serialize clean. - assert.deepEqual(Object.keys(agent).sort(), ['handler', 'schedules', 'triggers']); + assert.deepEqual(Object.keys(agent).sort(), ['handler', 'launchedBy', 'schedules', 'triggers']); }); test('defineAgent omits absent listener fields and requires a function handler', () => { diff --git a/packages/runtime/src/define-agent.ts b/packages/runtime/src/define-agent.ts index 9a2a2185..2bb5e7fa 100644 --- a/packages/runtime/src/define-agent.ts +++ b/packages/runtime/src/define-agent.ts @@ -1,4 +1,5 @@ import type { + AgentSpec, PersonaSchedule, TypedTriggerMap, WatchRule @@ -93,6 +94,12 @@ export interface AgentDefinition< Tr extends TypedTriggerMap = TypedTriggerMap, S extends readonly PersonaSchedule[] = readonly PersonaSchedule[] > { + /** + * Alternate launch path for agents without direct listeners. Team members are + * spawned by a dispatcher agent, so they intentionally declare no triggers, + * schedules, or watch rules. + */ + launchedBy?: AgentSpec['launchedBy']; /** Radio listeners: provider-keyed map of typed event triggers. */ triggers?: Tr; /** Clock listeners: cron schedules. */ @@ -110,6 +117,7 @@ export interface AgentDefinition< */ export interface WorkforceAgentExport { readonly __workforceAgent: true; + readonly launchedBy?: AgentSpec['launchedBy']; readonly triggers?: TypedTriggerMap; readonly schedules?: readonly PersonaSchedule[]; readonly watch?: readonly WatchRule[]; @@ -149,11 +157,13 @@ export function defineAgent< throw new TypeError('defineAgent({ handler }) — handler must be a function'); } const agent: { + launchedBy?: AgentSpec['launchedBy']; triggers?: TypedTriggerMap; schedules?: readonly PersonaSchedule[]; watch?: readonly WatchRule[]; handler: WorkforceHandlerExport; } = { + ...(input.launchedBy ? { launchedBy: input.launchedBy } : {}), ...(input.triggers ? { triggers: input.triggers as TypedTriggerMap } : {}), ...(input.schedules ? { schedules: input.schedules } : {}), ...(input.watch ? { watch: input.watch } : {}), From aaf6c4cf1998e30820fdba944a6d54cb21c39258 Mon Sep 17 00:00:00 2001 From: "agent-relay-code[bot]" Date: Thu, 4 Jun 2026 21:14:51 +0000 Subject: [PATCH 2/2] chore: apply pr-reviewer fixes for #201 --- packages/deploy/src/bundle.ts | 4 ++-- packages/deploy/src/deploy.test.ts | 20 ++++++++++++++++++++ packages/deploy/src/extract-agent.ts | 3 ++- packages/runtime/src/define-agent.ts | 2 +- 4 files changed, 25 insertions(+), 4 deletions(-) diff --git a/packages/deploy/src/bundle.ts b/packages/deploy/src/bundle.ts index dbc38592..f91de017 100644 --- a/packages/deploy/src/bundle.ts +++ b/packages/deploy/src/bundle.ts @@ -135,7 +135,7 @@ let agentSpec; if (exported && exported.__workforceAgent) { candidate = exported.handler; agentSpec = { - ...(exported.launchedBy ? { launchedBy: exported.launchedBy } : {}), + ...(exported.launchedBy !== undefined ? { launchedBy: exported.launchedBy } : {}), ...(exported.triggers ? { triggers: exported.triggers } : {}), ...(exported.schedules ? { schedules: exported.schedules } : {}), ...(exported.watch ? { watch: exported.watch } : {}) @@ -143,7 +143,7 @@ if (exported && exported.__workforceAgent) { } else if (exported && typeof exported.handler === 'function') { candidate = exported.handler; agentSpec = { - ...(exported.launchedBy ? { launchedBy: exported.launchedBy } : {}), + ...(exported.launchedBy !== undefined ? { launchedBy: exported.launchedBy } : {}), ...(exported.triggers ? { triggers: exported.triggers } : {}), ...(exported.schedules ? { schedules: exported.schedules } : {}), ...(exported.watch ? { watch: exported.watch } : {}) diff --git a/packages/deploy/src/deploy.test.ts b/packages/deploy/src/deploy.test.ts index af1c377e..d81d0372 100644 --- a/packages/deploy/src/deploy.test.ts +++ b/packages/deploy/src/deploy.test.ts @@ -54,6 +54,14 @@ export default defineAgent({ }); `; +const INVALID_LAUNCHED_BY_AGENT_SRC = `import { defineAgent } from '@agentworkforce/runtime'; +export default defineAgent({ + launchedBy: '', + schedules: [{ name: 'weekly', cron: '0 9 * * 6' }], + handler: async () => {} +}); +`; + const MISSING_HANDLER_AGENT_SRC = `import { defineAgent } from '@agentworkforce/runtime'; export default defineAgent({ schedules: [{ name: 'weekly', cron: '0 9 * * 6' }] @@ -272,6 +280,18 @@ test('preflightPersona accepts listener-free agents launched by the team dispatc } }); +test('preflightPersona rejects unsupported launchedBy values even when listeners exist', async () => { + const { personaPath, cleanup } = await withTempPersona( + basePersonaJson({ integrations: undefined }), + INVALID_LAUNCHED_BY_AGENT_SRC + ); + try { + await assert.rejects(preflightPersona(personaPath), /launchedBy must be one of: team-dispatcher/); + } finally { + await cleanup(); + } +}); + test('preflightPersona refuses when defineAgent omits handler', async () => { const { personaPath, cleanup } = await withTempPersona(basePersonaJson(), MISSING_HANDLER_AGENT_SRC); try { diff --git a/packages/deploy/src/extract-agent.ts b/packages/deploy/src/extract-agent.ts index 05a90189..4dabe474 100644 --- a/packages/deploy/src/extract-agent.ts +++ b/packages/deploy/src/extract-agent.ts @@ -33,9 +33,10 @@ import { * named imports without a static "no matching export" error. */ const RUNTIME_STUB = ` +const hasOwn = Object.prototype.hasOwnProperty; function defineAgent(input) { const out = {}; - if (input && input.launchedBy) out.launchedBy = input.launchedBy; + if (input && hasOwn.call(input, 'launchedBy')) out.launchedBy = input.launchedBy; if (input && input.triggers) out.triggers = input.triggers; if (input && input.schedules) out.schedules = input.schedules; if (input && input.watch) out.watch = input.watch; diff --git a/packages/runtime/src/define-agent.ts b/packages/runtime/src/define-agent.ts index 2bb5e7fa..fb09188d 100644 --- a/packages/runtime/src/define-agent.ts +++ b/packages/runtime/src/define-agent.ts @@ -163,7 +163,7 @@ export function defineAgent< watch?: readonly WatchRule[]; handler: WorkforceHandlerExport; } = { - ...(input.launchedBy ? { launchedBy: input.launchedBy } : {}), + ...(input.launchedBy !== undefined ? { launchedBy: input.launchedBy } : {}), ...(input.triggers ? { triggers: input.triggers as TypedTriggerMap } : {}), ...(input.schedules ? { schedules: input.schedules } : {}), ...(input.watch ? { watch: input.watch } : {}),