Skip to content
Open
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
13 changes: 13 additions & 0 deletions .github/workflows/skill-review.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
name: Skill Review
on:
pull_request:
paths: ['**/SKILL.md']
jobs:
review:
runs-on: ubuntu-latest
permissions:
pull-requests: write
contents: read
steps:
- uses: actions/checkout@v4
- uses: tesslio/skill-review@22e928dd837202b2b1d1397e0114c92e0fae5ead # main
152 changes: 93 additions & 59 deletions a2a-multi-agent/skills/a2a-task-lifecycle/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: a2a-task-lifecycle
description: Implement A2A task lifecycle management β€” task creation, state transitions, terminal states, history, and artifacts. Use when building task state machines, handling state transitions, or managing task persistence.
description: "Implement A2A (Agent-to-Agent) task lifecycle management β€” task creation, state transitions, terminal states, history, and artifacts. Use when building task state machines, handling state transitions, managing task persistence, or implementing task status tracking in agent-to-agent workflows."
allowed-tools: Read, Write, Edit, Bash, Grep, Glob, WebSearch, WebFetch
---

Expand All @@ -12,24 +12,8 @@ allowed-tools: Read, Write, Edit, Bash, Grep, Glob, WebSearch, WebFetch
1. Fetch `https://a2a-protocol.org/latest/specification/` for the Task object schema and state machine
2. Web-search `site:github.com a2aproject A2A task lifecycle states` for state transition rules
3. Web-search `site:github.com a2aproject a2a-samples task` for task handling examples
4. Fetch SDK docs for task-related classes and state management utilities

## Conceptual Architecture

### What a Task Is

A Task is the **central unit of work** in A2A. It represents a request from a client agent to a server agent, tracks progress through well-defined states, accumulates messages and artifacts, and reaches a terminal state when complete.

### Task Structure

Key fields of a Task object:
- **id** β€” Unique task identifier (set by client or server)
- **status** β€” Current state object containing `state` enum and optional `message`
- **messages** β€” Array of messages exchanged (if `stateTransitionHistory` enabled)
- **artifacts** β€” Array of output artifacts produced by the agent
- **metadata** β€” Optional key-value pairs for custom data

### The 9 States
## The 9 States

| State | Terminal? | Description |
|-------|-----------|-------------|
Expand All @@ -43,68 +27,118 @@ Key fields of a Task object:
| `rejected` | Yes | Server refused the task |
| `unknown` | β€” | Default/unknown state |

### Valid State Transitions
## Valid State Transitions

```
submitted β†’ working
submitted β†’ working β†’ completed
β†’ failed
β†’ canceled
β†’ input-required β†’ working (client provides input)
β†’ canceled

submitted β†’ rejected
submitted β†’ canceled

working β†’ completed
working β†’ failed
working β†’ canceled
working β†’ input-required

input-required β†’ working (when client provides more input)
input-required β†’ canceled

auth-required β†’ working (when auth is provided)
auth-required β†’ working (auth provided)
auth-required β†’ canceled
```

**Rules:**
- Terminal states (`completed`, `failed`, `canceled`, `rejected`) are final β€” no transitions out
- Only the server transitions the task state (except `canceled` which client can request)
- `input-required` β†’ `working` happens when the client sends a follow-up message

### Task Creation

Tasks are created implicitly when a client sends a message without a `taskId`:
1. Client sends `message/send` or `message/stream` without `taskId`
2. Server creates a new task, assigns an ID
3. Task starts in `submitted` state
4. Server may immediately transition to `working` or return `submitted`

### Task Continuation
**Rules:** Terminal states (`completed`, `failed`, `canceled`, `rejected`) are final β€” no transitions out. Only the server transitions state (except `canceled` which client can request).

## Task State Machine Implementation

```typescript
const TERMINAL_STATES = new Set(["completed", "failed", "canceled", "rejected"]);

const VALID_TRANSITIONS: Record<string, string[]> = {
submitted: ["working", "rejected", "canceled"],
working: ["completed", "failed", "canceled", "input-required"],
"input-required": ["working", "canceled"],
"auth-required": ["working", "canceled"],
};

interface Task {
id: string;
status: { state: string; message?: string; timestamp: string };
messages: Message[];
artifacts: Artifact[];
metadata?: Record<string, unknown>;
}

function transitionTask(task: Task, newState: string, message?: string): Task {
if (TERMINAL_STATES.has(task.status.state)) {
throw new Error(`Cannot transition from terminal state: ${task.status.state}`);
}
const allowed = VALID_TRANSITIONS[task.status.state];
if (!allowed?.includes(newState)) {
throw new Error(`Invalid transition: ${task.status.state} β†’ ${newState}`);
}
return {
...task,
status: { state: newState, message, timestamp: new Date().toISOString() },
};
}
```

When a client sends a message with an existing `taskId`:
1. The message is appended to the task's history
2. The server resumes processing
3. State typically transitions from `input-required` back to `working`
## Task Creation (message/send handler)

