Compare commits

..

6 Commits

Author SHA1 Message Date
Codex c84ebf272a Refresh planner priorities for 4622
govulncheck / govulncheck (push) Waiting to run
Harness (E2E) / Harnesses (mock LLM) (push) Waiting to run
Harness (E2E) / Provider harnesses (live LLM conformance) (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-07-10 22:33:44 +00:00
Asim Aslam 29d8544ce5 Harden universe A2A reachability probe (#4621)
Co-authored-by: Codex <codex@openai.com>
2026-07-10 22:59:29 +01:00
Asim Aslam c7d510349e Refresh planner priorities for 4617 (#4619)
Co-authored-by: Codex <codex@openai.com>
2026-07-10 22:35:20 +01:00
Asim Aslam 81f81460aa Handle AtlasCloud workspace repair fallback (#4616)
Co-authored-by: Codex <codex@openai.com>
2026-07-10 22:15:52 +01:00
Asim Aslam ba7db2f315 Refresh planner priorities for 4613 (#4614)
Co-authored-by: Codex <codex@openai.com>
2026-07-10 21:36:12 +01:00
Asim Aslam ed3e0e5a06 Handle AtlasCloud empty-arg text tool repair (#4612)
Co-authored-by: Codex <codex@openai.com>
2026-07-10 21:15:35 +01:00
5 changed files with 192 additions and 9 deletions
+1 -3
View File
@@ -21,9 +21,7 @@ changes, architectural rewrites. Those go to the human.
## Work queue (ranked)
1. **Handle incomplete AtlasCloud plan-delegate task list tool calls** ([#4606](https://github.com/micro/go-micro/issues/4606)) — Highest-value open Now-phase hardening item after #4599 shipped in #4608: the 0→hero plan/delegate path already created a task, then failed on an incomplete repaired `task_TaskService_List` tool call. Fix this first because partial side effects in the canonical services → agents → workflows demo directly undermine the adoption story even when the mock harness is green.
2. **Handle incomplete AtlasCloud agent-flow workspace tool calls** ([#4595](https://github.com/micro/go-micro/issues/4595)) — Same provider-repair seam in the event-driven agent-flow path: the expected workspace/notification side effects occurred once, duplicate side effects were suppressed, then the flow still reported failure on an incomplete repaired workspace tool call. Keep it adjacent to #4606 so the runtime handles malformed repaired tool calls consistently across plan/delegate and flow dispatch.
3. **Make AtlasCloud universe A2A reachability probe deterministic** ([#4504](https://github.com/micro/go-micro/issues/4504)) — The remaining live AtlasCloud interop seam is the universe A2A reachability probe intermittently timing out after the checkout flow succeeds. Keep it behind the repaired-tool-call failures because it is side-effect safe and isolated to reachability/probe timing rather than the core 0→hero side-effect path.
1. **Add a no-secret first-agent chat transcript check** ([#4618](https://github.com/micro/go-micro/issues/4618)) — Developer adoption is now the highest-value open Now-phase item after #4504 shipped in #4621. README, the website, and the v6.3.15 blog point at the provider-free first-agent path, but the 0→1 agent experience still needs an expected chat/tool-call transcript that a new developer can compare against before adding provider keys. Make the smallest first-agent path more walkable and CI-verifiable.
_Seeded by Claude Code from the roadmap + open issues; thereafter maintained by the
architecture-review pass._
+76
View File
@@ -557,8 +557,84 @@ func atlascloudFallbackTextToolCall(toolName string, req *ai.Request) string {
if atlascloudToolTakesNoArguments(toolName, req.Tools) {
return atlascloudEmptyArgumentFallbackTextToolCall(toolName)
}
return atlascloudServiceFallbackTextToolCall(toolName, req)
}
}
func atlascloudServiceFallbackTextToolCall(toolName string, req *ai.Request) string {
if req == nil {
return ""
}
for _, tool := range req.Tools {
if tool.Name != toolName {
continue
}
args := atlascloudFallbackArgsForProperties(tool.Properties, atlascloudRequestText(req))
if args == nil {
return ""
}
b, err := json.Marshal(args)
if err != nil {
return ""
}
return `<tool_call name="` + toolName + `">` + string(b) + `</tool_call>`
}
return ""
}
func atlascloudFallbackArgsForProperties(properties map[string]any, ctxText string) map[string]any {
if len(properties) == 0 {
return map[string]any{}
}
args := make(map[string]any, len(properties))
for name, schema := range properties {
value, ok := atlascloudFallbackArgValue(name, schema, ctxText)
if !ok {
return nil
}
args[name] = value
}
return args
}
func atlascloudFallbackArgValue(name string, schema any, ctxText string) (any, bool) {
typeName := "string"
if m, ok := schema.(map[string]any); ok {
if t, _ := m["type"].(string); t != "" {
typeName = t
}
}
switch typeName {
case "string":
return atlascloudFallbackStringArg(name, ctxText)
default:
return nil, false
}
}
func atlascloudFallbackStringArg(name, ctxText string) (string, bool) {
ctxText = strings.TrimSpace(ctxText)
if ctxText == "" {
return "", false
}
if strings.Contains(strings.ToLower(name), "email") || strings.Contains(strings.ToLower(name), "owner") {
if email := atlascloudFirstEmail(ctxText); email != "" {
return email, true
}
}
return ctxText, true
}
func atlascloudFirstEmail(text string) string {
for _, field := range strings.FieldsFunc(text, func(r rune) bool {
return strings.ContainsRune(" \t\n\r<>\"'(),;", r)
}) {
field = strings.Trim(field, ".:")
if strings.Contains(field, "@") && strings.Contains(field, ".") {
return field
}
}
return ""
}
func atlascloudToolTakesNoArguments(toolName string, tools []ai.Tool) bool {
+40
View File
@@ -658,6 +658,46 @@ func TestProvider_GenerateFallsBackAfterRepeatedPartialNoArgumentServiceTextTool
}
}
func TestProvider_GenerateFallsBackAfterRepeatedPartialWorkspaceServiceTextToolCall(t *testing.T) {
var bodies []map[string]any
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var body map[string]any
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
t.Fatalf("decode request: %v", err)
}
bodies = append(bodies, body)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"choices":[{"message":{"content":"<tool_call name=\"workspace_WorkspaceService_Create\">"}}]}`))
}))
defer ts.Close()
p := NewProvider(
ai.WithAPIKey("test-key"),
ai.WithBaseURL(ts.URL),
ai.WithModel("minimaxai/minimax-m3"),
)
resp, err := p.Generate(context.Background(), &ai.Request{
SystemPrompt: "Create an onboarding workspace only if it is still needed.",
Prompt: "Onboard alice@acme.com. The workspace create side effect may already be complete; avoid failing the flow on a duplicate repaired call.",
Tools: []ai.Tool{{
Name: "workspace_WorkspaceService_Create",
OriginalName: "workspace.WorkspaceService.Create",
Description: "Create an onboarding workspace",
Properties: map[string]any{"owner": map[string]any{"type": "string"}},
}},
})
if err != nil {
t.Fatalf("Generate returned error: %v", err)
}
want := `<tool_call name="workspace_WorkspaceService_Create">{"owner":"alice@acme.com"}</tool_call>`
if resp.Reply != want {
t.Fatalf("Reply = %q, want %q", resp.Reply, want)
}
if len(bodies) != 2 {
t.Fatalf("requests = %d, want initial plus repair", len(bodies))
}
}
func TestProvider_GenerateRetriesMinimaxBuiltInsAsTextTools(t *testing.T) {
var bodies []map[string]any
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+47 -6
View File
@@ -357,14 +357,55 @@ func check(cond bool, format string, args ...any) {
// should not depend on a live model deciding to send another notification.
func a2aReachable(ctx context.Context, base, agent string) error {
probe := "A2A reachability probe only. Reply with the words concierge reachable. Do not call tools or send notifications."
reply, err := a2a.NewClient(base+"/agents/"+agent).Send(ctx, probe)
if err != nil {
return err
deadline, ok := ctx.Deadline()
if !ok {
deadline = time.Now().Add(10 * time.Second)
}
if strings.TrimSpace(reply) == "" {
return fmt.Errorf("empty A2A reply")
var lastErr error
for attempt := 1; ; attempt++ {
if err := ctx.Err(); err != nil {
if lastErr != nil {
return fmt.Errorf("A2A reachability probe failed after %d attempt(s): %w", attempt-1, lastErr)
}
return err
}
remaining := time.Until(deadline)
if remaining <= 0 {
if lastErr != nil {
return fmt.Errorf("A2A reachability probe failed after %d attempt(s): %w", attempt-1, lastErr)
}
return context.DeadlineExceeded
}
attemptTimeout := 4 * time.Second
if remaining < attemptTimeout {
attemptTimeout = remaining
}
attemptCtx, cancel := context.WithTimeout(ctx, attemptTimeout)
reply, err := a2a.NewClient(base+"/agents/"+agent).Send(attemptCtx, probe)
cancel()
if err == nil && strings.TrimSpace(reply) != "" {
return nil
}
if err == nil {
err = fmt.Errorf("empty A2A reply")
}
lastErr = err
if time.Until(deadline) <= 0 {
return fmt.Errorf("A2A reachability probe failed after %d attempt(s): %w", attempt, lastErr)
}
time.Sleep(minDuration(200*time.Millisecond*time.Duration(attempt), time.Until(deadline)))
}
return nil
}
func minDuration(a, b time.Duration) time.Duration {
if a < b {
return a
}
return b
}
func providerKey(provider string) string {
+28
View File
@@ -3,6 +3,9 @@ package main
import (
"context"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"sync/atomic"
"testing"
"time"
@@ -25,6 +28,31 @@ func TestUniverseHarnessContract(t *testing.T) {
}
}
func TestA2AReachableRetriesTransientTimeout(t *testing.T) {
var calls int64
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if got, want := r.URL.Path, "/agents/concierge"; got != want {
t.Fatalf("path = %q, want %q", got, want)
}
if atomic.AddInt64(&calls, 1) == 1 {
time.Sleep(5 * time.Second)
return
}
w.Header().Set("Content-Type", "application/json")
fmt.Fprint(w, `{"jsonrpc":"2.0","id":1,"result":{"kind":"task","id":"task-1","contextId":"ctx-1","status":{"state":"completed"},"artifacts":[{"artifactId":"artifact-1","parts":[{"kind":"text","text":"concierge reachable"}]}]}}`)
}))
defer srv.Close()
ctx, cancel := context.WithTimeout(context.Background(), 6*time.Second)
defer cancel()
if err := a2aReachable(ctx, srv.URL, "concierge"); err != nil {
t.Fatalf("a2aReachable returned error: %v", err)
}
if got := atomic.LoadInt64(&calls); got < 2 {
t.Fatalf("A2A calls = %d, want retry after transient timeout", got)
}
}
func TestNotifyStepCompletesAfterObservedSideEffectTimeout(t *testing.T) {
ntf := new(Notify)
before := atomic.LoadInt64(&ntf.sent)