Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions agentapi/client_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,12 @@ func NewClientService(service *Service, principal host.Principal) (*ClientServic

func (client *ClientService) Info(context.Context) (ClientInfo, error) {
return ClientInfo{
ContractVersion: ContractVersion,
Principal: cloneTaskPrincipal(client.principal),
ContractVersion: ContractVersion,
Principal: cloneTaskPrincipal(client.principal),
TaskExecutionEvidenceVersion: cognition.TaskExecutionEvidenceVersion,
CompletionModes: []cognition.TaskCompletionMode{
cognition.CompletionModel, cognition.CompletionEvidence, cognition.CompletionHuman,
},
}, nil
}

Expand Down
1 change: 1 addition & 0 deletions agentapi/http_contract_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ func TestAgentOpenAPISchemaFieldsMatchGoDTOs(t *testing.T) {
"CompletionConfirmationInput": agentapi.CompletionConfirmationInput{},
"TaskSchedule": cognition.TaskSchedule{},
"TaskSession": cognition.TaskSession{},
"TaskExecutionEvidence": cognition.TaskExecutionEvidence{},
"TaskLookaheadState": cognition.TaskLookaheadState{},
"TaskOperationResult": cognition.TaskOperationResult{},
"SkillLearningState": cognition.SkillLearningState{},
Expand Down
6 changes: 4 additions & 2 deletions agentapi/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,10 @@ type TaskTimelinePage = timeline.Page
type TaskTimelineUpdate = timeline.Update

type ClientInfo struct {
ContractVersion string `json:"contract_version"`
Principal host.Principal `json:"principal"`
ContractVersion string `json:"contract_version"`
Principal host.Principal `json:"principal"`
TaskExecutionEvidenceVersion string `json:"task_execution_evidence_version"`
CompletionModes []cognition.TaskCompletionMode `json:"completion_modes"`
}

// CompletionConfirmationInput binds caller acceptance to one review revision.
Expand Down
101 changes: 100 additions & 1 deletion api/agent-openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -536,11 +536,29 @@
},
"principal": {
"$ref": "#/components/schemas/Principal"
},
"task_execution_evidence_version": {
"type": "string",
"const": "rin.task-execution/v1"
},
"completion_modes": {
"type": "array",
"items": {
"type": "string",
"enum": [
"model-declared",
"host-evidence",
"human-confirmation"
]
},
"uniqueItems": true
}
},
"required": [
"contract_version",
"principal"
"principal",
"task_execution_evidence_version",
"completion_modes"
]
},
"Principal": {
Expand Down Expand Up @@ -1346,6 +1364,9 @@
},
"lookahead": {
"$ref": "#/components/schemas/TaskLookaheadState"
},
"execution_evidence": {
"$ref": "#/components/schemas/TaskExecutionEvidence"
}
},
"required": [
Expand Down Expand Up @@ -2046,6 +2067,84 @@
"adopted",
"discarded"
]
},
"TaskExecutionEvidence": {
"type": "object",
"additionalProperties": false,
"description": "Runtime-owned cumulative execution evidence persisted with task transitions; independent of rolling history. complete=false means an older task has unavailable prior evidence.",
"properties": {
"contract_version": {
"type": "string",
"const": "rin.task-execution/v1"
},
"complete": {
"type": "boolean"
},
"event_sequence": {
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991
},
"selected_action_count": {
"type": "integer",
"minimum": 0,
"maximum": 4294967295
},
"terminal_operation_count": {
"type": "integer",
"minimum": 0,
"maximum": 4294967295
},
"successful_operation_count": {
"type": "integer",
"minimum": 0,
"maximum": 4294967295
},
"rejected_action_count": {
"type": "integer",
"minimum": 0,
"maximum": 4294967295
},
"started_macro_count": {
"type": "integer",
"minimum": 0,
"maximum": 4294967295
},
"terminal_macro_count": {
"type": "integer",
"minimum": 0,
"maximum": 4294967295
},
"task_completed_count": {
"type": "integer",
"minimum": 0,
"maximum": 4294967295
},
"selected_capabilities": {
"type": "array",
"items": {
"type": "string"
},
"maxItems": 128,
"uniqueItems": true
},
"last_operation_id": {
"type": "string"
}
},
"required": [
"contract_version",
"complete",
"event_sequence",
"selected_action_count",
"terminal_operation_count",
"successful_operation_count",
"rejected_action_count",
"started_macro_count",
"terminal_macro_count",
"task_completed_count",
"selected_capabilities"
]
}
}
}
Expand Down
25 changes: 19 additions & 6 deletions cognition/agent_task.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ import (
"github.com/sunrioa/rin/timeline"
)