```typescript
import { randomUUID } from "crypto";

async function handleMessageSend(request: {
taskId?: string;
message: Message;
}): Promise<Task> {
let task: Task;

if (request.taskId) {
task = await taskStore.get(request.taskId);
if (!task) throw new Error(`Task not found: ${request.taskId}`);
task.messages.push(request.message);
task = transitionTask(task, "working");
} else {
task = {
id: randomUUID(),
status: { state: "submitted", timestamp: new Date().toISOString() },
messages: [request.message],
artifacts: [],
};
}

await taskStore.save(task);
task = transitionTask(task, "working", "Processing request");
await taskStore.save(task);

const result = await processTask(task);
task.artifacts.push(result.artifact);
task = transitionTask(task, "completed", "Done");
await taskStore.save(task);
return task;
}
```

### Artifacts
## Artifacts

Artifacts are the **outputs** of a task:
- Produced during `working` state
Artifacts are the outputs of a task, produced during `working` state:
- Each artifact has `id`, `name`, optional `description`, and `parts`
- Parts can be TextPart, FilePart, or DataPart
- In streaming mode, artifacts are delivered incrementally via `TaskArtifactUpdateEvent`
- Multiple artifacts can be produced per task

### State Transition History
## Verification Workflow

If the agent declares `stateTransitionHistory: true` in its Agent Card:
- The task object includes a complete history of all state transitions
- Each transition records the state, timestamp, and optional message
- Useful for auditing and debugging
1. Create a task via `message/send` without `taskId` β€” verify task is created with `submitted` state
2. Verify automatic transition to `working` β€” check status updates
3. Attempt an invalid transition (e.g., `submitted` β†’ `completed`) β€” verify error is thrown
4. Complete a task β€” verify state is `completed` and artifacts are present
5. Attempt to transition a completed task β€” verify error (terminal state)
6. Test `input-required` flow: send a task that needs more input, provide follow-up, verify it resumes

### Best Practices
## Best Practices

- Always validate state transitions β€” reject invalid ones with appropriate errors
- Use task IDs that are globally unique (UUIDs recommended)
- Use UUIDs for task IDs
- Store task state durably for production (not just in-memory)
- Set timeouts for tasks stuck in non-terminal states
- Clean up old tasks to prevent unbounded storage growth
- Include meaningful messages in status updates (not just the state enum)
- Use artifacts for structured outputs, messages for conversational exchanges
- Implement idempotency β€” handle duplicate messages for the same task gracefully
Expand Down
139 changes: 79 additions & 60 deletions stripe-mpp/skills/mpp-session-flow/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: mpp-session-flow
description: Implement MPP session-based streaming payment flows β€” authorize-once pay-as-you-go patterns for continuous data feeds, per-token billing, and micropayment aggregation. Use when building streaming APIs or services that charge incrementally.
description: "Implement MPP session-based streaming payment flows β€” authorize-once pay-as-you-go patterns for continuous data feeds, per-token billing, and micropayment aggregation. Use when building streaming APIs or services that charge incrementally, implementing pay-per-use or metered billing, or adding usage-based pricing to an API."
allowed-tools: Read, Write, Edit, Bash, Grep, Glob, WebSearch, WebFetch
---

Expand All @@ -12,91 +12,110 @@ allowed-tools: Read, Write, Edit, Bash, Grep, Glob, WebSearch, WebFetch
1. Fetch `https://www.npmjs.com/package/mppx` for the session middleware API and payment channel configuration
2. Fetch `https://paymentauth.org/` for the canonical session intent specification
3. Web-search `mpp session streaming micropayments payment channel` for session implementation patterns
4. Web-search `site:mpp.dev session` for session-specific documentation

## Conceptual Architecture

### What Session Intent Is

The session intent implements **streaming micropayments** β€” often described as **"OAuth for money"**. The agent authorizes a spending limit upfront, then streams micropayments continuously as it consumes resources:
## Session Lifecycle

```
1. Client opens session with spending cap
2. Server creates payment channel
3. Client makes requests β€” each deducts from the spending cap
4. Micropayments stream at sub-cent costs, sub-millisecond latency
5. Session closes β€” final settlement on-chain (single transaction)
Open β†’ Authorize β†’ Active β†’ Refill (optional) β†’ Close β†’ Settled
```

### When to Use Session
1. **Open** β€” Client sends initial request; server returns 402 with session challenge
2. **Authorize** β€” Client authorizes spending cap (e.g., 10,000 units)
3. **Active** β€” Client makes requests; each deducts from the cap
4. **Refill** β€” Client can extend the cap before it runs out
5. **Close** β€” Either party closes; final settlement happens on-chain
6. **Settled** β€” Single on-chain transaction for the total consumed amount

## Server-Side Implementation

