diff --git a/docs/openapi.json b/docs/openapi.json index 75ee0c9..55a31b4 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -8830,6 +8830,1079 @@ } ] } + }, + "/api/v1/minds/{mind}/wants": { + "get": { + "operationId": "wantsList", + "summary": "wants list", + "x-required-role": "observer", + "tags": [ + "wants" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Error" + }, + "403": { + "$ref": "#/components/responses/Error" + }, + "404": { + "$ref": "#/components/responses/Error" + }, + "422": { + "$ref": "#/components/responses/Error" + } + }, + "parameters": [ + { + "name": "mind", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "mind" + } + ] + } + }, + "/api/v1/minds/{mind}/wants/{id}": { + "get": { + "operationId": "wantGet", + "summary": "want get", + "x-required-role": "observer", + "tags": [ + "wants" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Error" + }, + "403": { + "$ref": "#/components/responses/Error" + }, + "404": { + "$ref": "#/components/responses/Error" + }, + "422": { + "$ref": "#/components/responses/Error" + } + }, + "parameters": [ + { + "name": "mind", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "mind" + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "identifier" + } + ] + } + }, + "/api/v1/minds/{mind}/want-conflicts": { + "get": { + "operationId": "wantConflictsList", + "summary": "want conflicts list", + "x-required-role": "observer", + "tags": [ + "want-conflicts" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Error" + }, + "403": { + "$ref": "#/components/responses/Error" + }, + "404": { + "$ref": "#/components/responses/Error" + }, + "422": { + "$ref": "#/components/responses/Error" + } + }, + "parameters": [ + { + "name": "mind", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "mind" + } + ] + } + }, + "/api/v1/minds/{mind}/commitments": { + "get": { + "operationId": "commitmentsList", + "summary": "commitments list", + "x-required-role": "observer", + "tags": [ + "commitments" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Error" + }, + "403": { + "$ref": "#/components/responses/Error" + }, + "404": { + "$ref": "#/components/responses/Error" + }, + "422": { + "$ref": "#/components/responses/Error" + } + }, + "parameters": [ + { + "name": "mind", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "mind" + } + ] + }, + "post": { + "operationId": "commitmentCreate", + "summary": "commitment create", + "x-required-role": "operator", + "tags": [ + "commitments" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Error" + }, + "403": { + "$ref": "#/components/responses/Error" + }, + "404": { + "$ref": "#/components/responses/Error" + }, + "422": { + "$ref": "#/components/responses/Error" + }, + "201": { + "description": "Created" + } + }, + "parameters": [ + { + "name": "mind", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "mind" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + } + }, + "/api/v1/minds/{mind}/commitments/{id}/close": { + "post": { + "operationId": "commitmentClose", + "summary": "commitment close", + "x-required-role": "operator", + "tags": [ + "close" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Error" + }, + "403": { + "$ref": "#/components/responses/Error" + }, + "404": { + "$ref": "#/components/responses/Error" + }, + "422": { + "$ref": "#/components/responses/Error" + }, + "201": { + "description": "Created" + } + }, + "parameters": [ + { + "name": "mind", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "mind" + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "identifier" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + } + }, + "/api/v1/minds/{mind}/intentions": { + "get": { + "operationId": "intentionsList", + "summary": "intentions list", + "x-required-role": "observer", + "tags": [ + "intentions" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Error" + }, + "403": { + "$ref": "#/components/responses/Error" + }, + "404": { + "$ref": "#/components/responses/Error" + }, + "422": { + "$ref": "#/components/responses/Error" + } + }, + "parameters": [ + { + "name": "mind", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "mind" + } + ] + } + }, + "/api/v1/minds/{mind}/intentions/{id}": { + "get": { + "operationId": "intentionGet", + "summary": "intention get", + "x-required-role": "observer", + "tags": [ + "intentions" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Error" + }, + "403": { + "$ref": "#/components/responses/Error" + }, + "404": { + "$ref": "#/components/responses/Error" + }, + "422": { + "$ref": "#/components/responses/Error" + } + }, + "parameters": [ + { + "name": "mind", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "mind" + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "identifier" + } + ] + } + }, + "/api/v1/minds/{mind}/intentions/{id}/abandon": { + "post": { + "operationId": "intentionAbandon", + "summary": "intention abandon", + "x-required-role": "operator", + "tags": [ + "abandon" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Error" + }, + "403": { + "$ref": "#/components/responses/Error" + }, + "404": { + "$ref": "#/components/responses/Error" + }, + "422": { + "$ref": "#/components/responses/Error" + }, + "201": { + "description": "Created" + } + }, + "parameters": [ + { + "name": "mind", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "mind" + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "identifier" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + } + }, + "/api/v1/minds/{mind}/decisions": { + "get": { + "operationId": "decisionsList", + "summary": "decisions list", + "x-required-role": "observer", + "tags": [ + "decisions" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Error" + }, + "403": { + "$ref": "#/components/responses/Error" + }, + "404": { + "$ref": "#/components/responses/Error" + }, + "422": { + "$ref": "#/components/responses/Error" + } + }, + "parameters": [ + { + "name": "mind", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "mind" + } + ] + } + }, + "/api/v1/minds/{mind}/plans": { + "get": { + "operationId": "plansList", + "summary": "plans list", + "x-required-role": "observer", + "tags": [ + "plans" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Error" + }, + "403": { + "$ref": "#/components/responses/Error" + }, + "404": { + "$ref": "#/components/responses/Error" + }, + "422": { + "$ref": "#/components/responses/Error" + } + }, + "parameters": [ + { + "name": "mind", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "mind" + } + ] + } + }, + "/api/v1/minds/{mind}/plans/{id}": { + "get": { + "operationId": "planGet", + "summary": "plan get", + "x-required-role": "observer", + "tags": [ + "plans" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Error" + }, + "403": { + "$ref": "#/components/responses/Error" + }, + "404": { + "$ref": "#/components/responses/Error" + }, + "422": { + "$ref": "#/components/responses/Error" + } + }, + "parameters": [ + { + "name": "mind", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "mind" + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "identifier" + } + ] + } + }, + "/api/v1/minds/{mind}/plans/{id}/cancel": { + "post": { + "operationId": "planCancel", + "summary": "plan cancel", + "x-required-role": "operator", + "tags": [ + "cancel" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Error" + }, + "403": { + "$ref": "#/components/responses/Error" + }, + "404": { + "$ref": "#/components/responses/Error" + }, + "422": { + "$ref": "#/components/responses/Error" + }, + "201": { + "description": "Created" + } + }, + "parameters": [ + { + "name": "mind", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "mind" + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "identifier" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + } + }, + "/api/v1/minds/{mind}/plans/{id}/steps/{step}/retry": { + "post": { + "operationId": "planStepRetry", + "summary": "plan step retry", + "x-required-role": "operator", + "tags": [ + "retry" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Error" + }, + "403": { + "$ref": "#/components/responses/Error" + }, + "404": { + "$ref": "#/components/responses/Error" + }, + "422": { + "$ref": "#/components/responses/Error" + }, + "201": { + "description": "Created" + } + }, + "parameters": [ + { + "name": "mind", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "mind" + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "identifier" + }, + { + "name": "step", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "step" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + } + }, + "/api/v1/minds/{mind}/why": { + "get": { + "operationId": "why", + "summary": "why", + "x-required-role": "observer", + "tags": [ + "why" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Error" + }, + "403": { + "$ref": "#/components/responses/Error" + }, + "404": { + "$ref": "#/components/responses/Error" + }, + "422": { + "$ref": "#/components/responses/Error" + } + }, + "parameters": [ + { + "name": "mind", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "mind" + } + ] + } + }, + "/api/v1/minds/{mind}/events/{id}/attribution": { + "get": { + "operationId": "attribution", + "summary": "attribution", + "x-required-role": "observer", + "tags": [ + "attribution" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Error" + }, + "403": { + "$ref": "#/components/responses/Error" + }, + "404": { + "$ref": "#/components/responses/Error" + }, + "422": { + "$ref": "#/components/responses/Error" + } + }, + "parameters": [ + { + "name": "mind", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "mind" + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "identifier" + } + ] + } + }, + "/api/v1/minds/{mind}/agency/timeline": { + "get": { + "operationId": "agencyTimeline", + "summary": "agency timeline", + "x-required-role": "observer", + "tags": [ + "agency" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Error" + }, + "403": { + "$ref": "#/components/responses/Error" + }, + "404": { + "$ref": "#/components/responses/Error" + }, + "422": { + "$ref": "#/components/responses/Error" + } + }, + "parameters": [ + { + "name": "mind", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "mind" + } + ] + } + }, + "/api/v1/research/measures": { + "get": { + "operationId": "researchMeasures", + "summary": "research measures", + "x-required-role": "observer", + "tags": [ + "measures" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Error" + }, + "403": { + "$ref": "#/components/responses/Error" + }, + "404": { + "$ref": "#/components/responses/Error" + }, + "422": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/api/v1/minds/{mind}/research/agency": { + "get": { + "operationId": "researchAgency", + "summary": "research agency", + "x-required-role": "observer", + "tags": [ + "agency" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Error" + }, + "403": { + "$ref": "#/components/responses/Error" + }, + "404": { + "$ref": "#/components/responses/Error" + }, + "422": { + "$ref": "#/components/responses/Error" + } + }, + "parameters": [ + { + "name": "mind", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "mind" + } + ] + } + }, + "/api/v1/minds/{mind}/research/calibration": { + "get": { + "operationId": "researchCalibration", + "summary": "research calibration", + "x-required-role": "observer", + "tags": [ + "calibration" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Error" + }, + "403": { + "$ref": "#/components/responses/Error" + }, + "404": { + "$ref": "#/components/responses/Error" + }, + "422": { + "$ref": "#/components/responses/Error" + } + }, + "parameters": [ + { + "name": "mind", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "mind" + } + ] + } + }, + "/api/v1/minds/{mind}/research/measurements": { + "get": { + "operationId": "researchMeasurements", + "summary": "research measurements", + "x-required-role": "observer", + "tags": [ + "measurements" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Error" + }, + "403": { + "$ref": "#/components/responses/Error" + }, + "404": { + "$ref": "#/components/responses/Error" + }, + "422": { + "$ref": "#/components/responses/Error" + } + }, + "parameters": [ + { + "name": "mind", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "mind" + } + ] + } } } } \ No newline at end of file diff --git a/internal/api/openapi.json b/internal/api/openapi.json index 75ee0c9..55a31b4 100644 --- a/internal/api/openapi.json +++ b/internal/api/openapi.json @@ -8830,6 +8830,1079 @@ } ] } + }, + "/api/v1/minds/{mind}/wants": { + "get": { + "operationId": "wantsList", + "summary": "wants list", + "x-required-role": "observer", + "tags": [ + "wants" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Error" + }, + "403": { + "$ref": "#/components/responses/Error" + }, + "404": { + "$ref": "#/components/responses/Error" + }, + "422": { + "$ref": "#/components/responses/Error" + } + }, + "parameters": [ + { + "name": "mind", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "mind" + } + ] + } + }, + "/api/v1/minds/{mind}/wants/{id}": { + "get": { + "operationId": "wantGet", + "summary": "want get", + "x-required-role": "observer", + "tags": [ + "wants" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Error" + }, + "403": { + "$ref": "#/components/responses/Error" + }, + "404": { + "$ref": "#/components/responses/Error" + }, + "422": { + "$ref": "#/components/responses/Error" + } + }, + "parameters": [ + { + "name": "mind", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "mind" + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "identifier" + } + ] + } + }, + "/api/v1/minds/{mind}/want-conflicts": { + "get": { + "operationId": "wantConflictsList", + "summary": "want conflicts list", + "x-required-role": "observer", + "tags": [ + "want-conflicts" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Error" + }, + "403": { + "$ref": "#/components/responses/Error" + }, + "404": { + "$ref": "#/components/responses/Error" + }, + "422": { + "$ref": "#/components/responses/Error" + } + }, + "parameters": [ + { + "name": "mind", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "mind" + } + ] + } + }, + "/api/v1/minds/{mind}/commitments": { + "get": { + "operationId": "commitmentsList", + "summary": "commitments list", + "x-required-role": "observer", + "tags": [ + "commitments" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Error" + }, + "403": { + "$ref": "#/components/responses/Error" + }, + "404": { + "$ref": "#/components/responses/Error" + }, + "422": { + "$ref": "#/components/responses/Error" + } + }, + "parameters": [ + { + "name": "mind", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "mind" + } + ] + }, + "post": { + "operationId": "commitmentCreate", + "summary": "commitment create", + "x-required-role": "operator", + "tags": [ + "commitments" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Error" + }, + "403": { + "$ref": "#/components/responses/Error" + }, + "404": { + "$ref": "#/components/responses/Error" + }, + "422": { + "$ref": "#/components/responses/Error" + }, + "201": { + "description": "Created" + } + }, + "parameters": [ + { + "name": "mind", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "mind" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + } + }, + "/api/v1/minds/{mind}/commitments/{id}/close": { + "post": { + "operationId": "commitmentClose", + "summary": "commitment close", + "x-required-role": "operator", + "tags": [ + "close" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Error" + }, + "403": { + "$ref": "#/components/responses/Error" + }, + "404": { + "$ref": "#/components/responses/Error" + }, + "422": { + "$ref": "#/components/responses/Error" + }, + "201": { + "description": "Created" + } + }, + "parameters": [ + { + "name": "mind", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "mind" + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "identifier" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + } + }, + "/api/v1/minds/{mind}/intentions": { + "get": { + "operationId": "intentionsList", + "summary": "intentions list", + "x-required-role": "observer", + "tags": [ + "intentions" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Error" + }, + "403": { + "$ref": "#/components/responses/Error" + }, + "404": { + "$ref": "#/components/responses/Error" + }, + "422": { + "$ref": "#/components/responses/Error" + } + }, + "parameters": [ + { + "name": "mind", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "mind" + } + ] + } + }, + "/api/v1/minds/{mind}/intentions/{id}": { + "get": { + "operationId": "intentionGet", + "summary": "intention get", + "x-required-role": "observer", + "tags": [ + "intentions" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Error" + }, + "403": { + "$ref": "#/components/responses/Error" + }, + "404": { + "$ref": "#/components/responses/Error" + }, + "422": { + "$ref": "#/components/responses/Error" + } + }, + "parameters": [ + { + "name": "mind", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "mind" + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "identifier" + } + ] + } + }, + "/api/v1/minds/{mind}/intentions/{id}/abandon": { + "post": { + "operationId": "intentionAbandon", + "summary": "intention abandon", + "x-required-role": "operator", + "tags": [ + "abandon" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Error" + }, + "403": { + "$ref": "#/components/responses/Error" + }, + "404": { + "$ref": "#/components/responses/Error" + }, + "422": { + "$ref": "#/components/responses/Error" + }, + "201": { + "description": "Created" + } + }, + "parameters": [ + { + "name": "mind", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "mind" + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "identifier" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + } + }, + "/api/v1/minds/{mind}/decisions": { + "get": { + "operationId": "decisionsList", + "summary": "decisions list", + "x-required-role": "observer", + "tags": [ + "decisions" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Error" + }, + "403": { + "$ref": "#/components/responses/Error" + }, + "404": { + "$ref": "#/components/responses/Error" + }, + "422": { + "$ref": "#/components/responses/Error" + } + }, + "parameters": [ + { + "name": "mind", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "mind" + } + ] + } + }, + "/api/v1/minds/{mind}/plans": { + "get": { + "operationId": "plansList", + "summary": "plans list", + "x-required-role": "observer", + "tags": [ + "plans" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Error" + }, + "403": { + "$ref": "#/components/responses/Error" + }, + "404": { + "$ref": "#/components/responses/Error" + }, + "422": { + "$ref": "#/components/responses/Error" + } + }, + "parameters": [ + { + "name": "mind", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "mind" + } + ] + } + }, + "/api/v1/minds/{mind}/plans/{id}": { + "get": { + "operationId": "planGet", + "summary": "plan get", + "x-required-role": "observer", + "tags": [ + "plans" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Error" + }, + "403": { + "$ref": "#/components/responses/Error" + }, + "404": { + "$ref": "#/components/responses/Error" + }, + "422": { + "$ref": "#/components/responses/Error" + } + }, + "parameters": [ + { + "name": "mind", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "mind" + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "identifier" + } + ] + } + }, + "/api/v1/minds/{mind}/plans/{id}/cancel": { + "post": { + "operationId": "planCancel", + "summary": "plan cancel", + "x-required-role": "operator", + "tags": [ + "cancel" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Error" + }, + "403": { + "$ref": "#/components/responses/Error" + }, + "404": { + "$ref": "#/components/responses/Error" + }, + "422": { + "$ref": "#/components/responses/Error" + }, + "201": { + "description": "Created" + } + }, + "parameters": [ + { + "name": "mind", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "mind" + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "identifier" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + } + }, + "/api/v1/minds/{mind}/plans/{id}/steps/{step}/retry": { + "post": { + "operationId": "planStepRetry", + "summary": "plan step retry", + "x-required-role": "operator", + "tags": [ + "retry" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Error" + }, + "403": { + "$ref": "#/components/responses/Error" + }, + "404": { + "$ref": "#/components/responses/Error" + }, + "422": { + "$ref": "#/components/responses/Error" + }, + "201": { + "description": "Created" + } + }, + "parameters": [ + { + "name": "mind", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "mind" + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "identifier" + }, + { + "name": "step", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "step" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + } + }, + "/api/v1/minds/{mind}/why": { + "get": { + "operationId": "why", + "summary": "why", + "x-required-role": "observer", + "tags": [ + "why" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Error" + }, + "403": { + "$ref": "#/components/responses/Error" + }, + "404": { + "$ref": "#/components/responses/Error" + }, + "422": { + "$ref": "#/components/responses/Error" + } + }, + "parameters": [ + { + "name": "mind", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "mind" + } + ] + } + }, + "/api/v1/minds/{mind}/events/{id}/attribution": { + "get": { + "operationId": "attribution", + "summary": "attribution", + "x-required-role": "observer", + "tags": [ + "attribution" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Error" + }, + "403": { + "$ref": "#/components/responses/Error" + }, + "404": { + "$ref": "#/components/responses/Error" + }, + "422": { + "$ref": "#/components/responses/Error" + } + }, + "parameters": [ + { + "name": "mind", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "mind" + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "identifier" + } + ] + } + }, + "/api/v1/minds/{mind}/agency/timeline": { + "get": { + "operationId": "agencyTimeline", + "summary": "agency timeline", + "x-required-role": "observer", + "tags": [ + "agency" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Error" + }, + "403": { + "$ref": "#/components/responses/Error" + }, + "404": { + "$ref": "#/components/responses/Error" + }, + "422": { + "$ref": "#/components/responses/Error" + } + }, + "parameters": [ + { + "name": "mind", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "mind" + } + ] + } + }, + "/api/v1/research/measures": { + "get": { + "operationId": "researchMeasures", + "summary": "research measures", + "x-required-role": "observer", + "tags": [ + "measures" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Error" + }, + "403": { + "$ref": "#/components/responses/Error" + }, + "404": { + "$ref": "#/components/responses/Error" + }, + "422": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/api/v1/minds/{mind}/research/agency": { + "get": { + "operationId": "researchAgency", + "summary": "research agency", + "x-required-role": "observer", + "tags": [ + "agency" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Error" + }, + "403": { + "$ref": "#/components/responses/Error" + }, + "404": { + "$ref": "#/components/responses/Error" + }, + "422": { + "$ref": "#/components/responses/Error" + } + }, + "parameters": [ + { + "name": "mind", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "mind" + } + ] + } + }, + "/api/v1/minds/{mind}/research/calibration": { + "get": { + "operationId": "researchCalibration", + "summary": "research calibration", + "x-required-role": "observer", + "tags": [ + "calibration" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Error" + }, + "403": { + "$ref": "#/components/responses/Error" + }, + "404": { + "$ref": "#/components/responses/Error" + }, + "422": { + "$ref": "#/components/responses/Error" + } + }, + "parameters": [ + { + "name": "mind", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "mind" + } + ] + } + }, + "/api/v1/minds/{mind}/research/measurements": { + "get": { + "operationId": "researchMeasurements", + "summary": "research measurements", + "x-required-role": "observer", + "tags": [ + "measurements" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Error" + }, + "403": { + "$ref": "#/components/responses/Error" + }, + "404": { + "$ref": "#/components/responses/Error" + }, + "422": { + "$ref": "#/components/responses/Error" + } + }, + "parameters": [ + { + "name": "mind", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "mind" + } + ] + } } } } \ No newline at end of file diff --git a/internal/api/server.go b/internal/api/server.go index aa42f1b..41c8e93 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -77,6 +77,7 @@ func (s *Server) routes() { s.graphRoutes() s.capabilityRoutes() s.negotiationRoutes() + s.volitionRoutes() s.integrationRoutes() s.dreamRoutes() s.studyRoutes() diff --git a/internal/api/volition.go b/internal/api/volition.go new file mode 100644 index 0000000..5ab6128 --- /dev/null +++ b/internal/api/volition.go @@ -0,0 +1,639 @@ +package api + +import ( + "errors" + "net/http" + "strconv" + "strings" + "time" + + "github.com/andydixon/noema/internal/agency" + "github.com/andydixon/noema/internal/auth" + evstore "github.com/andydixon/noema/internal/events" + "github.com/andydixon/noema/internal/planning" + "github.com/andydixon/noema/internal/research" + "github.com/andydixon/noema/internal/storage" + "github.com/andydixon/noema/internal/volition" + "github.com/andydixon/noema/pkg/events" + "github.com/andydixon/noema/pkg/ids" +) + +// volitionRoutes: wants, conflicts, commitments, intentions, decisions, +// plans, forensic explanations, attribution and research measures. Reads +// are open to observers; operators may add or close commitments, abandon +// intentions, cancel plans and release blocked steps. Nothing here grants +// capability authority. +func (s *Server) volitionRoutes() { + m := s.mux + m.Handle("GET /api/v1/minds/{mind}/wants", require(auth.RoleObserver, s.wantsList)) + m.Handle("GET /api/v1/minds/{mind}/wants/{id}", require(auth.RoleObserver, s.wantGet)) + m.Handle("GET /api/v1/minds/{mind}/want-conflicts", require(auth.RoleObserver, s.wantConflictsList)) + m.Handle("GET /api/v1/minds/{mind}/commitments", require(auth.RoleObserver, s.commitmentsList)) + m.Handle("POST /api/v1/minds/{mind}/commitments", require(auth.RoleOperator, s.commitmentCreate)) + m.Handle("POST /api/v1/minds/{mind}/commitments/{id}/close", require(auth.RoleOperator, s.commitmentClose)) + m.Handle("GET /api/v1/minds/{mind}/intentions", require(auth.RoleObserver, s.intentionsList)) + m.Handle("GET /api/v1/minds/{mind}/intentions/{id}", require(auth.RoleObserver, s.intentionGet)) + m.Handle("POST /api/v1/minds/{mind}/intentions/{id}/abandon", require(auth.RoleOperator, s.intentionAbandon)) + m.Handle("GET /api/v1/minds/{mind}/decisions", require(auth.RoleObserver, s.decisionsList)) + m.Handle("GET /api/v1/minds/{mind}/plans", require(auth.RoleObserver, s.plansList)) + m.Handle("GET /api/v1/minds/{mind}/plans/{id}", require(auth.RoleObserver, s.planGet)) + m.Handle("POST /api/v1/minds/{mind}/plans/{id}/cancel", require(auth.RoleOperator, s.planCancel)) + m.Handle("POST /api/v1/minds/{mind}/plans/{id}/steps/{step}/retry", require(auth.RoleOperator, s.planStepRetry)) + m.Handle("GET /api/v1/minds/{mind}/why", require(auth.RoleObserver, s.why)) + m.Handle("GET /api/v1/minds/{mind}/events/{id}/attribution", require(auth.RoleObserver, s.attribution)) + m.Handle("GET /api/v1/minds/{mind}/agency/timeline", require(auth.RoleObserver, s.agencyTimeline)) + m.Handle("GET /api/v1/research/measures", require(auth.RoleObserver, s.researchMeasures)) + m.Handle("GET /api/v1/minds/{mind}/research/agency", require(auth.RoleObserver, s.researchAgency)) + m.Handle("GET /api/v1/minds/{mind}/research/calibration", require(auth.RoleObserver, s.researchCalibration)) + m.Handle("GET /api/v1/minds/{mind}/research/measurements", require(auth.RoleObserver, s.researchMeasurements)) +} + +func volErr(err error) error { + if errors.Is(err, volition.ErrInvalid) || errors.Is(err, volition.ErrClosed) || errors.Is(err, volition.ErrNotRevocable) || errors.Is(err, planning.ErrInvalidPlan) { + return ValidationError(err.Error()) + } + return err +} + +func pathID(r *http.Request, name string) (ids.ID, error) { + id, err := ids.Parse(r.PathValue(name)) + if err != nil { + return ids.Nil, ValidationError("bad " + name) + } + return id, nil +} + +func csv(r *http.Request, key string) []string { + var out []string + for _, v := range strings.Split(r.URL.Query().Get(key), ",") { + if v = strings.TrimSpace(v); v != "" { + out = append(out, v) + } + } + return out +} + +func sinceParam(r *http.Request, def time.Duration) time.Time { + if v := r.URL.Query().Get("since"); v != "" { + if t, err := time.Parse(time.RFC3339, v); err == nil { + return t + } + if d, err := time.ParseDuration(v); err == nil { + return time.Now().Add(-d) + } + } + return time.Now().Add(-def) +} + +func (s *Server) wantsList(w http.ResponseWriter, r *http.Request) { + m, ok := s.mind(w, r) + if !ok { + return + } + var st []volition.WantStatus + for _, v := range csv(r, "status") { + st = append(st, volition.WantStatus(v)) + } + list, err := s.deps.Volition.Wants(r.Context(), nil, m.ID, st, 500) + if err != nil { + fail(w, r, err) + return + } + type row struct { + volition.Want + Effective float64 `json:"effective_strength"` + } + out := []row{} + for _, x := range list { + out = append(out, row{x, x.Effective(time.Now())}) + } + writeJSON(w, http.StatusOK, page[row]{Items: out}) +} + +// WantDetail is a want with the decisions and conflicts that concern it. +type WantDetail struct { + Want volition.Want `json:"want"` + Effective float64 `json:"effective_strength"` + Decisions []volition.Decision `json:"decisions"` + Conflicts []volition.Conflict `json:"conflicts"` + Intentions []volition.Intention `json:"intentions"` +} + +// WantDetail assembles a want's record; shared with the UI. +func (s *Server) WantDetail(r *http.Request, mind, id ids.ID) (WantDetail, error) { + wt, err := s.deps.Volition.GetWant(r.Context(), nil, mind, id) + if err != nil { + return WantDetail{}, err + } + d := WantDetail{Want: wt, Effective: wt.Effective(time.Now()), Conflicts: []volition.Conflict{}, Intentions: []volition.Intention{}} + d.Decisions, _ = s.deps.Volition.Decisions(r.Context(), mind, volition.DecisionFilter{WantID: id, Limit: 100}) + if cs, err := s.deps.Volition.Conflicts(r.Context(), mind, "", 500); err == nil { + for _, c := range cs { + if c.WantA == id || c.WantB == id { + d.Conflicts = append(d.Conflicts, c) + } + } + } + if its, err := s.deps.Volition.Intentions(r.Context(), nil, mind, nil, 500); err == nil { + for _, it := range its { + for _, sw := range it.SourceWants { + if sw == id { + d.Intentions = append(d.Intentions, it) + } + } + } + } + if d.Decisions == nil { + d.Decisions = []volition.Decision{} + } + return d, nil +} + +func (s *Server) wantGet(w http.ResponseWriter, r *http.Request) { + m, ok := s.mind(w, r) + if !ok { + return + } + id, err := pathID(r, "id") + if err != nil { + fail(w, r, err) + return + } + d, err := s.WantDetail(r, m.ID, id) + if err != nil { + fail(w, r, err) + return + } + writeJSON(w, http.StatusOK, d) +} + +func (s *Server) wantConflictsList(w http.ResponseWriter, r *http.Request) { + m, ok := s.mind(w, r) + if !ok { + return + } + list, err := s.deps.Volition.Conflicts(r.Context(), m.ID, r.URL.Query().Get("status"), 500) + if err != nil { + fail(w, r, err) + return + } + if list == nil { + list = []volition.Conflict{} + } + writeJSON(w, http.StatusOK, page[volition.Conflict]{Items: list}) +} + +func (s *Server) commitmentsList(w http.ResponseWriter, r *http.Request) { + m, ok := s.mind(w, r) + if !ok { + return + } + list, err := s.deps.Volition.Commitments(r.Context(), nil, m.ID, csv(r, "status"), 500) + if err != nil { + fail(w, r, err) + return + } + if list == nil { + list = []volition.Commitment{} + } + writeJSON(w, http.StatusOK, page[volition.Commitment]{Items: list}) +} + +// CreateCommitment records an operator-given commitment; shared with the UI. +func (s *Server) CreateCommitment(r *http.Request, n volition.NewCommitment) (volition.Commitment, error) { + n.CreatedBy, n.Origin = actor(r), "operator" + rec := s.deps.Volition.Direct(s.deps.Events, "operator") + c, err := s.deps.Volition.CreateCommitment(r.Context(), rec, n) + if err != nil { + return c, volErr(err) + } + s.deps.Audit.Log(r.Context(), "commitment.create", "commitment", c.ID.String(), map[string]any{"mind": c.MindID, "kind": c.Kind, "polarity": c.Polarity, "unless": c.Unless, "description": c.Description}) + return c, nil +} + +func (s *Server) commitmentCreate(w http.ResponseWriter, r *http.Request) { + m, ok := s.mind(w, r) + if !ok { + return + } + var in struct { + Subject string `json:"subject"` + Description string `json:"description"` + Kind string `json:"kind"` + Polarity string `json:"polarity"` + Matcher volition.Matcher `json:"matcher"` + Unless string `json:"unless"` + Beneficiary string `json:"beneficiary"` + Strength float64 `json:"strength"` + Priority float64 `json:"priority"` + Conditions string `json:"conditions"` + DueAt *time.Time `json:"due_at"` + ExpiresAt *time.Time `json:"expires_at"` + Revocable *bool `json:"revocable"` + GoalID ids.ID `json:"goal_id"` + } + if err := decode(r, &in); err != nil { + fail(w, r, err) + return + } + c, err := s.CreateCommitment(r, volition.NewCommitment{MindID: m.ID, Subject: in.Subject, Description: in.Description, Kind: volition.CommitmentKind(in.Kind), Polarity: in.Polarity, Matcher: in.Matcher, Unless: in.Unless, + Beneficiary: in.Beneficiary, Strength: in.Strength, Priority: in.Priority, Conditions: in.Conditions, DueAt: in.DueAt, ExpiresAt: in.ExpiresAt, Revocable: in.Revocable, GoalID: in.GoalID}) + if err != nil { + fail(w, r, err) + return + } + writeJSON(w, http.StatusCreated, c) +} + +// CloseCommitment ends a commitment as an operator; shared with the UI. +func (s *Server) CloseCommitment(r *http.Request, mind, id ids.ID, status, reason string) (volition.Commitment, error) { + rec := s.deps.Volition.Direct(s.deps.Events, "operator") + c, err := s.deps.Volition.CloseCommitment(r.Context(), rec, mind, id, status, firstNonEmpty(reason, "closed by operator "+actor(r)), true) + if err != nil { + return c, volErr(err) + } + s.deps.Audit.Log(r.Context(), "commitment.close", "commitment", id.String(), map[string]any{"mind": mind, "status": status, "reason": reason}) + return c, nil +} + +func (s *Server) commitmentClose(w http.ResponseWriter, r *http.Request) { + m, ok := s.mind(w, r) + if !ok { + return + } + id, err := pathID(r, "id") + if err != nil { + fail(w, r, err) + return + } + var in struct { + Status string `json:"status"` + Reason string `json:"reason"` + } + if err := decode(r, &in); err != nil { + fail(w, r, err) + return + } + c, err := s.CloseCommitment(r, m.ID, id, in.Status, in.Reason) + if err != nil { + fail(w, r, err) + return + } + writeJSON(w, http.StatusOK, c) +} + +func (s *Server) intentionsList(w http.ResponseWriter, r *http.Request) { + m, ok := s.mind(w, r) + if !ok { + return + } + var st []volition.IntentionStatus + for _, v := range csv(r, "status") { + st = append(st, volition.IntentionStatus(v)) + } + list, err := s.deps.Volition.Intentions(r.Context(), nil, m.ID, st, 500) + if err != nil { + fail(w, r, err) + return + } + if list == nil { + list = []volition.Intention{} + } + writeJSON(w, http.StatusOK, page[volition.Intention]{Items: list}) +} + +// IntentionDetail is an intention with its plans, subordinate intentions, +// decisions and source wants. +type IntentionDetail struct { + Intention volition.Intention `json:"intention"` + Wants []volition.Want `json:"wants"` + Plans []planning.Plan `json:"plans"` + Subordinates []volition.Intention `json:"subordinates"` + Decisions []volition.Decision `json:"decisions"` +} + +// IntentionDetail assembles an intention's record; shared with the UI. +func (s *Server) IntentionDetail(r *http.Request, mind, id ids.ID) (IntentionDetail, error) { + ctx := r.Context() + it, err := s.deps.Volition.GetIntention(ctx, nil, mind, id) + if err != nil { + return IntentionDetail{}, err + } + d := IntentionDetail{Intention: it, Wants: []volition.Want{}, Plans: []planning.Plan{}, Subordinates: []volition.Intention{}} + for _, wid := range it.SourceWants { + if wt, err := s.deps.Volition.GetWant(ctx, nil, mind, wid); err == nil { + d.Wants = append(d.Wants, wt) + } + } + if ps, err := s.deps.Plans.ForIntention(ctx, nil, id); err == nil { + for _, p := range ps { + if full, err := s.deps.Plans.Get(ctx, nil, p.ID); err == nil { + d.Plans = append(d.Plans, full) + } + } + } + if all, err := s.deps.Volition.Intentions(ctx, nil, mind, nil, 500); err == nil { + for _, x := range all { + if x.ParentID == id { + d.Subordinates = append(d.Subordinates, x) + } + } + } + d.Decisions, _ = s.deps.Volition.Decisions(ctx, mind, volition.DecisionFilter{IntentionID: id, Limit: 200}) + if d.Decisions == nil { + d.Decisions = []volition.Decision{} + } + return d, nil +} + +func (s *Server) intentionGet(w http.ResponseWriter, r *http.Request) { + m, ok := s.mind(w, r) + if !ok { + return + } + id, err := pathID(r, "id") + if err != nil { + fail(w, r, err) + return + } + d, err := s.IntentionDetail(r, m.ID, id) + if err != nil { + fail(w, r, err) + return + } + writeJSON(w, http.StatusOK, d) +} + +// AbandonIntention is an operator correction: the intention is abandoned, +// its open plan cancelled and pending requests withdrawn. Shared with the UI. +func (s *Server) AbandonIntention(r *http.Request, mind, id ids.ID, reason string) (volition.Intention, error) { + ctx := r.Context() + reason = firstNonEmpty(reason, "no reason given") + it, err := s.deps.Volition.GetIntention(ctx, nil, mind, id) + if err != nil { + return it, err + } + if !it.PlanID.IsZero() { + if err := s.deps.Executor.Cancel(ctx, it.PlanID, "operator "+actor(r)+" abandoned the intention: "+reason); err != nil { + return it, err + } + } + rec := s.deps.Volition.Direct(s.deps.Events, "operator") + out, err := s.deps.Volition.Apply(ctx, rec, mind, id, volition.Reconsideration{Outcome: volition.Abandon, Reason: "operator " + actor(r) + ": " + reason}, volition.Trigger{Kind: volition.TriggerOperator, Detail: reason}) + if err != nil { + return out, volErr(err) + } + s.deps.Audit.Log(ctx, "intention.abandon", "intention", id.String(), map[string]any{"mind": mind, "reason": reason}) + return out, nil +} + +func (s *Server) intentionAbandon(w http.ResponseWriter, r *http.Request) { + m, ok := s.mind(w, r) + if !ok { + return + } + id, err := pathID(r, "id") + if err != nil { + fail(w, r, err) + return + } + var in struct { + Reason string `json:"reason"` + } + if r.ContentLength != 0 { + decode(r, &in) //nolint:errcheck + } + out, err := s.AbandonIntention(r, m.ID, id, in.Reason) + if err != nil { + fail(w, r, err) + return + } + writeJSON(w, http.StatusOK, out) +} + +func (s *Server) decisionsList(w http.ResponseWriter, r *http.Request) { + m, ok := s.mind(w, r) + if !ok { + return + } + q := r.URL.Query() + f := volition.DecisionFilter{Kind: q.Get("kind"), Limit: 300} + f.WantID, _ = ids.Parse(q.Get("want")) + f.IntentionID, _ = ids.Parse(q.Get("intention")) + f.CommitmentID, _ = ids.Parse(q.Get("commitment")) + list, err := s.deps.Volition.Decisions(r.Context(), m.ID, f) + if err != nil { + fail(w, r, err) + return + } + if list == nil { + list = []volition.Decision{} + } + writeJSON(w, http.StatusOK, page[volition.Decision]{Items: list}) +} + +func (s *Server) plansList(w http.ResponseWriter, r *http.Request) { + m, ok := s.mind(w, r) + if !ok { + return + } + list, err := s.deps.Plans.List(r.Context(), m.ID, csv(r, "status"), 200) + if err != nil { + fail(w, r, err) + return + } + if list == nil { + list = []planning.Plan{} + } + writeJSON(w, http.StatusOK, page[planning.Plan]{Items: list}) +} + +func (s *Server) planByPath(w http.ResponseWriter, r *http.Request) (planning.Plan, bool) { + m, ok := s.mind(w, r) + if !ok { + return planning.Plan{}, false + } + id, err := pathID(r, "id") + if err != nil { + fail(w, r, err) + return planning.Plan{}, false + } + p, err := s.deps.Plans.Get(r.Context(), nil, id) + if err != nil || p.MindID != m.ID { + fail(w, r, storage.ErrNotFound) + return planning.Plan{}, false + } + return p, true +} + +func (s *Server) planGet(w http.ResponseWriter, r *http.Request) { + p, ok := s.planByPath(w, r) + if !ok { + return + } + writeJSON(w, http.StatusOK, p) +} + +func (s *Server) planCancel(w http.ResponseWriter, r *http.Request) { + p, ok := s.planByPath(w, r) + if !ok { + return + } + var in struct { + Reason string `json:"reason"` + } + if r.ContentLength != 0 { + decode(r, &in) //nolint:errcheck + } + if err := s.deps.Executor.Cancel(r.Context(), p.ID, "cancelled by operator "+actor(r)+": "+firstNonEmpty(in.Reason, "no reason given")); err != nil { + fail(w, r, err) + return + } + s.deps.Audit.Log(r.Context(), "plan.cancel", "plan", p.ID.String(), map[string]any{"mind": p.MindID, "reason": in.Reason}) + p, _ = s.deps.Plans.Get(r.Context(), nil, p.ID) + writeJSON(w, http.StatusOK, p) +} + +// RetryStep releases a blocked plan step as an operator; shared with the UI. +func (s *Server) RetryStep(r *http.Request, p planning.Plan, stepKey string) error { + for _, st := range p.Steps { + if st.Key == stepKey || string(st.ID) == stepKey { + if err := s.deps.Executor.RetryStep(r.Context(), st.ID, actor(r)); err != nil { + return volErr(err) + } + s.deps.Audit.Log(r.Context(), "plan.step.retry", "plan_step", st.ID.String(), map[string]any{"plan": p.ID, "key": st.Key}) + return nil + } + } + return storage.ErrNotFound +} + +func (s *Server) planStepRetry(w http.ResponseWriter, r *http.Request) { + p, ok := s.planByPath(w, r) + if !ok { + return + } + if err := s.RetryStep(r, p, r.PathValue("step")); err != nil { + fail(w, r, err) + return + } + p, _ = s.deps.Plans.Get(r.Context(), nil, p.ID) + writeJSON(w, http.StatusOK, p) +} + +func (s *Server) why(w http.ResponseWriter, r *http.Request) { + m, ok := s.mind(w, r) + if !ok { + return + } + id, err := ids.Parse(r.URL.Query().Get("id")) + if err != nil { + fail(w, r, ValidationError("id is required")) + return + } + x, err := s.deps.Agency.Explain(r.Context(), m.ID, agency.Ref{Kind: firstNonEmpty(r.URL.Query().Get("kind"), "event"), ID: id}) + if err != nil { + fail(w, r, ValidationError(err.Error())) + return + } + writeJSON(w, http.StatusOK, map[string]any{"explanation": x, "points": x.Points()}) +} + +func (s *Server) attribution(w http.ResponseWriter, r *http.Request) { + m, ok := s.mind(w, r) + if !ok { + return + } + id, err := pathID(r, "id") + if err != nil { + fail(w, r, err) + return + } + a, err := s.deps.Agency.Attribute(r.Context(), m.ID, id) + if err != nil { + fail(w, r, ValidationError(err.Error())) + return + } + writeJSON(w, http.StatusOK, a) +} + +// AgencyTimelineTypes are the event types of the agency timeline: wanted, +// intended, committed, planned, requested, approved, acted, observed, +// reconsidered. +var AgencyTimelineTypes = []events.Type{events.WantCreated, events.WantSuppressed, events.WantConflictDetected, events.IntentionFormed, events.IntentionBlocked, events.IntentionSuspended, + events.IntentionResumed, events.IntentionReconsidered, events.IntentionAbandoned, events.IntentionFulfilled, events.IntentionSuperseded, events.CommitmentCreated, events.CommitmentDue, + events.CommitmentConflict, events.CommitmentFulfilled, events.CommitmentBroken, events.CommitmentRevoked, events.PlanCreated, events.PlanReplanned, events.PlanStepStarted, events.PlanStepCompleted, + events.PlanStepFailed, events.PlanCompleted, events.PlanAbandoned, events.ToolRequested, events.ToolAuthorised, events.ToolDenied, events.ToolExecuted, events.ToolFailed, + events.PredictionCreated, events.PredictionResolved, events.PredictionError} + +// AgencyTimeline lists agency events, newest first; shared with the UI. +func (s *Server) AgencyTimeline(r *http.Request, mind ids.ID, limit int, correlation ids.ID) ([]events.Event, error) { + if limit <= 0 || limit > 500 { + limit = 200 + } + return s.deps.Events.Query(r.Context(), evstore.Filter{MindID: mind, Types: AgencyTimelineTypes, CorrelationID: correlation, Limit: limit}) +} + +func (s *Server) agencyTimeline(w http.ResponseWriter, r *http.Request) { + m, ok := s.mind(w, r) + if !ok { + return + } + limit, _ := strconv.Atoi(r.URL.Query().Get("limit")) + corr, _ := ids.Parse(r.URL.Query().Get("intention")) + evs, err := s.AgencyTimeline(r, m.ID, limit, corr) + if err != nil { + fail(w, r, err) + return + } + if evs == nil { + evs = []events.Event{} + } + writeJSON(w, http.StatusOK, page[events.Event]{Items: evs}) +} + +func (s *Server) researchMeasures(w http.ResponseWriter, r *http.Request) { + writeJSON(w, http.StatusOK, map[string]any{"disclaimer": research.Disclaimer, "items": research.Measures}) +} + +func (s *Server) researchAgency(w http.ResponseWriter, r *http.Request) { + m, ok := s.mind(w, r) + if !ok { + return + } + out, err := s.deps.Research.Agency(r.Context(), m.ID, sinceParam(r, 30*24*time.Hour)) + if err != nil { + fail(w, r, err) + return + } + writeJSON(w, http.StatusOK, map[string]any{"disclaimer": research.Disclaimer, "metrics": out}) +} + +func (s *Server) researchCalibration(w http.ResponseWriter, r *http.Request) { + m, ok := s.mind(w, r) + if !ok { + return + } + out, err := s.deps.Research.MetacognitiveCalibration(r.Context(), m.ID, sinceParam(r, 90*24*time.Hour)) + if err != nil { + fail(w, r, err) + return + } + writeJSON(w, http.StatusOK, map[string]any{"disclaimer": research.Disclaimer, "calibration": out}) +} + +func (s *Server) researchMeasurements(w http.ResponseWriter, r *http.Request) { + m, ok := s.mind(w, r) + if !ok { + return + } + list, err := s.deps.Research.List(r.Context(), m.ID, r.URL.Query().Get("measure"), sinceParam(r, 30*24*time.Hour), 1000) + if err != nil { + fail(w, r, err) + return + } + if list == nil { + list = []research.Measurement{} + } + writeJSON(w, http.StatusOK, page[research.Measurement]{Items: list}) +} diff --git a/tests/agency/api_test.go b/tests/agency/api_test.go new file mode 100644 index 0000000..c0e76a2 --- /dev/null +++ b/tests/agency/api_test.go @@ -0,0 +1,98 @@ +package agency + +import ( + "context" + "encoding/json" + "strings" + "testing" + "time" + + "github.com/andydixon/noema/internal/volition" + "github.com/andydixon/noema/tests/harness" +) + +func TestVolitionAPI(t *testing.T) { + h := harness.StartWith(t, harness.Options{HostRunner: newHost()}) + ctx := context.Background() + m := h.Mind("api") + body := map[string]any{"description": "no production writes without approval", "kind": "operator_instruction", "polarity": "refrain", "unless": "approval", "matcher": map[string]any{"access": "write", "targets": []string{"atlas-*"}}} + if code, _ := h.Do(h.Obs, "POST", "/api/v1/minds/api/commitments", body); code != 403 { + t.Fatalf("observer created a commitment: %d", code) + } + if code, out := h.Do(h.Admin, "POST", "/api/v1/minds/api/commitments", body); code != 201 { + t.Fatalf("commitment: %d %s", code, out) + } + if code, out := h.Do(h.Admin, "POST", "/api/v1/minds/api/commitments", map[string]any{"description": "x", "kind": "vow", "polarity": "do"}); code != 400 { + t.Fatalf("invalid commitment: %d %s", code, out) + } + h.Do(h.Admin, "POST", "/api/v1/minds/api/ask", map[string]any{"text": "Please restart atlas-worker."}) + var it volition.Intention + waitFor(t, h, m, 25*time.Second, "an intention waiting for approval", func() bool { + open, _ := h.Deps.Volition.Open(ctx, nil, m.ID) + for _, x := range open { + if x.ParentID.IsZero() && x.Status == volition.IntentionBlocked { + it = x + return true + } + } + return false + }) + get := func(path string, v any) { + t.Helper() + code, out := h.Do(h.Obs, "GET", path, nil) + if code != 200 { + t.Fatalf("GET %s: %d %s", path, code, out) + } + if err := json.Unmarshal(out, v); err != nil { + t.Fatalf("GET %s: %v", path, err) + } + } + var wants struct{ Items []map[string]any } + get("/api/v1/minds/api/wants?status=active", &wants) + if len(wants.Items) == 0 || wants.Items[0]["effective_strength"] == nil { + t.Fatalf("wants: %+v", wants) + } + var detail struct { + Intention volition.Intention + Plans []map[string]any + Subordinates []volition.Intention + Decisions []volition.Decision + } + get("/api/v1/minds/api/intentions/"+it.ID.String(), &detail) + if len(detail.Plans) != 1 || len(detail.Subordinates) != 1 || len(detail.Decisions) == 0 { + t.Fatalf("intention detail: %+v", detail) + } + var why struct{ Points []string } + get("/api/v1/minds/api/why?kind=intention&id="+it.ID.String(), &why) + if !strings.Contains(strings.Join(why.Points, "\n"), "Commitment: no production writes without approval") { + t.Fatalf("why: %v", why.Points) + } + var metrics struct{ Metrics map[string]any } + get("/api/v1/minds/api/research/agency", &metrics) + if metrics.Metrics["intentions_formed"].(float64) < 1 { + t.Fatalf("agency metrics: %+v", metrics) + } + var measures struct { + Disclaimer string + Items []map[string]any + } + get("/api/v1/research/measures", &measures) + if !strings.Contains(measures.Disclaimer, "does not claim") || len(measures.Items) < 10 { + t.Fatalf("measures: %+v", measures) + } + var timeline struct{ Items []map[string]any } + get("/api/v1/minds/api/agency/timeline", &timeline) + if len(timeline.Items) < 4 { + t.Fatalf("timeline: %d", len(timeline.Items)) + } + if code, _ := h.Do(h.Obs, "POST", "/api/v1/minds/api/intentions/"+it.ID.String()+"/abandon", map[string]any{"reason": "no"}); code != 403 { + t.Fatalf("observer abandoned an intention: %d", code) + } + if code, out := h.Do(h.Admin, "POST", "/api/v1/minds/api/intentions/"+it.ID.String()+"/abandon", map[string]any{"reason": "change freeze"}); code != 200 { + t.Fatalf("abandon: %d %s", code, out) + } + pending, _ := h.Deps.Capabilities.Approvals(ctx, m.ID, "pending", 10) + if len(pending) != 0 { + t.Fatalf("abandoned intention left %d approval request(s) open", len(pending)) + } +}