Status: Draft
Version: 1.0.0
Owner: PlexusOne Team
Last Updated: 2025-01-20
Overview
This Technical Requirements Document specifies the implementation details for the AgentKit Local Platform, enabling local multi-agent orchestration with filesystem access and multi-provider LLM support.
Execution Modes
AgentKit Local supports two execution modes built on a shared core:
| Mode |
Binary |
Transport |
State |
Priority |
| CLI |
agent-cli |
Stdin/stdout |
Filesystem |
P0 (Phase 1-3) |
| Service |
agent-server |
HTTP/gRPC |
In-memory + Redis |
P1 (Phase 4) |
Phase 1-3 focus on CLI mode. Service mode reuses all core components (DAG executor, spec loader, LLM client, state backend) with an HTTP/gRPC transport layer added on top.
Architecture
System Context
┌─────────────────────────────────────────────────────────────────────┐
│ User │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ agent-cli │ │
│ └──────┬──────┘ │
│ │ │
├─────────────────────────────┼───────────────────────────────────────┤
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ platforms/local │ │
│ │ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌──────────┐ │ │
│ │ │ Runner │ │ DAG │ │ State │ │ Spec │ │ │
│ │ │ │ │ Executor │ │ Backend │ │ Loader │ │ │
│ │ └─────┬─────┘ └─────┬─────┘ └─────┬─────┘ └────┬─────┘ │ │
│ │ │ │ │ │ │ │
│ │ ▼ ▼ ▼ ▼ │ │
│ │ ┌───────────────────────────────────────────────────────┐ │ │
│ │ │ EmbeddedAgent │ │ │
│ │ │ ┌─────────┐ ┌─────────┐ ┌─────────────────────────┐│ │ │
│ │ │ │ Tools │ │ LLM │ │ Agent Loop ││ │ │
│ │ │ │ │ │ Client │ │ (tool calling cycle) ││ │ │
│ │ │ └─────────┘ └────┬────┘ └─────────────────────────┘│ │ │
│ │ └────────────────────┼───────────────────────────────────┘ │ │
│ └───────────────────────┼─────────────────────────────────────┘ │
│ │ │
├──────────────────────────┼──────────────────────────────────────────┤
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ omnillm │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────┐ ┌───────┐ │ │
│ │ │Anthropic │ │ OpenAI │ │ Gemini │ │ xAI │ │Ollama │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ └──────┘ └───────┘ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ State Backends │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │ │
│ │ │ Filesystem │ │ Redis │ │ In-Memory │ │ │
│ │ │ .agent-state │ │ (optional) │ │ (default) │ │ │
│ │ └──────────────┘ └──────────────┘ └──────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
Component Architecture
platforms/local/
├── cmd/
│ ├── agent-cli/
│ │ └── main.go # CLI entry point (NEW - Phase 1)
│ └── agent-server/
│ └── main.go # Service entry point (NEW - Phase 4)
│
├── # Core (shared between CLI and Service)
├── agent.go # EmbeddedAgent (EXISTS)
├── config.go # Config loading (EXISTS)
├── tools.go # Tool implementations (EXISTS)
├── runner.go # Orchestration (EXISTS - extend)
├── dag.go # DAG executor (NEW)
├── llm.go # omnillm LLMClient (NEW)
├── state.go # State backend interface (NEW)
├── state_file.go # Filesystem state (NEW)
├── state_redis.go # Redis state (NEW)
├── spec.go # multi-agent-spec loader (NEW)
├── mapping.go # Tool/model mapping (NEW)
├── report.go # Report generation (NEW)
│
├── # Service-specific (Phase 4)
├── server.go # HTTP server (NEW - Phase 4)
├── handlers.go # API handlers (NEW - Phase 4)
├── streaming.go # SSE/WebSocket (NEW - Phase 4)
│
└── schema.json # Config schema (EXISTS)
Dependency Flow:
┌─────────────┐ ┌──────────────┐
│ agent-cli │ │ agent-server │
└──────┬──────┘ └──────┬───────┘
│ │
└─────────┬─────────┘
▼
┌────────────────────────┐
│ Shared Core │
│ ┌──────┐ ┌──────────┐│
│ │ DAG │ │ Spec ││
│ │Exec │ │ Loader ││
│ └──────┘ └──────────┘│
│ ┌──────┐ ┌──────────┐│
│ │State │ │ LLM ││
│ │Backend│ │ Client ││
│ └──────┘ └──────────┘│
│ ┌──────┐ ┌──────────┐│
│ │Report│ │ Mapping ││
│ │ Gen │ │(tool/mdl)││
│ └──────┘ └──────────┘│
└────────────────────────┘
│
▼
┌────────────────────────┐
│ Existing Components │
│ agent.go, runner.go │
│ tools.go, config.go │
└────────────────────────┘
Multi-Agent Spec Compliance
AgentKit Local implements the multi-agent-spec schema for portable workflow definitions.
Schema References
| Schema |
Path |
Purpose |
| Agent |
schema/agent/agent.schema.json |
Agent definition with YAML frontmatter |
| Team |
schema/orchestration/team.schema.json |
Team composition and DAG workflow |
| Deployment |
schema/deployment/deployment.schema.json |
Runtime configuration |
| Agent Result |
schema/report/agent-result.schema.json |
Per-agent execution result |
| Team Report |
schema/report/team-report.schema.json |
Workflow execution report |
AgentKit Local is registered as agentkit-local platform in multi-agent-spec:
{
"name": "local-dev",
"platform": "agentkit-local",
"mode": "single-process",
"priority": "p1",
"runtime": {
"defaults": {
"timeout": "5m",
"retry": {"max_attempts": 2, "backoff": "exponential"}
}
},
"config": {
"workspace": ".",
"llm": {
"provider": "anthropic",
"model": "claude-sonnet-4-20250514"
},
"mcp": {"enabled": true, "transport": "stdio"}
}
}
// CanonicalToolMap maps multi-agent-spec tools to AgentKit tools
var CanonicalToolMap = map[string]string{
"Read": "read",
"Write": "write",
"Edit": "write", // Edit uses write with diff
"Glob": "glob",
"Grep": "grep",
"Bash": "shell",
"WebSearch": "shell", // Via curl/external
"WebFetch": "shell", // Via curl/external
"Task": "", // Handled by orchestration
}
func mapCanonicalTools(canonical []string) []string {
var local []string
for _, tool := range canonical {
if mapped, ok := CanonicalToolMap[tool]; ok && mapped != "" {
local = append(local, mapped)
}
}
return local
}
Model Mapping Implementation
// CanonicalModelMap maps multi-agent-spec models to provider-specific IDs
var CanonicalModelMap = map[string]map[string]string{
"haiku": {
"anthropic": "claude-3-5-haiku-20241022",
"openai": "gpt-4o-mini",
"gemini": "gemini-2.0-flash",
},
"sonnet": {
"anthropic": "claude-sonnet-4-20250514",
"openai": "gpt-4o",
"gemini": "gemini-2.5-pro",
},
"opus": {
"anthropic": "claude-opus-4-20250514",
"openai": "gpt-4.5",
"gemini": "gemini-2.5-pro",
},
}
func mapCanonicalModel(canonical, provider string) string {
if models, ok := CanonicalModelMap[canonical]; ok {
if model, ok := models[provider]; ok {
return model
}
}
return canonical // Pass through if not canonical
}
Technical Specifications
1. CLI Interface (cmd/agent-cli/main.go)
Commands
// Command structure using urfave/cli/v2
app := &cli.App{
Name: "agent-cli",
Usage: "Run AI agents locally with multi-provider LLM support",
Commands: []*cli.Command{
{
Name: "run",
Usage: "Run a single agent",
Action: runAgent,
Flags: []cli.Flag{
&cli.StringFlag{Name: "agent", Aliases: []string{"a"}, Required: true},
&cli.StringFlag{Name: "task", Aliases: []string{"t"}, Required: true},
&cli.StringFlag{Name: "config", Aliases: []string{"c"}, Value: "agentkit.yaml"},
&cli.StringFlag{Name: "session", Aliases: []string{"s"}},
&cli.StringFlag{Name: "provider", Value: "anthropic"},
&cli.StringFlag{Name: "model"},
&cli.StringFlag{Name: "output", Aliases: []string{"o"}, Value: "text"},
},
},
{
Name: "workflow",
Usage: "Run a multi-agent workflow",
Action: runWorkflow,
Flags: []cli.Flag{
&cli.StringFlag{Name: "spec", Required: true},
&cli.StringFlag{Name: "steps", Usage: "Comma-separated step IDs to run"},
&cli.StringFlag{Name: "config", Aliases: []string{"c"}, Value: "agentkit.yaml"},
&cli.StringFlag{Name: "session", Aliases: []string{"s"}},
&cli.StringFlag{Name: "provider", Value: "anthropic"},
&cli.BoolFlag{Name: "dry-run"},
},
},
{
Name: "list",
Usage: "List available agents",
Action: listAgents,
},
},
}
| Format |
Description |
Use Case |
text |
Human-readable terminal output |
Interactive use |
json |
Structured JSON output |
Programmatic consumption |
toon |
Token-optimized notation |
LLM consumption (8x smaller) |
2. DAG Executor (dag.go)
{
"name": "prd-workflow",
"version": "1.0.0",
"steps": [
{
"id": "problem-discovery",
"agent": "problem-discovery",
"depends_on": [],
"timeout": "5m",
"retry": {"max_attempts": 2, "backoff": "exponential"}
},
{
"id": "user-research",
"agent": "user-research",
"depends_on": [],
"timeout": "5m"
},
{
"id": "solution-ideation",
"agent": "solution-ideation",
"depends_on": ["problem-discovery", "user-research"],
"input_mapping": {
"problem": "$.problem-discovery.output.problem_statement",
"users": "$.user-research.output.personas"
}
}
]
}
DAG Execution Algorithm
// DAGExecutor manages workflow execution
type DAGExecutor struct {
steps map[string]*WorkflowStep
runner *Runner
state StateBackend
logger *slog.Logger
}
// Execute runs the workflow respecting dependencies
func (e *DAGExecutor) Execute(ctx context.Context, workflowPath string) (*WorkflowResult, error) {
workflow, err := e.loadWorkflow(workflowPath)
if err != nil {
return nil, err
}
// Validate DAG (check for cycles)
if err := e.validateDAG(workflow); err != nil {
return nil, err
}
// Topological sort
order := e.topologicalSort(workflow.Steps)
// Group by dependency level for parallel execution
levels := e.groupByLevel(order, workflow.Steps)
results := make(map[string]*StepResult)
for _, level := range levels {
// Execute all steps in this level in parallel
levelResults, err := e.executeLevel(ctx, level, results)
if err != nil {
return nil, err
}
for stepID, result := range levelResults {
results[stepID] = result
e.state.Set(ctx, stepID, result)
}
}
return &WorkflowResult{
Workflow: workflow.Name,
Steps: results,
Success: e.allSuccessful(results),
}, nil
}
// topologicalSort returns steps in dependency order
func (e *DAGExecutor) topologicalSort(steps []WorkflowStep) []string {
// Kahn's algorithm
inDegree := make(map[string]int)
graph := make(map[string][]string)
for _, step := range steps {
inDegree[step.ID] = len(step.DependsOn)
for _, dep := range step.DependsOn {
graph[dep] = append(graph[dep], step.ID)
}
}
var queue []string
for _, step := range steps {
if inDegree[step.ID] == 0 {
queue = append(queue, step.ID)
}
}
var order []string
for len(queue) > 0 {
node := queue[0]
queue = queue[1:]
order = append(order, node)
for _, neighbor := range graph[node] {
inDegree[neighbor]--
if inDegree[neighbor] == 0 {
queue = append(queue, neighbor)
}
}
}
return order
}
// groupByLevel groups steps that can run in parallel
func (e *DAGExecutor) groupByLevel(order []string, steps []WorkflowStep) [][]string {
stepMap := make(map[string]*WorkflowStep)
for i := range steps {
stepMap[steps[i].ID] = &steps[i]
}
levels := make(map[string]int)
for _, stepID := range order {
step := stepMap[stepID]
maxDepLevel := -1
for _, dep := range step.DependsOn {
if levels[dep] > maxDepLevel {
maxDepLevel = levels[dep]
}
}
levels[stepID] = maxDepLevel + 1
}
// Group by level
maxLevel := 0
for _, level := range levels {
if level > maxLevel {
maxLevel = level
}
}
result := make([][]string, maxLevel+1)
for stepID, level := range levels {
result[level] = append(result[level], stepID)
}
return result
}
3. omnillm Integration (llm.go)
LLMClient Implementation
package local
import (
"context"
"github.com/plexusone/omnillm"
)
// OmniLLMClient implements LLMClient using omnillm
type OmniLLMClient struct {
client *omnillm.ChatClient
model string
}
// NewOmniLLMClient creates a new omnillm-based LLM client
func NewOmniLLMClient(cfg LLMConfig) (*OmniLLMClient, error) {
providerCfg := omnillm.ProviderConfig{
Provider: toOmniProvider(cfg.Provider),
APIKey: resolveEnvVar(cfg.APIKey),
}
if cfg.BaseURL != "" {
providerCfg.BaseURL = cfg.BaseURL
}
client, err := omnillm.NewClient(omnillm.ClientConfig{
Providers: []omnillm.ProviderConfig{providerCfg},
})
if err != nil {
return nil, fmt.Errorf("failed to create omnillm client: %w", err)
}
return &OmniLLMClient{
client: client,
model: cfg.Model,
}, nil
}
// Complete generates a completion for the given messages
func (c *OmniLLMClient) Complete(ctx context.Context, messages []Message, tools []ToolDefinition) (*CompletionResponse, error) {
// Convert to omnillm format
omniMessages := make([]omnillm.Message, len(messages))
for i, msg := range messages {
omniMessages[i] = omnillm.Message{
Role: msg.Role,
Content: msg.Content,
ToolCallID: msg.ToolID,
}
}
// Convert tools
omniTools := make([]omnillm.Tool, len(tools))
for i, tool := range tools {
omniTools[i] = omnillm.Tool{
Type: "function",
Function: omnillm.Function{
Name: tool.Name,
Description: tool.Description,
Parameters: tool.Parameters,
},
}
}
// Make request
resp, err := c.client.CreateChatCompletion(ctx, &omnillm.ChatCompletionRequest{
Model: c.model,
Messages: omniMessages,
Tools: omniTools,
})
if err != nil {
return nil, err
}
// Convert response
result := &CompletionResponse{
Content: resp.Choices[0].Message.Content,
Done: true,
}
// Convert tool calls
for _, tc := range resp.Choices[0].Message.ToolCalls {
result.ToolCalls = append(result.ToolCalls, ToolCall{
ID: tc.ID,
Name: tc.Function.Name,
Arguments: parseArguments(tc.Function.Arguments),
})
result.Done = false
}
return result, nil
}
func toOmniProvider(provider string) omnillm.ProviderName {
switch provider {
case "anthropic":
return omnillm.ProviderNameAnthropic
case "openai":
return omnillm.ProviderNameOpenAI
case "gemini":
return omnillm.ProviderNameGemini
case "xai":
return omnillm.ProviderNameXAI
case "ollama":
return omnillm.ProviderNameOllama
default:
return omnillm.ProviderNameOpenAI
}
}
4. State Backend Interface (state.go)
package local
import (
"context"
"encoding/json"
)
// StateBackend defines the interface for state persistence
type StateBackend interface {
// Get retrieves a value by key
Get(ctx context.Context, key string) (json.RawMessage, error)
// Set stores a value by key
Set(ctx context.Context, key string, value any) error
// Delete removes a value by key
Delete(ctx context.Context, key string) error
// List returns all keys with optional prefix filter
List(ctx context.Context, prefix string) ([]string, error)
// Close cleans up resources
Close() error
}
// Session represents a workflow session with state
type Session struct {
ID string `json:"id"`
Workspace string `json:"workspace"`
State StateBackend `json:"-"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// NewSession creates a new session with the specified backend
func NewSession(id, workspace string, backend StateBackend) *Session {
now := time.Now()
return &Session{
ID: id,
Workspace: workspace,
State: backend,
CreatedAt: now,
UpdatedAt: now,
}
}
Filesystem Backend (state_file.go)
package local
import (
"context"
"encoding/json"
"os"
"path/filepath"
)
// FileStateBackend stores state in filesystem
type FileStateBackend struct {
baseDir string
}
// NewFileStateBackend creates a filesystem-based state backend
func NewFileStateBackend(sessionID, workspace string) (*FileStateBackend, error) {
baseDir := filepath.Join(workspace, ".agent-state", sessionID)
if err := os.MkdirAll(baseDir, 0755); err != nil {
return nil, fmt.Errorf("failed to create state directory: %w", err)
}
return &FileStateBackend{baseDir: baseDir}, nil
}
func (f *FileStateBackend) Get(ctx context.Context, key string) (json.RawMessage, error) {
path := filepath.Join(f.baseDir, key+".json")
data, err := os.ReadFile(path)
if os.IsNotExist(err) {
return nil, nil
}
if err != nil {
return nil, err
}
return json.RawMessage(data), nil
}
func (f *FileStateBackend) Set(ctx context.Context, key string, value any) error {
data, err := json.MarshalIndent(value, "", " ")
if err != nil {
return err
}
path := filepath.Join(f.baseDir, key+".json")
return os.WriteFile(path, data, 0644)
}
func (f *FileStateBackend) Delete(ctx context.Context, key string) error {
path := filepath.Join(f.baseDir, key+".json")
return os.Remove(path)
}
func (f *FileStateBackend) List(ctx context.Context, prefix string) ([]string, error) {
entries, err := os.ReadDir(f.baseDir)
if err != nil {
return nil, err
}
var keys []string
for _, entry := range entries {
if entry.IsDir() {
continue
}
name := strings.TrimSuffix(entry.Name(), ".json")
if prefix == "" || strings.HasPrefix(name, prefix) {
keys = append(keys, name)
}
}
return keys, nil
}
func (f *FileStateBackend) Close() error {
return nil
}
5. Specification Loader (spec.go) - multi-agent-spec Compliant
package local
import (
"bytes"
"os"
"path/filepath"
"strings"
"gopkg.in/yaml.v3"
)
// SpecLoader loads multi-agent-spec compliant specifications
type SpecLoader struct {
specsDir string
provider string // For model mapping
}
// NewSpecLoader creates a new spec loader
func NewSpecLoader(specsDir, provider string) *SpecLoader {
return &SpecLoader{specsDir: specsDir, provider: provider}
}
// AgentFrontmatter represents YAML frontmatter in agent markdown (multi-agent-spec)
type AgentFrontmatter struct {
Name string `yaml:"name"`
Description string `yaml:"description"`
Model string `yaml:"model"` // Canonical: haiku, sonnet, opus
Tools []string `yaml:"tools"` // Canonical: Read, Write, Glob, Grep, Bash
Dependencies []string `yaml:"dependencies"` // Agents this can spawn
Requires []string `yaml:"requires"` // External binaries
Instructions string `yaml:"instructions"` // Optional inline instructions
Tasks []Task `yaml:"tasks"` // Validation tasks
}
// Task represents a validation task in agent spec
type Task struct {
ID string `yaml:"id"`
Description string `yaml:"description"`
Type string `yaml:"type"` // command, pattern, file, manual
Command string `yaml:"command,omitempty"`
Pattern string `yaml:"pattern,omitempty"`
File string `yaml:"file,omitempty"`
Files string `yaml:"files,omitempty"`
Required bool `yaml:"required"`
ExpectedOutput string `yaml:"expected_output,omitempty"`
HumanInLoop string `yaml:"human_in_loop,omitempty"`
}
// LoadAgentFromMarkdown parses multi-agent-spec markdown with YAML frontmatter
func (l *SpecLoader) LoadAgentFromMarkdown(path string) (*AgentConfig, error) {
content, err := os.ReadFile(path)
if err != nil {
return nil, err
}
// Parse YAML frontmatter (between --- markers)
var frontmatter AgentFrontmatter
var body string
if bytes.HasPrefix(content, []byte("---\n")) {
parts := bytes.SplitN(content[4:], []byte("\n---"), 2)
if len(parts) == 2 {
if err := yaml.Unmarshal(parts[0], &frontmatter); err != nil {
return nil, fmt.Errorf("failed to parse frontmatter: %w", err)
}
body = string(parts[1])
}
} else {
body = string(content)
}
// Use frontmatter name or derive from filename
name := frontmatter.Name
if name == "" {
name = strings.TrimSuffix(filepath.Base(path), ".md")
}
// Map canonical tools to local tools
localTools := mapCanonicalTools(frontmatter.Tools)
// Map canonical model to provider-specific model
model := mapCanonicalModel(frontmatter.Model, l.provider)
// Use frontmatter instructions or markdown body
instructions := frontmatter.Instructions
if instructions == "" {
instructions = strings.TrimSpace(body)
}
return &AgentConfig{
Name: name,
Description: frontmatter.Description,
Instructions: instructions,
Tools: localTools,
Model: model,
}, nil
}
// TeamSpec represents multi-agent-spec team.schema.json
type TeamSpec struct {
Name string `json:"name" yaml:"name"`
Version string `json:"version" yaml:"version"`
Description string `json:"description" yaml:"description"`
Agents []string `json:"agents" yaml:"agents"`
Orchestrator string `json:"orchestrator" yaml:"orchestrator"`
Context string `json:"context" yaml:"context"`
Workflow WorkflowSpec `json:"workflow" yaml:"workflow"`
}
// WorkflowSpec represents workflow configuration
type WorkflowSpec struct {
Type string `json:"type" yaml:"type"` // orchestrated, sequential, parallel, dag
Steps []WorkflowStep `json:"steps" yaml:"steps"`
}
// WorkflowStep represents a step in multi-agent-spec workflow
type WorkflowStep struct {
Name string `json:"name" yaml:"name"`
Agent string `json:"agent" yaml:"agent"`
DependsOn []string `json:"depends_on" yaml:"depends_on"`
Inputs []PortSpec `json:"inputs" yaml:"inputs"`
Outputs []PortSpec `json:"outputs" yaml:"outputs"`
Timeout string `json:"timeout,omitempty" yaml:"timeout,omitempty"`
Retry *RetrySpec `json:"retry,omitempty" yaml:"retry,omitempty"`
Condition string `json:"condition,omitempty" yaml:"condition,omitempty"`
}
// PortSpec represents typed input/output ports
type PortSpec struct {
Name string `json:"name" yaml:"name"`
Type string `json:"type" yaml:"type"` // string, number, boolean, object, array, file
Description string `json:"description" yaml:"description"`
Required bool `json:"required" yaml:"required"`
From string `json:"from,omitempty" yaml:"from,omitempty"` // e.g., "step-name.output_port"
Schema interface{} `json:"schema,omitempty" yaml:"schema,omitempty"`
Default interface{} `json:"default,omitempty" yaml:"default,omitempty"`
}
// RetrySpec represents retry configuration
type RetrySpec struct {
MaxAttempts int `json:"max_attempts" yaml:"max_attempts"`
Backoff string `json:"backoff" yaml:"backoff"` // exponential, fixed, linear
InitialDelay string `json:"initial_delay" yaml:"initial_delay"`
MaxDelay string `json:"max_delay" yaml:"max_delay"`
RetryableErrors []string `json:"retryable_errors" yaml:"retryable_errors"`
}
// DeploymentSpec represents multi-agent-spec deployment.schema.json
type DeploymentSpec struct {
Team string `json:"team" yaml:"team"`
Targets []TargetSpec `json:"targets" yaml:"targets"`
}
// TargetSpec represents a deployment target
type TargetSpec struct {
Name string `json:"name" yaml:"name"`
Platform string `json:"platform" yaml:"platform"` // agentkit-local
Mode string `json:"mode" yaml:"mode"` // single-process
Priority string `json:"priority" yaml:"priority"` // p1, p2, p3
Output string `json:"output" yaml:"output"`
Runtime RuntimeSpec `json:"runtime" yaml:"runtime"`
Config LocalConfigSpec `json:"config" yaml:"config"`
}
// RuntimeSpec represents runtime configuration
type RuntimeSpec struct {
Defaults StepRuntimeSpec `json:"defaults" yaml:"defaults"`
Steps map[string]StepRuntimeSpec `json:"steps" yaml:"steps"`
Observability ObservabilitySpec `json:"observability" yaml:"observability"`
}
// StepRuntimeSpec represents per-step runtime config
type StepRuntimeSpec struct {
Timeout string `json:"timeout" yaml:"timeout"`
Retry *RetrySpec `json:"retry" yaml:"retry"`
Condition string `json:"condition" yaml:"condition"`
Concurrency int `json:"concurrency" yaml:"concurrency"`
Resources ResourcesSpec `json:"resources" yaml:"resources"`
}
// ResourcesSpec represents resource limits
type ResourcesSpec struct {
CPU string `json:"cpu" yaml:"cpu"`
Memory string `json:"memory" yaml:"memory"`
GPU int `json:"gpu" yaml:"gpu"`
}
// ObservabilitySpec represents observability configuration
type ObservabilitySpec struct {
Tracing TracingSpec `json:"tracing" yaml:"tracing"`
Metrics MetricsSpec `json:"metrics" yaml:"metrics"`
Logging LoggingSpec `json:"logging" yaml:"logging"`
}
// TracingSpec, MetricsSpec, LoggingSpec for observability
type TracingSpec struct {
Enabled bool `json:"enabled" yaml:"enabled"`
Exporter string `json:"exporter" yaml:"exporter"`
Endpoint string `json:"endpoint" yaml:"endpoint"`
SampleRate float64 `json:"sample_rate" yaml:"sample_rate"`
}
type MetricsSpec struct {
Enabled bool `json:"enabled" yaml:"enabled"`
Exporter string `json:"exporter" yaml:"exporter"`
Endpoint string `json:"endpoint" yaml:"endpoint"`
}
type LoggingSpec struct {
Level string `json:"level" yaml:"level"`
Format string `json:"format" yaml:"format"`
}
// LocalConfigSpec represents agentkit-local specific config
type LocalConfigSpec struct {
Workspace string `json:"workspace" yaml:"workspace"`
LLM LLMSpec `json:"llm" yaml:"llm"`
MCP MCPSpec `json:"mcp" yaml:"mcp"`
}
type LLMSpec struct {
Provider string `json:"provider" yaml:"provider"`
Model string `json:"model" yaml:"model"`
APIKey string `json:"api_key" yaml:"api_key"` // ${ENV_VAR} syntax
}
type MCPSpec struct {
Enabled bool `json:"enabled" yaml:"enabled"`
Transport string `json:"transport" yaml:"transport"`
}
// LoadTeam loads a multi-agent-spec team definition
func (l *SpecLoader) LoadTeam(path string) (*TeamSpec, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var team TeamSpec
if err := json.Unmarshal(data, &team); err != nil {
return nil, fmt.Errorf("failed to parse team spec: %w", err)
}
return &team, nil
}
// LoadDeployment loads a multi-agent-spec deployment definition
func (l *SpecLoader) LoadDeployment(path string) (*DeploymentSpec, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var deployment DeploymentSpec
if err := json.Unmarshal(data, &deployment); err != nil {
return nil, fmt.Errorf("failed to parse deployment spec: %w", err)
}
return &deployment, nil
}
// GetLocalTarget returns the agentkit-local target from deployment spec
func (d *DeploymentSpec) GetLocalTarget() (*TargetSpec, error) {
for i := range d.Targets {
if d.Targets[i].Platform == "agentkit-local" {
return &d.Targets[i], nil
}
}
return nil, fmt.Errorf("no agentkit-local target found in deployment")
}
6. Report Generation (report.go) - multi-agent-spec Compliant
package local
import (
"time"
)
// Status represents NASA Go/No-Go terminology
type Status string
const (
StatusGO Status = "GO"
StatusWARN Status = "WARN"
StatusNOGO Status = "NO-GO"
StatusSKIP Status = "SKIP"
)
// AgentResultReport conforms to agent-result.schema.json
type AgentResultReport struct {
AgentID string `json:"agent_id"`
StepID string `json:"step_id"`
Inputs map[string]interface{} `json:"inputs"`
Outputs map[string]interface{} `json:"outputs"`
Tasks []TaskResult `json:"tasks"`
Status Status `json:"status"`
ExecutedAt time.Time `json:"executed_at"`
AgentModel string `json:"agent_model"`
Duration string `json:"duration"`
Error string `json:"error,omitempty"`
}
// TaskResult represents a validation task result
type TaskResult struct {
ID string `json:"id"`
Status Status `json:"status"`
Detail string `json:"detail"`
DurationMS int64 `json:"duration_ms"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
// TeamReport conforms to team-report.schema.json
type TeamReport struct {
Project string `json:"project"`
Version string `json:"version"`
Phase string `json:"phase"`
Teams []TeamMemberReport `json:"teams"`
Status Status `json:"status"`
GeneratedAt time.Time `json:"generated_at"`
GeneratedBy string `json:"generated_by"`
}
// TeamMemberReport represents a team member's result
type TeamMemberReport struct {
ID string `json:"id"`
Name string `json:"name"`
AgentID string `json:"agent_id"`
Model string `json:"model"`
DependsOn []string `json:"depends_on"`
Tasks []TaskResult `json:"tasks"`
Status Status `json:"status"`
}
// AggregateStatus determines overall status from member statuses
func AggregateStatus(members []TeamMemberReport) Status {
hasWarn := false
for _, m := range members {
if m.Status == StatusNOGO {
return StatusNOGO
}
if m.Status == StatusWARN {
hasWarn = true
}
}
if hasWarn {
return StatusWARN
}
return StatusGO
}
// GenerateTeamReport creates a multi-agent-spec compliant report
func GenerateTeamReport(project, version, phase string, results map[string]*AgentResultReport) *TeamReport {
var members []TeamMemberReport
for stepID, result := range results {
members = append(members, TeamMemberReport{
ID: stepID,
Name: result.AgentID,
AgentID: result.AgentID,
Model: result.AgentModel,
Tasks: result.Tasks,
Status: result.Status,
})
}
return &TeamReport{
Project: project,
Version: version,
Phase: phase,
Teams: members,
Status: AggregateStatus(members),
GeneratedAt: time.Now(),
GeneratedBy: "agentkit-local",
}
}
Comparison: Implementation Approach vs Google ADK
Feature Comparison
| Feature |
Google ADK |
AgentKit Local |
Notes |
| Agent Loop |
Built-in with transfer support |
Built-in (agent.go:105-169) |
Both complete |
| Tool System |
Manual implementation required |
Built-in (tools.go) |
AgentKit advantage |
| Sequential Execution |
SequentialAgent |
Runner.InvokeSequential |
Both complete |
| Parallel Execution |
ParallelAgent |
Runner.InvokeParallel |
Both complete |
| DAG Execution |
Not available |
DAGExecutor (new) |
AgentKit advantage |
| LLM Provider |
Gemini SDK only |
omnillm (5+ providers) |
AgentKit advantage |
| Session State |
session.State |
StateBackend interface |
Equivalent |
| Memory (long-term) |
memory.Service |
Planned |
ADK advantage |
| CLI Tool |
Not available |
agent-cli (new) |
AgentKit advantage |
| MCP Server |
Built-in |
Built-in (config.go) |
Both complete |
| A2A Protocol |
Built-in |
Via agentkit/a2a |
Both complete |
| Spec Loading |
Code-only |
JSON/YAML/Markdown |
AgentKit advantage |
Why Build Our Own
| Reason |
Justification |
| LLM Vendor Lock-in |
ADK is Gemini-focused. Enterprise customers require multi-provider support. omnillm already supports 5+ providers with fallback. |
| No DAG Support |
ADK only provides sequential/parallel/loop. Complex workflows like PRD creation require arbitrary dependency graphs. |
| No CLI Tool |
ADK is a library only. We need a standalone CLI for developer workflows and CI/CD integration. |
| Code-Only Config |
ADK requires Go code to define agents. We need spec files (JSON/YAML/Markdown) for non-developer workflow definition. |
| Existing Investment |
platforms/local/ already has agent loop, tools, config, runner. Building DAG executor and CLI is incremental. |
What We Reuse from ADK Patterns
| Pattern |
Usage |
| Agent interface design |
EmbeddedAgent follows similar patterns |
| Session state concept |
StateBackend mirrors session.State |
| Tool definition schema |
JSON Schema for parameters |
| Event-based execution |
AgentResult for step outcomes |
Testing Strategy
Unit Tests
| Component |
Test Coverage Target |
| DAG executor |
90% - cycle detection, topological sort, parallel grouping |
| State backends |
90% - all CRUD operations |
| Spec loader |
80% - JSON, YAML, Markdown parsing |
| omnillm client |
70% - mock LLM responses |
Integration Tests
| Scenario |
Description |
| Single agent execution |
Run agent with real LLM (Claude Haiku for speed) |
| DAG workflow |
Execute 3-step DAG with dependencies |
| State persistence |
Verify state survives CLI restart |
| Provider fallback |
Primary fails, fallback succeeds |
Example Workflows for Testing
# test/workflows/simple-dag.yaml
name: simple-dag-test
steps:
- id: step-a
agent: echo-agent
depends_on: []
- id: step-b
agent: echo-agent
depends_on: []
- id: step-c
agent: echo-agent
depends_on: [step-a, step-b]
Security Considerations
| Concern |
Mitigation |
| Shell injection |
Workspace sandboxing in tools.go (validatePath) |
| Secret exposure |
API keys via env vars, never in spec files |
| Filesystem escape |
Path validation prevents .. traversal |
| Runaway execution |
Timeout configuration per step |
Implementation Plan
Phase 1: CLI Foundation (P0)
- [ ]
cmd/agent-cli/main.go - CLI entry point with urfave/cli
- [ ]
llm.go - omnillm LLMClient implementation
- [ ]
state_file.go - Filesystem state backend
- [ ]
mapping.go - Canonical tool/model mapping
- [ ]
spec.go - multi-agent-spec agent loading (Markdown + YAML frontmatter)
- [ ] Unit tests for new components
Phase 2: DAG Orchestration (P0)
- [ ]
dag.go - DAG executor with topological sort and parallel execution
- [ ]
spec.go - multi-agent-spec team/workflow loading
- [ ]
report.go - multi-agent-spec report generation (GO/WARN/NO-GO)
- [ ] Typed port data flow between steps
- [ ] Integration tests with sample workflows
- [ ] Documentation
Phase 3: Enhanced CLI (P1)
- [ ]
state_redis.go - Redis state backend
- [ ]
spec.go - multi-agent-spec deployment config loading
- [ ]
agent-cli validate command for spec validation
- [ ] Conditional step execution
- [ ] CI/CD integration examples
Phase 4: Service Mode (P1)
- [ ]
cmd/agent-server/main.go - HTTP server entry point
- [ ]
server.go - HTTP server with graceful shutdown
- [ ]
handlers.go - REST API handlers
POST /workflows - Submit workflow
GET /workflows/:id - Get workflow status
GET /workflows/:id/stream - SSE stream
POST /agents/:name/invoke - Invoke single agent
- [ ]
streaming.go - SSE/WebSocket for real-time output
- [ ] Session management with warm LLM connections
- [ ] Health check endpoint
Phase 5: Polish (P2)
- [ ]
agent-cli interactive - REPL mode
- [ ] Tool approval workflow
- [ ] Custom tool registration API
- [ ] MCP server integration
Dependencies
New Dependencies
| Package |
Version |
Purpose |
github.com/urfave/cli/v2 |
v2.27+ |
CLI framework |
github.com/redis/go-redis/v9 |
v9.x |
Redis client (optional) |
Existing Dependencies (Already in agentkit)
| Package |
Purpose |
github.com/plexusone/omnillm |
Multi-provider LLM |
gopkg.in/yaml.v3 |
YAML parsing |
File Changes Summary
Phase 1-3: CLI Mode (~1,130 lines)
| File |
Action |
Lines (Est.) |
cmd/agent-cli/main.go |
Create |
150 |
dag.go |
Create |
200 |
llm.go |
Create |
100 |
state.go |
Create |
50 |
state_file.go |
Create |
80 |
state_redis.go |
Create |
100 |
spec.go |
Create |
250 (multi-agent-spec compliant) |
report.go |
Create |
100 (multi-agent-spec report generation) |
mapping.go |
Create |
50 (tool/model mapping) |
runner.go |
Extend |
+50 |
| Subtotal |
|
~1,130 lines |
Phase 4: Service Mode (~400 lines)
| File |
Action |
Lines (Est.) |
cmd/agent-server/main.go |
Create |
100 |
server.go |
Create |
150 |
handlers.go |
Create |
100 |
streaming.go |
Create |
50 |
| Subtotal |
|
~400 lines |
Total: ~1,530 lines
multi-agent-spec Integration Points
| Component |
Schema |
Implementation |
| Agent loading |
agent.schema.json |
spec.go:LoadAgentFromMarkdown() |
| Team loading |
team.schema.json |
spec.go:LoadTeam() |
| Deployment loading |
deployment.schema.json |
spec.go:LoadDeployment() |
| Report generation |
team-report.schema.json |
report.go:GenerateTeamReport() |
| Tool mapping |
Canonical → Local |
mapping.go:mapCanonicalTools() |
| Model mapping |
Canonical → Provider |
mapping.go:mapCanonicalModel() |
API Specification (Phase 4: Service Mode)
REST Endpoints
| Method |
Path |
Description |
POST |
/workflows |
Submit workflow for execution |
GET |
/workflows/:id |
Get workflow status and results |
GET |
/workflows/:id/stream |
SSE stream of workflow progress |
DELETE |
/workflows/:id |
Cancel running workflow |
POST |
/agents/:name/invoke |
Invoke single agent |
GET |
/agents |
List available agents |
GET |
/health |
Health check |
Workflow Submission Request
{
"spec_path": "specs/teams/prd-team.json",
"session_id": "prd-001",
"steps": ["discovery", "solution"],
"context": {
"project": "my-project"
}
}
SSE Stream Events
event: step_start
data: {"step_id": "problem-discovery", "agent": "problem-discovery"}
event: step_progress
data: {"step_id": "problem-discovery", "content": "Analyzing..."}
event: step_complete
data: {"step_id": "problem-discovery", "status": "GO", "duration": "45s"}
event: workflow_complete
data: {"status": "GO", "report": {...}}