Compare commits

..

4 Commits

Author SHA1 Message Date
Codex d3b246722b docs: align public x402 flag examples
Harness (E2E) / Provider harnesses (live LLM conformance) (push) Waiting to run
Harness (E2E) / Harnesses (mock LLM) (push) Waiting to run
Lint / golangci-lint (push) Waiting to run
Run Tests / Unit Tests (push) Waiting to run
Run Tests / Etcd Integration Tests (push) Waiting to run
2026-06-27 08:08:24 +00:00
Asim Aslam ba231efe71 Update README.md 2026-06-27 07:08:08 +01:00
Asim Aslam f755eadd47 Propagate flow checkpoint failures (#3150)
Co-authored-by: Codex <codex@openai.com>
2026-06-27 06:55:56 +01:00
Asim Aslam 467c937873 Improve flow run history validation (#3148)
Co-authored-by: Codex <codex@openai.com>
2026-06-27 04:55:49 +01:00
5 changed files with 92 additions and 20 deletions
+5 -3
View File
@@ -2,7 +2,9 @@
Go Micro is an **agent harness** and service framework for Go.
A harness is the runtime around an agent: the tools it can call, the memory it keeps, the guardrails that bound it, the workflows that trigger it, the services it depends on, and the protocols other agents use to reach it. Go Micro gives you that harness as Go code. Build an agent and it gets a model, memory, tools, planning, delegation, guardrails, and service discovery; it is reachable over [MCP](https://modelcontextprotocol.io/) and [A2A](https://a2a-protocol.org). Write services and every endpoint becomes an AI-callable tool. Orchestrate the deterministic parts with durable flows. Agents, services, and flows share one runtime because an agent is a distributed system, and building one is building a service.
A harness is the runtime around an agent: the tools it can call, the memory it keeps, the guardrails that bound it, the workflows that trigger it, the services it depends on, and the protocols other agents use to reach it.
Go Micro gives you the harness as Go code. Build an agent and it gets a model, memory, tools, planning, delegation, guardrails, and service discovery; it is reachable over [MCP](https://modelcontextprotocol.io/) and [A2A](https://a2a-protocol.org). Write services and every endpoint becomes an AI-callable tool. Orchestrate the deterministic parts with durable flows. Agents, services, and flows share one runtime because an agent is a distributed system, and building one is building a service.
## Sponsors
@@ -253,9 +255,9 @@ Every endpoint is an AI-callable tool — and it can be a *paid* tool. Go Micro
```bash
# Charge for tool calls at the MCP gateway (off unless you set a pay-to address)
micro mcp serve --x402-pay-to 0xYourAddress --x402-network solana --x402-amount 10000
micro mcp serve --x402_pay_to 0xYourAddress --x402_network solana --x402_amount 10000
# Per-tool amounts via a config file
micro mcp serve --x402-config x402.json
micro mcp serve --x402_config x402.json
```
See the [Payments (x402) guide](internal/website/docs/guides/x402-payments.md).
+24 -7
View File
@@ -408,7 +408,10 @@ func (f *Flow) runFrom(ctx context.Context, run Run) (Run, error) {
step := steps[i]
run.State.Stage = step.Name
run.Steps[i].Status = "in_progress"
f.save(ctx, run)
if err := f.save(ctx, run); err != nil {
spanErr = err
return run, err
}
out, attempts, err := f.runStepSpan(ctx, step, run.State)
run.Steps[i].Attempts = attempts
@@ -417,7 +420,10 @@ func (f *Flow) runFrom(ctx context.Context, run Run) (Run, error) {
run.Steps[i].Status = "failed"
run.Steps[i].Error = err.Error()
run.Status = "failed"
f.save(ctx, run)
if saveErr := f.save(ctx, run); saveErr != nil {
spanErr = saveErr
return run, fmt.Errorf("%w; additionally failed to checkpoint failed run: %v", err, saveErr)
}
f.record(resultFromRun(f.opts.TriggerTopic, run))
f.log.Logf(logger.ErrorLevel, "Flow %s run %s failed at step %q: %v", f.name, run.ID, step.Name, err)
return run, err
@@ -431,13 +437,22 @@ func (f *Flow) runFrom(ctx context.Context, run Run) (Run, error) {
} else {
run.State.Stage = ""
}
f.save(ctx, run)
if err := f.save(ctx, run); err != nil {
spanErr = err
return run, err
}
}
run.Status = "done"
f.save(ctx, run)
if err := f.save(ctx, run); err != nil {
spanErr = err
return run, err
}
if f.opts.DeleteOnSuccess && f.checkpoint != nil {
_ = f.checkpoint.Delete(ctx, run.ID)
if err := f.checkpoint.Delete(ctx, run.ID); err != nil {
spanErr = err
return run, err
}
}
f.record(resultFromRun(f.opts.TriggerTopic, run))
f.log.Logf(logger.InfoLevel, "Flow %s run %s completed (%d steps)", f.name, run.ID, len(steps))
@@ -479,13 +494,15 @@ func (f *Flow) runStep(ctx context.Context, step Step, in State) (State, int, er
return in, retries + 1, lastErr
}
func (f *Flow) save(ctx context.Context, run Run) {
func (f *Flow) save(ctx context.Context, run Run) error {
if f.checkpoint == nil {
return
return nil
}
if err := f.checkpoint.Save(ctx, run); err != nil {
f.log.Logf(logger.ErrorLevel, "Flow %s checkpoint save: %v", f.name, err)
return fmt.Errorf("flow %s checkpoint save: %w", f.name, err)
}
return nil
}
func validateSteps(steps []Step) error {
+53
View File
@@ -417,6 +417,59 @@ func TestStoreCheckpointHonorsCanceledContext(t *testing.T) {
}
}
type failingCheckpoint struct {
err error
}
func (c failingCheckpoint) Save(context.Context, Run) error { return c.err }
func (c failingCheckpoint) Load(context.Context, string) (Run, bool, error) {
return Run{}, false, c.err
}
func (c failingCheckpoint) Delete(context.Context, string) error { return c.err }
func (c failingCheckpoint) List(context.Context) ([]Run, error) { return nil, c.err }
func TestFlowCheckpointSaveFailureStopsRun(t *testing.T) {
checkpointErr := errors.New("checkpoint unavailable")
var ran bool
f := New("checkpoint-fails",
WithCheckpoint(failingCheckpoint{err: checkpointErr}),
Steps(Step{Name: "work", Run: func(_ context.Context, in State) (State, error) {
ran = true
return in, nil
}}),
)
err := f.Execute(context.Background(), "start")
if !errors.Is(err, checkpointErr) {
t.Fatalf("Execute error = %v, want checkpoint error", err)
}
if ran {
t.Fatal("step ran even though the in-progress checkpoint failed")
}
}
func TestFlowDeleteOnSuccessFailureIsReturned(t *testing.T) {
checkpointErr := errors.New("delete unavailable")
cp := &deleteFailCheckpoint{Checkpoint: StoreCheckpoint(store.NewMemoryStore(), "delete-fails"), err: checkpointErr}
f := New("delete-fails",
WithCheckpoint(cp),
DeleteOnSuccess(),
Steps(appendStep("work")),
)
err := f.Execute(context.Background(), "")
if !errors.Is(err, checkpointErr) {
t.Fatalf("Execute error = %v, want delete error", err)
}
}
type deleteFailCheckpoint struct {
Checkpoint
err error
}
func (c *deleteFailCheckpoint) Delete(context.Context, string) error { return c.err }
func TestStateSetScan(t *testing.T) {
var s State
type payload struct {
@@ -42,10 +42,10 @@ Because every endpoint is already an MCP tool, the gateway is where you charge.
```bash
micro mcp serve --address :3000 \
--x402-pay-to 0xYourAddress \
--x402-network solana \
--x402-amount 10000 \
--x402-facilitator https://facilitator.example
--x402_pay_to 0xYourAddress \
--x402_network solana \
--x402_amount 10000 \
--x402_facilitator https://facilitator.example
```
## A shoppable catalog
@@ -69,7 +69,7 @@ Free tools carry no `payment` block. This is the foundation for a tool marketpla
Different tools can cost different amounts. Pricing is an **operator** concern — the payTo address is the operator's, and amounts change without redeploying anyone's service — so it's configured at the gateway with a file, the same way per-tool scopes and rate limits are. Point the gateway at an x402 config:
```bash
micro mcp serve --address :3000 --x402-config x402.json
micro mcp serve --address :3000 --x402_config x402.json
```
```json
@@ -85,7 +85,7 @@ micro mcp serve --address :3000 --x402-config x402.json
}
```
`amount` is the default (here `"0"` — free unless priced), and `amounts` sets per-tool overrides keyed by tool name. There is no "pricing" abstraction; it's the x402 `amount`, resolved per tool, in the protocol's own vocabulary. The standalone gateway accepts the same file via `--x402-config` or the `X402_CONFIG` environment variable.
`amount` is the default (here `"0"` — free unless priced), and `amounts` sets per-tool overrides keyed by tool name. There is no "pricing" abstraction; it's the x402 `amount`, resolved per tool, in the protocol's own vocabulary. `micro mcp serve` accepts the file via `--x402_config`; the standalone gateway accepts the same file via `--x402-config` or the `X402_CONFIG` environment variable.
## Paying for tools (the consumer side)
+4 -4
View File
@@ -34,7 +34,7 @@ about the framework.
- [Transport](transport.html)
- [Store](store.html)
- [Plugins](plugins.html)
- [Examples](examples/index.md)
- [Examples](examples/)
## Development & Deployment
@@ -52,9 +52,9 @@ about the framework.
## Advanced
- [Framework Comparison](guides/comparison.html)
- [Architecture Decisions](architecture/index.md)
- [Real-World Examples](examples/realworld/index.md)
- [Migration Guides](guides/migration/index.md)
- [Architecture Decisions](architecture/)
- [Real-World Examples](examples/realworld/)
- [Migration Guides](guides/migration/)
- [Observability](observability.html)
- [Contributing](contributing.html)
- [Roadmap](roadmap.html)