const TaskSnapshotVersion = "rin.cognition.tasks/v6"
const TaskSnapshotVersion = "rin.cognition.tasks/v7"

const lookaheadTaskSnapshotVersion = "rin.cognition.tasks/v6"

const completionTaskSnapshotVersion = "rin.cognition.tasks/v5"

Expand Down Expand Up @@ -130,10 +132,11 @@ type TaskSession struct {
Step uint32 `json:"step"`
Budget TaskBudget `json:"budget"`

ModelCalls uint32 `json:"model_calls"`
ModelTokens uint64 `json:"model_tokens"`
ActionCount uint32 `json:"action_count"`
Lookahead *TaskLookaheadState `json:"lookahead,omitempty"`
ModelCalls uint32 `json:"model_calls"`
ModelTokens uint64 `json:"model_tokens"`
ActionCount uint32 `json:"action_count"`
Lookahead *TaskLookaheadState `json:"lookahead,omitempty"`
ExecutionEvidence *TaskExecutionEvidence `json:"execution_evidence,omitempty"`

ControllerLease controlplane.ControllerLease `json:"controller_lease"`
PendingAction *host.ActionRequest `json:"pending_action,omitempty"`
Expand Down Expand Up @@ -195,7 +198,7 @@ func RestoreLocalTaskStore(maxTasks uint32, snapshot TaskSnapshot) (*LocalTaskSt
if err != nil {
return nil, err
}
if (snapshot.Version != TaskSnapshotVersion && snapshot.Version != completionTaskSnapshotVersion && snapshot.Version != schedulingTaskSnapshotVersion && snapshot.Version != legacyTaskSnapshotVersion) || snapshot.Revision == 0 {
if (snapshot.Version != TaskSnapshotVersion && snapshot.Version != lookaheadTaskSnapshotVersion && snapshot.Version != completionTaskSnapshotVersion && snapshot.Version != schedulingTaskSnapshotVersion && snapshot.Version != legacyTaskSnapshotVersion) || snapshot.Revision == 0 {
return nil, errors.New("task snapshot version or revision is invalid")
}
if len(snapshot.Tasks) > int(store.maxTasks) {
Expand Down Expand Up @@ -552,6 +555,9 @@ func sealTaskSession(task TaskSession) (TaskSession, error) {
if task.EventSequence > maxProviderWireInteger {
return TaskSession{}, errors.New("task event sequence is out of bounds")
}
if err := validateTaskExecutionEvidence(task); err != nil {
return TaskSession{}, err
}
if task.CreatedAtUnixMillis < 0 || task.UpdatedAtUnixMillis < task.CreatedAtUnixMillis ||
task.CreatedAtUnixMillis > maxProviderWireInteger ||
task.UpdatedAtUnixMillis > maxProviderWireInteger {
Expand Down Expand Up @@ -653,6 +659,7 @@ func validateTaskID(taskID string) error {
}

func cloneTaskSession(task TaskSession) TaskSession {
task.ExecutionEvidence = cloneTaskExecutionEvidence(task.ExecutionEvidence)
task.PendingSignals = append([]TaskSignal(nil), task.PendingSignals...)
task.SeenSignalIDs = append([]string(nil), task.SeenSignalIDs...)
task.Completion = cloneTaskCompletion(task.Completion)
Expand Down Expand Up @@ -773,8 +780,14 @@ func cloneTaskActionRequest(request host.ActionRequest) host.ActionRequest {
}

func appendTaskEvent(task *TaskSession, event TaskEvent) {
if task.ExecutionEvidence == nil {
task.ExecutionEvidence = executionEvidenceFromHistory(*task)
} else {
task.ExecutionEvidence = cloneTaskExecutionEvidence(task.ExecutionEvidence)
}
task.EventSequence++
event.Sequence = task.EventSequence
task.ExecutionEvidence.record(event)
if len(task.History) == 512 {
copy(task.History, task.History[1:])
task.History = task.History[:511]
Expand Down
118 changes: 118 additions & 0 deletions cognition/task_execution_evidence.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package cognition

import (
"errors"
"slices"
)

const TaskExecutionEvidenceVersion = "rin.task-execution/v1"

// TaskExecutionEvidence is updated by the runtime in the same task CAS as each
// state transition. It survives History retention; it is never model-authored.
// Complete is false for legacy tasks whose earlier events are unavailable.
type TaskExecutionEvidence struct {
ContractVersion string `json:"contract_version"`
Complete bool `json:"complete"`
EventSequence uint64 `json:"event_sequence"`
SelectedActionCount uint32 `json:"selected_action_count"`
TerminalOperationCount uint32 `json:"terminal_operation_count"`
SuccessfulOperationCount uint32 `json:"successful_operation_count"`
RejectedActionCount uint32 `json:"rejected_action_count"`
StartedMacroCount uint32 `json:"started_macro_count"`
TerminalMacroCount uint32 `json:"terminal_macro_count"`
TaskCompletedCount uint32 `json:"task_completed_count"`
SelectedCapabilities []string `json:"selected_capabilities"`
LastOperationID string `json:"last_operation_id,omitempty"`
}

func cloneTaskExecutionEvidence(value *TaskExecutionEvidence) *TaskExecutionEvidence {
if value == nil {
return nil
}
copy := *value
copy.SelectedCapabilities = append([]string{}, value.SelectedCapabilities...)
return &copy
}

func executionEvidenceFromHistory(task TaskSession) *TaskExecutionEvidence {
evidence := &TaskExecutionEvidence{
ContractVersion: TaskExecutionEvidenceVersion,
Complete: uint64(len(task.History)) == task.EventSequence,
SelectedCapabilities: []string{},
}
for i, event := range task.History {
if event.Sequence != uint64(i+1) {
evidence.Complete = false
}
evidence.record(event)
}
evidence.EventSequence = task.EventSequence
return evidence
}

func (evidence *TaskExecutionEvidence) record(event TaskEvent) {
evidence.EventSequence = event.Sequence
switch event.Kind {
case "action.selected":
evidence.SelectedActionCount++
if !slices.Contains(evidence.SelectedCapabilities, event.Code) {
evidence.SelectedCapabilities = append(evidence.SelectedCapabilities, event.Code)
slices.Sort(evidence.SelectedCapabilities)
}
case "action.rejected", "action.invalidated":
// Both discard a selected intent before an operation was submitted.
evidence.RejectedActionCount++
case "macro.started":
evidence.StartedMacroCount++
case "operation.terminal", "macro.terminal":
evidence.TerminalOperationCount++
evidence.LastOperationID = event.OperationID
if event.Kind == "macro.terminal" {
evidence.TerminalMacroCount++
}
if event.Code == "succeeded" {
evidence.SuccessfulOperationCount++
}
case "task.completed":
evidence.TaskCompletedCount++
}
}

func validateTaskExecutionEvidence(task TaskSession) error {
e := task.ExecutionEvidence
if e == nil {
// Existing snapshots remain readable without fabricating lost evidence.
return nil
}
if e.ContractVersion != TaskExecutionEvidenceVersion || e.EventSequence != task.EventSequence ||
e.SelectedActionCount > task.ActionCount ||
e.SuccessfulOperationCount > e.TerminalOperationCount ||
e.TerminalOperationCount > task.ActionCount || e.RejectedActionCount > task.ActionCount ||
uint64(e.TerminalOperationCount)+uint64(e.RejectedActionCount) > uint64(task.ActionCount) ||
e.StartedMacroCount > task.ActionCount || e.TerminalMacroCount > e.TerminalOperationCount ||
e.TaskCompletedCount > 1 || len(e.SelectedCapabilities) > 128 {
return errors.New("invalid task execution evidence")
}
for i, capability := range e.SelectedCapabilities {
if validateProviderID("execution_evidence.selected_capabilities", capability) != nil ||
!taskAllowsCapability(task, capability) ||
(i > 0 && e.SelectedCapabilities[i-1] >= capability) {
return errors.New("invalid execution evidence capability scope")
}
}
if e.LastOperationID != "" && validateProviderID("execution_evidence.last_operation_id", e.LastOperationID) != nil {
return errors.New("invalid execution evidence operation id")
}
if e.Complete && (e.SelectedActionCount != task.ActionCount ||
e.TerminalMacroCount > e.StartedMacroCount ||
(e.TerminalOperationCount > 0 && e.LastOperationID == "") ||
(e.SelectedActionCount > 0 && len(e.SelectedCapabilities) == 0)) {
return errors.New("task execution evidence is inconsistent with its state")
}
if e.Complete && task.Status == TaskCompleted &&
(e.TaskCompletedCount != 1 || e.StartedMacroCount != e.TerminalMacroCount ||
uint64(e.TerminalOperationCount)+uint64(e.RejectedActionCount) != uint64(task.ActionCount)) {
return errors.New("completed task retains unresolved execution evidence")
}
return nil
}
Loading