Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gh-resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
diberry/squad
56 changes: 56 additions & 0 deletions .github/workflows/squad-pr-target-guard.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
name: PR Target Guard

on:
pull_request_target:
branches: [dev, main, preview, insider]
types: [opened, edited, reopened]

permissions:
contents: read
pull-requests: read

jobs:
target-repo-check:
name: "🔒 PR target repo check"
runs-on: ubuntu-latest
steps:
- name: Verify PR targets this fork (not upstream)
env:
EXPECTED_REPO: diberry/squad
PR_REPO: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_HEAD: ${{ github.event.pull_request.head.repo.full_name }}
PR_BASE: ${{ github.event.pull_request.base.repo.full_name }}
run: |
echo "PR #$PR_NUMBER"
echo " Head: $PR_HEAD"
echo " Base: $PR_BASE"
echo " Expected base repo: $EXPECTED_REPO"

if [ "$PR_BASE" != "$EXPECTED_REPO" ]; then
echo "::error::PR targets '$PR_BASE' instead of '$EXPECTED_REPO'."
echo "::error::This is a fork — PRs should target diberry/squad, not upstream."
echo ""
echo "To fix: close this PR and re-create with:"
echo " gh pr create --base dev --repo diberry/squad"
exit 1
fi

echo "✅ PR correctly targets $EXPECTED_REPO"

review-requirement-check:
name: "🔒 Review requirement check"
runs-on: ubuntu-latest
if: github.event.action == 'opened' || github.event.action == 'reopened'
steps:
- name: Remind about review requirement
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
echo "⚠️ REVIEW POLICY: All PRs to dev/main require at least 1 approving review."
echo ""
echo "This check is informational — branch protection rules enforce the requirement."
echo "If branch protection is not yet configured, see:"
echo " docs/branch-protection-setup.md in this repo"
echo ""
echo "✅ Review reminder posted for PR #$PR_NUMBER"
41 changes: 24 additions & 17 deletions docs/src/content/docs/guide/building-resilient-agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ Each failed probe attempt resets the backoff. This gives flaky services time to

## Configuration

Squad includes sensible defaults — most agents won't need to change these. Configure the circuit breaker in your agent's `squad.json` or initialization code only if you want to customize:
Squad includes sensible defaults — most agents won't need to change these. Configure the circuit breaker in your agent's `squad.config.ts` or initialization code only if you want to customize:

```json
{
Expand All @@ -57,7 +57,7 @@ Squad includes sensible defaults — most agents won't need to change these. Con
| Parameter | Default | Meaning |
|-----------|---------|---------|
| `failureThreshold` | 5 | Open circuit after this many failures |
| `successThreshold` | 2 | Close circuit after this many successes in half-open |
| `successThreshold` | 2 | Number of consecutive successes during half-open state before closing circuit |
| `timeWindow` | 60s | Count failures within this window |
| `initialBackoff` | 2m | Start backoff at this duration |
| `maxBackoff` | 30m | Cap backoff at this duration |
Expand All @@ -72,38 +72,45 @@ The circuit breaker persists its state to disk. If an agent restarts while the c

## How to apply to custom agents

When building a custom agent, wrap your external calls with circuit breaker protection:
When building a custom agent, wrap your external calls with circuit breaker protection. This example shows the conceptual pattern (note: a `@squad/resilience` module is planned but not yet available):

```typescript
import { CircuitBreaker } from '@squad/resilience';

const breaker = new CircuitBreaker({
failureThreshold: 5,
timeWindow: 60000,
});
// Pseudocode: Circuit breaker pattern for external calls
async function callDownstreamAPI(breaker) {
try {
// Check circuit state before making the call
if (breaker.state === 'OPEN') {
throw new Error('Circuit is open; retrying later');
}

async function callDownstreamAPI() {
return breaker.execute(async () => {
const response = await fetch('https://api.example.com/data');
if (!response.ok) throw new Error(`API error: ${response.status}`);
if (!response.ok) {
breaker.recordFailure();
throw new Error(`API error: ${response.status}`);
}

breaker.recordSuccess();
return response.json();
});
} catch (err) {
breaker.recordFailure();
throw err;
}
}

// Circuit breaker automatically handles state transitions
// and exponential backoff — just call it.
// and exponential backoff.
try {
const data = await callDownstreamAPI();
const data = await callDownstreamAPI(circuitBreaker);
} catch (err) {
if (err.code === 'CIRCUIT_OPEN') {
if (err.message.includes('Circuit is open')) {
console.log('Circuit is open; retrying later');
} else {
console.error('Request failed:', err);
}
}
```

When the circuit opens, `execute()` throws a `CIRCUIT_OPEN` error. Your agent can catch this and backoff gracefully, or fail-fast to upstream callers.
The circuit breaker automatically transitions between states and applies exponential backoff. Failures increment a counter within the time window; successes during half-open close the circuit.

---

Expand Down
Loading