Skip to content

SMOODEV-590: TS server honors per-agent config + conversation workflows#126

Merged
brentrager merged 4 commits into
mainfrom
agent-config-instructions-workflow-ts
Jul 2, 2026
Merged

SMOODEV-590: TS server honors per-agent config + conversation workflows#126
brentrager merged 4 commits into
mainfrom
agent-config-instructions-workflow-ts

Conversation

@brentrager

Copy link
Copy Markdown
Contributor

Problem

Agents served by the TypeScript smooth-operator server ignored their per-agent config and always used one generic org-level customer-support persona. The server resolved behavior only from a single static systemPrompt set at construction — so every agent behaved identically, per-agent instructions were never applied, and conversation_workflow was unimplemented.

In the SmooAI monorepo agents table each agent has instructions (jsonb {prompt}) and conversation_workflow (jsonb); the TS server applied neither.

Approach

Ports the monorepo general-agent behavior (packages/backend/src/ai/graphs/general-agent/) into the TS server, mirroring the Rust agent-config-instructions-workflow design.

Config-delivery seamAgentConfigResolver.resolve(agentId) (new src/agentConfig.ts), following the server's existing pluggable-seam pattern (AuthVerifier.resolve, AccessKnowledge.forAccess). Resolution is server-side because the create_conversation_session payload carries only an agentId (per spec/actions/create-conversation-session.schema.json). The reference StaticAgentConfigResolver is in-memory; a real deployment plugs in one backed by the agents table. An un-configured agent (no resolver, or resolver returns/throws) falls back to the server/org default prompt + full tool set — behavior unchanged.

Per-agent instructionsassembleSystemPrompt(base, config, currentStepId) makes the agent's own instructions the primary persona body, keeps the base org rules, and folds in optional greeting / personality. tool_config filters the server's tool set to an allow-list.

Conversation workflow (new src/workflow.ts) — the current step's intent + criteria render into the system prompt (renderWorkflowPromptSection); after each turn a cheap, failure-tolerant judge LLM call (judgeStep) decides whether the criteria were met and advances the pointer (explicit next or array order). currentStepId is tracked on the session (SessionStore.setCurrentStep).

ToleranceparseWorkflow / parseAgentConfig degrade malformed config to the default flow and never throw; a judge error (or unparseable verdict) stays on the current step. A broken workflow doesn't discard a valid instructions.prompt.

Verification

  • pnpm test98 passing (41 pre-existing + 57 new across workflow.test.ts, agentConfig.test.ts, agent-config-turn.test.ts): parse tolerance + hostile input, step resolve/next/render/advance, all judge verdict paths (yes/no/maybe/unparseable/thrown/empty-reply), prompt assembly with/without instructions/workflow/greeting/personality, and end-to-end over a real WebSocket — per-agent isolation, instructions honored, workflow advancement across turns, malformed-config + throwing-resolver fallback.
  • pnpm typecheck clean (src + tests).
  • Changeset added (@smooai/smooth-operator-server minor).

Notes

  • Mirrors the Rust PR on agent-config-instructions-workflow (the canonical reference). That branch was not yet on origin when this was built; the only surface to reconcile is the resolver interface — the workflow render/judge/step-tracking semantics follow the shared monorepo reference. Flagged for a re-check once the Rust branch lands.
  • The judge adds one cheap model call per turn only when a workflow is configured, and runs inline before the terminal eventual_response (content tokens have already streamed) so the advanced currentStepId is persisted before any next turn — no race.

🤖 Generated with Claude Code

https://claude.ai/code/session_011va1JyN3rTsfd2xuNGALed

Agents served by the TypeScript operator ignored their per-agent config and
always used one generic org-level customer-support persona. This ports the
monorepo general-agent behavior into the TS server:

- AgentConfigResolver seam resolves a session's agentId -> AgentConfig
  (instructions, conversationWorkflow, greeting, personality, tool allow-list).
  Server-side because create_conversation_session carries only an agentId.
  Un-configured agents fall back to the default prompt + tools (unchanged).
- conversationWorkflow: current step's intent + criteria rendered into the
  system prompt; a cheap, failure-tolerant post-turn judge decides whether the
  criteria were met and advances the pointer (explicit next or array order),
  tracked as currentStepId on the session.
- Tolerant parsing: malformed config degrades to the default flow, never
  crashes a session. Judge errors stay on the current step.

Mirrors the Rust agent-config-instructions-workflow design.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011va1JyN3rTsfd2xuNGALed
@changeset-bot

changeset-bot Bot commented Jul 2, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 9fd15a7

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@smooai/smooth-operator-server Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

brentrager and others added 3 commits July 1, 2026 21:54
Compute is_first_turn server-side (no prior messages) in the dispatcher and
drop the <GreetingAwareness> section entirely on later turns, matching the
Python server's is_first_turn semantics instead of relying on the model to
self-gate a conditional instruction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011va1JyN3rTsfd2xuNGALed
tool_config is { enabledTools: [{ toolId, enabled, authLevel, config? }] }
(defaults to []), not a flat string list. Parse it per the monorepo
AgentToolConfig schema: enabled defaults true, authLevel "none", authLevel/
config preserved on the parsed type; skip malformed entries. Empty/missing
enabledTools → full tool set; non-empty → restrict to enabled=true entries
matched by snake_case toolId; unknown toolIds ignored.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011va1JyN3rTsfd2xuNGALed
Close the parsed-but-unenforced gaps, mirroring the monorepo general-agent
tool-execution gate:

- authLevel enforcement at execution time (new src/toolGating.ts, wraps each
  tool's execute — no engine fork). Gates ONLY when the enabledTools entry sets
  authLevel != 'none' AND the tool declares supportsAuthRequirement (new opt-in
  flag on ServerTool, default false). admin + public → blocked message;
  visibility 'internal' → auto-satisfied; public + end_user → consult
  SessionAuthenticator (new seam, fail-closed) → blocked identity-verification
  message when unauthenticated. Adds AgentConfig.visibility ('public' default).
- Per-tool config delivered to execute's 2nd arg at execution (was only
  preserved on the parsed type).
- Judge default aligned to the cross-lane haiku tier (claude-haiku-4-5).

OTP issuance stays host wiring behind SessionAuthenticator — the server only
gates and leaves the hook point.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011va1JyN3rTsfd2xuNGALed
@brentrager brentrager merged commit a15b3b9 into main Jul 2, 2026
@brentrager brentrager deleted the agent-config-instructions-workflow-ts branch July 2, 2026 02:56
brentrager added a commit that referenced this pull request Jul 2, 2026
PRs touching only python/, typescript/, or go/ got ZERO checks — only
rust/dotnet paths triggered a lane, so SMOODEV-590's port PRs (#125/#126/#128)
merged with no CI. Add one path-filtered lane per language, mirroring the
existing .NET lane's shape (triggers, path filters, ubuntu-latest, timeout)
plus concurrency + read-only permissions from the kind-smoke lane:

- Python: uv sync + ruff check + ruff format --check + pytest, matrix over
  python/ and python/server/.
- TypeScript: pnpm install + typecheck + test (vitest), scoped to the two
  workspace packages (@smooai/smooth-operator + …-server).
- Go: gofmt check + go vet + go test ./... -race, matrix over go/ and go/server/.

Each lane also triggers on spec/** because all three conformance suites validate
against spec/conformance/fixtures.json at runtime. Live-gateway E2E tests
self-skip without SMOOTH_AGENT_E2E + SMOOAI_GATEWAY_KEY, so no secrets needed.

Also ran `ruff format` on the 7 unformatted files under python/ so the new
format gate is green (pre-existing drift from the port PRs).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant