Install any skill in seconds. Free to start, no credit card required.
Get Started Free →專責處理 RIF (Required Behavior Frame) 類型的需求。讀取規格目錄結構,生成/審查 Event Handler 設計與實作。支援冪等性、重試、死信佇列。
.claude/skills/majiayu000-reactor-sub-agent/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-11 | ✗→✓ | ▲ Improved | 199% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 145% | 0% |
| case-10 | ✗→✓ | ▲ Improved | 139% | 0% |
| case-12 | ✗→✓ | ▲ Improved | 131% | 0% |
| case-14 | ✗→✓ | ▲ Improved | 157% | 0% |
docs/specs/{feature-name}/
├── frame.yaml # 讀取 frame_concerns
├── requirements/ # 讀取反應需求
│ └── req-{n}-{feature}.yaml
├── machine/ # 讀取 Reactor 規格
│ └── reactor.yaml # Event Handler 規格
└── cross-context/ # 若需觸發其他 BC
└── {context}.yamlyaml# docs/specs/{feature-name}/machine/reactor.yaml reactor: name: "{EventName}Handler" # 監聽的事件 subscribes_to: - event: "{DomainEvent}" source_context: "{SourceBC}" subscription_type: "durable" # | ephemeral # 反應動作 actions: - name: "{ActionName}" type: "command" # | query | notification | integration target: "{TargetUseCase or Service}" # 冪等性設計 idempotency: enabled: true key_source: "event.id" # 用於判斷是否已處理 storage: "database" # | redis | in-memory ttl: "7d" # 重試策略 retry: max_attempts: 3 backoff: type: "exponential" # | fixed | linear initial_delay: "1s" max_delay: "30s" multiplier: 2 retryable_errors: - "NetworkError" - "TimeoutError" non_retryable_errors: - "ValidationError" - "UnauthorizedError" # 死信佇列 dead_letter: enabled: true queue: "dlq.{feature-name}" alert_threshold: 10 # 交易邊界 transaction: type: "eventual" # | immediate consistency: "at-least-once" # | exactly-once | at-most-once
saga-orchestrator → runSubagent → reactor-sub-agent
├── 讀取規格目錄
├── 套用 coding-standards
└── 輸出 Event Handler 代碼yamltask: type: "reactor" spec_dir: "docs/specs/on-workflow-created/" language: "typescript" output_paths: handlers: "src/application/event-handlers/"
typescript// src/application/event-handlers/WorkflowCreatedHandler.ts // Generated from: docs/specs/on-workflow-created/machine/reactor.yaml import { EventHandler, OnEvent } from '@/infrastructure/events/EventHandler'; import { IdempotencyService } from '@/infrastructure/idempotency/IdempotencyService'; import { RetryPolicy } from '@/infrastructure/retry/RetryPolicy'; import { DeadLetterQueue } from '@/infrastructure/dlq/DeadLetterQueue'; import { NotificationService } from '@/domain/services/NotificationService'; // ===== Event Type (from subscribes_to) ===== export interface WorkflowCreatedEvent { readonly id: string; // Event ID for idempotency readonly workflowId: string; readonly boardId: string; readonly name: string; readonly createdBy: string; readonly createdAt: Date; } // ===== Handler Configuration (from reactor.yaml) ===== const RETRY_CONFIG = { maxAttempts: 3, backoff: { type: 'exponential' as const, initialDelay: 1000, maxDelay: 30000, multiplier: 2, }, retryableErrors: ['NetworkError', 'TimeoutError'], }; const IDEMPOTENCY_TTL = 7 * 24 * 60 * 60 * 1000; // 7 days // ===== Event Handler ===== export class WorkflowCreatedHandler implements EventHandler<WorkflowCreatedEvent> { constructor( private readonly idempotencyService: IdempotencyService, private readonly retryPolicy: RetryPolicy, private readonly deadLetterQueue: DeadLetterQueue, private readonly notificationService: NotificationService, private readonly analyticsService: AnalyticsService, ) {} @OnEvent('WorkflowCreatedEvent') async handle(event: WorkflowCreatedEvent): Promise<void> { const idempotencyKey = `workflow-created:${event.id}`; try { // ===== Idempotency Check (from reactor.yaml#idempotency) ===== const alreadyProcessed = await this.idempotencyService.check(idempotencyKey); if (alreadyProcessed) { console.log(`Event ${event.id} already processed, skipping`); return; } // ===== Execute with Retry (from reactor.yaml#retry) ===== await this.retryPolicy.execute( async () => { await this.processEvent(event); }, RETRY_CONFIG, ); // ===== Mark as Processed ===== await this.idempotencyService.markProcessed(idempotencyKey, IDEMPOTENCY_TTL); } catch (error) { // ===== Dead Letter Queue (from reactor.yaml#dead_letter) ===== if (this.isNonRetryable(error)) { await this.deadLetterQueue.send({ event, error: error.message, timestamp: new Date(), reason: 'non-retryable-error', }); return; } // Re-throw for retry infrastructure throw error; } } private async processEvent(event: WorkflowCreatedEvent): Promise<void> { // ===== Actions (from reactor.yaml#actions) ===== // Action 1: Send notification to board members await this.notificationService.notifyBoardMembers({ boardId: event.boardId, message: `New workflow "${event.name}" created`, createdBy: event.createdBy, }); // Action 2: Track analytics await this.analyticsService.track({ event: 'workflow_created', properties: { workflowId: event.workflowId, boardId: event.boardId, }, }); } private isNonRetryable(error: Error): boolean { return ['ValidationError', 'UnauthorizedError'].some( type => error.name === type || error.constructor.name === type ); } }
typescript// src/infrastructure/idempotency/IdempotencyService.ts export interface IdempotencyService { check(key: string): Promise<boolean>; markProcessed(key: string, ttl: number): Promise<void>; } export class RedisIdempotencyService implements IdempotencyService { constructor(private readonly redis: Redis) {} async check(key: string): Promise<boolean> { const exists = await this.redis.exists(`idempotency:${key}`); return exists === 1; } async markProcessed(key: string, ttl: number): Promise<void> { await this.redis.set( `idempotency:${key}`, JSON.stringify({ processedAt: new Date() }), 'PX', ttl, ); } }
typescript// src/infrastructure/retry/RetryPolicy.ts export interface RetryConfig { maxAttempts: number; backoff: { type: 'exponential' | 'fixed' | 'linear'; initialDelay: number; maxDelay: number; multiplier?: number; }; retryableErrors?: string[]; } export class RetryPolicy { async execute<T>( fn: () => Promise<T>, config: RetryConfig, ): Promise<T> { let lastError: Error; for (let attempt = 1; attempt <= config.maxAttempts; attempt++) { try { return await fn(); } catch (error) { lastError = error; // Check if retryable if (config.retryableErrors?.length) { const isRetryable = config.retryableErrors.some( type => error.name === type ); if (!isRetryable) { throw error; } } // Last attempt, throw if (attempt === config.maxAttempts) { throw error; } // Calculate delay const delay = this.calculateDelay(attempt, config.backoff); await this.sleep(delay); } } throw lastError!; } private calculateDelay(attempt: number, backoff: RetryConfig['backoff']): number { let delay: number; switch (backoff.type) { case 'exponential': delay = backoff.initialDelay * Math.pow(backoff.multiplier ?? 2, attempt - 1); break; case 'linear': delay = backoff.initialDelay * attempt; break; case 'fixed': default: delay = backoff.initialDelay; } return Math.min(delay, backoff.maxDelay); } private sleep(ms: number): Promise<void> { return new Promise(resolve => setTimeout(resolve, ms)); } }
go// src/application/handlers/workflow_created_handler.go package handlers import ( "context" "fmt" "time" "myapp/domain/events" "myapp/infrastructure/idempotency" "myapp/infrastructure/retry" "myapp/infrastructure/dlq" ) const ( idempotencyTTL = 7 * 24 * time.Hour maxRetryAttempts = 3 ) type WorkflowCreatedHandler struct { idempotency idempotency.Service retryPolicy *retry.Policy dlq *dlq.DeadLetterQueue notification NotificationService analytics AnalyticsService } func NewWorkflowCreatedHandler( idem idempotency.Service, rp *retry.Policy, dlq *dlq.DeadLetterQueue, notif NotificationService, analytics AnalyticsService, ) *WorkflowCreatedHandler { return &WorkflowCreatedHandler{ idempotency: idem, retryPolicy: rp, dlq: dlq, notification: notif, analytics: analytics, } } func (h *WorkflowCreatedHandler) Handle(ctx context.Context, event events.WorkflowCreatedEvent) error { idempotencyKey := fmt.Sprintf("workflow-created:%s", event.ID) // ===== Idempotency Check ===== processed, err := h.idempotency.Check(ctx, idempotencyKey) if err != nil { return err } if processed { return nil // Already processed } // ===== Execute with Retry ===== err = h.retryPolicy.Execute(ctx, func() error { return h.processEvent(ctx, event) }, retry.Config{ MaxAttempts: maxRetryAttempts, InitialDelay: time.Second, MaxDelay: 30 * time.Second, BackoffType: retry.Exponential, }) if err != nil { // ===== Dead Letter Queue ===== if isNonRetryable(err) { return h.dlq.Send(ctx, dlq.Message{ Event: event, Error: err.Error(), Timestamp: time.Now(), Reason: "non-retryable-error", }) } return err } // ===== Mark as Processed ===== return h.idempotency.MarkProcessed(ctx, idempotencyKey, idempotencyTTL) } func (h *WorkflowCreatedHandler) processEvent(ctx context.Context, event events.WorkflowCreatedEvent) error { // Action 1: Notify board members if err := h.notification.NotifyBoardMembers(ctx, NotifyRequest{ BoardID: event.BoardID, Message: fmt.Sprintf("New workflow %q created", event.Name), CreatedBy: event.CreatedBy, }); err != nil { return err } // Action 2: Track analytics return h.analytics.Track(ctx, AnalyticsEvent{ Name: "workflow_created", Properties: map[string]interface{}{ "workflow_id": event.WorkflowID, "board_id": event.BoardID, }, }) } func isNonRetryable(err error) bool { switch err.(type) { case *ValidationError, *UnauthorizedError: return true default: return false } }
go// src/infrastructure/retry/policy.go package retry import ( "context" "math" "time" ) type BackoffType int const ( Fixed BackoffType = iota Linear Exponential ) type Config struct { MaxAttempts int InitialDelay time.Duration MaxDelay time.Duration BackoffType BackoffType Multiplier float64 } type Policy struct{} func NewPolicy() *Policy { return &Policy{} } func (p *Policy) Execute(ctx context.Context, fn func() error, cfg Config) error { var lastErr error for attempt := 1; attempt <= cfg.MaxAttempts; attempt++ { if err := fn(); err == nil { return nil } else { lastErr = err } if attempt == cfg.MaxAttempts { break } delay := p.calculateDelay(attempt, cfg) select { case <-ctx.Done(): return ctx.Err() case <-time.After(delay): } } return lastErr } func (p *Policy) calculateDelay(attempt int, cfg Config) time.Duration { multiplier := cfg.Multiplier if multiplier == 0 { multiplier = 2 } var delay time.Duration switch cfg.BackoffType { case Exponential: delay = time.Duration(float64(cfg.InitialDelay) * math.Pow(multiplier, float64(attempt-1))) case Linear: delay = cfg.InitialDelay * time.Duration(attempt) default: delay = cfg.InitialDelay } if delay > cfg.MaxDelay { return cfg.MaxDelay } return delay }
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-11 | fail→pass | 12,459 | 19,606 | +57% | 1 | 1 | 0% | 2,195 | 6,563 | +199% | 0 | 0 | — |
case-01 | fail→fail | 16,047 | 14,588 | -9% | 1 | 1 | 0% | 216 | 4,212 | +1850% | 0 | 0 | — |
case-02 | pass→pass | 31,374 | 26,706 | -15% | 1 | 1 | 0% | 4,445 | 8,702 | +96% | 0 | 0 | — |
case-03 | pass→pass | 34,174 | 20,796 | -39% | 1 | 1 | 0% | 4,579 | 7,718 | +69% | 0 | 0 | — |
case-04 | pass→pass | 15,033 | 19,005 | +26% | 1 | 1 | 0% | 2,647 | 6,792 | +157% | 0 | 0 | — |
case-05 | pass→pass | 26,298 | 27,656 | +5% | 1 | 1 | 0% | 5,697 | 9,205 | +62% | 0 | 0 | — |
case-06 | pass→pass | 15,709 | 18,374 | +17% | 1 | 1 | 0% | 3,070 | 6,570 | +114% | 0 | 0 | — |
case-07 | fail→fail | 11,470 | 7,950 | -31% | 1 | 1 | 0% | 1,987 | 5,435 | +174% | 0 | 0 | — |
case-08 | pass→pass | 25,134 | 23,540 | -6% | 1 | 1 | 0% | 3,665 | 7,506 | +105% | 0 | 0 | — |
case-09 | fail→pass | 22,002 | 23,052 | +5% | 1 | 1 | 0% | 2,828 | 6,920 | +145% | 0 | 0 | — |
case-10 | fail→pass | 18,543 | 12,762 | -31% | 1 | 1 | 0% | 2,215 | 5,299 | +139% | 0 | 0 | — |
case-12 | fail→pass | 16,675 | 5,100 | -69% | 1 | 1 | 0% | 2,067 | 4,778 | +131% | 0 | 0 | — |
case-13 | pass→pass | 20,744 | 21,807 | +5% | 1 | 1 | 0% | 2,798 | 7,072 | +153% | 0 | 0 | — |
case-14 | fail→pass | 20,525 | 14,338 | -30% | 1 | 1 | 0% | 2,421 | 6,221 | +157% | 0 | 0 | — |
case-15 | fail→pass | 26,993 | 13,714 | -49% | 1 | 1 | 0% | 2,604 | 5,619 | +116% | 0 | 0 | — |
case-16 | fail→fail | 15,542 | 17,675 | +14% | 1 | 1 | 0% | 2,772 | 7,154 | +158% | 0 | 0 | — |
case-17 | fail→fail | 20,703 | 24,382 | +18% | 1 | 1 | 0% | 3,300 | 7,284 | +121% | 0 | 0 | — |
case-18 | fail→pass | 13,097 | 14,416 | +10% | 1 | 1 | 0% | 2,201 | 6,434 | +192% | 0 | 0 | — |
case-19 | fail→pass | 16,775 | 14,156 | -16% | 1 | 1 | 0% | 1,942 | 5,390 | +178% | 0 | 0 | — |
case-20 | fail→pass | 15,645 | 17,720 | +13% | 1 | 1 | 0% | 2,584 | 6,444 | +149% | 0 | 0 | — |
case-21 | pass→pass | 12,622 | 35,419 | +181% | 1 | 1 | 0% | 2,261 | 6,845 | +203% | 0 | 0 | — |
case-22 | pass→pass | 14,786 | 5,028 | -66% | 1 | 1 | 0% | 1,734 | 4,916 | +184% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted, and 21 counted toward the lift figure. The other 1 produced results that are not comparable between the two arms, so they are excluded from the headline rather than averaged into it. The headline lift of +41 percentage points is the difference between those two pass rates over the 21 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.