refactor(agent): remove enabled state

This commit is contained in:
RockChinQ
2026-08-25 23:27:45 +08:00
parent 22d9053bf1
commit 80f1790e1d
27 changed files with 104 additions and 190 deletions
+1 -2
View File
@@ -34,7 +34,6 @@ class Agent(Base):
kind: str # 固定为 "agent" kind: str # 固定为 "agent"
component_ref: str # AgentRunner id component_ref: str # AgentRunner id
config: dict # runner + runner_config config: dict # runner + runner_config
enabled: bool
supported_event_patterns: list[str] supported_event_patterns: list[str]
``` ```
@@ -113,7 +112,7 @@ Binding 只保存引用与路由条件。它不复制 Pipeline 或 Agent 配置
1. 忽略 `enabled = false` 的 binding。 1. 忽略 `enabled = false` 的 binding。
2. 检查 `event_pattern` 与结构化 filters。 2. 检查 `event_pattern` 与结构化 filters。
3. 校验目标存在、启用且声明支持该事件。 3. 校验目标存在且声明支持该事件。
4.`priority` 从高到低选择;同优先级按稳定列表顺序。 4.`priority` 从高到低选择;同优先级按稳定列表顺序。
5. 只执行一个响应目标。 5. 只执行一个响应目标。
@@ -70,7 +70,6 @@ class Agent(Base):
kind: str # 首版固定为 "agent" kind: str # 首版固定为 "agent"
component_ref: str # runner id / workflow id / future external ref component_ref: str # runner id / workflow id / future external ref
config: dict # runner 与 runner_config config: dict # runner 与 runner_config
enabled: bool
supported_event_patterns: list[str] supported_event_patterns: list[str]
created_at: datetime created_at: datetime
updated_at: datetime updated_at: datetime
@@ -22,7 +22,7 @@ class AgentBindingResolver:
event: AgentEventEnvelope, event: AgentEventEnvelope,
agents: list[AgentConfig], agents: list[AgentConfig],
) -> AgentBinding: ) -> AgentBinding:
"""Resolve exactly one enabled Agent for the event. """Resolve exactly one Agent for the event.
Callers that source agents from bot/workspace/global configuration must Callers that source agents from bot/workspace/global configuration must
pre-filter candidates to the event scope before calling this resolver. pre-filter candidates to the event scope before calling this resolver.
@@ -30,7 +30,7 @@ class AgentBindingResolver:
Agent and does not carry enough scope metadata to make that decision Agent and does not carry enough scope metadata to make that decision
safely here. safely here.
""" """
matches = [agent for agent in agents if agent.enabled and event.event_type in agent.event_types] matches = [agent for agent in agents if event.event_type in agent.event_types]
if not matches: if not matches:
raise AgentBindingResolutionError(f'No Agent binding matches event_type={event.event_type}') raise AgentBindingResolutionError(f'No Agent binding matches event_type={event.event_type}')
@@ -59,7 +59,6 @@ class AgentBindingResolver:
resource_policy=agent.resource_policy, resource_policy=agent.resource_policy,
state_policy=agent.state_policy, state_policy=agent.state_policy,
delivery_policy=agent.delivery_policy, delivery_policy=agent.delivery_policy,
enabled=agent.enabled,
agent_id=agent.agent_id, agent_id=agent.agent_id,
processor_type=agent.processor_type, processor_type=agent.processor_type,
processor_id=agent.processor_id or agent.agent_id, processor_id=agent.processor_id or agent.agent_id,
@@ -181,9 +181,6 @@ class AgentConfig(pydantic.BaseModel):
event_types: list[str] = pydantic.Field(default_factory=lambda: ['message.received']) event_types: list[str] = pydantic.Field(default_factory=lambda: ['message.received'])
"""Event types this Agent handles.""" """Event types this Agent handles."""
enabled: bool = True
"""Whether this Agent can be selected by a binding resolver."""
metadata: dict[str, typing.Any] = pydantic.Field(default_factory=dict) metadata: dict[str, typing.Any] = pydantic.Field(default_factory=dict)
"""Non-protocol diagnostic metadata, such as legacy config source.""" """Non-protocol diagnostic metadata, such as legacy config source."""
@@ -219,9 +216,6 @@ class AgentBinding(pydantic.BaseModel):
delivery_policy: DeliveryPolicy = pydantic.Field(default_factory=DeliveryPolicy) delivery_policy: DeliveryPolicy = pydantic.Field(default_factory=DeliveryPolicy)
"""Delivery policy.""" """Delivery policy."""
enabled: bool = True
"""Whether binding is enabled."""
agent_id: str | None = None agent_id: str | None = None
"""Host-side Agent/config identifier for this binding.""" """Host-side Agent/config identifier for this binding."""
@@ -151,7 +151,6 @@ class QueryEntryAdapter:
state_policy=state_policy, state_policy=state_policy,
delivery_policy=delivery_policy, delivery_policy=delivery_policy,
event_types=[event_type], event_types=[event_type],
enabled=True,
metadata={'source': 'pipeline_adapter'}, metadata={'source': 'pipeline_adapter'},
) )
+5 -14
View File
@@ -201,7 +201,6 @@ class AgentService:
enable_reply=False, enable_reply=False,
enable_interactions=False, enable_interactions=False,
), ),
enabled=True,
agent_id=agent_uuid, agent_id=agent_uuid,
processor_type='agent', processor_type='agent',
processor_id=agent_uuid, processor_id=agent_uuid,
@@ -293,7 +292,6 @@ class AgentService:
'kind': AGENT_KIND_AGENT, 'kind': AGENT_KIND_AGENT,
'component_ref': runner_id, 'component_ref': runner_id,
'config': config, 'config': config,
'enabled': agent_data.get('enabled', True),
'supported_event_patterns': agent_data.get('supported_event_patterns') or AGENT_DEFAULT_EVENT_PATTERNS, 'supported_event_patterns': agent_data.get('supported_event_patterns') or AGENT_DEFAULT_EVENT_PATTERNS,
} }
await self.ap.persistence_mgr.execute_async(sqlalchemy.insert(persistence_agent.Agent).values(**values)) await self.ap.persistence_mgr.execute_async(sqlalchemy.insert(persistence_agent.Agent).values(**values))
@@ -308,17 +306,11 @@ class AgentService:
await self.ap.pipeline_service.update_pipeline(context, agent_uuid, agent_data) await self.ap.pipeline_service.update_pipeline(context, agent_uuid, agent_data)
return return
update_data = agent_data.copy() update_data = {
for protected_field in ( field: agent_data[field]
'uuid', for field in ('name', 'description', 'emoji', 'config', 'supported_event_patterns')
'workspace_uuid', if field in agent_data
'kind', }
'component_ref',
'created_at',
'updated_at',
'capability',
):
update_data.pop(protected_field, None)
if 'config' in update_data: if 'config' in update_data:
config, runner_id, _ = RunnerConfigResolver.resolve_agent_runner_config(update_data['config']) config, runner_id, _ = RunnerConfigResolver.resolve_agent_runner_config(update_data['config'])
update_data['config'] = config update_data['config'] = config
@@ -425,7 +417,6 @@ class AgentService:
item = pipeline.copy() item = pipeline.copy()
item['kind'] = AGENT_KIND_PIPELINE item['kind'] = AGENT_KIND_PIPELINE
item['component_ref'] = 'pipeline' item['component_ref'] = 'pipeline'
item['enabled'] = True
item['supported_event_patterns'] = PIPELINE_EVENT_PATTERNS item['supported_event_patterns'] = PIPELINE_EVENT_PATTERNS
item['capability'] = { item['capability'] = {
'supported_event_patterns': PIPELINE_EVENT_PATTERNS, 'supported_event_patterns': PIPELINE_EVENT_PATTERNS,
-20
View File
@@ -18,7 +18,6 @@ class BotService:
ap: app.Application ap: app.Application
FAILURE_ROUTE_NOT_FOUND = 'route_not_found' FAILURE_ROUTE_NOT_FOUND = 'route_not_found'
FAILURE_PROCESSOR_DISABLED = 'processor_disabled'
FAILURE_PROCESSOR_NOT_FOUND = 'processor_not_found' FAILURE_PROCESSOR_NOT_FOUND = 'processor_not_found'
FAILURE_PROCESSOR_INCOMPATIBLE = 'processor_incompatible' FAILURE_PROCESSOR_INCOMPATIBLE = 'processor_incompatible'
FAILURE_INVALID_EVENT = 'invalid_event' FAILURE_INVALID_EVENT = 'invalid_event'
@@ -436,25 +435,6 @@ class BotService:
} }
], ],
) )
if not getattr(agent, 'enabled', True):
return self._diagnostic_result(
matched=False,
binding=selected_binding,
failure_code=self.FAILURE_PROCESSOR_DISABLED,
reason='Agent target is disabled',
diagnostic_steps=diagnostic_steps
+ [
{
'step': 'validate_processor',
'binding_id': selected_binding.get('id'),
'target_type': target_type,
'target_uuid': target_uuid,
'matched': False,
'failure_code': self.FAILURE_PROCESSOR_DISABLED,
'reason': 'Agent target is disabled',
}
],
)
if not RuntimeBot._agent_supports_event_type(getattr(agent, 'supported_event_patterns', None), event_type): if not RuntimeBot._agent_supports_event_type(getattr(agent, 'supported_event_patterns', None), event_type):
return self._diagnostic_result( return self._diagnostic_result(
matched=False, matched=False,
@@ -20,7 +20,6 @@ class Agent(Base):
kind = sqlalchemy.Column(sqlalchemy.String(50), nullable=False, default='agent') kind = sqlalchemy.Column(sqlalchemy.String(50), nullable=False, default='agent')
component_ref = sqlalchemy.Column(sqlalchemy.String(255), nullable=True) component_ref = sqlalchemy.Column(sqlalchemy.String(255), nullable=True)
config = sqlalchemy.Column(sqlalchemy.JSON, nullable=False, default={}) config = sqlalchemy.Column(sqlalchemy.JSON, nullable=False, default={})
enabled = sqlalchemy.Column(sqlalchemy.Boolean, nullable=False, default=True)
supported_event_patterns = sqlalchemy.Column(sqlalchemy.JSON, nullable=False, default=['*']) supported_event_patterns = sqlalchemy.Column(sqlalchemy.JSON, nullable=False, default=['*'])
created_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False, server_default=sqlalchemy.func.now()) created_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False, server_default=sqlalchemy.func.now())
updated_at = sqlalchemy.Column( updated_at = sqlalchemy.Column(
@@ -0,0 +1,37 @@
"""Drop the obsolete Agent enabled state.
Revision ID: 0023_drop_agent_enabled
Revises: 0022_merge_agent_reasoning_heads
Create Date: 2026-08-25
"""
from __future__ import annotations
from alembic import op
import sqlalchemy as sa
revision = '0023_drop_agent_enabled'
down_revision = '0022_merge_agent_reasoning_heads'
branch_labels = None
depends_on = None
def _column_exists(inspector: sa.Inspector, table_name: str, column_name: str) -> bool:
if table_name not in inspector.get_table_names():
return False
return any(column['name'] == column_name for column in inspector.get_columns(table_name))
def upgrade() -> None:
inspector = sa.inspect(op.get_bind())
if _column_exists(inspector, 'agents', 'enabled'):
with op.batch_alter_table('agents') as batch_op:
batch_op.drop_column('enabled')
def downgrade() -> None:
inspector = sa.inspect(op.get_bind())
if 'agents' in inspector.get_table_names() and not _column_exists(inspector, 'agents', 'enabled'):
with op.batch_alter_table('agents') as batch_op:
batch_op.add_column(sa.Column('enabled', sa.Boolean(), nullable=False, server_default=sa.true()))
+3 -19
View File
@@ -1135,7 +1135,6 @@ class RuntimeBot:
enable_reply=True, enable_reply=True,
enable_interactions=True, enable_interactions=True,
), ),
enabled=True,
agent_id=agent.get('uuid'), agent_id=agent.get('uuid'),
processor_type='agent', processor_type='agent',
processor_id=agent.get('uuid'), processor_id=agent.get('uuid'),
@@ -1322,17 +1321,6 @@ class RuntimeBot:
reason='Agent target not found', reason='Agent target not found',
text=f'EBA event {event_type} target agent not found: {target_uuid}', text=f'EBA event {event_type} target agent not found: {target_uuid}',
) )
if not agent.get('enabled', True):
return await self._record_event_route_trace(
event_type=event_type,
status='failed',
binding=event_binding,
target_type=target_type,
target_uuid=target_uuid,
failure_code='processor_disabled',
reason='Agent target is disabled',
text=f'EBA event {event_type} target agent disabled: {target_uuid}',
)
if not self._agent_supports_event_type(agent.get('supported_event_patterns'), event_type): if not self._agent_supports_event_type(agent.get('supported_event_patterns'), event_type):
return await self._record_event_route_trace( return await self._record_event_route_trace(
event_type=event_type, event_type=event_type,
@@ -1711,7 +1699,7 @@ class RuntimeBot:
self.execution_context, self.execution_context,
record['processor_id'], record['processor_id'],
) )
if not agent or agent.get('kind') != 'agent' or not agent.get('enabled', True): if not agent or agent.get('kind') != 'agent':
raise ValueError(f'Interaction target Agent is unavailable: {record["processor_id"]}') raise ValueError(f'Interaction target Agent is unavailable: {record["processor_id"]}')
binding = self._agent_product_to_binding( binding = self._agent_product_to_binding(
@@ -1810,9 +1798,7 @@ class RuntimeBot:
def tenant_scoped_listener(listener): def tenant_scoped_listener(listener):
@functools.wraps(listener) @functools.wraps(listener)
async def wrapped(*args, **kwargs): async def wrapped(*args, **kwargs):
tenant_scope = getattr( tenant_scope = getattr(self.ap.persistence_mgr, 'tenant_scope', None)
self.ap.persistence_mgr, 'tenant_scope', None
)
cloud_runtime = ( cloud_runtime = (
getattr( getattr(
getattr(self.ap.persistence_mgr, 'mode', None), getattr(self.ap.persistence_mgr, 'mode', None),
@@ -1823,9 +1809,7 @@ class RuntimeBot:
) )
if cloud_runtime: if cloud_runtime:
if not callable(tenant_scope): if not callable(tenant_scope):
raise RuntimeError( raise RuntimeError('Cloud platform callbacks require a tenant scope')
'Cloud platform callbacks require a tenant scope'
)
async with tenant_scope(self.workspace_uuid): async with tenant_scope(self.workspace_uuid):
return await listener(*args, **kwargs) return await listener(*args, **kwargs)
return await listener(*args, **kwargs) return await listener(*args, **kwargs)
@@ -108,7 +108,7 @@ class TestSQLiteMigrationUpgrade:
await run_alembic_upgrade(sqlite_engine, 'head') await run_alembic_upgrade(sqlite_engine, 'head')
assert await get_alembic_current(sqlite_engine) == _get_script_head() assert await get_alembic_current(sqlite_engine) == _get_script_head()
assert _get_script_head() == '0022_merge_agent_reasoning_heads' assert _get_script_head() == '0023_drop_agent_enabled'
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_upgrade_from_development_workspace_head_to_merged_head(self, sqlite_engine): async def test_upgrade_from_development_workspace_head_to_merged_head(self, sqlite_engine):
@@ -120,7 +120,7 @@ class TestSQLiteMigrationUpgrade:
await run_alembic_upgrade(sqlite_engine, 'head') await run_alembic_upgrade(sqlite_engine, 'head')
assert await get_alembic_current(sqlite_engine) == _get_script_head() assert await get_alembic_current(sqlite_engine) == _get_script_head()
assert _get_script_head() == '0022_merge_agent_reasoning_heads' assert _get_script_head() == '0023_drop_agent_enabled'
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_upgrade_from_reasoning_config_head_to_merged_head(self, sqlite_engine): async def test_upgrade_from_reasoning_config_head_to_merged_head(self, sqlite_engine):
@@ -131,7 +131,23 @@ class TestSQLiteMigrationUpgrade:
await run_alembic_stamp(sqlite_engine, '0018_llm_reasoning_config') await run_alembic_stamp(sqlite_engine, '0018_llm_reasoning_config')
await run_alembic_upgrade(sqlite_engine, 'head') await run_alembic_upgrade(sqlite_engine, 'head')
assert await get_alembic_current(sqlite_engine) == '0022_merge_agent_reasoning_heads' assert await get_alembic_current(sqlite_engine) == '0023_drop_agent_enabled'
@pytest.mark.asyncio
async def test_upgrade_removes_agent_enabled_column(self, sqlite_engine):
async with sqlite_engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
await conn.exec_driver_sql('ALTER TABLE agents ADD COLUMN enabled BOOLEAN NOT NULL DEFAULT 1')
await run_alembic_stamp(sqlite_engine, '0022_merge_agent_reasoning_heads')
await run_alembic_upgrade(sqlite_engine, 'head')
async with sqlite_engine.connect() as conn:
columns = await conn.run_sync(
lambda sync_conn: {column['name'] for column in sqlalchemy.inspect(sync_conn).get_columns('agents')}
)
assert 'enabled' not in columns
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_upgrade_from_baseline_to_head(self, sqlite_engine): async def test_upgrade_from_baseline_to_head(self, sqlite_engine):
@@ -74,7 +74,6 @@ class TestContextValidation:
runner_id='plugin:test/plugin/runner', runner_id='plugin:test/plugin/runner',
runner_config={'timeout': 300}, runner_config={'timeout': 300},
agent_id='pipeline_1', agent_id='pipeline_1',
enabled=True,
) )
def _make_resources(self) -> BuilderResources: def _make_resources(self) -> BuilderResources:
@@ -1069,7 +1069,6 @@ class TestQueryEntrySessionQueryId:
resource_policy=ResourcePolicy(), resource_policy=ResourcePolicy(),
state_policy=StatePolicy(enable_state=False, state_scopes=[]), state_policy=StatePolicy(enable_state=False, state_scopes=[]),
delivery_policy=DeliveryPolicy(enable_streaming=True, enable_reply=True), delivery_policy=DeliveryPolicy(enable_streaming=True, enable_reply=True),
enabled=True,
) )
messages = [ messages = [
@@ -46,7 +46,6 @@ def _agent_row(
'runner': {'id': 'plugin:test/runner/default', 'expire-time': 0}, 'runner': {'id': 'plugin:test/runner/default', 'expire-time': 0},
'runner_config': {'plugin:test/runner/default': {'temperature': 0.2}}, 'runner_config': {'plugin:test/runner/default': {'temperature': 0.2}},
}, },
enabled=True,
supported_event_patterns=supported_event_patterns or ['*'], supported_event_patterns=supported_event_patterns or ['*'],
created_at=dt.datetime(2026, 1, 1, 9, 0, 0), created_at=dt.datetime(2026, 1, 1, 9, 0, 0),
updated_at=updated_at or dt.datetime(2026, 1, 1, 10, 0, 0), updated_at=updated_at or dt.datetime(2026, 1, 1, 10, 0, 0),
@@ -63,7 +62,6 @@ def _serialize_agent(model_cls, entity, masked_columns=None):
'kind': entity.kind, 'kind': entity.kind,
'component_ref': entity.component_ref, 'component_ref': entity.component_ref,
'config': entity.config, 'config': entity.config,
'enabled': entity.enabled,
'supported_event_patterns': entity.supported_event_patterns, 'supported_event_patterns': entity.supported_event_patterns,
'created_at': entity.created_at, 'created_at': entity.created_at,
'updated_at': entity.updated_at, 'updated_at': entity.updated_at,
@@ -145,7 +143,6 @@ class TestAgentServiceDebug:
return_value={ return_value={
'uuid': 'agent-1', 'uuid': 'agent-1',
'kind': AGENT_KIND_AGENT, 'kind': AGENT_KIND_AGENT,
'enabled': True,
'supported_event_patterns': ['*'], 'supported_event_patterns': ['*'],
'config': _agent_row().config, 'config': _agent_row().config,
} }
@@ -288,7 +285,7 @@ class TestAgentServiceListAndLookup:
result = await AgentService(app).get_agent(WORKSPACE_UUID, 'pipeline-1') result = await AgentService(app).get_agent(WORKSPACE_UUID, 'pipeline-1')
assert result['kind'] == AGENT_KIND_PIPELINE assert result['kind'] == AGENT_KIND_PIPELINE
assert result['enabled'] is True assert 'enabled' not in result
assert result['config'] == {'ai': {'runner': {'id': 'pipeline-runner'}}} assert result['config'] == {'ai': {'runner': {'id': 'pipeline-runner'}}}
assert result['capability']['message_only'] is True assert result['capability']['message_only'] is True
@@ -329,7 +326,7 @@ class TestAgentServiceCreateUpdateDelete:
'runner': {'id': runner.id, 'expire-time': 0}, 'runner': {'id': runner.id, 'expire-time': 0},
'runner_config': {runner.id: {'model': 'gpt-4.1', 'temperature': 0.2}}, 'runner_config': {runner.id: {'model': 'gpt-4.1', 'temperature': 0.2}},
} }
assert insert_values['enabled'] is True assert 'enabled' not in insert_values
assert insert_values['supported_event_patterns'] == AGENT_DEFAULT_EVENT_PATTERNS assert insert_values['supported_event_patterns'] == AGENT_DEFAULT_EVENT_PATTERNS
app.pipeline_service._get_default_values_from_schema.assert_called_once_with(runner.config_schema) app.pipeline_service._get_default_values_from_schema.assert_called_once_with(runner.config_schema)
@@ -81,9 +81,9 @@ class TestEventRouteTrace:
'target_type': 'agent', 'target_type': 'agent',
'target_uuid': 'agent-1', 'target_uuid': 'agent-1',
}, },
failure_code='processor_disabled', failure_code='processor_not_found',
reason='Agent target is disabled', reason='Agent target is unavailable',
text='disabled', text='unavailable',
) )
bot.logger.warning.assert_awaited_once() bot.logger.warning.assert_awaited_once()
@@ -24,7 +24,6 @@ import DynamicFormComponent from '@/app/home/components/dynamic-form/DynamicForm
import { extractI18nObject } from '@/i18n/I18nProvider'; import { extractI18nObject } from '@/i18n/I18nProvider';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Switch } from '@/components/ui/switch';
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from '@/components/ui/textarea';
import { import {
Card, Card,
@@ -141,7 +140,6 @@ function AgentFormComponent(
name: z.string().min(1, { message: t('agents.nameRequired') }), name: z.string().min(1, { message: t('agents.nameRequired') }),
description: z.string().optional(), description: z.string().optional(),
emoji: z.string().optional(), emoji: z.string().optional(),
enabled: z.boolean().optional(),
}), }),
runner: z.record(z.string(), z.any()), runner: z.record(z.string(), z.any()),
runner_config: z.record(z.string(), z.any()), runner_config: z.record(z.string(), z.any()),
@@ -156,7 +154,6 @@ function AgentFormComponent(
name: '', name: '',
description: '', description: '',
emoji: '🤖', emoji: '🤖',
enabled: true,
}, },
runner: {}, runner: {},
runner_config: {}, runner_config: {},
@@ -190,7 +187,6 @@ function AgentFormComponent(
name: agent.name ?? '', name: agent.name ?? '',
description: agent.description ?? '', description: agent.description ?? '',
emoji: agent.emoji || '🤖', emoji: agent.emoji || '🤖',
enabled: agent.enabled ?? true,
}, },
runner: (config.runner as Record<string, unknown>) ?? {}, runner: (config.runner as Record<string, unknown>) ?? {},
runner_config: runner_config:
@@ -287,12 +283,12 @@ function AgentFormComponent(
: t('pipelines.configuration'), : t('pipelines.configuration'),
icon: SlidersHorizontal, icon: SlidersHorizontal,
}, },
{
name: 'basic',
label: t('common.management'),
icon: Power,
},
]; ];
const managementSection = {
name: 'basic' as const,
label: t('common.management'),
icon: Power,
};
const runnerStatus = useMemo<AgentRunnerStatus>(() => { const runnerStatus = useMemo<AgentRunnerStatus>(() => {
if (pluginStatusLoading) { if (pluginStatusLoading) {
@@ -465,7 +461,6 @@ function AgentFormComponent(
name: values.basic.name, name: values.basic.name,
description: values.basic.description ?? '', description: values.basic.description ?? '',
emoji: values.basic.emoji, emoji: values.basic.emoji,
enabled: values.basic.enabled ?? true,
component_ref: (runner.id as string) || null, component_ref: (runner.id as string) || null,
supported_event_patterns: normalizeEventPatterns( supported_event_patterns: normalizeEventPatterns(
values.supported_event_patterns_text, values.supported_event_patterns_text,
@@ -568,11 +563,19 @@ function AgentFormComponent(
} }
> >
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<TabsList className="grid min-w-[34rem] w-full grid-cols-3"> <TabsList className="grid min-w-[42rem] w-full grid-cols-[repeat(3,minmax(0,1fr))_auto]">
{primarySections.map((section) => { {primarySections.map((section) => {
const Icon = section.icon; const Icon = section.icon;
return ( return (
<TabsTrigger key={section.name} value={section.name}> <TabsTrigger
key={section.name}
value={section.name}
className={
section.name === 'basic'
? 'px-4 text-muted-foreground data-[state=active]:text-foreground'
: undefined
}
>
<Icon /> <Icon />
{section.label} {section.label}
</TabsTrigger> </TabsTrigger>
@@ -580,21 +583,6 @@ function AgentFormComponent(
})} })}
</TabsList> </TabsList>
</div> </div>
<div className="flex justify-end pt-2">
<Button
type="button"
variant={
activeSection === managementSection.name
? 'secondary'
: 'ghost'
}
size="sm"
onClick={() => setActiveSection(managementSection.name)}
>
<Power />
{managementSection.label}
</Button>
</div>
</Tabs> </Tabs>
</nav> </nav>
@@ -671,40 +659,6 @@ function AgentFormComponent(
{activeSection === 'basic' && ( {activeSection === 'basic' && (
<div className="space-y-6"> <div className="space-y-6">
<Card>
<CardHeader>
<CardTitle>{t('agents.availability')}</CardTitle>
<CardDescription>
{t('agents.availabilityDescription')}
</CardDescription>
</CardHeader>
<CardContent>
<FormField
control={form.control}
name="basic.enabled"
render={({ field }) => (
<FormItem className="flex items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<FormLabel className="flex items-center gap-2">
<Power className="size-4" />
{t('agents.enabled')}
</FormLabel>
<FormDescription>
{t('agents.enabledDescription')}
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value ?? true}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
</CardContent>
</Card>
<Card className="border-destructive/50"> <Card className="border-destructive/50">
<CardHeader> <CardHeader>
<CardTitle className="text-destructive"> <CardTitle className="text-destructive">
-1
View File
@@ -177,7 +177,6 @@ export interface Agent {
kind: AgentKind; kind: AgentKind;
component_ref?: string | null; component_ref?: string | null;
config?: Record<string, unknown>; config?: Record<string, unknown>;
enabled?: boolean;
supported_event_patterns?: string[]; supported_event_patterns?: string[];
capability?: AgentCapability; capability?: AgentCapability;
created_at?: string; created_at?: string;
-1
View File
@@ -839,7 +839,6 @@ export default function WizardPage() {
runner: { id: selectedRunner, 'expire-time': 0 }, runner: { id: selectedRunner, 'expire-time': 0 },
runner_config: { [selectedRunner]: runnerConfig }, runner_config: { [selectedRunner]: runnerConfig },
}, },
enabled: true,
supported_event_patterns: [selectedScenarioDefinition.eventType], supported_event_patterns: [selectedScenarioDefinition.eventType],
}); });
processorUuid = agentResp.uuid; processorUuid = agentResp.uuid;
+1 -8
View File
@@ -469,7 +469,6 @@ const enUS = {
processor_incompatible: processor_incompatible:
'The selected processor cannot handle this event.', 'The selected processor cannot handle this event.',
processor_not_found: 'The selected processor is unavailable.', processor_not_found: 'The selected processor is unavailable.',
processor_disabled: 'The selected processor is disabled.',
bot_runtime_unavailable: bot_runtime_unavailable:
'The bot is not running. Check its platform settings and enable it before running a full test.', 'The bot is not running. Check its platform settings and enable it before running a full test.',
runner_failed: 'The Agent runner failed while processing the event.', runner_failed: 'The Agent runner failed while processing the event.',
@@ -691,10 +690,7 @@ const enUS = {
allEvents: 'Supports all events', allEvents: 'Supports all events',
messageEventsOnly: 'Message events only', messageEventsOnly: 'Message events only',
basicInfo: 'Basic Information', basicInfo: 'Basic Information',
basicInfoDescription: 'Set the name, icon, description and enabled state', basicInfoDescription: 'Set the name, icon and description',
availability: 'Availability',
availabilityDescription:
'Control whether this Agent can receive and process events.',
runnerSettings: 'Runner', runnerSettings: 'Runner',
advanced: 'Advanced', advanced: 'Advanced',
bindableEvents: 'Bindable Event Range', bindableEvents: 'Bindable Event Range',
@@ -703,9 +699,6 @@ const enUS = {
supportedEvents: 'Event Range', supportedEvents: 'Event Range',
supportedEventsDescription: supportedEventsDescription:
'Use one event pattern per line, for example *, message.received, group.*. Pipelines are fixed to message.*.', 'Use one event pattern per line, for example *, message.received, group.*. Pipelines are fixed to message.*.',
enabled: 'Enable Agent',
enabledDescription:
'When disabled, this Agent should not be selected by event routing.',
nameRequired: 'Name cannot be empty', nameRequired: 'Name cannot be empty',
createSuccess: 'Created successfully', createSuccess: 'Created successfully',
createError: 'Creation failed: ', createError: 'Creation failed: ',
+1 -5
View File
@@ -525,8 +525,7 @@ const esES = {
allEvents: 'Compatible con todos los eventos', allEvents: 'Compatible con todos los eventos',
messageEventsOnly: 'Solo eventos de mensaje', messageEventsOnly: 'Solo eventos de mensaje',
basicInfo: 'Información básica', basicInfo: 'Información básica',
basicInfoDescription: basicInfoDescription: 'Establece el nombre, icono y descripción',
'Establece el nombre, icono, descripción y estado de habilitación',
runnerSettings: 'Runner', runnerSettings: 'Runner',
advanced: 'Avanzado', advanced: 'Avanzado',
bindableEvents: 'Rango de eventos vinculables', bindableEvents: 'Rango de eventos vinculables',
@@ -535,9 +534,6 @@ const esES = {
supportedEvents: 'Rango de eventos', supportedEvents: 'Rango de eventos',
supportedEventsDescription: supportedEventsDescription:
'Usa un patrón de evento por línea, por ejemplo *, message.received, group.*. Los Pipelines están fijos en message.*.', 'Usa un patrón de evento por línea, por ejemplo *, message.received, group.*. Los Pipelines están fijos en message.*.',
enabled: 'Habilitar Agent',
enabledDescription:
'Cuando está deshabilitado, este Agent no debe ser seleccionado por el enrutamiento de eventos.',
nameRequired: 'El nombre no puede estar vacío', nameRequired: 'El nombre no puede estar vacío',
createSuccess: 'Creado correctamente', createSuccess: 'Creado correctamente',
createError: 'Error al crear: ', createError: 'Error al crear: ',
+1 -8
View File
@@ -476,7 +476,6 @@ const jaJP = {
processor_incompatible: processor_incompatible:
'選択したプロセッサーはこのイベントを処理できません。', '選択したプロセッサーはこのイベントを処理できません。',
processor_not_found: '選択したプロセッサーを利用できません。', processor_not_found: '選択したプロセッサーを利用できません。',
processor_disabled: '選択したプロセッサーは無効です。',
bot_runtime_unavailable: bot_runtime_unavailable:
'ボットが実行されていません。プラットフォーム設定を確認してボットを有効にした後、完全テストを実行してください。', 'ボットが実行されていません。プラットフォーム設定を確認してボットを有効にした後、完全テストを実行してください。',
runner_failed: 'Agent Runner がイベント処理中に失敗しました。', runner_failed: 'Agent Runner がイベント処理中に失敗しました。',
@@ -706,10 +705,7 @@ const jaJP = {
allEvents: 'すべてのイベントに対応', allEvents: 'すべてのイベントに対応',
messageEventsOnly: 'メッセージイベントのみ', messageEventsOnly: 'メッセージイベントのみ',
basicInfo: '基本情報', basicInfo: '基本情報',
basicInfoDescription: '名前、アイコン、説明、有効状態を設定します', basicInfoDescription: '名前、アイコン、説明を設定します',
availability: '有効状態',
availabilityDescription:
'この Agent がイベントを受信して処理できるかを制御します。',
runnerSettings: 'Runner', runnerSettings: 'Runner',
advanced: '詳細', advanced: '詳細',
bindableEvents: '紐付け可能なイベント範囲', bindableEvents: '紐付け可能なイベント範囲',
@@ -718,9 +714,6 @@ const jaJP = {
supportedEvents: 'イベント範囲', supportedEvents: 'イベント範囲',
supportedEventsDescription: supportedEventsDescription:
'1 行に 1 つのイベントパターンを指定します。例: *、message.received、group.*。Pipeline は message.* 固定です。', '1 行に 1 つのイベントパターンを指定します。例: *、message.received、group.*。Pipeline は message.* 固定です。',
enabled: 'Agent を有効化',
enabledDescription:
'無効化すると、この Agent はイベントルーティングで選択されません。',
nameRequired: '名前は必須です', nameRequired: '名前は必須です',
createSuccess: '作成に成功しました', createSuccess: '作成に成功しました',
createError: '作成に失敗しました:', createError: '作成に失敗しました:',
+1 -4
View File
@@ -522,7 +522,7 @@ const ruRU = {
allEvents: 'Поддерживает все события', allEvents: 'Поддерживает все события',
messageEventsOnly: 'Только события сообщений', messageEventsOnly: 'Только события сообщений',
basicInfo: 'Основная информация', basicInfo: 'Основная информация',
basicInfoDescription: 'Задайте имя, иконку, описание и статус активации', basicInfoDescription: 'Задайте имя, иконку и описание',
runnerSettings: 'Runner', runnerSettings: 'Runner',
advanced: 'Дополнительно', advanced: 'Дополнительно',
bindableEvents: 'Диапазон привязываемых событий', bindableEvents: 'Диапазон привязываемых событий',
@@ -531,9 +531,6 @@ const ruRU = {
supportedEvents: 'Диапазон событий', supportedEvents: 'Диапазон событий',
supportedEventsDescription: supportedEventsDescription:
'Один шаблон события в строке, например *, message.received, group.*. Pipeline фиксирован на message.*.', 'Один шаблон события в строке, например *, message.received, group.*. Pipeline фиксирован на message.*.',
enabled: 'Включить Agent',
enabledDescription:
'При отключении этот Agent не должен выбираться маршрутизацией событий.',
nameRequired: 'Имя не может быть пустым', nameRequired: 'Имя не может быть пустым',
createSuccess: 'Успешно создано', createSuccess: 'Успешно создано',
createError: 'Ошибка создания: ', createError: 'Ошибка создания: ',
+1 -4
View File
@@ -508,7 +508,7 @@ const thTH = {
allEvents: 'รองรับทุกเหตุการณ์', allEvents: 'รองรับทุกเหตุการณ์',
messageEventsOnly: 'เฉพาะเหตุการณ์ข้อความ', messageEventsOnly: 'เฉพาะเหตุการณ์ข้อความ',
basicInfo: 'ข้อมูลพื้นฐาน', basicInfo: 'ข้อมูลพื้นฐาน',
basicInfoDescription: 'ตั้งชื่อ ไอคอน คำอธิบาย และสถานะการเปิดใช้งาน', basicInfoDescription: 'ตั้งชื่อ ไอคอน และคำอธิบาย',
runnerSettings: 'Runner', runnerSettings: 'Runner',
advanced: 'ขั้นสูง', advanced: 'ขั้นสูง',
bindableEvents: 'ช่วงเหตุการณ์ที่ผูกได้', bindableEvents: 'ช่วงเหตุการณ์ที่ผูกได้',
@@ -517,9 +517,6 @@ const thTH = {
supportedEvents: 'ช่วงเหตุการณ์', supportedEvents: 'ช่วงเหตุการณ์',
supportedEventsDescription: supportedEventsDescription:
'หนึ่งรูปแบบเหตุการณ์ต่อบรรทัด เช่น *, message.received, group.* Pipeline ถูกกำหนดไว้ที่ message.*', 'หนึ่งรูปแบบเหตุการณ์ต่อบรรทัด เช่น *, message.received, group.* Pipeline ถูกกำหนดไว้ที่ message.*',
enabled: 'เปิดใช้งาน Agent',
enabledDescription:
'เมื่อปิดใช้งาน Agent นี้จะไม่ถูกเลือกโดยการกำหนดเส้นทางเหตุการณ์',
nameRequired: 'ชื่อต้องไม่ว่างเปล่า', nameRequired: 'ชื่อต้องไม่ว่างเปล่า',
createSuccess: 'สร้างสำเร็จ', createSuccess: 'สร้างสำเร็จ',
createError: 'สร้างล้มเหลว: ', createError: 'สร้างล้มเหลว: ',
+1 -4
View File
@@ -518,7 +518,7 @@ const viVN = {
allEvents: 'Hỗ trợ tất cả sự kiện', allEvents: 'Hỗ trợ tất cả sự kiện',
messageEventsOnly: 'Chỉ sự kiện tin nhắn', messageEventsOnly: 'Chỉ sự kiện tin nhắn',
basicInfo: 'Thông tin cơ bản', basicInfo: 'Thông tin cơ bản',
basicInfoDescription: 'Đặt tên, biểu tượng, mô tả và trạng thái kích hoạt', basicInfoDescription: 'Đặt tên, biểu tượng và mô tả',
runnerSettings: 'Runner', runnerSettings: 'Runner',
advanced: 'Nâng cao', advanced: 'Nâng cao',
bindableEvents: 'Phạm vi sự kiện có thể gắn', bindableEvents: 'Phạm vi sự kiện có thể gắn',
@@ -527,9 +527,6 @@ const viVN = {
supportedEvents: 'Phạm vi sự kiện', supportedEvents: 'Phạm vi sự kiện',
supportedEventsDescription: supportedEventsDescription:
'Mỗi dòng một mẫu sự kiện, ví dụ *, message.received, group.*. Pipeline cố định ở message.*.', 'Mỗi dòng một mẫu sự kiện, ví dụ *, message.received, group.*. Pipeline cố định ở message.*.',
enabled: 'Kích hoạt Agent',
enabledDescription:
'Khi bị tắt, Agent này sẽ không được định tuyến sự kiện chọn.',
nameRequired: 'Tên không được để trống', nameRequired: 'Tên không được để trống',
createSuccess: 'Tạo thành công', createSuccess: 'Tạo thành công',
createError: 'Tạo thất bại: ', createError: 'Tạo thất bại: ',
+1 -6
View File
@@ -447,7 +447,6 @@ const zhHans = {
route_not_found: '没有路由命中此事件。', route_not_found: '没有路由命中此事件。',
processor_incompatible: '所选处理器无法处理此事件。', processor_incompatible: '所选处理器无法处理此事件。',
processor_not_found: '所选处理器不可用。', processor_not_found: '所选处理器不可用。',
processor_disabled: '所选处理器已禁用。',
bot_runtime_unavailable: bot_runtime_unavailable:
'机器人尚未运行。请检查平台配置并启用机器人,再运行完整测试。', '机器人尚未运行。请检查平台配置并启用机器人,再运行完整测试。',
runner_failed: 'Agent Runner 处理事件时失败。', runner_failed: 'Agent Runner 处理事件时失败。',
@@ -661,9 +660,7 @@ const zhHans = {
allEvents: '支持全部事件', allEvents: '支持全部事件',
messageEventsOnly: '仅支持消息事件', messageEventsOnly: '仅支持消息事件',
basicInfo: '基础信息', basicInfo: '基础信息',
basicInfoDescription: '设置名称、图标描述和启用状态', basicInfoDescription: '设置名称、图标描述',
availability: '启用状态',
availabilityDescription: '控制此 Agent 是否可以接收并处理事件。',
runnerSettings: '运行器', runnerSettings: '运行器',
advanced: '高级', advanced: '高级',
bindableEvents: '可绑定事件范围', bindableEvents: '可绑定事件范围',
@@ -672,8 +669,6 @@ const zhHans = {
supportedEvents: '事件范围', supportedEvents: '事件范围',
supportedEventsDescription: supportedEventsDescription:
'每行一个事件模式,例如 *、message.received、group.*。Pipeline 固定仅支持 message.*。', '每行一个事件模式,例如 *、message.received、group.*。Pipeline 固定仅支持 message.*。',
enabled: '启用 Agent',
enabledDescription: '禁用后,此 Agent 不应被事件路由选中。',
nameRequired: '名称不能为空', nameRequired: '名称不能为空',
createSuccess: '创建成功', createSuccess: '创建成功',
createError: '创建失败:', createError: '创建失败:',
+1 -3
View File
@@ -491,7 +491,7 @@ const zhHant = {
allEvents: '支援全部事件', allEvents: '支援全部事件',
messageEventsOnly: '僅支援訊息事件', messageEventsOnly: '僅支援訊息事件',
basicInfo: '基本資訊', basicInfo: '基本資訊',
basicInfoDescription: '設定名稱、圖示描述和啟用狀態', basicInfoDescription: '設定名稱、圖示描述',
runnerSettings: '執行器', runnerSettings: '執行器',
advanced: '進階', advanced: '進階',
bindableEvents: '可綁定事件範圍', bindableEvents: '可綁定事件範圍',
@@ -500,8 +500,6 @@ const zhHant = {
supportedEvents: '事件範圍', supportedEvents: '事件範圍',
supportedEventsDescription: supportedEventsDescription:
'每行一個事件模式,例如 *、message.received、group.*。Pipeline 固定僅支援 message.*。', '每行一個事件模式,例如 *、message.received、group.*。Pipeline 固定僅支援 message.*。',
enabled: '啟用 Agent',
enabledDescription: '停用後,此 Agent 不應被事件路由選中。',
nameRequired: '名稱不能為空', nameRequired: '名稱不能為空',
createSuccess: '建立成功', createSuccess: '建立成功',
createError: '建立失敗:', createError: '建立失敗:',
@@ -50,11 +50,9 @@ test.describe('processor detail workbench', () => {
); );
await expect(flow.getByRole('tab').nth(1)).toContainText('Runner'); await expect(flow.getByRole('tab').nth(1)).toContainText('Runner');
await expect(flow.getByRole('tab').nth(2)).toContainText('Local Agent'); await expect(flow.getByRole('tab').nth(2)).toContainText('Local Agent');
const agentManagement = configPanel.getByRole('button', { const agentManagement = flow.getByRole('tab').nth(3);
name: 'Management', await expect(agentManagement).toContainText('Management');
}); await expect(agentManagement).toHaveClass(/text-muted-foreground/);
await expect(agentManagement).toBeVisible();
await expect(flow.getByText('Management')).toHaveCount(0);
await expect( await expect(
page.getByRole('heading', { name: /agent-workbench/ }), page.getByRole('heading', { name: /agent-workbench/ }),
@@ -85,6 +83,12 @@ test.describe('processor detail workbench', () => {
await expect( await expect(
configPanel.getByText('Local Agent', { exact: true }).last(), configPanel.getByText('Local Agent', { exact: true }).last(),
).toBeVisible(); ).toBeVisible();
await agentManagement.click();
await expect(configPanel.getByText('Danger Zone')).toBeVisible();
await expect(configPanel.getByText('Availability')).toHaveCount(0);
await expect(
configPanel.getByRole('switch', { name: 'Enable Agent' }),
).toHaveCount(0);
}); });
test('agent saves edits before debugging and shows the real output', async ({ test('agent saves edits before debugging and shows the real output', async ({