Skip to content

feat(smithy,smithy-web): allow disabling agents (#58) - #60

Merged
notadamking merged 14 commits into
stoneforge-ai:masterfrom
komoreka:feat/agent-disable
May 4, 2026
Merged

notadamking merged 14 commits into
stoneforge-ai:masterfrom
komoreka:feat/agent-disable

Conversation

@komoreka

Copy link
Copy Markdown
Contributor

Summary

Closes #58. Adds a disabled flag on agent metadata plus the matching CLI commands and UI controls so an agent can be parked (excluded from dispatch, pool selection, scheduler triggers, and the Start action) without being removed from the agent list. In-flight sessions are NOT terminated when an agent is disabled; only future work is blocked.

Backend

  • disabled?: boolean on BaseAgentMetadata (default omitted, treated as enabled).
  • isAgentDisabled / isAgentEnabled helpers on the registry.
  • getAvailableDirector skips disabled directors even when their session is running.
  • dispatch-daemon skips disabled agents at all candidate-selection sites in pollWorkerAvailability, pollInboxes, pollWorkflowTasks, and recoverOrphanedAssignments. Attribution-only getAgentMetadata reads (task creator, director identity for branch propagation, fallback chain resolution) are deliberately not filtered.
  • steward-scheduler.registerSteward returns false and registers no triggers for disabled stewards. registerAllStewards inherits the filter through the existing call chain.
  • PATCH /api/agents/:id accepts disabled?: boolean. Toggling on a steward triggers an immediate scheduler register/unregister so behavior changes without an orchestrator restart.

CLI

  • sf agent disable <id> and sf agent enable <id> flip the flag through updateAgentMetadata. Enable deletes the property so the absent-means-enabled contract holds in the JSON-serialised metadata.
  • sf agent start <id> refuses with Agent <id> is disabled. Run 'sf agent enable <id>' first to bring it back online. and exits with ExitCode.VALIDATION.
  • sf agent list appends (disabled) to the status column for affected agents (table output only; JSON serialisation is unchanged because the field passes through with the metadata).

Web UI (smithy-web)

  • useToggleAgentDisabled hook (mirrors useChangeAgentTriggers).
  • AgentCard: card dims to 60% opacity and shows a yellow Disabled pill in the header when set; menu item toggles between Disable agent and Enable agent (with Power / PowerOff icons); Start button is hidden while disabled (the badge plus the menu item surface the state and the action).
  • BaseAgentMetadata.disabled propagated through the web types.

Out of scope

  • Smithy-next (the scaffold-stage UI): no UI for disable yet. Worth a follow-up issue.
  • Bulk operations (sf agent disable --all --role steward).
  • A --reason annotation persisted as disabledReason on metadata.
  • Auto-disable on repeated failure (separate concern: needs a circuit-breaker design).

Test plan

  • cd packages/smithy && bun test src — 1559 pass, 19 skip, 0 fail.
  • NODE_ENV=development npx turbo run typecheck — 16/16 packages.
  • NODE_ENV=development pnpm build — 13/13 packages.
  • Manual CLI: sf agent disable <id> then sf agent list shows (disabled); sf agent start refuses with the hinted command; sf agent enable clears the marker and sf agent start works again.
  • Manual UI: AgentCard menu Disable/Enable round-trips; the Disabled pill, dimmed card, and hidden Start button appear and disappear correctly; toggling a steward live updates the scheduler without sf serve smithy restart.
  • Behavioural unit tests for the disable/enable round-trip via api.updateAgentMetadata, the registry filter on getAvailableDirector, the dispatch-daemon worker filter, the start-handler refusal, the list marker, and the steward-scheduler skip path. All colocated as *.bun.test.ts.

Notes for reviewers

  • The change to OrchestratorAPI.updateAgentMetadata (private to public on the impl class plus added to the interface) is required because the new CLI handlers and PATCH route call it externally. The method body is unchanged.
  • The disabled field is intentionally separate from sessionStatus: sessionStatus models in-flight session lifecycle, while disabled is a property of the agent itself. A disabled agent can still have sessionStatus: 'running' from a session that started before it was disabled; that session is allowed to finish.

komoreka and others added 10 commits April 30, 2026 11:39
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Expose updateAgentMetadata on OrchestratorAPI interface (was private)
so CLI handlers can flip metadata.agent.disabled without bypassing the
API layer. Add agentDisableCommand and agentEnableCommand mirroring the
agentStopCommand pattern, register them in agentCommand subcommands, and
update the help string. Add structure and validation tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds two tests that verify the actual metadata mutation:
- disable round-trip: updateAgentMetadata sets disabled to true
- enable round-trip: updateAgentMetadata removes disabled key from serialised JSON

Uses an in-memory SQLite-backed OrchestratorAPI (same harness pattern as
agent-registry.bun.test.ts) rather than invoking the handler literally,
because createOrchestratorClient() always calls findStoneforgeDir() before
honouring options.db - there is no .stoneforge dir in the test environment.
The handler bodies are thin wrappers around api.updateAgentMetadata, so
the coverage is equivalent.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add isAgentDisabled guard to agentStartHandler so a disabled agent
returns a VALIDATION failure pointing the user at sf agent enable.
Covers the new path with a chdir-based behavioural test that exercises
the real handler via a tmpdir .stoneforge setup.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Append '(disabled)' to the status column in the table formatter when
an agent's metadata has disabled: true. JSON and quiet output paths are
unchanged. Adds a behavioural test that asserts the annotation is present
for the disabled row and absent for the enabled row.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add isAgentDisabled guard inside registerSteward so disabled stewards
return false and register no cron or event triggers. registerAllStewards
gets the filter for free since it calls registerSteward per agent.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- packages/smithy: PATCH /api/agents/:id accepts disabled?: boolean.
  Toggling on a steward also (un)registers it with the running scheduler so
  behavior changes immediately without an sf serve restart.
- smithy-web: useToggleAgentDisabled hook (mirrors useChangeAgentTriggers).
- AgentCard: Disable/Enable menu item, dimmed card style + 'Disabled' badge
  when set, Start button hidden when disabled (the badge plus the menu item
  surface the state and the action). Disabled propagates through
  BaseAgentMetadata.disabled in the web types.
@CLAassistant

CLAassistant commented Apr 30, 2026 •

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

The CLI guard added in the disable feature only catches 'sf agent start'.
The web UI's Start Session button (in WorkspacePane, DirectorPanel,
DirectorTabContent) calls /api/agents/:id/start directly, bypassing the
CLI guard. Same for /api/agents/:id/resume. Add the disabled check at
both endpoints so every UI surface (current and future) inherits the
refusal.
Mirrors the AgentCard guard. The server already refuses these requests
(see prior commit), but proactively dimming the buttons makes the state
discoverable instead of relying on a server error toast after click.

