diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 70cd94af7..ac95eb82c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3013,6 +3013,15 @@ or containment of descendants that deliberately leave the group. ### Harness qualification and onboarding +`apps/parsar-daemon/internal/agent/harness.go` is the single source entry point +for Harness authors. Keep required lifecycle declarations, separate optional +interfaces and the existing registration methods there. Operation result types, +errors and Registry lookup/storage implementation may remain in focused files. +Use the existing `proto.SupportedAgentKind` and `AgentKindCapabilities` schema; +do not introduce a second capability descriptor or a combined optional interface. +Core service qualification remains separate from Runtime registration. Keep the +README and onboarding guide linked to this entry point. + Codex, Claude and future harnesses have equal architectural status. The common Runtime wire protocol and Executor/Turn interfaces own lifecycle, input receipts, cancellation, recovery and resource access; each native adapter diff --git a/README.md b/README.md index 116f30d7a..f73ebd5d2 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ native differences. | [API reference](docs/api/README.md) | The `/v1`, `/core/v1` and `/api/v1` namespaces | | [Nodes and sandbox backends: operator reference](services/agents-api/HOSTED-SANDBOX-MANAGER.md) | Node protocol, manual registration, placement, maintenance | | [Protocol coverage](contracts/agents-api/README.md) and [harness selection](contracts/agents-api/harness-selection.md) | Supported operations and native differences | -| [Add a Harness](contracts/agents-api/harness-onboarding.md) | Required adapter interfaces, lifecycle, capabilities, registration and acceptance | +| [Add a Harness](contracts/agents-api/harness-onboarding.md) | Start with [agent/harness.go](apps/parsar-daemon/internal/agent/harness.go): required interfaces, optional capabilities and registration; then follow the guide for acceptance | | [Landing page source](site/index.html) | The public site | ### Maintainers and advanced deployments diff --git a/apps/parsar-daemon/internal/agent/executor.go b/apps/parsar-daemon/internal/agent/executor.go deleted file mode 100644 index 1e1346cf2..000000000 --- a/apps/parsar-daemon/internal/agent/executor.go +++ /dev/null @@ -1,61 +0,0 @@ -package agent - -import ( - "context" - "fmt" - - "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" -) - -// TurnSettlement describes native readiness after this Turn has fully drained. -// A false result must carry a reason; it requires confirmed Executor.Close. -type TurnSettlement struct { - Reusable bool - Reason string -} - -// Turn owns one output stream and never retargets cancellation to a successor. -type Turn interface { - Session - CancellationOutcome() proto.DonePayload - // Success confirms closed output and settled native input, function, - // interaction and child-work obligations. Errors cannot prove cancellation. - AwaitSettlement(context.Context) (TurnSettlement, error) -} - -// Executor retains a fixed native configuration across independently owned Turns. -type Executor interface { - // A nil Turn guarantees no input was submitted or output retained and leaves - // out with the caller. A non-nil Turn owns out, including on error; input - // may have been submitted and must never be replayed automatically. - StartTurn(context.Context, string, proto.MessageInput, chan<- proto.Envelope) (Turn, error) - // Success confirms all native work and owned output streams have stopped. - // It does not establish an otherwise unknown Turn result. An error retains - // resource ownership; another caller may await or retry Close. - Close(context.Context) error -} - -// A failed factory retains any unconfirmed cleanup in its non-nil Executor. -type ExecutorFactory func(context.Context, proto.PromptRequestPayload) (Executor, error) - -func (r *Registry) RegisterExecutor(kind string, factory ExecutorFactory) { - r.mu.Lock() - defer r.mu.Unlock() - info, exists := r.kinds[kind] - if !exists || factory == nil { - panic("agent.Registry.RegisterExecutor: registered kind and factory required") - } - r.executors[kind] = factory - info.Capabilities.Preparation = true - r.kinds[kind] = info -} - -func (r *Registry) ResolveExecutor(kind string) (ExecutorFactory, error) { - r.mu.RLock() - defer r.mu.RUnlock() - factory := r.executors[kind] - if factory == nil { - return nil, fmt.Errorf("agent: executor unavailable for %q", kind) - } - return factory, nil -} diff --git a/apps/parsar-daemon/internal/agent/functions.go b/apps/parsar-daemon/internal/agent/functions.go index 9dee7b992..c34d8b32c 100644 --- a/apps/parsar-daemon/internal/agent/functions.go +++ b/apps/parsar-daemon/internal/agent/functions.go @@ -1,14 +1,7 @@ package agent import ( - "context" "errors" - - "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" ) var ErrUnknownFunctionCall = errors.New("agent: function call is no longer pending") - -type FunctionResultSubmitter interface { - SubmitFunctionResult(context.Context, proto.FunctionResultPayload) error -} diff --git a/apps/parsar-daemon/internal/agent/harness.go b/apps/parsar-daemon/internal/agent/harness.go new file mode 100644 index 000000000..664115aba --- /dev/null +++ b/apps/parsar-daemon/internal/agent/harness.go @@ -0,0 +1,220 @@ +// Package agent defines the Runtime's native Harness adapter contract. +// Start with this file when adding a Harness; the onboarding guide is at +// contracts/agents-api/harness-onboarding.md. +// +// Required lifecycle: ExecutorFactory prepares a fixed Session-owned Executor; +// each StartTurn returns a fresh Turn with its own output and settlement. +// The current public text profile also requires DurableSteerer. Keep optional +// interfaces separate and implement them only for qualified operations. MCP, +// images, structured output and Subagent observations use protocol messages +// rather than additional Go interfaces; qualify and advertise them separately. +// +// Registration: RegisterKind takes the existing proto.SupportedAgentKind +// descriptor (kind, availability, native version and AgentKindCapabilities) and +// direct-call Factory. RegisterExecutor supplies the Session-owned lifecycle. +// RegisterPreparation optionally adds workspace access without model input. +// RegisterKind resets the other factories, so register it first. Preparation +// flags are derived by these methods; other advertised capabilities must reflect +// verified native behavior. Built-in registration lives in internal/cli. +// +// Runtime registration and Core service qualification remain separate. A public +// Harness also needs a profile in services/agents-api/internal/engine; advertising +// a capability cannot authorize it. Requests, events and capability descriptors +// use the existing internal/agentdaemon/proto types. +package agent + +import ( + "context" + + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" +) + +// Required execution lifecycle. + +// ExecutorFactory prepares without model input. A failed factory retains any +// unconfirmed cleanup in its non-nil Executor. +type ExecutorFactory func(context.Context, proto.PromptRequestPayload) (Executor, error) + +// Executor retains a fixed native configuration across independently owned Turns. +type Executor interface { + // A nil Turn guarantees no input was submitted or output retained and leaves + // out with the caller. A non-nil Turn owns out, including on error; input + // may have been submitted and must never be replayed automatically. + StartTurn(context.Context, string, proto.MessageInput, chan<- proto.Envelope) (Turn, error) + // Success confirms all native work and owned output streams have stopped. + // It does not establish an otherwise unknown Turn result. An error retains + // resource ownership; another caller may await or retry Close. + Close(context.Context) error +} + +// Turn owns one output stream and never retargets cancellation to a successor. +type Turn interface { + Session + CancellationOutcome() proto.DonePayload + // Success confirms closed output and settled native input, function, + // interaction and child-work obligations. Errors cannot prove cancellation. + AwaitSettlement(context.Context) (TurnSettlement, error) +} + +// TurnSettlement describes native readiness after this Turn has fully drained. +// A false result must carry a reason; it requires confirmed Executor.Close. +type TurnSettlement struct { + Reusable bool + Reason string +} + +// Session is the cancellation surface shared by direct prompt runs and Turns. +// For Executor-owned Turns, AwaitSettlement and Executor.Close define settlement +// and resource retirement; Cancel alone does not transfer resource ownership. +type Session interface { + // Cancel signals the session to abort. Idempotent. Actual teardown + // happens asynchronously and is signalled via the out channel close. + Cancel(ctx context.Context) error +} + +// Turn capabilities. Public profiles determine which operations are required. + +// DurableSteerer reports one complete write synchronously, then waits for the native receipt. +type DurableSteerer interface { + SteerWithReceipt(context.Context, proto.PromptSteerPayload, func()) error +} + +// Steerer optionally delivers additional text to the session's active turn. +type Steerer interface { + Steer(context.Context, proto.PromptSteerPayload) error +} + +// FunctionResultSubmitter delivers a result for an outstanding native call. +// A successful return requires its native application receipt. +type FunctionResultSubmitter interface { + SubmitFunctionResult(context.Context, proto.FunctionResultPayload) error +} + +// PermissionResponder optionally accepts decisions for emitted permission requests. +// Unknown or expired requests return ErrUnknownPermission. +type PermissionResponder interface { + SubmitPermission(context.Context, string, proto.PermissionDecisionPayload) error +} + +// UserChoiceResponder optionally accepts answers for emitted user-choice requests. +// Unknown or expired requests return ErrUnknownAsk. +type UserChoiceResponder interface { + SubmitPromptForUserChoice(context.Context, string, proto.PromptForUserChoiceDecisionPayload) error +} + +// Optional workspace capabilities, implemented by the authorized resource owner. + +// WorkspaceReader returns success only after acknowledged native close on an existing owner. +type WorkspaceReader interface { + ReadWorkspaceFile(context.Context, string, int) (WorkspaceReadResult, error) +} + +// WorkspaceDirectoryLister reads one directory through an existing workspace owner. +// An empty directory selects the root; other paths contain only relative components. +// Entry names are single components. Kind is file, directory, symlink or other; +// SizeBytes is present and nonnegative only for regular files. Results have no +// prescribed order, and Truncated must not be presented as a complete inventory. +// maxEntries is positive; adapters may reject limits above their private bound. +// Successful return requires settled directory/metadata access and handle cleanup. +// Implementations retain workspace authorization and isolation and use the existing +// WorkspaceRead errors for unsupported, unavailable, busy, invalid or uncertain reads. +// This interface does not establish public Files pagination or feature admission. +type WorkspaceDirectoryLister interface { + ListWorkspaceDirectory(context.Context, string, int) (WorkspaceDirectoryResult, error) +} + +// WorkspaceWriter confirms a native commit on an already authorized prepared owner. +type WorkspaceWriter interface { + WriteWorkspaceFile(context.Context, string, []byte) (WorkspaceWriteResult, error) +} + +// Separate preparation for qualified workspace access and direct-call paths. + +// Prepared owns native resources until Start returns a non-nil Session. The +// preparation owner context spans the eventual Session; Start's context is local +// to that operation. A nil Session leaves preparation cleanup with the caller. +type Prepared interface { + // Start transfers output ownership only when it returns a non-nil Session. + // A nil Session leaves the caller as the sole owner of closing out, and the + // implementation must not retain or write to it after Start returns. + Start(context.Context, string, proto.MessageInput, chan<- proto.Envelope) (Session, error) + // Close retains unused ownership on error; callers may retry settlement. + Close() error +} + +// PreparedCancellation is required for executable preparations and follows the +// same native resource across Start. Read-only preparations need only Prepared. +type PreparedCancellation interface { + Prepared + // Cancel returns after local cleanup and all output writes have stopped. + // An error retains ownership so callers can retry this exact object serially. + Cancel(context.Context) error + CancellationOutcome() proto.DonePayload +} + +// A factory may return both a resource and an error when construction failed but +// cleanup remains unconfirmed. The caller must retain and close that resource. +type PreparationFactory func(context.Context, proto.PromptRequestPayload) (Prepared, error) + +// Direct-call factory and registration. These use the existing Registry behavior. + +// Factory builds a Session for one prompt_request. out is the upstream +// channel the agent writes into and closes exactly once after terminal output. +// ctx is cancelled by the router to wind the session down. +type Factory func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (Session, error) + +// RegisterKind installs f and the heartbeat descriptor for an +// agent_kind. Callers may set Available=false when an adapter exists +// but its underlying CLI is not usable. +func (r *Registry) RegisterKind(info proto.SupportedAgentKind, f Factory) { + kind := info.Kind + if kind == "" { + panic("agent.Registry.Register: empty kind") + } + if f == nil { + panic("agent.Registry.Register: nil factory") + } + r.mu.Lock() + defer r.mu.Unlock() + r.factories[kind] = f + delete(r.preparers, kind) + delete(r.executors, kind) + info.Capabilities.Preparation = false + info.Capabilities.WorkspaceReadPreparation = false + r.kinds[kind] = info +} + +// RegisterExecutor installs the shared lifecycle after RegisterKind and derives +// the Preparation capability. It does not enable other public operations. +func (r *Registry) RegisterExecutor(kind string, factory ExecutorFactory) { + r.mu.Lock() + defer r.mu.Unlock() + info, exists := r.kinds[kind] + if !exists || factory == nil { + panic("agent.Registry.RegisterExecutor: registered kind and factory required") + } + r.executors[kind] = factory + info.Capabilities.Preparation = true + r.kinds[kind] = info +} + +// RegisterPreparation installs a separate execution-only path. Product factory +// wrappers must not add authoring or capability-download side effects to it. +func (r *Registry) RegisterPreparation(kind string, workspaceRead bool, prepare PreparationFactory) { + r.mu.Lock() + defer r.mu.Unlock() + info, exists := r.kinds[kind] + if !exists || prepare == nil { + panic("agent.Registry.RegisterPreparation: registered kind and factory required") + } + r.preparers[kind] = prepare + info.Capabilities.Preparation = true + info.Capabilities.WorkspaceReadPreparation = workspaceRead + r.kinds[kind] = info +} + +// Register installs f as the factory for kind with a basic available +// descriptor. Panics on empty kind or nil factory. +func (r *Registry) Register(kind string, f Factory) { + r.RegisterKind(proto.SupportedAgentKind{Kind: kind, Available: true}, f) +} diff --git a/apps/parsar-daemon/internal/agent/interactions.go b/apps/parsar-daemon/internal/agent/interactions.go deleted file mode 100644 index 2613c96e8..000000000 --- a/apps/parsar-daemon/internal/agent/interactions.go +++ /dev/null @@ -1,18 +0,0 @@ -package agent - -import ( - "context" - "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" -) - -// PermissionResponder optionally accepts decisions for emitted permission requests. -// Unknown or expired requests return ErrUnknownPermission. -type PermissionResponder interface { - SubmitPermission(context.Context, string, proto.PermissionDecisionPayload) error -} - -// UserChoiceResponder optionally accepts answers for emitted user-choice requests. -// Unknown or expired requests return ErrUnknownAsk. -type UserChoiceResponder interface { - SubmitPromptForUserChoice(context.Context, string, proto.PromptForUserChoiceDecisionPayload) error -} diff --git a/apps/parsar-daemon/internal/agent/preparation.go b/apps/parsar-daemon/internal/agent/preparation.go deleted file mode 100644 index bd303d464..000000000 --- a/apps/parsar-daemon/internal/agent/preparation.go +++ /dev/null @@ -1,59 +0,0 @@ -package agent - -import ( - "context" - "fmt" - - "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" -) - -// Prepared owns native resources until Start returns a non-nil Session. The -// preparation owner context spans the eventual Session; Start's context is local -// to that operation. A nil Session leaves preparation cleanup with the caller. -type Prepared interface { - // Start transfers output ownership only when it returns a non-nil Session. - // A nil Session leaves the caller as the sole owner of closing out, and the - // implementation must not retain or write to it after Start returns. - Start(context.Context, string, proto.MessageInput, chan<- proto.Envelope) (Session, error) - // Close retains unused ownership on error; callers may retry settlement. - Close() error -} - -// PreparedCancellation is required for executable preparations and follows the -// same native resource across Start. Read-only preparations need only Prepared. -type PreparedCancellation interface { - Prepared - // Cancel returns after local cleanup and all output writes have stopped. - // An error retains ownership so callers can retry this exact object serially. - Cancel(context.Context) error - CancellationOutcome() proto.DonePayload -} - -// A factory may return both a resource and an error when construction failed but -// cleanup remains unconfirmed. The caller must retain and close that resource. -type PreparationFactory func(context.Context, proto.PromptRequestPayload) (Prepared, error) - -// RegisterPreparation installs a separate execution-only path. Product factory -// wrappers must not add authoring or capability-download side effects to it. -func (r *Registry) RegisterPreparation(kind string, workspaceRead bool, prepare PreparationFactory) { - r.mu.Lock() - defer r.mu.Unlock() - info, exists := r.kinds[kind] - if !exists || prepare == nil { - panic("agent.Registry.RegisterPreparation: registered kind and factory required") - } - r.preparers[kind] = prepare - info.Capabilities.Preparation = true - info.Capabilities.WorkspaceReadPreparation = workspaceRead - r.kinds[kind] = info -} - -func (r *Registry) ResolvePreparation(kind string) (PreparationFactory, error) { - r.mu.RLock() - defer r.mu.RUnlock() - f := r.preparers[kind] - if f == nil { - return nil, fmt.Errorf("agent: preparation unavailable for %q", kind) - } - return f, nil -} diff --git a/apps/parsar-daemon/internal/agent/registry.go b/apps/parsar-daemon/internal/agent/registry.go index 97d79a4c9..a96080c74 100644 --- a/apps/parsar-daemon/internal/agent/registry.go +++ b/apps/parsar-daemon/internal/agent/registry.go @@ -1,23 +1,6 @@ -// Package agent is the registration surface for agent_kind -// implementations. The dispatch router consults agent.Registry to map -// an inbound prompt_request's AgentKind to its factory. -// -// Lifetime / channel ownership: -// -// - Factory takes an out chan<- proto.Envelope owned by the dispatch -// router. The agent SENDS upstream events on it and OWNS the -// close: it MUST close(out) exactly once after emitting the current -// run's terminal "done" or "error" frame. The underlying CLI may -// remain alive during the router's idle window and is terminated -// through Session.Cancel. -// -// - Session.Cancel is best-effort and idempotent: a session that -// already finished naturally must accept a Cancel call without -// panicking. package agent import ( - "context" "errors" "fmt" "slices" @@ -26,20 +9,6 @@ import ( "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" ) -// Factory builds a Session for one prompt_request. out is the upstream -// channel the agent writes into; the agent owns its close (see package -// doc). ctx is cancelled by the router to wind the session down. -type Factory func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (Session, error) - -// Session owns one prompt run and any CLI process retained after that -// run. Run completion is signalled by closing out; process teardown is -// signalled separately through Cancel. -type Session interface { - // Cancel signals the session to abort. Idempotent. Actual teardown - // happens asynchronously and is signalled via the out channel close. - Cancel(ctx context.Context) error -} - // ErrUnknownPermission is returned by PermissionResponder.SubmitPermission when // the permID doesn't match any outstanding request. The router uses // this to distinguish a benign race from a real forwarding failure. @@ -71,33 +40,6 @@ func NewRegistry() *Registry { } } -// Register installs f as the factory for kind with a basic available -// descriptor. Panics on empty kind or nil factory. -func (r *Registry) Register(kind string, f Factory) { - r.RegisterKind(proto.SupportedAgentKind{Kind: kind, Available: true}, f) -} - -// RegisterKind installs f and the heartbeat descriptor for an -// agent_kind. Callers may set Available=false when an adapter exists -// but its underlying CLI is not usable. -func (r *Registry) RegisterKind(info proto.SupportedAgentKind, f Factory) { - kind := info.Kind - if kind == "" { - panic("agent.Registry.Register: empty kind") - } - if f == nil { - panic("agent.Registry.Register: nil factory") - } - r.mu.Lock() - defer r.mu.Unlock() - r.factories[kind] = f - delete(r.preparers, kind) - delete(r.executors, kind) - info.Capabilities.Preparation = false - info.Capabilities.WorkspaceReadPreparation = false - r.kinds[kind] = info -} - // Resolve returns the factory for kind, or wraps ErrUnsupportedKind. func (r *Registry) Resolve(kind string) (Factory, error) { r.mu.RLock() @@ -145,3 +87,23 @@ func (r *Registry) SupportedAgentKinds() []proto.SupportedAgentKind { }) return out } + +func (r *Registry) ResolveExecutor(kind string) (ExecutorFactory, error) { + r.mu.RLock() + defer r.mu.RUnlock() + factory := r.executors[kind] + if factory == nil { + return nil, fmt.Errorf("agent: executor unavailable for %q", kind) + } + return factory, nil +} + +func (r *Registry) ResolvePreparation(kind string) (PreparationFactory, error) { + r.mu.RLock() + defer r.mu.RUnlock() + f := r.preparers[kind] + if f == nil { + return nil, fmt.Errorf("agent: preparation unavailable for %q", kind) + } + return f, nil +} diff --git a/apps/parsar-daemon/internal/agent/steering.go b/apps/parsar-daemon/internal/agent/steering.go index a9807a74f..aea8885bc 100644 --- a/apps/parsar-daemon/internal/agent/steering.go +++ b/apps/parsar-daemon/internal/agent/steering.go @@ -1,22 +1,9 @@ package agent import ( - "context" "errors" - - "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" ) -// Steerer optionally delivers additional text to the session's active turn. -type Steerer interface { - Steer(context.Context, proto.PromptSteerPayload) error -} - -// DurableSteerer reports one complete write synchronously, then waits for the native receipt. -type DurableSteerer interface { - SteerWithReceipt(context.Context, proto.PromptSteerPayload, func()) error -} - // ErrSteeringNotReady means no input was sent because the turn is starting. var ErrSteeringNotReady = errors.New("agent: turn is not ready for input") diff --git a/apps/parsar-daemon/internal/agent/workspace_directory.go b/apps/parsar-daemon/internal/agent/workspace_directory.go index 6ed788a0c..d344f49a9 100644 --- a/apps/parsar-daemon/internal/agent/workspace_directory.go +++ b/apps/parsar-daemon/internal/agent/workspace_directory.go @@ -1,7 +1,5 @@ package agent -import "context" - // WorkspaceDirectoryEntry describes an entry observed without following its final symlink. type WorkspaceDirectoryEntry struct { Name string @@ -14,17 +12,3 @@ type WorkspaceDirectoryResult struct { Entries []WorkspaceDirectoryEntry Truncated bool } - -// WorkspaceDirectoryLister reads one directory through an existing workspace owner. -// An empty directory selects the root; other paths contain only relative components. -// Entry names are single components. Kind is file, directory, symlink or other; -// SizeBytes is present and nonnegative only for regular files. Results have no -// prescribed order, and Truncated must not be presented as a complete inventory. -// maxEntries is positive; adapters may reject limits above their private bound. -// Successful return requires settled directory/metadata access and handle cleanup. -// Implementations retain workspace authorization and isolation and use the existing -// WorkspaceRead errors for unsupported, unavailable, busy, invalid or uncertain reads. -// This interface does not establish public Files pagination or feature admission. -type WorkspaceDirectoryLister interface { - ListWorkspaceDirectory(context.Context, string, int) (WorkspaceDirectoryResult, error) -} diff --git a/apps/parsar-daemon/internal/agent/workspace_read.go b/apps/parsar-daemon/internal/agent/workspace_read.go index 403a9b070..03e270d5c 100644 --- a/apps/parsar-daemon/internal/agent/workspace_read.go +++ b/apps/parsar-daemon/internal/agent/workspace_read.go @@ -1,7 +1,6 @@ package agent import ( - "context" "errors" ) @@ -10,11 +9,6 @@ type WorkspaceReadResult struct { Truncated bool } -// WorkspaceReader returns success only after acknowledged native close on an existing owner. -type WorkspaceReader interface { - ReadWorkspaceFile(context.Context, string, int) (WorkspaceReadResult, error) -} - var ( ErrWorkspaceReadUnsupported = errors.New("workspace read unsupported") ErrWorkspaceReadUnavailable = errors.New("workspace read unavailable") diff --git a/apps/parsar-daemon/internal/agent/workspace_write.go b/apps/parsar-daemon/internal/agent/workspace_write.go index 237b73997..11130d377 100644 --- a/apps/parsar-daemon/internal/agent/workspace_write.go +++ b/apps/parsar-daemon/internal/agent/workspace_write.go @@ -1,7 +1,6 @@ package agent import ( - "context" "errors" "fmt" ) @@ -10,11 +9,6 @@ type WorkspaceWriteResult struct { SizeBytes int64 } -// WorkspaceWriter confirms a native commit on an already authorized prepared owner. -type WorkspaceWriter interface { - WriteWorkspaceFile(context.Context, string, []byte) (WorkspaceWriteResult, error) -} - var ( ErrWorkspaceWriteUnsupported = errors.New("workspace write unsupported") ErrWorkspaceWriteUnavailable = errors.New("workspace write unavailable") diff --git a/contracts/agents-api/harness-onboarding.md b/contracts/agents-api/harness-onboarding.md index c9fdde33e..423819da4 100644 --- a/contracts/agents-api/harness-onboarding.md +++ b/contracts/agents-api/harness-onboarding.md @@ -1,6 +1,8 @@ # Add a native harness to OpenAgentCore -Implement one adapter for a native SDK or machine-readable protocol. Core and +Start with [`agent/harness.go`](../../apps/parsar-daemon/internal/agent/harness.go). +It defines the required lifecycle, separate optional interfaces and existing +registration methods in one place. Implement one adapter for a native SDK or machine-readable protocol. Core and Runtime use the common contract; adding a harness should not add engine-name branches to public handlers, storage, scheduling or environment providers. @@ -45,28 +47,11 @@ model communication configuration, not Turn scheduling or native process ownersh ## Required adapter interfaces -The canonical signatures and ownership comments are in -[`agent/executor.go`](../../apps/parsar-daemon/internal/agent/executor.go): - -```go -type ExecutorFactory func(context.Context, proto.PromptRequestPayload) (Executor, error) - -type Executor interface { - StartTurn(context.Context, string, proto.MessageInput, chan<- proto.Envelope) (Turn, error) - Close(context.Context) error -} - -type Turn interface { - Cancel(context.Context) error - CancellationOutcome() proto.DonePayload - AwaitSettlement(context.Context) (TurnSettlement, error) -} - -type TurnSettlement struct { - Reusable bool - Reason string -} -``` +[`agent/harness.go`](../../apps/parsar-daemon/internal/agent/harness.go) is the +canonical interface entry point. Its required lifecycle is `ExecutorFactory`, +`Executor`, `Turn` and `TurnSettlement`. Optional Turn and workspace interfaces +remain separate; their result types and error values stay in the corresponding +operation files in the same package. All use the existing neutral protocol types. The factory prepares a fixed configuration without sending model input. Executor owns the native process or connection, native Session, capability configuration and @@ -115,7 +100,7 @@ ambiguous required history fails before new model input. | `agent.DurableSteerer` | Current public text execution | Distinguish write and application receipts; preserve retry identity | | `agent.FunctionResultSubmitter` | Public function tools | Match call/result identity and acknowledge native application | | `agent.PermissionResponder`, `agent.UserChoiceResponder` | When emitting these interactions | Route exact identities and settle receipts | -| `agent.WorkspaceReader`, `agent.WorkspaceDirectoryLister` | Qualified workspace operations | Use the fixed authorized workspace and retain accepted operations through close | +| `agent.WorkspaceReader`, `agent.WorkspaceDirectoryLister`, `agent.WorkspaceWriter` | Qualified workspace operations | Use the fixed authorized workspace and retain accepted operations through close | | Neutral message, image, MCP, structured-output and Subagent observations | Only when qualified and advertised | Preserve the operation-specific contract and reject unsupported combinations | Optional features need not match another harness. The service profile qualifies @@ -127,6 +112,14 @@ lifecycle. ## Register a supported operation set +The registration methods are also defined in `agent/harness.go`. The existing +`proto.SupportedAgentKind` descriptor supplies kind, availability, version and +`AgentKindCapabilities`; its schema remains in `internal/agentdaemon/proto`. +`RegisterKind` resets the Executor and preparation registrations, so call it +first. `RegisterExecutor` and `RegisterPreparation` derive preparation flags; +other capability declarations must match verified behavior. Core qualification +still belongs to the service profile and is not granted by Runtime registration. + 1. Pin the upstream source/package version and document the native entry point. 2. Implement the adapter using its SDK or native protocol. Reuse shared process, credential/configuration and local workspace helpers where applicable.