Building Claude Agent SDK background jobs with voice I/O...
Repeatable process for creating agentic background jobs with voice I/O and queue integration.
Use this workflow when building agents that:
RunningFifoQueuecosa-voice MCP toolsAgenticJobBase interface contractSlash Command: /lupin-new-claude-agent-sdk-voice-workflow
Reference Agents:
src/cosa/agents/deep_research/ - Research agent patternsrc/cosa/agents/podcast_generator/ - Audio generation pattern| Phase | Purpose | Output |
|---|---|---|
| Phase 0 | Interactive Discovery | Agent characteristics |
| Phase 1-2 | Skeletal Foundation | Basic agent structure |
| Phase 3 | Voice Notifications | cosa-voice integration |
| Phase 4 | Queue Integration | RunningFifoQueue hooks |
| Phase 5 | Testing | Validation and debugging |
| Phase 5b | Q&A Script | Notification Proxy profile for automated testing |
| Phase 5c | UI E2E Testing | Playwright browser tests (planned v0.1.6) |
Before creating files, answer:
pdf_summarizerps → job IDs like ps-a1b2c3d4class OrchestratorState( Enum ):
# Active states
INITIALIZING = "initializing"
PROCESSING = "processing"
GENERATING = "generating"
# Waiting states (human-in-the-loop)
WAITING_APPROVAL = "waiting_approval"
# Terminal states
COMPLETED = "completed"
FAILED = "failed"
# Job lifecycle: set_job_id at start, clear in finally
voice_io.set_job_id( self.id_hash )
try:
# Progress update (MUST include queue_name="run")
await voice_io.notify( "Starting research phase", priority="low", queue_name="run" )
# Human-in-the-loop
response = ask_yes_no( "Approve this plan?", default="yes" )
# Completion
await voice_io.notify( "Agent completed successfully", priority="medium", queue_name="run" )
# Progressive breadcrumbs (inside loops or long phases)
for i, item in enumerate( items ):
await voice_io.notify( f"Processing {i + 1} of {len( items )}", priority="low", queue_name="run" )
finally:
voice_io.clear_job_id()
CRITICAL: Every notify() call MUST include queue_name="run" for proper queue routing.
set_job_id() / clear_job_id() enables job card activity log routing in the UI.
Breadcrumb notifications are required for loops, long phases (>10s), and dry-run mode. See "Progressive Breadcrumb Notifications" in the full workflow doc.
run() - Main entry pointget_state() - Current orchestrator stateget_progress() - Completion percentageget_result() - Final output{prefix}-{uuid4[:8]}MANDATE: Every new agentic job MUST satisfy ALL items before merge.
This checklist was created after a Session 381 audit found consistency gaps
across 6 existing job implementations (see src/rnd/v0.1.6/2026.03.27-bug-fix-expediter/).
from_config( config_mgr, debug ) classmethodlupin-app.ini under agent-specific prefixlupin-app-splainer.ini_execute method)voice_io.set_job_id( self.id_hash ) called at start of _execute()voice_io.clear_job_id() called in finally blocknotify() calls include queue_name="run" (live AND dry-run)self.answer_conversational set before job completesself.error set on failure with descriptive message_execute_dry_run() is a separate method (not flag check in _execute())job_id=self.id_hash and queue_name="run"abstract with mock cost summaryself.artifacts[ "cost_summary" ]agent_registry.py with required_params and fallback_questionsagentic_job_factory.pysrc/cosa/rest/routers/Use src/cosa/agents/deep_research/job.py as the gold standard — it satisfies
every item in this checklist.
Full Workflow Document: src/workflow/agentic-voice-workflow.md
Contains:
All agentic jobs automatically support timed execution and exclusive mode. These are
runtime infrastructure concerns, not agent-specific features. No per-agent registration,
factory changes, or _execute() modifications needed.
UI form path: Every job submission card includes a "Schedule for later" checkbox +
datetime picker and an "Exclusive mode" checkbox. The JS _getSchedulingParams() helper
adds scheduled_at (ISO string) and monopolize (bool) to the POST body when set.
Voice path: The Runtime Argument Expeditor's confirmation summary automatically includes:
---
**Scheduling**
- **run_at**: immediately
- **exclusive_mode**: no
Users can modify via the existing [comment: ...] pattern:
scheduled_atmonopolize = TrueRuntime arg extraction: In _handle_agentic_command(), scheduled_at and monopolize
are popped from args_dict before the factory creates the job, then set directly on the
job object. The factory never sees these — they're infrastructure, not agent params.
scheduled_at = None → immediate execution (default)scheduled_at = "2026-03-31T02:00:00" → consumer sleeps until that timemonopolize = True → no-op in serial mode; when Hybrid Fast Lane is added, blocks
all concurrent jobs until this one completesscheduled_at / monopolize to your agent's registry entry_execute() methodqueue_name="run" from notify() calls - breaks queue routingset_job_id() / clear_job_id() - breaks job card activity logfrom_config() classmethodvoice_io without strong justificationEvery new agent MUST have an automated live pipeline test before merge. Do not rely on manual curl or UI-click testing for pipeline validation. The automated infrastructure exists — use it.
Prefer automated smoke test scripts over manual curl submissions.
| Approach | Effort | Repeatability | Example |
|---|---|---|---|
Manual curl POST to /api/push |
High (copy-paste, edit JSON, poll manually) | Low | Ad-hoc debugging only |
| Automated smoke test script | Low (single command) | High | src/tests/smoke/test_calculator_live_pipeline.py |
Pattern: test_calculator_live_pipeline.py demonstrates the preferred approach:
/auth/login, get JWT/api/mode/current/api/push, extract job_id from response/api/get-queue/done by job_id until completionWhen building new agents, create an automated smoke test following this pattern rather than relying on manual curl commands.
LivePipelineTestBase)Copy and adapt this template for agents that do not ask interactive questions:
#!/usr/bin/env python3
"""
Smoke test for {AgentName} agent via live pipeline.
Usage:
python src/tests/smoke/test_{agent_name}_live_pipeline.py
python src/tests/smoke/test_{agent_name}_live_pipeline.py -q 0,2
Requires:
- Server running on localhost:7999
- LUPIN_TEST_INTERACTIVE_MOCK_JOBS_EMAIL / LUPIN_TEST_INTERACTIVE_MOCK_JOBS_PASSWORD
"""
import os
import sys
lupin_root = os.environ.get( "LUPIN_ROOT" )
if lupin_root:
sys.path.insert( 0, os.path.join( lupin_root, "src" ) )
from tests.smoke.utilities.live_pipeline_base import LivePipelineTestBase
{AGENT_NAME_UPPER}_QUERIES = [
{
"id" : "SCENARIO_1",
"query" : "Your test query here",
"expected_keywords" : [ "expected", "words" ],
},
# Add more scenarios...
]
class {AgentName}PipelineTest( LivePipelineTestBase ):
TEST_NAME = "{Agent Name} Live Pipeline"
SCENARIOS = {AGENT_NAME_UPPER}_QUERIES
DEFAULT_TIMEOUT = 120
def build_argparser( self ):
parser = super().build_argparser()
parser.add_argument( "--queries", "-q", type=str, default=None,
help="Comma-separated query indices (e.g., '0,1,3'). Default: all." )
return parser
def get_scenario_indices( self, args ):
if hasattr( args, "queries" ) and args.queries:
return [ int( x.strip() ) for x in args.queries.split( "," )
if int( x.strip() ) < len( self.SCENARIOS ) ]
return list( range( len( self.SCENARIOS ) ) )
def get_mode_for_scenario( self, scenario ):
return "{agent_name}" # Or None for auto-route testing
def quick_smoke_test():
import argparse
test = {AgentName}PipelineTest()
args = argparse.Namespace( queries=None, debug=False, verbose=False )
return test.run_scenarios( args )
def test_{agent_name}_live_pipeline():
assert quick_smoke_test()
if __name__ == "__main__":
test = {AgentName}PipelineTest()
success = test.run( sys.argv[ 1: ] )
sys.exit( 0 if success else 1 )
InteractiveSmokeTest)For agents that ask interactive questions via the Runtime Argument Expediter, use InteractiveSmokeTest instead:
from tests.smoke.utilities.interactive_smoke_test import InteractiveSmokeTest
class {AgentName}InteractiveTest( InteractiveSmokeTest ):
TEST_NAME = "{Agent Name} Interactive"
SCENARIOS = {AGENT_NAME_UPPER}_SCENARIOS
PROXY_PROFILE = "{agent_name}"
DEFAULT_TIMEOUT = 180
Run with: python src/tests/smoke/test_{agent_name}_live_pipeline.py --auto-proxy --no-confirm
| File | Purpose |
|---|---|
src/tests/smoke/utilities/live_pipeline_base.py |
Base class: auth, submit-and-poll, validation, reporting |
src/tests/smoke/utilities/interactive_smoke_test.py |
Adds proxy auto-launch for interactive agents |
src/tests/smoke/test_calculator_live_pipeline.py |
Reference: non-interactive (6 scenarios) |
src/tests/smoke/test_proxy_integration.py |
Reference: interactive (12 scenarios, 3 agent groups) |
src/docs/automated-interactive-testing.md |
Comprehensive proxy testing guide |
For agents with interactive questions: Also create a Notification Proxy Q&A script so expediter questions are auto-answered during automated testing. See "Notification Proxy" section below.
Planned (v0.1.6): Playwright-based UI E2E tests will add browser-level validation (submit via UI, verify job cards, check notification rendering). When implemented, update this SKILL.md with the Playwright test template and add a Phase 5c section.
When agents ask interactive questions (via Runtime Argument Expediter), smoke tests stall without human input. The Notification Proxy solves this by loading a JSON Q&A script at startup and using Phi-4 local LLM to semantically match incoming questions to scripted answers.
fallback_questions in agent_registry.pycp _template.json your-agent.json in src/conf/notification-proxy-scripts/question_pattern, answer, arg_name, response_types__main__.py choices and config.py TEST_PROFILESEach entry in the entries array follows this format:
{
"question_pattern" : "What topic would you like me to research?",
"answer" : "quantum computing breakthroughs 2026",
"arg_name" : "query",
"response_types" : [ "open_ended", "open_ended_batch" ]
}
For all-agents.json, scope entries to specific agents with the agents tag:
{
"question_pattern" : "What topic?",
"answer" : "quantum computing",
"agents" : [ "deep_research", "research_to_podcast" ]
}
Entries without an agents tag are universal and apply to any agent.
# Terminal 1: Start proxy with your agent's profile
python -m cosa.agents.notification_proxy --profile your_agent --debug
# Terminal 2: Run the smoke test
python src/tests/smoke/test_your_agent_live_pipeline.py
| File | Purpose |
|---|---|
src/conf/notification-proxy-scripts/README.md |
Full guide for creating Q&A scripts |
src/conf/notification-proxy-scripts/_template.json |
Copy-and-modify starter template |
src/cosa/agents/notification_proxy/config.py |
Profile registration (backward compat) |
src/cosa/agents/runtime_argument_expeditor/agent_registry.py |
Defines questions per agent |
Reference: src/conf/notification-proxy-scripts/README.md