Touches: WorkspacePane (worker pane, two start buttons), DirectorTabContent
(idle overlay start + resume buttons), DirectorPanel (active director top
bar, start + restart buttons). Each reads the disabled flag from agent
metadata and adds it to the existing disabled prop plus a tooltip
explaining the state.
@notadamking notadamking added the enhancement New feature or request label May 3, 2026
@notadamking

Copy link
Copy Markdown
Contributor

Looks good overall — comprehensive filter coverage, sensible separation from sessionStatus, and the in-flight session semantics are right. Three things to address before merge:

  1. CLI bypasses the running scheduler. sf agent disable <steward-id> writes through api.updateAgentMetadata but doesn't call stewardScheduler.unregisterSteward, so when sf serve smithy is running the steward keeps firing cron/event triggers until the next scheduler restart. Only the HTTP PATCH route reconciles. Please either route the CLI through the HTTP endpoint when the server is up, or have the scheduler observe metadata changes — otherwise the CLI and UI behave differently for the same operation.

  2. No tests on the PATCH /api/agents/:id disabled path. The boolean validation branch, the conditional registerSteward/unregisterSteward invocation, and the warning-swallowing try/catch are all untested. Behavioral tests cover the storage layer but not the route handler that wires in the live reconciliation.

  3. Docs not updated. Please update the CLI Reference docs (el-4i7mp6, el-5ivp0w) and the AgentRegistry Reference (el-1mmk) to cover sf agent disable/enable, the (disabled) list marker, and the disabled flag semantics.

…reconciliation, route tests, docs

Three review items from @notadamking on PR stoneforge-ai#60:

1. CLI bypassed the running scheduler.
   sf agent disable / enable now route through PATCH /api/agents/:id when
   STONEFORGE_API_URL (default http://localhost:3457) is reachable. The HTTP
   route already calls registerSteward / unregisterSteward live, so the
   scheduler reconciles immediately. When the server is unreachable, the CLI
   falls back to a direct updateAgentMetadata call and prints "Scheduler will
   pick up the change on next start" instead of the immediate-effect message.
   The end state is identical either way; only the live reconciliation differs.

2. PATCH /api/agents/:id disabled path was untested.
   New packages/smithy/src/server/routes/agents.test.ts covers nine cases:
   accepts disabled true / false, rejects non-boolean with 400 VALIDATION_ERROR,
   unregisters a steward when disabling and registers when enabling, leaves
   the scheduler untouched for non-stewards, leaves the scheduler untouched
   when isRunning() is false, swallows scheduler errors and still returns 200,
   and returns 404 for an unknown agent. Uses the existing tasks.test.ts
   harness pattern (vitest, vi.fn() mocks, app.request).

3. Docs not updated.
   - apps/docs/src/content/docs/reference/cli-orchestration.mdx adds
     sf agent disable / enable subcommand sections plus a note on the
     (disabled) marker in sf agent list and how disabled is independent of
     --status filtering.
   - apps/docs/src/content/docs/reference/orchestrator-api.mdx adds a
     "Disabled agents" subsection covering the flag semantics, the four
     enforcement sites (getAvailableDirector, dispatch daemon, scheduler,
     start/resume routes), the in-flight session contract, and the canonical
     read (isAgentDisabled / isAgentEnabled) and write (delete vs undefined)
     idioms.
@komoreka

komoreka commented May 3, 2026

Copy link
Copy Markdown
Contributor Author

Pushed 339106d addressing all three.

CLI now PATCHes the running orchestrator first (default http://localhost:3457, override via STONEFORGE_API_URL, 1.5s timeout). Server-up gives immediate scheduler reconciliation; server-down falls back to direct DB write and prints "scheduler picks up on next start." Same end state either way.

PATCH route now has tests in packages/smithy/src/server/routes/agents.test.ts — nine cases including the validation 400, the steward register/unregister branches, the warning-swallow, and 404 on unknown agent. Mirrors tasks.test.ts vitest harness.

Docs updated in cli-orchestration.mdx and orchestrator-api.mdx — disable/enable subcommands, list marker, helpers, semantics. The IDs you mentioned (el-4i7mp6, el-5ivp0w, el-1mmk) aren't anywhere in the public repo — looks like local DB entities. If they mirror the .mdx files I just edited, the re-import should pick it up. If they're standalone, let me know and I'll figure something out.

Comment thread packages/smithy/src/server/routes/agents.test.ts Outdated
Comment thread packages/smithy/src/server/routes/agents.test.ts Outdated
@notadamking

Copy link
Copy Markdown
Contributor

Pushed 339106d addressing all three.

CLI now PATCHes the running orchestrator first (default http://localhost:3457, override via STONEFORGE_API_URL, 1.5s timeout). Server-up gives immediate scheduler reconciliation; server-down falls back to direct DB write and prints "scheduler picks up on next start." Same end state either way.

PATCH route now has tests in packages/smithy/src/server/routes/agents.test.ts — nine cases including the validation 400, the steward register/unregister branches, the warning-swallow, and 404 on unknown agent. Mirrors tasks.test.ts vitest harness.

Docs updated in cli-orchestration.mdx and orchestrator-api.mdx — disable/enable subcommands, list marker, helpers, semantics. The IDs you mentioned (el-4i7mp6, el-5ivp0w, el-1mmk) aren't anywhere in the public repo — looks like local DB entities. If they mirror the .mdx files I just edited, the re-import should pick it up. If they're standalone, let me know and I'll figure something out.

el-4i7mp6, el-5ivp0w, el-1mmk are docs in Stoneforge. You should be able to see/edit them if you run this repo within Stoneforge. That said, if you're unable to find/modify them I can create a follow-up task to update them once this gets merged.

@komoreka
komoreka requested a review from notadamking May 4, 2026 08:28
@komoreka

komoreka commented May 4, 2026

Copy link
Copy Markdown
Contributor Author

Tried running stoneforge against the fork — no docs in the workspace by default. Take you up on the post-merge follow-up.

@notadamking
notadamking merged commit e19b0d2 into stoneforge-ai:master May 4, 2026
2 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow disabling an agent without removing it from the agent list

3 participants