From 36a71d71a033d2de0c0245910f80dd4e92973e7c Mon Sep 17 00:00:00 2001 From: rick12345 Date: Mon, 7 Sep 2026 17:08:26 +0800 Subject: [PATCH] Persist cumulative task execution evidence and advertise completion capabilities --- agentapi/client_service.go | 8 +- agentapi/http_contract_test.go | 1 + agentapi/types.go | 6 +- api/agent-openapi.json | 101 +++++++++++++- cognition/agent_task.go | 25 +++- cognition/task_execution_evidence.go | 118 ++++++++++++++++ cognition/task_execution_evidence_test.go | 127 ++++++++++++++++++ docs/internal-agent-runtime.md | 15 +++ docs/internal-agent-runtime.zh-CN.md | 7 + .../io/github/sunrioa/rin/RinAgentClient.java | 5 + .../sunrioa/rin/RinAgentClientTest.java | 3 + 11 files changed, 405 insertions(+), 11 deletions(-) create mode 100644 cognition/task_execution_evidence.go create mode 100644 cognition/task_execution_evidence_test.go diff --git a/agentapi/client_service.go b/agentapi/client_service.go index 8b4da70..f39f86a 100644 --- a/agentapi/client_service.go +++ b/agentapi/client_service.go @@ -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 } diff --git a/agentapi/http_contract_test.go b/agentapi/http_contract_test.go index 2d704b6..8799220 100644 --- a/agentapi/http_contract_test.go +++ b/agentapi/http_contract_test.go @@ -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{}, diff --git a/agentapi/types.go b/agentapi/types.go index be83378..fb859b8 100644 --- a/agentapi/types.go +++ b/agentapi/types.go @@ -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. diff --git a/api/agent-openapi.json b/api/agent-openapi.json index 74dc41d..250a33f 100644 --- a/api/agent-openapi.json +++ b/api/agent-openapi.json @@ -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": { @@ -1346,6 +1364,9 @@ }, "lookahead": { "$ref": "#/components/schemas/TaskLookaheadState" + }, + "execution_evidence": { + "$ref": "#/components/schemas/TaskExecutionEvidence" } }, "required": [ @@ -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" + ] } } } diff --git a/cognition/agent_task.go b/cognition/agent_task.go index 5eb12ec..51f14c6 100644 --- a/cognition/agent_task.go +++ b/cognition/agent_task.go @@ -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" @@ -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"` @@ -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) { @@ -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 { @@ -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) @@ -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] diff --git a/cognition/task_execution_evidence.go b/cognition/task_execution_evidence.go new file mode 100644 index 0000000..9acec8a --- /dev/null +++ b/cognition/task_execution_evidence.go @@ -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 © +} + +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 +} diff --git a/cognition/task_execution_evidence_test.go b/cognition/task_execution_evidence_test.go new file mode 100644 index 0000000..b99212d --- /dev/null +++ b/cognition/task_execution_evidence_test.go @@ -0,0 +1,127 @@ +package cognition + +import ( + "context" + "fmt" + "path/filepath" + "testing" +) + +func longEvidenceTask() TaskSession { + task := sqliteTestTask() + task.Revision = 1 + task.Budget = TaskBudget{MaxSteps: 512, MaxModelCalls: 512, MaxActions: 512, MaxModelTokens: 100_000} + appendTaskEvent(&task, TaskEvent{Kind: "task.created", AtUnixMillis: 10}) + for i := 0; i < 300; i++ { + task.ActionCount++ + appendTaskEvent(&task, TaskEvent{Kind: "action.selected", Code: "activity.wait", AtUnixMillis: 10}) + appendTaskEvent(&task, TaskEvent{Kind: "operation.terminal", Code: "succeeded", OperationID: fmt.Sprintf("operation.evidence.%d", i), AtUnixMillis: 10}) + } + task.Status = TaskCompleted + appendTaskEvent(&task, TaskEvent{Kind: "task.completed", AtUnixMillis: 10}) + return task +} + +func TestTaskExecutionEvidenceSurvivesRetentionAndSQLiteReopen(t *testing.T) { + ctx := context.Background() + path := filepath.Join(t.TempDir(), "tasks.db") + store, err := OpenSQLiteTaskStore(path, 10) + if err != nil { + t.Fatal(err) + } + input := longEvidenceTask() + input.Revision = 0 + task, err := store.Create(ctx, input) + if err != nil { + store.Close() + t.Fatal(err) + } + if len(task.History) != 512 || task.History[0].Sequence <= 1 { + t.Fatal("fixture did not exceed history retention") + } + // A returned copy cannot alter the authoritative aggregate. + task.ExecutionEvidence.SelectedCapabilities[0] = "tampered.capability" + task.ExecutionEvidence.SuccessfulOperationCount = 0 + if err := store.Close(); err != nil { + t.Fatal(err) + } + store, err = OpenSQLiteTaskStore(path, 10) + if err != nil { + t.Fatal(err) + } + defer store.Close() + loaded, err := store.Load(ctx, task.TaskID) + if err != nil { + t.Fatal(err) + } + e := loaded.ExecutionEvidence + if e == nil || !e.Complete || e.EventSequence != 602 || e.SelectedActionCount != 300 || + e.TerminalOperationCount != 300 || e.SuccessfulOperationCount != 300 || e.TaskCompletedCount != 1 || + e.SelectedCapabilities[0] != "activity.wait" || e.LastOperationID != "operation.evidence.299" { + t.Fatalf("durable evidence changed: %+v", e) + } +} + +func TestLegacyTaskEvidenceNeverTreatsTruncatedHistoryAsComplete(t *testing.T) { + task := longEvidenceTask() + task.ExecutionEvidence = nil + appendTaskEvent(&task, TaskEvent{Kind: "skill.learning-skipped", AtUnixMillis: 10}) + if task.ExecutionEvidence.Complete { + t.Fatal("fabricated complete evidence from a retained history tail") + } + if _, err := sealTaskSession(task); err != nil { + t.Fatalf("legacy task became unreadable: %v", err) + } + full := sqliteTestTask() + full.History = []TaskEvent{{Sequence: 1, Kind: "task.created", AtUnixMillis: 10}} + full.EventSequence = 1 + appendTaskEvent(&full, TaskEvent{Kind: "task.resumed", AtUnixMillis: 10}) + if !full.ExecutionEvidence.Complete || full.ExecutionEvidence.EventSequence != 2 { + t.Fatal("lost complete legacy history") + } + store, _ := NewLocalTaskStore(10) + _, err := store.Create(context.Background(), full) + if err != nil { + t.Fatal(err) + } + snapshot, _ := store.Snapshot(context.Background()) + snapshot.Version = lookaheadTaskSnapshotVersion + snapshot.Tasks[0].ExecutionEvidence = nil + if _, err := RestoreLocalTaskStore(10, snapshot); err != nil { + t.Fatalf("v6 snapshot migration: %v", err) + } +} + +func TestTaskExecutionEvidenceCountsMacrosAndInvalidatedIntents(t *testing.T) { + task := sqliteTestTask() + task.Revision = 1 + appendTaskEvent(&task, TaskEvent{Kind: "task.created", AtUnixMillis: 10}) + task.ActionCount++ + appendTaskEvent(&task, TaskEvent{Kind: "action.selected", Code: "macro.collect", AtUnixMillis: 10}) + appendTaskEvent(&task, TaskEvent{Kind: "macro.started", OperationID: "operation.parent", AtUnixMillis: 10}) + task.ActionCount++ + appendTaskEvent(&task, TaskEvent{Kind: "action.selected", Code: "resource.pickup", AtUnixMillis: 10}) + appendTaskEvent(&task, TaskEvent{Kind: "operation.terminal", Code: "failed", OperationID: "operation.child", AtUnixMillis: 10}) + appendTaskEvent(&task, TaskEvent{Kind: "macro.terminal", Code: "succeeded", OperationID: "operation.parent", AtUnixMillis: 10}) + task.ActionCount++ + appendTaskEvent(&task, TaskEvent{Kind: "action.selected", Code: "resource.pickup", AtUnixMillis: 10}) + appendTaskEvent(&task, TaskEvent{Kind: "action.invalidated", Code: "stale", AtUnixMillis: 10}) + task.Status = TaskCompleted + appendTaskEvent(&task, TaskEvent{Kind: "task.completed", AtUnixMillis: 10}) + if _, err := sealTaskSession(task); err != nil { + t.Fatal(err) + } + e := task.ExecutionEvidence + if e.SelectedActionCount != 3 || e.TerminalOperationCount != 2 || e.RejectedActionCount != 1 || + e.SuccessfulOperationCount != 1 || e.StartedMacroCount != 1 || e.TerminalMacroCount != 1 { + t.Fatalf("incorrect aggregate: %+v", e) + } + broken := cloneTaskSession(task) + broken.ExecutionEvidence.RejectedActionCount = 0 + if _, err := sealTaskSession(broken); err == nil { + t.Fatal("accepted completed task with unresolved evidence") + } + if task.ExecutionEvidence.RejectedActionCount != 1 { + t.Fatal("cloning aliased execution evidence") + } +} diff --git a/docs/internal-agent-runtime.md b/docs/internal-agent-runtime.md index 46c92a1..eea1e4b 100644 --- a/docs/internal-agent-runtime.md +++ b/docs/internal-agent-runtime.md @@ -404,6 +404,21 @@ token never grant world authority. ## Recovery and settled history +Task responses expose `execution_evidence` (`rin.task-execution/v1`), updated +atomically with the task transition. It retains selected, rejected, terminal and +successful action counts, macro counts, selected capability IDs, the last +terminal operation ID and completion count after the 512-event history window +rolls over. `event_sequence` binds the aggregate to the returned task state. +Clients should validate task identity, completion policy, scope and pending +operations against this aggregate; history is for explanation, not a complete +execution ledger. The Agent info response advertises this contract and the +supported `completion_modes` for compatibility checks. + +Snapshot v7 reads v3–v6. Existing tasks with complete retained history can derive +the aggregate on their next transition. A task whose earlier events are already +lost has `complete=false`; migration never fabricates complete evidence. Such +legacy tasks need separate review instead of being accepted from partial counts. + Unknown outcomes stop model decisions but keep reconciling the original intent or Operation, including a late Host result and the required Plan projection. A Run request is also a read-only reconciliation entry while the result remains unknown. diff --git a/docs/internal-agent-runtime.zh-CN.md b/docs/internal-agent-runtime.zh-CN.md index 41177c7..a84c6b6 100644 --- a/docs/internal-agent-runtime.zh-CN.md +++ b/docs/internal-agent-runtime.zh-CN.md @@ -299,6 +299,13 @@ Gateway 的阶段。恢复时通过进程内 `FindActionOperation` 查询原始 {"mode":"host-evidence","conditions":[{"condition_id":"goal.collect","kind":"operation-outcome","summary":"成功采集木材两次。","capability":{"id":"game.item.collect","version":"1.0.0"}}],"operation_requirements":[{"condition_id":"goal.collect","arguments_json":"{\"item\":\"wood\"}","minimum_count":2}]} ``` +任务返回的 `execution_evidence`(`rin.task-execution/v1`)与任务状态在同一次提交中持久化, +累计记录动作选择、拒绝、终态、成功次数、宏次数、能力集合和完成次数, +不会随 512 条历史窗口滚动而丢失。客户端应核对任务身份、完成策略、能力范围、 +未完成操作,以及累计证据的 `event_sequence`,不再把历史日志当成完整执行账本。 +Agent info 会公布累计证据版本和 `completion_modes`。v7 快照兼容 v3–v6; +旧任务若已丢失早期历史,迁移后的 `complete=false`,不能据部分计数自动验收。 + 人工验收时暂停码为 `completion.confirmation-required`。向 `POST /agent/v1/tasks/confirm-completion` 提交 `task_id` 和 `expected_revision`; 过期版本返回冲突。确认不能覆盖取消、未结束的动作或未完成的 Plan;恢复任务则重新进入 diff --git a/sdk/java/src/main/java/io/github/sunrioa/rin/RinAgentClient.java b/sdk/java/src/main/java/io/github/sunrioa/rin/RinAgentClient.java index 4a3495b..b086908 100644 --- a/sdk/java/src/main/java/io/github/sunrioa/rin/RinAgentClient.java +++ b/sdk/java/src/main/java/io/github/sunrioa/rin/RinAgentClient.java @@ -60,6 +60,11 @@ public CompletableFuture> cancelTask(Map input) { return post("/agent/v1/tasks/cancel", input); } + /** Accepts the exact task revision that was presented for human review. */ + public CompletableFuture> confirmTaskCompletion(Map input) { + return post("/agent/v1/tasks/confirm-completion", input); + } + private CompletableFuture> post( String path, Map input) { diff --git a/sdk/java/test/io/github/sunrioa/rin/RinAgentClientTest.java b/sdk/java/test/io/github/sunrioa/rin/RinAgentClientTest.java index 2e1526d..c2f36b3 100644 --- a/sdk/java/test/io/github/sunrioa/rin/RinAgentClientTest.java +++ b/sdk/java/test/io/github/sunrioa/rin/RinAgentClientTest.java @@ -63,6 +63,9 @@ static void run() throws Exception { "POST", "/agent/v1/tasks/run"), new RequestCase(() -> client.resumeTask(target), "POST", "/agent/v1/tasks/resume"), + new RequestCase(() -> client.confirmTaskCompletion(Map.of( + "task_id", "task.fixture", "expected_revision", 19L)), + "POST", "/agent/v1/tasks/confirm-completion"), new RequestCase(() -> client.cancelTask(target), "POST", "/agent/v1/tasks/cancel"));