-
Notifications
You must be signed in to change notification settings - Fork 103
Initial commit of the Substrate refactor #17
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
4d3bdad
New Substrate deployment modal
rakyll b1c9af5
feat: add axharness build target and deployment manifest
rakyll a83c35c
feat: add ReplicaSet configuration for ax-server deployment
rakyll 237f707
feat: implement controller2 with agent registry, validation logic, an…
rakyll 55f0cec
Please provide the list of changes or the files modified so I can gen…
rakyll 0c50b3a
feat: integrate harness into controller execution and replace legacy …
rakyll 3ece735
chore: document substrate refactor cleanup tasks in internal tracking…
rakyll 4ae10ec
chore: add TODO comments for execution resumption and remote harness …
rakyll d3dcd40
refactor: rename harnesstest.NewHarness to harnesstest.New for consis…
rakyll fc41076
feat: implement SubstrateHarness for managing sandboxed actor executi…
rakyll bf7592e
refactor: remove ate build tag from k8s client
rakyll 6cc0db6
refactor: rename execID parameter to id in CreateActor and SuspendAct…
rakyll 5575f73
refactor: remove axepp from substrate refactor cleanups list
rakyll 3535564
chore: remove axepp and ate build tag from refactor cleanups document
rakyll 73a5019
chore: update deployment path and remove obsolete harness and axepp c…
rakyll c917a9f
refactor: remove conversation ID metadata from harness service stream…
rakyll 0aaf0d8
Remove unnecessary ate build tags because Substrate is now public
rakyll 9ba40e4
refactor: remove PlannerBuilder from controller and move reserved age…
rakyll File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| // Copyright 2026 Google LLC | ||
| // | ||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||
| // you may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software | ||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
|
|
||
| package main | ||
|
|
||
| import ( | ||
| "flag" | ||
| "fmt" | ||
| "log" | ||
| "net" | ||
| "os" | ||
| "os/signal" | ||
| "syscall" | ||
|
|
||
| "google.golang.org/grpc" | ||
|
|
||
| "github.com/google/ax/proto" | ||
| ) | ||
|
|
||
| var ( | ||
| port = flag.Int("port", 50053, "The port for the gRPC HarnessService to listen on") | ||
| ) | ||
|
|
||
| func main() { | ||
| flag.Parse() | ||
|
|
||
| lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port)) | ||
| if err != nil { | ||
| log.Fatalf("Failed to listen on port :%d: %v", *port, err) | ||
| } | ||
|
|
||
| // Start gRPC Server | ||
| grpcServer := grpc.NewServer() | ||
| harnessServer := NewHarnessServiceServer() | ||
| proto.RegisterHarnessServiceServer(grpcServer, harnessServer) | ||
|
|
||
| // Graceful shutdown handling | ||
| sigChan := make(chan os.Signal, 1) | ||
| signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) | ||
| go func() { | ||
| <-sigChan | ||
| log.Println("\nReceived shutdown signal, stopping gRPC HarnessService server gracefully...") | ||
| grpcServer.GracefulStop() | ||
| }() | ||
|
|
||
| log.Printf("gRPC HarnessService listening on port :%d...\n", *port) | ||
| if err := grpcServer.Serve(lis); err != nil { | ||
| log.Fatalf("Failed to serve gRPC: %v", err) | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| // Copyright 2026 Google LLC | ||
| // | ||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||
| // you may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software | ||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
|
|
||
| package main | ||
|
|
||
| import ( | ||
| "io" | ||
|
|
||
| "github.com/google/ax/proto" | ||
| ) | ||
|
|
||
| // HarnessServiceServer implements the gRPC proto.HarnessServiceServer interface. | ||
| type HarnessServiceServer struct { | ||
| proto.UnimplementedHarnessServiceServer | ||
| } | ||
|
|
||
| // NewHarnessServiceServer creates a new HarnessServiceServer. | ||
| func NewHarnessServiceServer() *HarnessServiceServer { | ||
| return &HarnessServiceServer{} | ||
| } | ||
|
|
||
| // Connect implements the bidirectional gRPC streaming capability. | ||
| // It receives client inputs and responds only with "Hello world". | ||
| func (s *HarnessServiceServer) Connect(stream proto.HarnessService_ConnectServer) error { | ||
| for { | ||
| _, err := stream.Recv() | ||
| if err == io.EOF { | ||
| return nil | ||
| } | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| err = stream.Send(&proto.HarnessMessage{ | ||
| Messages: []*proto.Message{ | ||
| { | ||
| Role: "assistant", | ||
| Content: &proto.Content{ | ||
| Type: &proto.Content_Text{ | ||
| Text: &proto.TextContent{ | ||
| Text: "Hello world", | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| }) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| * Make internal/manifests/ax-deployment2.yaml the new templated ax-deployment.yaml.tmpl. | ||
| * Remove harnesstest package. | ||
| * Update HarnessService with the actual protocol. | ||
| * Remove axepp. | ||
| * Remove ate build tag. | ||
| * Remove harnessHandler once Exec RPC is revisited. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,192 @@ | ||
| // Copyright 2026 Google LLC | ||
| // | ||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||
| // you may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software | ||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
|
|
||
| // Package controller implements the single-writer orchestrator that coordinates | ||
| // agentic loops, manages executions, and communicates with local and remote agents. | ||
| package controller2 | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
|
|
||
| "github.com/google/ax/internal/controller/executor" | ||
| "github.com/google/ax/internal/harness/harnesstest" | ||
| "github.com/google/ax/proto" | ||
| "github.com/google/uuid" | ||
| ) | ||
|
|
||
| type ExecHandler func(resp *proto.ExecResponse) error | ||
|
|
||
| // Controller is the main controller that coordinates all components. | ||
| // It acts as a single-writer system for managing agentic loops. | ||
| type Controller struct { | ||
| registry *Registry | ||
| eventLog executor.EventLog | ||
| } | ||
|
|
||
| // Config configures the controller. | ||
| type Config struct { | ||
| EventLogBuilder executor.EventLogBuilder | ||
| } | ||
|
|
||
| // New creates a new controller instance. | ||
| func New(ctx context.Context, cfg Config) (*Controller, error) { | ||
| // Initialize agent registry | ||
| registry := NewRegistry() | ||
|
|
||
| if cfg.EventLogBuilder == nil { | ||
| return nil, fmt.Errorf("event log builder is required") | ||
| } | ||
| eventLog, err := cfg.EventLogBuilder() | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to create event log: %w", err) | ||
| } | ||
|
|
||
| return &Controller{ | ||
| registry: registry, | ||
| eventLog: eventLog, | ||
| }, nil | ||
| } | ||
|
|
||
| // Exec executes a new agentic loop execution or resumes an existing one. | ||
| // If id is empty, a UUID will be generated. | ||
| // If the execution already exists, it will be resumed with optional new inputs. | ||
| func (d *Controller) Exec(ctx context.Context, req *proto.ExecRequest, handler ExecHandler) error { | ||
| if req.ConversationId == "" { | ||
| return fmt.Errorf("conversation_id is required") | ||
| } | ||
|
|
||
| // TODO(jbd): Resume an incomplete execution if there exists one. | ||
| // TODO(jbd): Enable bringing a remote harness that implements HarnessService. | ||
|
|
||
| h := harnesstest.New() | ||
| exec, err := h.Start(ctx, req.ConversationId) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to start harness session: %w", err) | ||
| } | ||
| defer exec.Close(ctx) | ||
|
|
||
| if err := exec.Queue(ctx, req.Inputs...); err != nil { | ||
| return fmt.Errorf("failed to queue inputs: %w", err) | ||
| } | ||
|
|
||
| hhandler := &harnessHandler{ | ||
|
rakyll marked this conversation as resolved.
|
||
| execHandler: handler, | ||
| } | ||
| if err := exec.Run(ctx, hhandler); err != nil { | ||
| return fmt.Errorf("harness execution turn failed: %w", err) | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| type harnessHandler struct { | ||
| execHandler ExecHandler | ||
| } | ||
|
|
||
| func (a *harnessHandler) OnMessage(ctx context.Context, execID string, msg *proto.Message) error { | ||
| if a.execHandler == nil { | ||
| return nil | ||
| } | ||
| return a.execHandler(&proto.ExecResponse{ | ||
| Outputs: []*proto.Message{msg}, | ||
| }) | ||
| } | ||
|
|
||
| func (a *harnessHandler) OnComplete(ctx context.Context, execID string) error { | ||
| return nil | ||
| } | ||
|
|
||
| // Delete deletes all events for a specific conversation ID. | ||
| func (d *Controller) Delete(ctx context.Context, conversationID string) error { | ||
| if conversationID == "" { | ||
| return fmt.Errorf("conversation_id is required") | ||
| } | ||
|
|
||
| return d.eventLog.DeleteEvents(ctx, conversationID) | ||
| } | ||
|
|
||
| // Fork forks an event log from a specific conversation up to a checkpoint. | ||
| func (d *Controller) Fork(ctx context.Context, srcConversationID string, srcSeq int32, destConversationID string) (string, error) { | ||
| if srcConversationID == "" { | ||
| return "", fmt.Errorf("src_conversation_id is required") | ||
| } | ||
| // TODO(anj-s): Check whether destination ID already exists and reject collisions. | ||
| if destConversationID == "" { | ||
| destConversationID = uuid.NewString() | ||
| } | ||
|
|
||
| events, err := d.eventLog.Events(ctx, srcConversationID) | ||
| if err != nil { | ||
| return "", fmt.Errorf("failed to retrieve source events: %w", err) | ||
| } | ||
| if len(events) == 0 { | ||
| return "", fmt.Errorf("source conversation %s not found or has no events", srcConversationID) | ||
| } | ||
|
|
||
| // When the caller specifies srcSeq, require that it actually exists in | ||
| // the source event log. Without this check a typo or stale checkpoint | ||
| // silently degrades to "fork all events", which is misleading. Walk | ||
| // the events once: stop as soon as we pass the requested seq, and | ||
| // truncate the slice on an exact match so the copy loop below doesn't | ||
| // need to re-check the bound. | ||
| if srcSeq > 0 { | ||
| found := false | ||
| for i, ev := range events { | ||
| if ev.Seq == srcSeq { | ||
| events = events[:i+1] | ||
| found = true | ||
| break | ||
| } | ||
| if ev.Seq > srcSeq { | ||
| break | ||
| } | ||
| } | ||
| if !found { | ||
| return "", fmt.Errorf("src_seq %d not found in conversation %s", srcSeq, srcConversationID) | ||
| } | ||
| } | ||
|
|
||
| for _, ev := range events { | ||
| // Clone the event to update the conversation ID. | ||
| newEvent := &proto.ConversationEvent{ | ||
| ConversationId: destConversationID, | ||
| Seq: ev.Seq, | ||
| ExecId: ev.ExecId, | ||
| Messages: ev.Messages, | ||
| State: ev.State, | ||
| } | ||
| if _, err := d.eventLog.Append(ctx, newEvent); err != nil { | ||
| return "", fmt.Errorf("failed to append forked event: %w", err) | ||
| } | ||
| } | ||
|
|
||
| return destConversationID, nil | ||
| } | ||
|
|
||
| // Registry returns the agent registry. | ||
| func (d *Controller) Registry() *Registry { | ||
| return d.registry | ||
| } | ||
|
|
||
| // Close gracefully shuts down the controller. | ||
| func (d *Controller) Close() error { | ||
| if err := d.eventLog.Close(); err != nil { | ||
| return fmt.Errorf("failed to close event log: %w", err) | ||
| } | ||
| if err := d.registry.Close(); err != nil { | ||
| return fmt.Errorf("failed to close registry: %w", err) | ||
| } | ||
| return nil | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should queue be something that the controller owns Vs the execution interface which will be implemented by the user? I can see Run being the function that we want harness owners to implement but the queue, close, pause and resume are something will want to override from AX's side.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It sounds like a follow up conversation for the Harness interface. We need to implement one concrete harness to figure out the right interface in my opinon.