- **Per-token billing** β€” LLM inference charged per token generated
- **Continuous data feeds** β€” Real-time market data, sensor streams
- **Compute metering** β€” Pay for actual CPU/GPU seconds used
- **Bandwidth metering** β€” Pay per KB transferred
- **Any high-frequency, low-value access pattern**
```typescript
import { mppx } from "mppx";

### Session vs Charge Comparison
// Protect a streaming endpoint with session-based payment
app.get("/api/stream", mppx.session({ maxAmount: "10000" }), async (c) => {
return c.json({ data: "streaming content" });
});

| Dimension | Charge | Session |
|-----------|--------|---------|
| Settlement | Per-request on-chain/card | Aggregated at session close |
| Latency | Includes payment settlement per call | Sub-millisecond after session open |
| Cost | One tx per request | One tx for entire session |
| Pricing | Fixed per request | Variable, metered |
| Use case | Infrequent, high-value calls | Frequent, low-value calls |
// Metered endpoint β€” charge per unit consumed
app.post("/api/inference", mppx.session({ maxAmount: "50000" }), async (c) => {
const result = await runInference(c.req.body);
const tokensUsed = result.usage.totalTokens;
await c.mpp.charge(tokensUsed);
return c.json({ result: result.output, charged: tokensUsed });
});
```

### Server-Side Implementation
## Client-Side Implementation

```typescript
// Protect a route with a session payment gate
app.get('/api/stream', mppx.session({ maxAmount: '10000' }), async (c) => {
// Deducts from the session's spending cap
return c.json({ data: 'streaming content' });
import { MppClient } from "mppx/client";

const client = new MppClient({ wallet: agentWallet });

// Open a session with a spending cap
const session = await client.openSession("https://api.example.com/api/stream", {
spendingCap: 10000,
});
```

### Session Lifecycle
// Make metered requests β€” each deducts from the cap
const response = await session.fetch("/api/inference", {
method: "POST",
body: JSON.stringify({ prompt: "Hello" }),
});

1. **Open** β€” Client sends initial request; server returns 402 with session challenge
2. **Authorize** β€” Client authorizes spending cap (e.g., 10,000 units)
3. **Active** β€” Client makes requests; each deducts from the cap
4. **Refill** β€” Client can extend the cap before it runs out
5. **Close** β€” Either party closes; final settlement happens on-chain
6. **Settled** β€” Single on-chain transaction for the total consumed amount
// Monitor remaining balance
console.log(`Remaining: ${session.remainingBalance}`);

### Payment Channel
// Extend the cap before it runs out
if (session.remainingBalance < 1000) {
await session.refill(5000);
}

Session payments use a **payment channel** β€” an off-chain mechanism where:
- Funds are locked upfront in a channel
- Each micropayment updates the channel state without on-chain transactions
- Only the opening and closing transactions go on-chain
- This enables thousands of sub-cent payments at sub-millisecond latency
// Close the session β€” triggers final settlement
await session.close();
```

### Spending Cap Management
## Handling Cap Exhaustion

- Agent sets the maximum they're willing to spend in the session
- Server deducts from this cap per request/unit consumed
- Agent can monitor remaining balance
- If cap is exhausted, server returns 402 for a new session
- Agent can proactively extend the cap
```typescript
// Server: return 402 when cap is exhausted
app.use("/api/*", async (c, next) => {
try {
await next();
} catch (err) {
if (err.code === "CAP_EXHAUSTED") {
return c.json({ error: "spending_cap_exhausted", remaining: 0 }, 402);
}
throw err;
}
});

// Client: handle 402 by opening a new session
async function fetchWithRetry(session, url, opts) {
const res = await session.fetch(url, opts);
if (res.status === 402) {
const newSession = await client.openSession(url, { spendingCap: 10000 });
return newSession.fetch(url, opts);
}
return res;
}
```

### Metering Patterns
## Verification Workflow

| Pattern | Description |
|---------|-------------|
| Fixed per request | Each request costs a fixed amount |
| Per-unit | Cost varies by units consumed (tokens, bytes, seconds) |
| Time-based | Cost accrues per time interval |
| Tiered | Rate decreases with volume (first 100 at $X, next 1000 at $Y) |
1. Start server with session middleware enabled
2. Open a session from the client β€” verify 402 challenge is returned, then authorization succeeds
3. Make a metered request β€” verify balance decreases by the correct amount
4. Exhaust the cap β€” verify server returns 402
5. Refill or open a new session β€” verify requests resume
6. Close the session β€” verify settlement transaction is recorded

### Best Practices
## Best Practices

- Set reasonable default spending caps (not too high for safety, not too low for UX)
- Implement cap exhaustion warnings before the cap runs out
- Log metering data for billing reconciliation
- Handle session interruptions gracefully (network drops, server restarts)
- Implement session resumption where possible
- Monitor session durations and spending patterns for pricing optimization

Fetch the latest mppx SDK documentation and MPP specification for exact session API, payment channel mechanics, and configuration options before implementing.