diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index b5ad3a815..d5a05a9a6 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -4233,6 +4233,238 @@ ], "type": "object" }, + "ThreadGoalPlanInsertNodeParams": { + "properties": { + "dependsOn": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "key": { + "type": "string" + }, + "objective": { + "type": "string" + }, + "planId": { + "type": "string" + }, + "position": { + "$ref": "#/$defs/ThreadGoalPlanNodeInsertPosition" + }, + "priority": { + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "referenceNodeId": { + "type": [ + "string", + "null" + ] + }, + "threadId": { + "type": "string" + }, + "title": { + "type": [ + "string", + "null" + ] + }, + "tokenBudget": { + "format": "int64", + "type": [ + "integer", + "null" + ] + } + }, + "required": [ + "threadId", + "planId", + "position", + "key", + "objective" + ], + "type": "object" + }, + "ThreadGoalPlanNodeCompletionStatus": { + "enum": [ + "complete", + "pending" + ], + "type": "string" + }, + "ThreadGoalPlanNodeInsertPosition": { + "enum": [ + "before", + "after", + "end" + ], + "type": "string" + }, + "ThreadGoalPlanNodeTitleUpdate": { + "oneOf": [ + { + "properties": { + "type": { + "const": "clear", + "title": "ClearThreadGoalPlanNodeTitleUpdateType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "ClearThreadGoalPlanNodeTitleUpdate", + "type": "object" + }, + { + "properties": { + "type": { + "const": "set", + "title": "SetThreadGoalPlanNodeTitleUpdateType", + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": [ + "type", + "value" + ], + "title": "SetThreadGoalPlanNodeTitleUpdate", + "type": "object" + } + ] + }, + "ThreadGoalPlanNodeTokenBudgetUpdate": { + "oneOf": [ + { + "properties": { + "type": { + "const": "clear", + "title": "ClearThreadGoalPlanNodeTokenBudgetUpdateType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "ClearThreadGoalPlanNodeTokenBudgetUpdate", + "type": "object" + }, + { + "properties": { + "type": { + "const": "set", + "title": "SetThreadGoalPlanNodeTokenBudgetUpdateType", + "type": "string" + }, + "value": { + "format": "int64", + "type": "integer" + } + }, + "required": [ + "type", + "value" + ], + "title": "SetThreadGoalPlanNodeTokenBudgetUpdate", + "type": "object" + } + ] + }, + "ThreadGoalPlanSetNodeStatusParams": { + "properties": { + "nodeId": { + "type": "string" + }, + "status": { + "$ref": "#/$defs/ThreadGoalPlanNodeCompletionStatus" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId", + "nodeId", + "status" + ], + "type": "object" + }, + "ThreadGoalPlanUpdateNodeParams": { + "properties": { + "dependsOn": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "key": { + "type": [ + "string", + "null" + ] + }, + "nodeId": { + "type": "string" + }, + "objective": { + "type": [ + "string", + "null" + ] + }, + "priority": { + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "threadId": { + "type": "string" + }, + "title": { + "anyOf": [ + { + "$ref": "#/$defs/ThreadGoalPlanNodeTitleUpdate" + }, + { + "type": "null" + } + ] + }, + "tokenBudget": { + "anyOf": [ + { + "$ref": "#/$defs/ThreadGoalPlanNodeTokenBudgetUpdate" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "threadId", + "nodeId" + ], + "type": "object" + }, "ThreadGoalSetParams": { "properties": { "objective": { @@ -6768,6 +7000,72 @@ "title": "Thread/goalPlan/addGoalRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/$defs/RequestId" + }, + "method": { + "const": "thread/goalPlan/updateNode", + "title": "Thread/goalPlan/updateNodeRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/$defs/ThreadGoalPlanUpdateNodeParams" + } + }, + "required": [ + "method", + "id", + "params" + ], + "title": "Thread/goalPlan/updateNodeRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/$defs/RequestId" + }, + "method": { + "const": "thread/goalPlan/insertNode", + "title": "Thread/goalPlan/insertNodeRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/$defs/ThreadGoalPlanInsertNodeParams" + } + }, + "required": [ + "method", + "id", + "params" + ], + "title": "Thread/goalPlan/insertNodeRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/$defs/RequestId" + }, + "method": { + "const": "thread/goalPlan/setNodeStatus", + "title": "Thread/goalPlan/setNodeStatusRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/$defs/ThreadGoalPlanSetNodeStatusParams" + } + }, + "required": [ + "method", + "id", + "params" + ], + "title": "Thread/goalPlan/setNodeStatusRequest", + "type": "object" + }, { "properties": { "id": { diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 5663be01d..139acff98 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -464,6 +464,72 @@ "title": "Thread/goalPlan/addGoalRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/v2/RequestId" + }, + "method": { + "const": "thread/goalPlan/updateNode", + "title": "Thread/goalPlan/updateNodeRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/ThreadGoalPlanUpdateNodeParams" + } + }, + "required": [ + "method", + "id", + "params" + ], + "title": "Thread/goalPlan/updateNodeRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/v2/RequestId" + }, + "method": { + "const": "thread/goalPlan/insertNode", + "title": "Thread/goalPlan/insertNodeRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/ThreadGoalPlanInsertNodeParams" + } + }, + "required": [ + "method", + "id", + "params" + ], + "title": "Thread/goalPlan/insertNodeRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/v2/RequestId" + }, + "method": { + "const": "thread/goalPlan/setNodeStatus", + "title": "Thread/goalPlan/setNodeStatusRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/ThreadGoalPlanSetNodeStatusParams" + } + }, + "required": [ + "method", + "id", + "params" + ], + "title": "Thread/goalPlan/setNodeStatusRequest", + "type": "object" + }, { "properties": { "id": { @@ -19542,6 +19608,87 @@ ], "type": "string" }, + "ThreadGoalPlanInsertNodeParams": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "dependsOn": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "key": { + "type": "string" + }, + "objective": { + "type": "string" + }, + "planId": { + "type": "string" + }, + "position": { + "$ref": "#/definitions/v2/ThreadGoalPlanNodeInsertPosition" + }, + "priority": { + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "referenceNodeId": { + "type": [ + "string", + "null" + ] + }, + "threadId": { + "type": "string" + }, + "title": { + "type": [ + "string", + "null" + ] + }, + "tokenBudget": { + "format": "int64", + "type": [ + "integer", + "null" + ] + } + }, + "required": [ + "threadId", + "planId", + "position", + "key", + "objective" + ], + "title": "ThreadGoalPlanInsertNodeParams", + "type": "object" + }, + "ThreadGoalPlanInsertNodeResponse": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "insertedNode": { + "$ref": "#/definitions/v2/ThreadGoalPlanNode" + }, + "plan": { + "$ref": "#/definitions/v2/ThreadGoalPlan" + } + }, + "required": [ + "plan", + "insertedNode" + ], + "title": "ThreadGoalPlanInsertNodeResponse", + "type": "object" + }, "ThreadGoalPlanNode": { "properties": { "assignedThreadId": { @@ -19648,6 +19795,21 @@ ], "type": "object" }, + "ThreadGoalPlanNodeCompletionStatus": { + "enum": [ + "complete", + "pending" + ], + "type": "string" + }, + "ThreadGoalPlanNodeInsertPosition": { + "enum": [ + "before", + "after", + "end" + ], + "type": "string" + }, "ThreadGoalPlanNodeStatus": { "enum": [ "pending", @@ -19662,6 +19824,141 @@ ], "type": "string" }, + "ThreadGoalPlanNodeTitleUpdate": { + "oneOf": [ + { + "properties": { + "type": { + "const": "clear", + "title": "ClearThreadGoalPlanNodeTitleUpdateType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "ClearThreadGoalPlanNodeTitleUpdate", + "type": "object" + }, + { + "properties": { + "type": { + "const": "set", + "title": "SetThreadGoalPlanNodeTitleUpdateType", + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": [ + "type", + "value" + ], + "title": "SetThreadGoalPlanNodeTitleUpdate", + "type": "object" + } + ] + }, + "ThreadGoalPlanNodeTokenBudgetUpdate": { + "oneOf": [ + { + "properties": { + "type": { + "const": "clear", + "title": "ClearThreadGoalPlanNodeTokenBudgetUpdateType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "ClearThreadGoalPlanNodeTokenBudgetUpdate", + "type": "object" + }, + { + "properties": { + "type": { + "const": "set", + "title": "SetThreadGoalPlanNodeTokenBudgetUpdateType", + "type": "string" + }, + "value": { + "format": "int64", + "type": "integer" + } + }, + "required": [ + "type", + "value" + ], + "title": "SetThreadGoalPlanNodeTokenBudgetUpdate", + "type": "object" + } + ] + }, + "ThreadGoalPlanSetNodeStatusParams": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "nodeId": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/v2/ThreadGoalPlanNodeCompletionStatus" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId", + "nodeId", + "status" + ], + "title": "ThreadGoalPlanSetNodeStatusParams", + "type": "object" + }, + "ThreadGoalPlanSetNodeStatusResponse": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "activatedGoal": { + "anyOf": [ + { + "$ref": "#/definitions/v2/ThreadGoal" + }, + { + "type": "null" + } + ] + }, + "clearedGoal": { + "type": "boolean" + }, + "goal": { + "anyOf": [ + { + "$ref": "#/definitions/v2/ThreadGoal" + }, + { + "type": "null" + } + ] + }, + "node": { + "$ref": "#/definitions/v2/ThreadGoalPlanNode" + }, + "plan": { + "$ref": "#/definitions/v2/ThreadGoalPlan" + } + }, + "required": [ + "clearedGoal", + "plan", + "node" + ], + "title": "ThreadGoalPlanSetNodeStatusResponse", + "type": "object" + }, "ThreadGoalPlanStatus": { "enum": [ "active", @@ -19673,6 +19970,98 @@ ], "type": "string" }, + "ThreadGoalPlanUpdateNodeParams": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "dependsOn": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "key": { + "type": [ + "string", + "null" + ] + }, + "nodeId": { + "type": "string" + }, + "objective": { + "type": [ + "string", + "null" + ] + }, + "priority": { + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "threadId": { + "type": "string" + }, + "title": { + "anyOf": [ + { + "$ref": "#/definitions/v2/ThreadGoalPlanNodeTitleUpdate" + }, + { + "type": "null" + } + ] + }, + "tokenBudget": { + "anyOf": [ + { + "$ref": "#/definitions/v2/ThreadGoalPlanNodeTokenBudgetUpdate" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "threadId", + "nodeId" + ], + "title": "ThreadGoalPlanUpdateNodeParams", + "type": "object" + }, + "ThreadGoalPlanUpdateNodeResponse": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "goal": { + "anyOf": [ + { + "$ref": "#/definitions/v2/ThreadGoal" + }, + { + "type": "null" + } + ] + }, + "node": { + "$ref": "#/definitions/v2/ThreadGoalPlanNode" + }, + "plan": { + "$ref": "#/definitions/v2/ThreadGoalPlan" + } + }, + "required": [ + "plan", + "node" + ], + "title": "ThreadGoalPlanUpdateNodeResponse", + "type": "object" + }, "ThreadGoalPlanUpdatedNotification": { "$schema": "https://json-schema.org/draft/2020-12/schema", "properties": { diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index 1c054ce13..4e7e50774 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -2401,6 +2401,72 @@ "title": "Thread/goalPlan/addGoalRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "const": "thread/goalPlan/updateNode", + "title": "Thread/goalPlan/updateNodeRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadGoalPlanUpdateNodeParams" + } + }, + "required": [ + "method", + "id", + "params" + ], + "title": "Thread/goalPlan/updateNodeRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "const": "thread/goalPlan/insertNode", + "title": "Thread/goalPlan/insertNodeRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadGoalPlanInsertNodeParams" + } + }, + "required": [ + "method", + "id", + "params" + ], + "title": "Thread/goalPlan/insertNodeRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "const": "thread/goalPlan/setNodeStatus", + "title": "Thread/goalPlan/setNodeStatusRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadGoalPlanSetNodeStatusParams" + } + }, + "required": [ + "method", + "id", + "params" + ], + "title": "Thread/goalPlan/setNodeStatusRequest", + "type": "object" + }, { "properties": { "id": { @@ -17603,6 +17669,87 @@ ], "type": "string" }, + "ThreadGoalPlanInsertNodeParams": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "dependsOn": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "key": { + "type": "string" + }, + "objective": { + "type": "string" + }, + "planId": { + "type": "string" + }, + "position": { + "$ref": "#/definitions/ThreadGoalPlanNodeInsertPosition" + }, + "priority": { + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "referenceNodeId": { + "type": [ + "string", + "null" + ] + }, + "threadId": { + "type": "string" + }, + "title": { + "type": [ + "string", + "null" + ] + }, + "tokenBudget": { + "format": "int64", + "type": [ + "integer", + "null" + ] + } + }, + "required": [ + "threadId", + "planId", + "position", + "key", + "objective" + ], + "title": "ThreadGoalPlanInsertNodeParams", + "type": "object" + }, + "ThreadGoalPlanInsertNodeResponse": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "insertedNode": { + "$ref": "#/definitions/ThreadGoalPlanNode" + }, + "plan": { + "$ref": "#/definitions/ThreadGoalPlan" + } + }, + "required": [ + "plan", + "insertedNode" + ], + "title": "ThreadGoalPlanInsertNodeResponse", + "type": "object" + }, "ThreadGoalPlanNode": { "properties": { "assignedThreadId": { @@ -17709,6 +17856,21 @@ ], "type": "object" }, + "ThreadGoalPlanNodeCompletionStatus": { + "enum": [ + "complete", + "pending" + ], + "type": "string" + }, + "ThreadGoalPlanNodeInsertPosition": { + "enum": [ + "before", + "after", + "end" + ], + "type": "string" + }, "ThreadGoalPlanNodeStatus": { "enum": [ "pending", @@ -17723,6 +17885,141 @@ ], "type": "string" }, + "ThreadGoalPlanNodeTitleUpdate": { + "oneOf": [ + { + "properties": { + "type": { + "const": "clear", + "title": "ClearThreadGoalPlanNodeTitleUpdateType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "ClearThreadGoalPlanNodeTitleUpdate", + "type": "object" + }, + { + "properties": { + "type": { + "const": "set", + "title": "SetThreadGoalPlanNodeTitleUpdateType", + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": [ + "type", + "value" + ], + "title": "SetThreadGoalPlanNodeTitleUpdate", + "type": "object" + } + ] + }, + "ThreadGoalPlanNodeTokenBudgetUpdate": { + "oneOf": [ + { + "properties": { + "type": { + "const": "clear", + "title": "ClearThreadGoalPlanNodeTokenBudgetUpdateType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "ClearThreadGoalPlanNodeTokenBudgetUpdate", + "type": "object" + }, + { + "properties": { + "type": { + "const": "set", + "title": "SetThreadGoalPlanNodeTokenBudgetUpdateType", + "type": "string" + }, + "value": { + "format": "int64", + "type": "integer" + } + }, + "required": [ + "type", + "value" + ], + "title": "SetThreadGoalPlanNodeTokenBudgetUpdate", + "type": "object" + } + ] + }, + "ThreadGoalPlanSetNodeStatusParams": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "nodeId": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/ThreadGoalPlanNodeCompletionStatus" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId", + "nodeId", + "status" + ], + "title": "ThreadGoalPlanSetNodeStatusParams", + "type": "object" + }, + "ThreadGoalPlanSetNodeStatusResponse": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "activatedGoal": { + "anyOf": [ + { + "$ref": "#/definitions/ThreadGoal" + }, + { + "type": "null" + } + ] + }, + "clearedGoal": { + "type": "boolean" + }, + "goal": { + "anyOf": [ + { + "$ref": "#/definitions/ThreadGoal" + }, + { + "type": "null" + } + ] + }, + "node": { + "$ref": "#/definitions/ThreadGoalPlanNode" + }, + "plan": { + "$ref": "#/definitions/ThreadGoalPlan" + } + }, + "required": [ + "clearedGoal", + "plan", + "node" + ], + "title": "ThreadGoalPlanSetNodeStatusResponse", + "type": "object" + }, "ThreadGoalPlanStatus": { "enum": [ "active", @@ -17734,6 +18031,98 @@ ], "type": "string" }, + "ThreadGoalPlanUpdateNodeParams": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "dependsOn": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "key": { + "type": [ + "string", + "null" + ] + }, + "nodeId": { + "type": "string" + }, + "objective": { + "type": [ + "string", + "null" + ] + }, + "priority": { + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "threadId": { + "type": "string" + }, + "title": { + "anyOf": [ + { + "$ref": "#/definitions/ThreadGoalPlanNodeTitleUpdate" + }, + { + "type": "null" + } + ] + }, + "tokenBudget": { + "anyOf": [ + { + "$ref": "#/definitions/ThreadGoalPlanNodeTokenBudgetUpdate" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "threadId", + "nodeId" + ], + "title": "ThreadGoalPlanUpdateNodeParams", + "type": "object" + }, + "ThreadGoalPlanUpdateNodeResponse": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "goal": { + "anyOf": [ + { + "$ref": "#/definitions/ThreadGoal" + }, + { + "type": "null" + } + ] + }, + "node": { + "$ref": "#/definitions/ThreadGoalPlanNode" + }, + "plan": { + "$ref": "#/definitions/ThreadGoalPlan" + } + }, + "required": [ + "plan", + "node" + ], + "title": "ThreadGoalPlanUpdateNodeResponse", + "type": "object" + }, "ThreadGoalPlanUpdatedNotification": { "$schema": "https://json-schema.org/draft/2020-12/schema", "properties": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadGoalPlanInsertNodeParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadGoalPlanInsertNodeParams.json new file mode 100644 index 000000000..6e997acc5 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadGoalPlanInsertNodeParams.json @@ -0,0 +1,74 @@ +{ + "$defs": { + "ThreadGoalPlanNodeInsertPosition": { + "enum": [ + "before", + "after", + "end" + ], + "type": "string" + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "dependsOn": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "key": { + "type": "string" + }, + "objective": { + "type": "string" + }, + "planId": { + "type": "string" + }, + "position": { + "$ref": "#/$defs/ThreadGoalPlanNodeInsertPosition" + }, + "priority": { + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "referenceNodeId": { + "type": [ + "string", + "null" + ] + }, + "threadId": { + "type": "string" + }, + "title": { + "type": [ + "string", + "null" + ] + }, + "tokenBudget": { + "format": "int64", + "type": [ + "integer", + "null" + ] + } + }, + "required": [ + "threadId", + "planId", + "position", + "key", + "objective" + ], + "title": "ThreadGoalPlanInsertNodeParams", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadGoalPlanInsertNodeResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadGoalPlanInsertNodeResponse.json new file mode 100644 index 000000000..13228afc6 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadGoalPlanInsertNodeResponse.json @@ -0,0 +1,277 @@ +{ + "$defs": { + "ThreadGoalPlan": { + "properties": { + "activeNodeCount": { + "format": "int64", + "type": "integer" + }, + "autoExecute": { + "$ref": "#/$defs/ThreadGoalPlanAutoExecute" + }, + "blockedNodeCount": { + "format": "int64", + "type": "integer" + }, + "budgetLimitedNodeCount": { + "format": "int64", + "type": "integer" + }, + "cancelledNodeCount": { + "format": "int64", + "type": "integer" + }, + "completedNodeCount": { + "format": "int64", + "type": "integer" + }, + "createdAt": { + "format": "int64", + "type": "integer" + }, + "deferredNodeCount": { + "format": "int64", + "type": "integer" + }, + "maxTokens": { + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "nodeCount": { + "format": "int64", + "type": "integer" + }, + "nodes": { + "items": { + "$ref": "#/$defs/ThreadGoalPlanNode" + }, + "type": "array" + }, + "pausedNodeCount": { + "format": "int64", + "type": "integer" + }, + "pendingNodeCount": { + "format": "int64", + "type": "integer" + }, + "planId": { + "type": "string" + }, + "readyNodeCount": { + "format": "int64", + "type": "integer" + }, + "remainingTokens": { + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "status": { + "$ref": "#/$defs/ThreadGoalPlanStatus" + }, + "threadId": { + "type": "string" + }, + "totalTimeUsedSeconds": { + "format": "int64", + "type": "integer" + }, + "totalTokensUsed": { + "format": "int64", + "type": "integer" + }, + "updatedAt": { + "format": "int64", + "type": "integer" + }, + "usageLimitedNodeCount": { + "format": "int64", + "type": "integer" + } + }, + "required": [ + "planId", + "threadId", + "status", + "autoExecute", + "totalTokensUsed", + "totalTimeUsedSeconds", + "nodeCount", + "completedNodeCount", + "readyNodeCount", + "activeNodeCount", + "pendingNodeCount", + "pausedNodeCount", + "blockedNodeCount", + "usageLimitedNodeCount", + "budgetLimitedNodeCount", + "deferredNodeCount", + "cancelledNodeCount", + "createdAt", + "updatedAt", + "nodes" + ], + "type": "object" + }, + "ThreadGoalPlanAutoExecute": { + "enum": [ + "off", + "readyOnly", + "aiDirected" + ], + "type": "string" + }, + "ThreadGoalPlanNode": { + "properties": { + "assignedThreadId": { + "type": "string" + }, + "createdAt": { + "format": "int64", + "type": "integer" + }, + "dependsOn": { + "items": { + "type": "string" + }, + "type": "array" + }, + "key": { + "type": "string" + }, + "nestingDepth": { + "format": "int64", + "type": "integer" + }, + "nodeId": { + "type": "string" + }, + "objective": { + "type": "string" + }, + "parentNodeId": { + "type": [ + "string", + "null" + ] + }, + "planId": { + "type": "string" + }, + "priority": { + "format": "int64", + "type": "integer" + }, + "projectedGoalId": { + "type": [ + "string", + "null" + ] + }, + "ready": { + "type": "boolean" + }, + "sequence": { + "format": "int64", + "type": "integer" + }, + "status": { + "$ref": "#/$defs/ThreadGoalPlanNodeStatus" + }, + "threadId": { + "type": "string" + }, + "timeUsedSeconds": { + "format": "int64", + "type": "integer" + }, + "title": { + "type": [ + "string", + "null" + ] + }, + "tokenBudget": { + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "tokensUsed": { + "format": "int64", + "type": "integer" + }, + "updatedAt": { + "format": "int64", + "type": "integer" + } + }, + "required": [ + "nodeId", + "planId", + "threadId", + "assignedThreadId", + "nestingDepth", + "key", + "sequence", + "priority", + "objective", + "status", + "ready", + "tokensUsed", + "timeUsedSeconds", + "dependsOn", + "createdAt", + "updatedAt" + ], + "type": "object" + }, + "ThreadGoalPlanNodeStatus": { + "enum": [ + "pending", + "active", + "paused", + "blocked", + "usageLimited", + "budgetLimited", + "deferred", + "complete", + "cancelled" + ], + "type": "string" + }, + "ThreadGoalPlanStatus": { + "enum": [ + "active", + "paused", + "blocked", + "budgetLimited", + "complete", + "cancelled" + ], + "type": "string" + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "insertedNode": { + "$ref": "#/$defs/ThreadGoalPlanNode" + }, + "plan": { + "$ref": "#/$defs/ThreadGoalPlan" + } + }, + "required": [ + "plan", + "insertedNode" + ], + "title": "ThreadGoalPlanInsertNodeResponse", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadGoalPlanSetNodeStatusParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadGoalPlanSetNodeStatusParams.json new file mode 100644 index 000000000..57c6e3b98 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadGoalPlanSetNodeStatusParams.json @@ -0,0 +1,30 @@ +{ + "$defs": { + "ThreadGoalPlanNodeCompletionStatus": { + "enum": [ + "complete", + "pending" + ], + "type": "string" + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "nodeId": { + "type": "string" + }, + "status": { + "$ref": "#/$defs/ThreadGoalPlanNodeCompletionStatus" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId", + "nodeId", + "status" + ], + "title": "ThreadGoalPlanSetNodeStatusParams", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadGoalPlanSetNodeStatusResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadGoalPlanSetNodeStatusResponse.json new file mode 100644 index 000000000..b6aef2b96 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadGoalPlanSetNodeStatusResponse.json @@ -0,0 +1,370 @@ +{ + "$defs": { + "ThreadGoal": { + "properties": { + "createdAt": { + "format": "int64", + "type": "integer" + }, + "goalId": { + "type": "string" + }, + "objective": { + "type": "string" + }, + "status": { + "$ref": "#/$defs/ThreadGoalStatus" + }, + "threadId": { + "type": "string" + }, + "timeUsedSeconds": { + "format": "int64", + "type": "integer" + }, + "title": { + "type": [ + "string", + "null" + ] + }, + "tokenBudget": { + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "tokensUsed": { + "format": "int64", + "type": "integer" + }, + "updatedAt": { + "format": "int64", + "type": "integer" + } + }, + "required": [ + "threadId", + "goalId", + "objective", + "status", + "tokensUsed", + "timeUsedSeconds", + "createdAt", + "updatedAt" + ], + "type": "object" + }, + "ThreadGoalPlan": { + "properties": { + "activeNodeCount": { + "format": "int64", + "type": "integer" + }, + "autoExecute": { + "$ref": "#/$defs/ThreadGoalPlanAutoExecute" + }, + "blockedNodeCount": { + "format": "int64", + "type": "integer" + }, + "budgetLimitedNodeCount": { + "format": "int64", + "type": "integer" + }, + "cancelledNodeCount": { + "format": "int64", + "type": "integer" + }, + "completedNodeCount": { + "format": "int64", + "type": "integer" + }, + "createdAt": { + "format": "int64", + "type": "integer" + }, + "deferredNodeCount": { + "format": "int64", + "type": "integer" + }, + "maxTokens": { + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "nodeCount": { + "format": "int64", + "type": "integer" + }, + "nodes": { + "items": { + "$ref": "#/$defs/ThreadGoalPlanNode" + }, + "type": "array" + }, + "pausedNodeCount": { + "format": "int64", + "type": "integer" + }, + "pendingNodeCount": { + "format": "int64", + "type": "integer" + }, + "planId": { + "type": "string" + }, + "readyNodeCount": { + "format": "int64", + "type": "integer" + }, + "remainingTokens": { + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "status": { + "$ref": "#/$defs/ThreadGoalPlanStatus" + }, + "threadId": { + "type": "string" + }, + "totalTimeUsedSeconds": { + "format": "int64", + "type": "integer" + }, + "totalTokensUsed": { + "format": "int64", + "type": "integer" + }, + "updatedAt": { + "format": "int64", + "type": "integer" + }, + "usageLimitedNodeCount": { + "format": "int64", + "type": "integer" + } + }, + "required": [ + "planId", + "threadId", + "status", + "autoExecute", + "totalTokensUsed", + "totalTimeUsedSeconds", + "nodeCount", + "completedNodeCount", + "readyNodeCount", + "activeNodeCount", + "pendingNodeCount", + "pausedNodeCount", + "blockedNodeCount", + "usageLimitedNodeCount", + "budgetLimitedNodeCount", + "deferredNodeCount", + "cancelledNodeCount", + "createdAt", + "updatedAt", + "nodes" + ], + "type": "object" + }, + "ThreadGoalPlanAutoExecute": { + "enum": [ + "off", + "readyOnly", + "aiDirected" + ], + "type": "string" + }, + "ThreadGoalPlanNode": { + "properties": { + "assignedThreadId": { + "type": "string" + }, + "createdAt": { + "format": "int64", + "type": "integer" + }, + "dependsOn": { + "items": { + "type": "string" + }, + "type": "array" + }, + "key": { + "type": "string" + }, + "nestingDepth": { + "format": "int64", + "type": "integer" + }, + "nodeId": { + "type": "string" + }, + "objective": { + "type": "string" + }, + "parentNodeId": { + "type": [ + "string", + "null" + ] + }, + "planId": { + "type": "string" + }, + "priority": { + "format": "int64", + "type": "integer" + }, + "projectedGoalId": { + "type": [ + "string", + "null" + ] + }, + "ready": { + "type": "boolean" + }, + "sequence": { + "format": "int64", + "type": "integer" + }, + "status": { + "$ref": "#/$defs/ThreadGoalPlanNodeStatus" + }, + "threadId": { + "type": "string" + }, + "timeUsedSeconds": { + "format": "int64", + "type": "integer" + }, + "title": { + "type": [ + "string", + "null" + ] + }, + "tokenBudget": { + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "tokensUsed": { + "format": "int64", + "type": "integer" + }, + "updatedAt": { + "format": "int64", + "type": "integer" + } + }, + "required": [ + "nodeId", + "planId", + "threadId", + "assignedThreadId", + "nestingDepth", + "key", + "sequence", + "priority", + "objective", + "status", + "ready", + "tokensUsed", + "timeUsedSeconds", + "dependsOn", + "createdAt", + "updatedAt" + ], + "type": "object" + }, + "ThreadGoalPlanNodeStatus": { + "enum": [ + "pending", + "active", + "paused", + "blocked", + "usageLimited", + "budgetLimited", + "deferred", + "complete", + "cancelled" + ], + "type": "string" + }, + "ThreadGoalPlanStatus": { + "enum": [ + "active", + "paused", + "blocked", + "budgetLimited", + "complete", + "cancelled" + ], + "type": "string" + }, + "ThreadGoalStatus": { + "enum": [ + "active", + "paused", + "blocked", + "usageLimited", + "budgetLimited", + "deferred", + "complete", + "cancelled" + ], + "type": "string" + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "activatedGoal": { + "anyOf": [ + { + "$ref": "#/$defs/ThreadGoal" + }, + { + "type": "null" + } + ] + }, + "clearedGoal": { + "type": "boolean" + }, + "goal": { + "anyOf": [ + { + "$ref": "#/$defs/ThreadGoal" + }, + { + "type": "null" + } + ] + }, + "node": { + "$ref": "#/$defs/ThreadGoalPlanNode" + }, + "plan": { + "$ref": "#/$defs/ThreadGoalPlan" + } + }, + "required": [ + "clearedGoal", + "plan", + "node" + ], + "title": "ThreadGoalPlanSetNodeStatusResponse", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadGoalPlanUpdateNodeParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadGoalPlanUpdateNodeParams.json new file mode 100644 index 000000000..b0039a062 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadGoalPlanUpdateNodeParams.json @@ -0,0 +1,140 @@ +{ + "$defs": { + "ThreadGoalPlanNodeTitleUpdate": { + "oneOf": [ + { + "properties": { + "type": { + "const": "clear", + "title": "ClearThreadGoalPlanNodeTitleUpdateType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "ClearThreadGoalPlanNodeTitleUpdate", + "type": "object" + }, + { + "properties": { + "type": { + "const": "set", + "title": "SetThreadGoalPlanNodeTitleUpdateType", + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": [ + "type", + "value" + ], + "title": "SetThreadGoalPlanNodeTitleUpdate", + "type": "object" + } + ] + }, + "ThreadGoalPlanNodeTokenBudgetUpdate": { + "oneOf": [ + { + "properties": { + "type": { + "const": "clear", + "title": "ClearThreadGoalPlanNodeTokenBudgetUpdateType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "ClearThreadGoalPlanNodeTokenBudgetUpdate", + "type": "object" + }, + { + "properties": { + "type": { + "const": "set", + "title": "SetThreadGoalPlanNodeTokenBudgetUpdateType", + "type": "string" + }, + "value": { + "format": "int64", + "type": "integer" + } + }, + "required": [ + "type", + "value" + ], + "title": "SetThreadGoalPlanNodeTokenBudgetUpdate", + "type": "object" + } + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "dependsOn": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "key": { + "type": [ + "string", + "null" + ] + }, + "nodeId": { + "type": "string" + }, + "objective": { + "type": [ + "string", + "null" + ] + }, + "priority": { + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "threadId": { + "type": "string" + }, + "title": { + "anyOf": [ + { + "$ref": "#/$defs/ThreadGoalPlanNodeTitleUpdate" + }, + { + "type": "null" + } + ] + }, + "tokenBudget": { + "anyOf": [ + { + "$ref": "#/$defs/ThreadGoalPlanNodeTokenBudgetUpdate" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "threadId", + "nodeId" + ], + "title": "ThreadGoalPlanUpdateNodeParams", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadGoalPlanUpdateNodeResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadGoalPlanUpdateNodeResponse.json new file mode 100644 index 000000000..36ee082b7 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadGoalPlanUpdateNodeResponse.json @@ -0,0 +1,356 @@ +{ + "$defs": { + "ThreadGoal": { + "properties": { + "createdAt": { + "format": "int64", + "type": "integer" + }, + "goalId": { + "type": "string" + }, + "objective": { + "type": "string" + }, + "status": { + "$ref": "#/$defs/ThreadGoalStatus" + }, + "threadId": { + "type": "string" + }, + "timeUsedSeconds": { + "format": "int64", + "type": "integer" + }, + "title": { + "type": [ + "string", + "null" + ] + }, + "tokenBudget": { + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "tokensUsed": { + "format": "int64", + "type": "integer" + }, + "updatedAt": { + "format": "int64", + "type": "integer" + } + }, + "required": [ + "threadId", + "goalId", + "objective", + "status", + "tokensUsed", + "timeUsedSeconds", + "createdAt", + "updatedAt" + ], + "type": "object" + }, + "ThreadGoalPlan": { + "properties": { + "activeNodeCount": { + "format": "int64", + "type": "integer" + }, + "autoExecute": { + "$ref": "#/$defs/ThreadGoalPlanAutoExecute" + }, + "blockedNodeCount": { + "format": "int64", + "type": "integer" + }, + "budgetLimitedNodeCount": { + "format": "int64", + "type": "integer" + }, + "cancelledNodeCount": { + "format": "int64", + "type": "integer" + }, + "completedNodeCount": { + "format": "int64", + "type": "integer" + }, + "createdAt": { + "format": "int64", + "type": "integer" + }, + "deferredNodeCount": { + "format": "int64", + "type": "integer" + }, + "maxTokens": { + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "nodeCount": { + "format": "int64", + "type": "integer" + }, + "nodes": { + "items": { + "$ref": "#/$defs/ThreadGoalPlanNode" + }, + "type": "array" + }, + "pausedNodeCount": { + "format": "int64", + "type": "integer" + }, + "pendingNodeCount": { + "format": "int64", + "type": "integer" + }, + "planId": { + "type": "string" + }, + "readyNodeCount": { + "format": "int64", + "type": "integer" + }, + "remainingTokens": { + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "status": { + "$ref": "#/$defs/ThreadGoalPlanStatus" + }, + "threadId": { + "type": "string" + }, + "totalTimeUsedSeconds": { + "format": "int64", + "type": "integer" + }, + "totalTokensUsed": { + "format": "int64", + "type": "integer" + }, + "updatedAt": { + "format": "int64", + "type": "integer" + }, + "usageLimitedNodeCount": { + "format": "int64", + "type": "integer" + } + }, + "required": [ + "planId", + "threadId", + "status", + "autoExecute", + "totalTokensUsed", + "totalTimeUsedSeconds", + "nodeCount", + "completedNodeCount", + "readyNodeCount", + "activeNodeCount", + "pendingNodeCount", + "pausedNodeCount", + "blockedNodeCount", + "usageLimitedNodeCount", + "budgetLimitedNodeCount", + "deferredNodeCount", + "cancelledNodeCount", + "createdAt", + "updatedAt", + "nodes" + ], + "type": "object" + }, + "ThreadGoalPlanAutoExecute": { + "enum": [ + "off", + "readyOnly", + "aiDirected" + ], + "type": "string" + }, + "ThreadGoalPlanNode": { + "properties": { + "assignedThreadId": { + "type": "string" + }, + "createdAt": { + "format": "int64", + "type": "integer" + }, + "dependsOn": { + "items": { + "type": "string" + }, + "type": "array" + }, + "key": { + "type": "string" + }, + "nestingDepth": { + "format": "int64", + "type": "integer" + }, + "nodeId": { + "type": "string" + }, + "objective": { + "type": "string" + }, + "parentNodeId": { + "type": [ + "string", + "null" + ] + }, + "planId": { + "type": "string" + }, + "priority": { + "format": "int64", + "type": "integer" + }, + "projectedGoalId": { + "type": [ + "string", + "null" + ] + }, + "ready": { + "type": "boolean" + }, + "sequence": { + "format": "int64", + "type": "integer" + }, + "status": { + "$ref": "#/$defs/ThreadGoalPlanNodeStatus" + }, + "threadId": { + "type": "string" + }, + "timeUsedSeconds": { + "format": "int64", + "type": "integer" + }, + "title": { + "type": [ + "string", + "null" + ] + }, + "tokenBudget": { + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "tokensUsed": { + "format": "int64", + "type": "integer" + }, + "updatedAt": { + "format": "int64", + "type": "integer" + } + }, + "required": [ + "nodeId", + "planId", + "threadId", + "assignedThreadId", + "nestingDepth", + "key", + "sequence", + "priority", + "objective", + "status", + "ready", + "tokensUsed", + "timeUsedSeconds", + "dependsOn", + "createdAt", + "updatedAt" + ], + "type": "object" + }, + "ThreadGoalPlanNodeStatus": { + "enum": [ + "pending", + "active", + "paused", + "blocked", + "usageLimited", + "budgetLimited", + "deferred", + "complete", + "cancelled" + ], + "type": "string" + }, + "ThreadGoalPlanStatus": { + "enum": [ + "active", + "paused", + "blocked", + "budgetLimited", + "complete", + "cancelled" + ], + "type": "string" + }, + "ThreadGoalStatus": { + "enum": [ + "active", + "paused", + "blocked", + "usageLimited", + "budgetLimited", + "deferred", + "complete", + "cancelled" + ], + "type": "string" + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "goal": { + "anyOf": [ + { + "$ref": "#/$defs/ThreadGoal" + }, + { + "type": "null" + } + ] + }, + "node": { + "$ref": "#/$defs/ThreadGoalPlanNode" + }, + "plan": { + "$ref": "#/$defs/ThreadGoalPlan" + } + }, + "required": [ + "plan", + "node" + ], + "title": "ThreadGoalPlanUpdateNodeResponse", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts b/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts index 8a0f81b28..c1a392a2c 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts @@ -82,6 +82,9 @@ import type { ThreadGoalGetParams } from "./v2/ThreadGoalGetParams"; import type { ThreadGoalListParams } from "./v2/ThreadGoalListParams"; import type { ThreadGoalPlanActivateNodeParams } from "./v2/ThreadGoalPlanActivateNodeParams"; import type { ThreadGoalPlanAddGoalParams } from "./v2/ThreadGoalPlanAddGoalParams"; +import type { ThreadGoalPlanInsertNodeParams } from "./v2/ThreadGoalPlanInsertNodeParams"; +import type { ThreadGoalPlanSetNodeStatusParams } from "./v2/ThreadGoalPlanSetNodeStatusParams"; +import type { ThreadGoalPlanUpdateNodeParams } from "./v2/ThreadGoalPlanUpdateNodeParams"; import type { ThreadGoalSetParams } from "./v2/ThreadGoalSetParams"; import type { ThreadInjectItemsParams } from "./v2/ThreadInjectItemsParams"; import type { ThreadListParams } from "./v2/ThreadListParams"; @@ -137,4 +140,4 @@ import type { WorktreeReleaseParams } from "./v2/WorktreeReleaseParams"; /** * Request from the client to the server. */ -export type ClientRequest ={ "method": "initialize", id: RequestId, params: InitializeParams, } | { "method": "thread/start", id: RequestId, params: ThreadStartParams, } | { "method": "thread/resume", id: RequestId, params: ThreadResumeParams, } | { "method": "thread/fork", id: RequestId, params: ThreadForkParams, } | { "method": "thread/archive", id: RequestId, params: ThreadArchiveParams, } | { "method": "thread/unsubscribe", id: RequestId, params: ThreadUnsubscribeParams, } | { "method": "thread/name/set", id: RequestId, params: ThreadSetNameParams, } | { "method": "thread/goal/set", id: RequestId, params: ThreadGoalSetParams, } | { "method": "thread/goal/get", id: RequestId, params: ThreadGoalGetParams, } | { "method": "thread/goal/list", id: RequestId, params: ThreadGoalListParams, } | { "method": "thread/goalPlan/activateNode", id: RequestId, params: ThreadGoalPlanActivateNodeParams, } | { "method": "thread/goalPlan/addGoal", id: RequestId, params: ThreadGoalPlanAddGoalParams, } | { "method": "thread/goal/clear", id: RequestId, params: ThreadGoalClearParams, } | { "method": "thread/schedule/create", id: RequestId, params: ThreadScheduleCreateParams, } | { "method": "thread/schedule/list", id: RequestId, params: ThreadScheduleListParams, } | { "method": "thread/schedule/get", id: RequestId, params: ThreadScheduleGetParams, } | { "method": "thread/schedule/update", id: RequestId, params: ThreadScheduleUpdateParams, } | { "method": "thread/schedule/pause", id: RequestId, params: ThreadSchedulePauseParams, } | { "method": "thread/schedule/resume", id: RequestId, params: ThreadScheduleResumeParams, } | { "method": "thread/schedule/delete", id: RequestId, params: ThreadScheduleDeleteParams, } | { "method": "thread/schedule/runNow", id: RequestId, params: ThreadScheduleRunNowParams, } | { "method": "thread/monitor/create", id: RequestId, params: ThreadMonitorCreateParams, } | { "method": "thread/monitor/list", id: RequestId, params: ThreadMonitorListParams, } | { "method": "thread/monitor/read", id: RequestId, params: ThreadMonitorReadParams, } | { "method": "thread/monitor/stop", id: RequestId, params: ThreadMonitorStopParams, } | { "method": "thread/monitor/restart", id: RequestId, params: ThreadMonitorRestartParams, } | { "method": "thread/monitor/delete", id: RequestId, params: ThreadMonitorDeleteParams, } | { "method": "webhook/event/list", id: RequestId, params: WebhookEventListParams, } | { "method": "webhook/event/read", id: RequestId, params: WebhookEventReadParams, } | { "method": "webhook/event/mark", id: RequestId, params: WebhookEventMarkParams, } | { "method": "webhook/event/ingest", id: RequestId, params: WebhookEventIngestParams, } | { "method": "missionControl/overview", id: RequestId, params: MissionControlOverviewParams, } | { "method": "missionControl/enqueueInstruction", id: RequestId, params: MissionControlEnqueueInstructionParams, } | { "method": "missionControl/mailboxReceipts", id: RequestId, params: MissionControlMailboxReceiptsParams, } | { "method": "missionControl/respondInteraction", id: RequestId, params: MissionControlRespondInteractionParams, } | { "method": "worktree/list", id: RequestId, params: WorktreeListParams, } | { "method": "worktree/read", id: RequestId, params: WorktreeReadParams, } | { "method": "worktree/create", id: RequestId, params: WorktreeCreateParams, } | { "method": "worktree/reconcile", id: RequestId, params: WorktreeReconcileParams, } | { "method": "worktree/attach", id: RequestId, params: WorktreeAttachParams, } | { "method": "worktree/detach", id: RequestId, params: WorktreeDetachParams, } | { "method": "worktree/release", id: RequestId, params: WorktreeReleaseParams, } | { "method": "worktree/cleanup", id: RequestId, params: WorktreeCleanupParams, } | { "method": "worktree/mergeCandidate/list", id: RequestId, params: WorktreeMergeCandidateListParams, } | { "method": "worktree/mergeCandidate/refresh", id: RequestId, params: WorktreeMergeCandidateRefreshParams, } | { "method": "worktree/mergeCandidate/apply", id: RequestId, params: WorktreeMergeCandidateApplyParams, } | { "method": "worktree/mergeCandidate/dismiss", id: RequestId, params: WorktreeMergeCandidateDismissParams, } | { "method": "thread/metadata/update", id: RequestId, params: ThreadMetadataUpdateParams, } | { "method": "thread/unarchive", id: RequestId, params: ThreadUnarchiveParams, } | { "method": "thread/compact/start", id: RequestId, params: ThreadCompactStartParams, } | { "method": "thread/recap", id: RequestId, params: ThreadRecapParams, } | { "method": "thread/continue", id: RequestId, params: ThreadContinueParams, } | { "method": "thread/shellCommand", id: RequestId, params: ThreadShellCommandParams, } | { "method": "thread/queuedMessage/list", id: RequestId, params: ThreadQueuedMessageListParams, } | { "method": "thread/queuedMessage/update", id: RequestId, params: ThreadQueuedMessageUpdateParams, } | { "method": "thread/queuedMessage/move", id: RequestId, params: ThreadQueuedMessageMoveParams, } | { "method": "thread/approveGuardianDeniedAction", id: RequestId, params: ThreadApproveGuardianDeniedActionParams, } | { "method": "thread/rollback", id: RequestId, params: ThreadRollbackParams, } | { "method": "thread/list", id: RequestId, params: ThreadListParams, } | { "method": "thread/loaded/list", id: RequestId, params: ThreadLoadedListParams, } | { "method": "activeSession/list", id: RequestId, params: ActiveSessionListParams, } | { "method": "activeSession/send", id: RequestId, params: ActiveSessionSendParams, } | { "method": "thread/read", id: RequestId, params: ThreadReadParams, } | { "method": "thread/inject_items", id: RequestId, params: ThreadInjectItemsParams, } | { "method": "skills/list", id: RequestId, params: SkillsListParams, } | { "method": "skills/extraRoots/set", id: RequestId, params: SkillsExtraRootsSetParams, } | { "method": "hooks/list", id: RequestId, params: HooksListParams, } | { "method": "marketplace/add", id: RequestId, params: MarketplaceAddParams, } | { "method": "marketplace/remove", id: RequestId, params: MarketplaceRemoveParams, } | { "method": "marketplace/upgrade", id: RequestId, params: MarketplaceUpgradeParams, } | { "method": "plugin/list", id: RequestId, params: PluginListParams, } | { "method": "plugin/installed", id: RequestId, params: PluginInstalledParams, } | { "method": "plugin/read", id: RequestId, params: PluginReadParams, } | { "method": "plugin/skill/read", id: RequestId, params: PluginSkillReadParams, } | { "method": "plugin/share/save", id: RequestId, params: PluginShareSaveParams, } | { "method": "plugin/share/updateTargets", id: RequestId, params: PluginShareUpdateTargetsParams, } | { "method": "plugin/share/list", id: RequestId, params: PluginShareListParams, } | { "method": "plugin/share/checkout", id: RequestId, params: PluginShareCheckoutParams, } | { "method": "plugin/share/delete", id: RequestId, params: PluginShareDeleteParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "fs/readFile", id: RequestId, params: FsReadFileParams, } | { "method": "fs/writeFile", id: RequestId, params: FsWriteFileParams, } | { "method": "fs/createDirectory", id: RequestId, params: FsCreateDirectoryParams, } | { "method": "fs/getMetadata", id: RequestId, params: FsGetMetadataParams, } | { "method": "fs/readDirectory", id: RequestId, params: FsReadDirectoryParams, } | { "method": "fs/remove", id: RequestId, params: FsRemoveParams, } | { "method": "fs/copy", id: RequestId, params: FsCopyParams, } | { "method": "fs/watch", id: RequestId, params: FsWatchParams, } | { "method": "fs/unwatch", id: RequestId, params: FsUnwatchParams, } | { "method": "skills/config/write", id: RequestId, params: SkillsConfigWriteParams, } | { "method": "plugin/install", id: RequestId, params: PluginInstallParams, } | { "method": "plugin/uninstall", id: RequestId, params: PluginUninstallParams, } | { "method": "turn/start", id: RequestId, params: TurnStartParams, } | { "method": "turn/steer", id: RequestId, params: TurnSteerParams, } | { "method": "turn/interrupt", id: RequestId, params: TurnInterruptParams, } | { "method": "review/start", id: RequestId, params: ReviewStartParams, } | { "method": "model/list", id: RequestId, params: ModelListParams, } | { "method": "modelGateway/list", id: RequestId, params: ModelGatewayListParams, } | { "method": "modelProvider/list", id: RequestId, params: ModelProviderListParams, } | { "method": "modelProvider/capabilities/read", id: RequestId, params: ModelProviderCapabilitiesReadParams, } | { "method": "experimentalFeature/list", id: RequestId, params: ExperimentalFeatureListParams, } | { "method": "permissionProfile/list", id: RequestId, params: PermissionProfileListParams, } | { "method": "experimentalFeature/enablement/set", id: RequestId, params: ExperimentalFeatureEnablementSetParams, } | { "method": "mcpServer/oauth/login", id: RequestId, params: McpServerOauthLoginParams, } | { "method": "config/mcpServer/reload", id: RequestId, params: undefined, } | { "method": "mcpServerStatus/list", id: RequestId, params: ListMcpServerStatusParams, } | { "method": "mcpServer/resource/read", id: RequestId, params: McpResourceReadParams, } | { "method": "mcpServer/tool/call", id: RequestId, params: McpServerToolCallParams, } | { "method": "windowsSandbox/setupStart", id: RequestId, params: WindowsSandboxSetupStartParams, } | { "method": "windowsSandbox/readiness", id: RequestId, params: undefined, } | { "method": "account/login/start", id: RequestId, params: LoginAccountParams, } | { "method": "account/login/cancel", id: RequestId, params: CancelLoginAccountParams, } | { "method": "authProfile/list", id: RequestId, params: AuthProfileListParams, } | { "method": "authProfile/saveCurrent", id: RequestId, params: AuthProfileSaveCurrentParams, } | { "method": "authProfile/switch", id: RequestId, params: AuthProfileSwitchParams, } | { "method": "account/logout", id: RequestId, params: undefined, } | { "method": "account/rateLimits/read", id: RequestId, params: GetAccountRateLimitsParams, } | { "method": "account/rateLimitResetCredit/consume", id: RequestId, params: ConsumeAccountRateLimitResetCreditParams, } | { "method": "account/usage/read", id: RequestId, params: undefined, } | { "method": "account/sendAddCreditsNudgeEmail", id: RequestId, params: SendAddCreditsNudgeEmailParams, } | { "method": "feedback/upload", id: RequestId, params: FeedbackUploadParams, } | { "method": "command/exec", id: RequestId, params: CommandExecParams, } | { "method": "command/exec/write", id: RequestId, params: CommandExecWriteParams, } | { "method": "command/exec/terminate", id: RequestId, params: CommandExecTerminateParams, } | { "method": "command/exec/resize", id: RequestId, params: CommandExecResizeParams, } | { "method": "config/read", id: RequestId, params: ConfigReadParams, } | { "method": "externalAgentConfig/detect", id: RequestId, params: ExternalAgentConfigDetectParams, } | { "method": "externalAgentConfig/import", id: RequestId, params: ExternalAgentConfigImportParams, } | { "method": "config/value/write", id: RequestId, params: ConfigValueWriteParams, } | { "method": "config/batchWrite", id: RequestId, params: ConfigBatchWriteParams, } | { "method": "configRequirements/read", id: RequestId, params: undefined, } | { "method": "account/read", id: RequestId, params: GetAccountParams, } | { "method": "getConversationSummary", id: RequestId, params: GetConversationSummaryParams, } | { "method": "gitDiffToRemote", id: RequestId, params: GitDiffToRemoteParams, } | { "method": "getAuthStatus", id: RequestId, params: GetAuthStatusParams, } | { "method": "fuzzyFileSearch", id: RequestId, params: FuzzyFileSearchParams, }; +export type ClientRequest ={ "method": "initialize", id: RequestId, params: InitializeParams, } | { "method": "thread/start", id: RequestId, params: ThreadStartParams, } | { "method": "thread/resume", id: RequestId, params: ThreadResumeParams, } | { "method": "thread/fork", id: RequestId, params: ThreadForkParams, } | { "method": "thread/archive", id: RequestId, params: ThreadArchiveParams, } | { "method": "thread/unsubscribe", id: RequestId, params: ThreadUnsubscribeParams, } | { "method": "thread/name/set", id: RequestId, params: ThreadSetNameParams, } | { "method": "thread/goal/set", id: RequestId, params: ThreadGoalSetParams, } | { "method": "thread/goal/get", id: RequestId, params: ThreadGoalGetParams, } | { "method": "thread/goal/list", id: RequestId, params: ThreadGoalListParams, } | { "method": "thread/goalPlan/activateNode", id: RequestId, params: ThreadGoalPlanActivateNodeParams, } | { "method": "thread/goalPlan/addGoal", id: RequestId, params: ThreadGoalPlanAddGoalParams, } | { "method": "thread/goalPlan/updateNode", id: RequestId, params: ThreadGoalPlanUpdateNodeParams, } | { "method": "thread/goalPlan/insertNode", id: RequestId, params: ThreadGoalPlanInsertNodeParams, } | { "method": "thread/goalPlan/setNodeStatus", id: RequestId, params: ThreadGoalPlanSetNodeStatusParams, } | { "method": "thread/goal/clear", id: RequestId, params: ThreadGoalClearParams, } | { "method": "thread/schedule/create", id: RequestId, params: ThreadScheduleCreateParams, } | { "method": "thread/schedule/list", id: RequestId, params: ThreadScheduleListParams, } | { "method": "thread/schedule/get", id: RequestId, params: ThreadScheduleGetParams, } | { "method": "thread/schedule/update", id: RequestId, params: ThreadScheduleUpdateParams, } | { "method": "thread/schedule/pause", id: RequestId, params: ThreadSchedulePauseParams, } | { "method": "thread/schedule/resume", id: RequestId, params: ThreadScheduleResumeParams, } | { "method": "thread/schedule/delete", id: RequestId, params: ThreadScheduleDeleteParams, } | { "method": "thread/schedule/runNow", id: RequestId, params: ThreadScheduleRunNowParams, } | { "method": "thread/monitor/create", id: RequestId, params: ThreadMonitorCreateParams, } | { "method": "thread/monitor/list", id: RequestId, params: ThreadMonitorListParams, } | { "method": "thread/monitor/read", id: RequestId, params: ThreadMonitorReadParams, } | { "method": "thread/monitor/stop", id: RequestId, params: ThreadMonitorStopParams, } | { "method": "thread/monitor/restart", id: RequestId, params: ThreadMonitorRestartParams, } | { "method": "thread/monitor/delete", id: RequestId, params: ThreadMonitorDeleteParams, } | { "method": "webhook/event/list", id: RequestId, params: WebhookEventListParams, } | { "method": "webhook/event/read", id: RequestId, params: WebhookEventReadParams, } | { "method": "webhook/event/mark", id: RequestId, params: WebhookEventMarkParams, } | { "method": "webhook/event/ingest", id: RequestId, params: WebhookEventIngestParams, } | { "method": "missionControl/overview", id: RequestId, params: MissionControlOverviewParams, } | { "method": "missionControl/enqueueInstruction", id: RequestId, params: MissionControlEnqueueInstructionParams, } | { "method": "missionControl/mailboxReceipts", id: RequestId, params: MissionControlMailboxReceiptsParams, } | { "method": "missionControl/respondInteraction", id: RequestId, params: MissionControlRespondInteractionParams, } | { "method": "worktree/list", id: RequestId, params: WorktreeListParams, } | { "method": "worktree/read", id: RequestId, params: WorktreeReadParams, } | { "method": "worktree/create", id: RequestId, params: WorktreeCreateParams, } | { "method": "worktree/reconcile", id: RequestId, params: WorktreeReconcileParams, } | { "method": "worktree/attach", id: RequestId, params: WorktreeAttachParams, } | { "method": "worktree/detach", id: RequestId, params: WorktreeDetachParams, } | { "method": "worktree/release", id: RequestId, params: WorktreeReleaseParams, } | { "method": "worktree/cleanup", id: RequestId, params: WorktreeCleanupParams, } | { "method": "worktree/mergeCandidate/list", id: RequestId, params: WorktreeMergeCandidateListParams, } | { "method": "worktree/mergeCandidate/refresh", id: RequestId, params: WorktreeMergeCandidateRefreshParams, } | { "method": "worktree/mergeCandidate/apply", id: RequestId, params: WorktreeMergeCandidateApplyParams, } | { "method": "worktree/mergeCandidate/dismiss", id: RequestId, params: WorktreeMergeCandidateDismissParams, } | { "method": "thread/metadata/update", id: RequestId, params: ThreadMetadataUpdateParams, } | { "method": "thread/unarchive", id: RequestId, params: ThreadUnarchiveParams, } | { "method": "thread/compact/start", id: RequestId, params: ThreadCompactStartParams, } | { "method": "thread/recap", id: RequestId, params: ThreadRecapParams, } | { "method": "thread/continue", id: RequestId, params: ThreadContinueParams, } | { "method": "thread/shellCommand", id: RequestId, params: ThreadShellCommandParams, } | { "method": "thread/queuedMessage/list", id: RequestId, params: ThreadQueuedMessageListParams, } | { "method": "thread/queuedMessage/update", id: RequestId, params: ThreadQueuedMessageUpdateParams, } | { "method": "thread/queuedMessage/move", id: RequestId, params: ThreadQueuedMessageMoveParams, } | { "method": "thread/approveGuardianDeniedAction", id: RequestId, params: ThreadApproveGuardianDeniedActionParams, } | { "method": "thread/rollback", id: RequestId, params: ThreadRollbackParams, } | { "method": "thread/list", id: RequestId, params: ThreadListParams, } | { "method": "thread/loaded/list", id: RequestId, params: ThreadLoadedListParams, } | { "method": "activeSession/list", id: RequestId, params: ActiveSessionListParams, } | { "method": "activeSession/send", id: RequestId, params: ActiveSessionSendParams, } | { "method": "thread/read", id: RequestId, params: ThreadReadParams, } | { "method": "thread/inject_items", id: RequestId, params: ThreadInjectItemsParams, } | { "method": "skills/list", id: RequestId, params: SkillsListParams, } | { "method": "skills/extraRoots/set", id: RequestId, params: SkillsExtraRootsSetParams, } | { "method": "hooks/list", id: RequestId, params: HooksListParams, } | { "method": "marketplace/add", id: RequestId, params: MarketplaceAddParams, } | { "method": "marketplace/remove", id: RequestId, params: MarketplaceRemoveParams, } | { "method": "marketplace/upgrade", id: RequestId, params: MarketplaceUpgradeParams, } | { "method": "plugin/list", id: RequestId, params: PluginListParams, } | { "method": "plugin/installed", id: RequestId, params: PluginInstalledParams, } | { "method": "plugin/read", id: RequestId, params: PluginReadParams, } | { "method": "plugin/skill/read", id: RequestId, params: PluginSkillReadParams, } | { "method": "plugin/share/save", id: RequestId, params: PluginShareSaveParams, } | { "method": "plugin/share/updateTargets", id: RequestId, params: PluginShareUpdateTargetsParams, } | { "method": "plugin/share/list", id: RequestId, params: PluginShareListParams, } | { "method": "plugin/share/checkout", id: RequestId, params: PluginShareCheckoutParams, } | { "method": "plugin/share/delete", id: RequestId, params: PluginShareDeleteParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "fs/readFile", id: RequestId, params: FsReadFileParams, } | { "method": "fs/writeFile", id: RequestId, params: FsWriteFileParams, } | { "method": "fs/createDirectory", id: RequestId, params: FsCreateDirectoryParams, } | { "method": "fs/getMetadata", id: RequestId, params: FsGetMetadataParams, } | { "method": "fs/readDirectory", id: RequestId, params: FsReadDirectoryParams, } | { "method": "fs/remove", id: RequestId, params: FsRemoveParams, } | { "method": "fs/copy", id: RequestId, params: FsCopyParams, } | { "method": "fs/watch", id: RequestId, params: FsWatchParams, } | { "method": "fs/unwatch", id: RequestId, params: FsUnwatchParams, } | { "method": "skills/config/write", id: RequestId, params: SkillsConfigWriteParams, } | { "method": "plugin/install", id: RequestId, params: PluginInstallParams, } | { "method": "plugin/uninstall", id: RequestId, params: PluginUninstallParams, } | { "method": "turn/start", id: RequestId, params: TurnStartParams, } | { "method": "turn/steer", id: RequestId, params: TurnSteerParams, } | { "method": "turn/interrupt", id: RequestId, params: TurnInterruptParams, } | { "method": "review/start", id: RequestId, params: ReviewStartParams, } | { "method": "model/list", id: RequestId, params: ModelListParams, } | { "method": "modelGateway/list", id: RequestId, params: ModelGatewayListParams, } | { "method": "modelProvider/list", id: RequestId, params: ModelProviderListParams, } | { "method": "modelProvider/capabilities/read", id: RequestId, params: ModelProviderCapabilitiesReadParams, } | { "method": "experimentalFeature/list", id: RequestId, params: ExperimentalFeatureListParams, } | { "method": "permissionProfile/list", id: RequestId, params: PermissionProfileListParams, } | { "method": "experimentalFeature/enablement/set", id: RequestId, params: ExperimentalFeatureEnablementSetParams, } | { "method": "mcpServer/oauth/login", id: RequestId, params: McpServerOauthLoginParams, } | { "method": "config/mcpServer/reload", id: RequestId, params: undefined, } | { "method": "mcpServerStatus/list", id: RequestId, params: ListMcpServerStatusParams, } | { "method": "mcpServer/resource/read", id: RequestId, params: McpResourceReadParams, } | { "method": "mcpServer/tool/call", id: RequestId, params: McpServerToolCallParams, } | { "method": "windowsSandbox/setupStart", id: RequestId, params: WindowsSandboxSetupStartParams, } | { "method": "windowsSandbox/readiness", id: RequestId, params: undefined, } | { "method": "account/login/start", id: RequestId, params: LoginAccountParams, } | { "method": "account/login/cancel", id: RequestId, params: CancelLoginAccountParams, } | { "method": "authProfile/list", id: RequestId, params: AuthProfileListParams, } | { "method": "authProfile/saveCurrent", id: RequestId, params: AuthProfileSaveCurrentParams, } | { "method": "authProfile/switch", id: RequestId, params: AuthProfileSwitchParams, } | { "method": "account/logout", id: RequestId, params: undefined, } | { "method": "account/rateLimits/read", id: RequestId, params: GetAccountRateLimitsParams, } | { "method": "account/rateLimitResetCredit/consume", id: RequestId, params: ConsumeAccountRateLimitResetCreditParams, } | { "method": "account/usage/read", id: RequestId, params: undefined, } | { "method": "account/sendAddCreditsNudgeEmail", id: RequestId, params: SendAddCreditsNudgeEmailParams, } | { "method": "feedback/upload", id: RequestId, params: FeedbackUploadParams, } | { "method": "command/exec", id: RequestId, params: CommandExecParams, } | { "method": "command/exec/write", id: RequestId, params: CommandExecWriteParams, } | { "method": "command/exec/terminate", id: RequestId, params: CommandExecTerminateParams, } | { "method": "command/exec/resize", id: RequestId, params: CommandExecResizeParams, } | { "method": "config/read", id: RequestId, params: ConfigReadParams, } | { "method": "externalAgentConfig/detect", id: RequestId, params: ExternalAgentConfigDetectParams, } | { "method": "externalAgentConfig/import", id: RequestId, params: ExternalAgentConfigImportParams, } | { "method": "config/value/write", id: RequestId, params: ConfigValueWriteParams, } | { "method": "config/batchWrite", id: RequestId, params: ConfigBatchWriteParams, } | { "method": "configRequirements/read", id: RequestId, params: undefined, } | { "method": "account/read", id: RequestId, params: GetAccountParams, } | { "method": "getConversationSummary", id: RequestId, params: GetConversationSummaryParams, } | { "method": "gitDiffToRemote", id: RequestId, params: GitDiffToRemoteParams, } | { "method": "getAuthStatus", id: RequestId, params: GetAuthStatusParams, } | { "method": "fuzzyFileSearch", id: RequestId, params: FuzzyFileSearchParams, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadGoalPlanInsertNodeParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadGoalPlanInsertNodeParams.ts new file mode 100644 index 000000000..369ba07a5 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadGoalPlanInsertNodeParams.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ThreadGoalPlanNodeInsertPosition } from "./ThreadGoalPlanNodeInsertPosition"; + +export type ThreadGoalPlanInsertNodeParams = { threadId: string, planId: string, position: ThreadGoalPlanNodeInsertPosition, referenceNodeId?: string | null, key: string, objective: string, title?: string | null, priority?: bigint | null, tokenBudget?: bigint | null, dependsOn?: Array | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadGoalPlanInsertNodeResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadGoalPlanInsertNodeResponse.ts new file mode 100644 index 000000000..51943a4dd --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadGoalPlanInsertNodeResponse.ts @@ -0,0 +1,7 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ThreadGoalPlan } from "./ThreadGoalPlan"; +import type { ThreadGoalPlanNode } from "./ThreadGoalPlanNode"; + +export type ThreadGoalPlanInsertNodeResponse = { plan: ThreadGoalPlan, insertedNode: ThreadGoalPlanNode, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadGoalPlanNodeCompletionStatus.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadGoalPlanNodeCompletionStatus.ts new file mode 100644 index 000000000..fc47abf2b --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadGoalPlanNodeCompletionStatus.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadGoalPlanNodeCompletionStatus = "complete" | "pending"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadGoalPlanNodeInsertPosition.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadGoalPlanNodeInsertPosition.ts new file mode 100644 index 000000000..e16baac2c --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadGoalPlanNodeInsertPosition.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadGoalPlanNodeInsertPosition = "before" | "after" | "end"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadGoalPlanNodeTitleUpdate.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadGoalPlanNodeTitleUpdate.ts new file mode 100644 index 000000000..cccbd29b1 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadGoalPlanNodeTitleUpdate.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadGoalPlanNodeTitleUpdate = { "type": "clear" } | { "type": "set", value: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadGoalPlanNodeTokenBudgetUpdate.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadGoalPlanNodeTokenBudgetUpdate.ts new file mode 100644 index 000000000..1c395e9aa --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadGoalPlanNodeTokenBudgetUpdate.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadGoalPlanNodeTokenBudgetUpdate = { "type": "clear" } | { "type": "set", value: bigint, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadGoalPlanSetNodeStatusParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadGoalPlanSetNodeStatusParams.ts new file mode 100644 index 000000000..d29755087 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadGoalPlanSetNodeStatusParams.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ThreadGoalPlanNodeCompletionStatus } from "./ThreadGoalPlanNodeCompletionStatus"; + +export type ThreadGoalPlanSetNodeStatusParams = { threadId: string, nodeId: string, status: ThreadGoalPlanNodeCompletionStatus, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadGoalPlanSetNodeStatusResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadGoalPlanSetNodeStatusResponse.ts new file mode 100644 index 000000000..a907740e6 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadGoalPlanSetNodeStatusResponse.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ThreadGoal } from "./ThreadGoal"; +import type { ThreadGoalPlan } from "./ThreadGoalPlan"; +import type { ThreadGoalPlanNode } from "./ThreadGoalPlanNode"; + +export type ThreadGoalPlanSetNodeStatusResponse = { goal: ThreadGoal | null, activatedGoal: ThreadGoal | null, clearedGoal: boolean, plan: ThreadGoalPlan, node: ThreadGoalPlanNode, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadGoalPlanUpdateNodeParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadGoalPlanUpdateNodeParams.ts new file mode 100644 index 000000000..41468e9af --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadGoalPlanUpdateNodeParams.ts @@ -0,0 +1,7 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ThreadGoalPlanNodeTitleUpdate } from "./ThreadGoalPlanNodeTitleUpdate"; +import type { ThreadGoalPlanNodeTokenBudgetUpdate } from "./ThreadGoalPlanNodeTokenBudgetUpdate"; + +export type ThreadGoalPlanUpdateNodeParams = { threadId: string, nodeId: string, key?: string | null, objective?: string | null, title?: ThreadGoalPlanNodeTitleUpdate | null, priority?: bigint | null, tokenBudget?: ThreadGoalPlanNodeTokenBudgetUpdate | null, dependsOn?: Array | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadGoalPlanUpdateNodeResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadGoalPlanUpdateNodeResponse.ts new file mode 100644 index 000000000..538d4c065 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadGoalPlanUpdateNodeResponse.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ThreadGoal } from "./ThreadGoal"; +import type { ThreadGoalPlan } from "./ThreadGoalPlan"; +import type { ThreadGoalPlanNode } from "./ThreadGoalPlanNode"; + +export type ThreadGoalPlanUpdateNodeResponse = { goal: ThreadGoal | null, plan: ThreadGoalPlan, node: ThreadGoalPlanNode, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts index f1aed2c54..e67b69f53 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -461,9 +461,19 @@ export type { ThreadGoalPlanActivateNodeResponse } from "./ThreadGoalPlanActivat export type { ThreadGoalPlanAddGoalParams } from "./ThreadGoalPlanAddGoalParams"; export type { ThreadGoalPlanAddGoalResponse } from "./ThreadGoalPlanAddGoalResponse"; export type { ThreadGoalPlanAutoExecute } from "./ThreadGoalPlanAutoExecute"; +export type { ThreadGoalPlanInsertNodeParams } from "./ThreadGoalPlanInsertNodeParams"; +export type { ThreadGoalPlanInsertNodeResponse } from "./ThreadGoalPlanInsertNodeResponse"; export type { ThreadGoalPlanNode } from "./ThreadGoalPlanNode"; +export type { ThreadGoalPlanNodeCompletionStatus } from "./ThreadGoalPlanNodeCompletionStatus"; +export type { ThreadGoalPlanNodeInsertPosition } from "./ThreadGoalPlanNodeInsertPosition"; export type { ThreadGoalPlanNodeStatus } from "./ThreadGoalPlanNodeStatus"; +export type { ThreadGoalPlanNodeTitleUpdate } from "./ThreadGoalPlanNodeTitleUpdate"; +export type { ThreadGoalPlanNodeTokenBudgetUpdate } from "./ThreadGoalPlanNodeTokenBudgetUpdate"; +export type { ThreadGoalPlanSetNodeStatusParams } from "./ThreadGoalPlanSetNodeStatusParams"; +export type { ThreadGoalPlanSetNodeStatusResponse } from "./ThreadGoalPlanSetNodeStatusResponse"; export type { ThreadGoalPlanStatus } from "./ThreadGoalPlanStatus"; +export type { ThreadGoalPlanUpdateNodeParams } from "./ThreadGoalPlanUpdateNodeParams"; +export type { ThreadGoalPlanUpdateNodeResponse } from "./ThreadGoalPlanUpdateNodeResponse"; export type { ThreadGoalPlanUpdatedNotification } from "./ThreadGoalPlanUpdatedNotification"; export type { ThreadGoalSetParams } from "./ThreadGoalSetParams"; export type { ThreadGoalSetResponse } from "./ThreadGoalSetResponse"; diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index 84ad88895..9241ec54b 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -627,6 +627,21 @@ client_request_definitions! { serialization: thread_id(params.thread_id), response: v2::ThreadGoalPlanAddGoalResponse, }, + ThreadGoalPlanUpdateNode => "thread/goalPlan/updateNode" { + params: v2::ThreadGoalPlanUpdateNodeParams, + serialization: thread_id(params.thread_id), + response: v2::ThreadGoalPlanUpdateNodeResponse, + }, + ThreadGoalPlanInsertNode => "thread/goalPlan/insertNode" { + params: v2::ThreadGoalPlanInsertNodeParams, + serialization: thread_id(params.thread_id), + response: v2::ThreadGoalPlanInsertNodeResponse, + }, + ThreadGoalPlanSetNodeStatus => "thread/goalPlan/setNodeStatus" { + params: v2::ThreadGoalPlanSetNodeStatusParams, + serialization: thread_id(params.thread_id), + response: v2::ThreadGoalPlanSetNodeStatusResponse, + }, ThreadGoalClear => "thread/goal/clear" { params: v2::ThreadGoalClearParams, serialization: thread_id(params.thread_id), @@ -4450,8 +4465,44 @@ mod tests { objective: "queue goal mode".to_string(), }, }; - let clear_request = ClientRequest::ThreadGoalClear { + let update_node_request = ClientRequest::ThreadGoalPlanUpdateNode { request_id: RequestId::Integer(6), + params: v2::ThreadGoalPlanUpdateNodeParams { + thread_id: "thr_123".to_string(), + node_id: "node_123".to_string(), + key: Some("renamed".to_string()), + objective: None, + title: None, + priority: None, + token_budget: None, + depends_on: None, + }, + }; + let insert_node_request = ClientRequest::ThreadGoalPlanInsertNode { + request_id: RequestId::Integer(7), + params: v2::ThreadGoalPlanInsertNodeParams { + thread_id: "thr_123".to_string(), + plan_id: "plan_123".to_string(), + position: v2::ThreadGoalPlanNodeInsertPosition::After, + reference_node_id: Some("node_123".to_string()), + key: "followup".to_string(), + objective: "Follow up".to_string(), + title: None, + priority: None, + token_budget: None, + depends_on: None, + }, + }; + let set_node_status_request = ClientRequest::ThreadGoalPlanSetNodeStatus { + request_id: RequestId::Integer(8), + params: v2::ThreadGoalPlanSetNodeStatusParams { + thread_id: "thr_123".to_string(), + node_id: "node_123".to_string(), + status: v2::ThreadGoalPlanNodeCompletionStatus::Complete, + }, + }; + let clear_request = ClientRequest::ThreadGoalClear { + request_id: RequestId::Integer(9), params: v2::ThreadGoalClearParams { thread_id: "thr_123".to_string(), }, @@ -4477,6 +4528,18 @@ mod tests { crate::experimental_api::ExperimentalApi::experimental_reason(&add_goal_request), None ); + assert_eq!( + crate::experimental_api::ExperimentalApi::experimental_reason(&update_node_request), + None + ); + assert_eq!( + crate::experimental_api::ExperimentalApi::experimental_reason(&insert_node_request), + None + ); + assert_eq!( + crate::experimental_api::ExperimentalApi::experimental_reason(&set_node_status_request), + None + ); assert_eq!( crate::experimental_api::ExperimentalApi::experimental_reason(&clear_request), None diff --git a/codex-rs/app-server-protocol/src/protocol/v2/thread.rs b/codex-rs/app-server-protocol/src/protocol/v2/thread.rs index 9c7694b8a..d29f48a36 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/thread.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/thread.rs @@ -1307,6 +1307,117 @@ pub struct ThreadGoalPlanAddGoalResponse { pub created_plan: bool, } +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(rename_all = "camelCase", export_to = "v2/")] +pub enum ThreadGoalPlanNodeInsertPosition { + Before, + After, + End, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(rename_all = "camelCase", export_to = "v2/")] +pub enum ThreadGoalPlanNodeCompletionStatus { + Complete, + Pending, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(tag = "type", rename_all = "camelCase")] +#[ts(tag = "type", rename_all = "camelCase", export_to = "v2/")] +pub enum ThreadGoalPlanNodeTitleUpdate { + Clear, + Set { value: String }, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(tag = "type", rename_all = "camelCase")] +#[ts(tag = "type", rename_all = "camelCase", export_to = "v2/")] +pub enum ThreadGoalPlanNodeTokenBudgetUpdate { + Clear, + Set { value: i64 }, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadGoalPlanUpdateNodeParams { + pub thread_id: String, + pub node_id: String, + #[ts(optional = nullable)] + pub key: Option, + #[ts(optional = nullable)] + pub objective: Option, + #[ts(optional = nullable)] + pub title: Option, + #[ts(optional = nullable)] + pub priority: Option, + #[ts(optional = nullable)] + pub token_budget: Option, + #[ts(optional = nullable)] + pub depends_on: Option>, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadGoalPlanUpdateNodeResponse { + pub goal: Option, + pub plan: ThreadGoalPlan, + pub node: ThreadGoalPlanNode, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadGoalPlanInsertNodeParams { + pub thread_id: String, + pub plan_id: String, + pub position: ThreadGoalPlanNodeInsertPosition, + #[ts(optional = nullable)] + pub reference_node_id: Option, + pub key: String, + pub objective: String, + #[ts(optional = nullable)] + pub title: Option, + #[ts(optional = nullable)] + pub priority: Option, + #[ts(optional = nullable)] + pub token_budget: Option, + #[ts(optional = nullable)] + pub depends_on: Option>, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadGoalPlanInsertNodeResponse { + pub plan: ThreadGoalPlan, + pub inserted_node: ThreadGoalPlanNode, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadGoalPlanSetNodeStatusParams { + pub thread_id: String, + pub node_id: String, + pub status: ThreadGoalPlanNodeCompletionStatus, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadGoalPlanSetNodeStatusResponse { + pub goal: Option, + pub activated_goal: Option, + pub cleared_goal: bool, + pub plan: ThreadGoalPlan, + pub node: ThreadGoalPlanNode, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 88530816a..9fda29e85 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -151,6 +151,10 @@ Example with notification opt-out: - `thread/goal/get` — fetch the current persisted goal for a materialized thread; returns `goal: null` when no goal exists. - `thread/goal/list` — page through the current goal plus durable goal plans and their goal nodes for a materialized thread. Goals and plan nodes expose an optional compact `title`; plan nodes also expose parent linkage and 1-based nesting depth. Each plan includes aggregate token/time usage, ready-node counts, and node status counts computed from its nodes. - `thread/goalPlan/activateNode` — manually activate a ready pending node in a durable goal plan; returns the activated current goal, using the node title when present, and refreshed plan snapshot. +- `thread/goalPlan/addGoal` — queue a new goal into the active goal plan, creating one from the current goal when needed. The runtime chooses append dependencies so existing terminal nodes remain prerequisites. +- `thread/goalPlan/updateNode` — edit a pending or active goal-plan node's key, objective/prompt, title, priority, token budget, or dependencies. Completed nodes are immutable until marked pending again. +- `thread/goalPlan/insertNode` — insert a new pending goal-plan node before, after, or at the end of an existing plan's display order. Dependencies remain explicit through `dependsOn`. +- `thread/goalPlan/setNodeStatus` — mark a goal-plan node `complete` or `pending`; use `pending` to mark a completed node undone before editing it. - `thread/goal/clear` — clear the current persisted goal for a materialized thread; returns whether a goal was removed and emits `thread/goal/cleared` when state changes. - `thread/goal/updated` — notification emitted whenever a thread goal changes; includes the full current goal. - `thread/goalPlan/updated` — notification emitted whenever a durable goal plan changes; includes the full plan snapshot with aggregate usage. diff --git a/codex-rs/app-server/src/message_processor.rs b/codex-rs/app-server/src/message_processor.rs index 8c283df1d..cb3584034 100644 --- a/codex-rs/app-server/src/message_processor.rs +++ b/codex-rs/app-server/src/message_processor.rs @@ -1299,6 +1299,21 @@ impl MessageProcessor { .thread_goal_plan_add_goal(request_id.clone(), params) .await } + ClientRequest::ThreadGoalPlanUpdateNode { params, .. } => { + self.thread_goal_processor + .thread_goal_plan_update_node(request_id.clone(), params) + .await + } + ClientRequest::ThreadGoalPlanInsertNode { params, .. } => { + self.thread_goal_processor + .thread_goal_plan_insert_node(request_id.clone(), params) + .await + } + ClientRequest::ThreadGoalPlanSetNodeStatus { params, .. } => { + self.thread_goal_processor + .thread_goal_plan_set_node_status(request_id.clone(), params) + .await + } ClientRequest::ThreadGoalClear { params, .. } => { self.thread_goal_processor .thread_goal_clear(request_id.clone(), params) diff --git a/codex-rs/app-server/src/request_processors.rs b/codex-rs/app-server/src/request_processors.rs index 39bdc0117..873877c21 100644 --- a/codex-rs/app-server/src/request_processors.rs +++ b/codex-rs/app-server/src/request_processors.rs @@ -242,9 +242,19 @@ use codex_app_server_protocol::ThreadGoalPlanActivateNodeResponse; use codex_app_server_protocol::ThreadGoalPlanAddGoalParams; use codex_app_server_protocol::ThreadGoalPlanAddGoalResponse; use codex_app_server_protocol::ThreadGoalPlanAutoExecute; +use codex_app_server_protocol::ThreadGoalPlanInsertNodeParams; +use codex_app_server_protocol::ThreadGoalPlanInsertNodeResponse; use codex_app_server_protocol::ThreadGoalPlanNode; +use codex_app_server_protocol::ThreadGoalPlanNodeCompletionStatus; +use codex_app_server_protocol::ThreadGoalPlanNodeInsertPosition; use codex_app_server_protocol::ThreadGoalPlanNodeStatus; +use codex_app_server_protocol::ThreadGoalPlanNodeTitleUpdate; +use codex_app_server_protocol::ThreadGoalPlanNodeTokenBudgetUpdate; +use codex_app_server_protocol::ThreadGoalPlanSetNodeStatusParams; +use codex_app_server_protocol::ThreadGoalPlanSetNodeStatusResponse; use codex_app_server_protocol::ThreadGoalPlanStatus; +use codex_app_server_protocol::ThreadGoalPlanUpdateNodeParams; +use codex_app_server_protocol::ThreadGoalPlanUpdateNodeResponse; use codex_app_server_protocol::ThreadGoalPlanUpdatedNotification; use codex_app_server_protocol::ThreadGoalSetParams; use codex_app_server_protocol::ThreadGoalSetResponse; diff --git a/codex-rs/app-server/src/request_processors/thread_goal_processor.rs b/codex-rs/app-server/src/request_processors/thread_goal_processor.rs index 09813417b..faccc1029 100644 --- a/codex-rs/app-server/src/request_processors/thread_goal_processor.rs +++ b/codex-rs/app-server/src/request_processors/thread_goal_processor.rs @@ -84,6 +84,36 @@ impl ThreadGoalRequestProcessor { .map(|()| None) } + pub(crate) async fn thread_goal_plan_update_node( + &self, + request_id: ConnectionRequestId, + params: ThreadGoalPlanUpdateNodeParams, + ) -> Result, JSONRPCErrorError> { + self.thread_goal_plan_update_node_inner(request_id, params) + .await + .map(|()| None) + } + + pub(crate) async fn thread_goal_plan_insert_node( + &self, + request_id: ConnectionRequestId, + params: ThreadGoalPlanInsertNodeParams, + ) -> Result, JSONRPCErrorError> { + self.thread_goal_plan_insert_node_inner(request_id, params) + .await + .map(|()| None) + } + + pub(crate) async fn thread_goal_plan_set_node_status( + &self, + request_id: ConnectionRequestId, + params: ThreadGoalPlanSetNodeStatusParams, + ) -> Result, JSONRPCErrorError> { + self.thread_goal_plan_set_node_status_inner(request_id, params) + .await + .map(|()| None) + } + pub(crate) async fn thread_goal_clear( &self, request_id: ConnectionRequestId, @@ -422,6 +452,228 @@ impl ThreadGoalRequestProcessor { Ok(()) } + async fn thread_goal_plan_update_node_inner( + &self, + request_id: ConnectionRequestId, + params: ThreadGoalPlanUpdateNodeParams, + ) -> Result<(), JSONRPCErrorError> { + if !self.config.features.enabled(Feature::Goals) { + return Err(invalid_request("goals feature is disabled")); + } + + let thread_id = parse_thread_id_for_request(params.thread_id.as_str())?; + let state_db = self.state_db_for_materialized_thread(thread_id).await?; + self.reconcile_thread_goal_rollout(thread_id, &state_db) + .await?; + + let listener_command_tx = { + let thread_state = self.thread_state_manager.thread_state(thread_id).await; + let thread_state = thread_state.lock().await; + thread_state.listener_command_tx() + }; + let outcome = self + .goal_service + .update_thread_goal_plan_node( + &state_db, + codex_state::ThreadGoalPlanNodeUpdateParams { + thread_id, + node_id: params.node_id, + key: params.key, + objective: params.objective, + title: match params.title { + Some(ThreadGoalPlanNodeTitleUpdate::Clear) => { + codex_state::ThreadGoalPlanNodeTitleUpdate::Set(None) + } + Some(ThreadGoalPlanNodeTitleUpdate::Set { value }) => { + codex_state::ThreadGoalPlanNodeTitleUpdate::Set(Some(value)) + } + None => codex_state::ThreadGoalPlanNodeTitleUpdate::Keep, + }, + priority: params.priority, + token_budget: match params.token_budget { + Some(ThreadGoalPlanNodeTokenBudgetUpdate::Clear) => { + codex_state::ThreadGoalPlanNodeTokenBudgetUpdate::Set(None) + } + Some(ThreadGoalPlanNodeTokenBudgetUpdate::Set { value }) => { + codex_state::ThreadGoalPlanNodeTokenBudgetUpdate::Set(Some(value)) + } + None => codex_state::ThreadGoalPlanNodeTokenBudgetUpdate::Keep, + }, + depends_on: params.depends_on, + }, + ) + .await + .map_err(goal_service_error)?; + let goal = outcome.goal.clone().map(ThreadGoal::from); + let plan = api_thread_goal_plan_from_state_for_thread(outcome.plan.clone(), thread_id); + let node = plan + .nodes + .iter() + .find(|node| node.node_id == outcome.node.node_id) + .cloned() + .ok_or_else(|| internal_error("updated goal-plan node missing from snapshot"))?; + self.outgoing + .send_response( + request_id, + ThreadGoalPlanUpdateNodeResponse { + goal: goal.clone(), + plan: plan.clone(), + node, + }, + ) + .await; + if let Some(goal) = goal { + self.emit_thread_goal_updated_ordered(thread_id, goal, listener_command_tx.clone()) + .await; + } + self.emit_thread_goal_plan_snapshot_updated_ordered( + thread_id, + outcome.plan.clone(), + listener_command_tx, + ) + .await; + outcome.apply_runtime_effects(&self.goal_service).await; + Ok(()) + } + + async fn thread_goal_plan_insert_node_inner( + &self, + request_id: ConnectionRequestId, + params: ThreadGoalPlanInsertNodeParams, + ) -> Result<(), JSONRPCErrorError> { + if !self.config.features.enabled(Feature::Goals) { + return Err(invalid_request("goals feature is disabled")); + } + + let thread_id = parse_thread_id_for_request(params.thread_id.as_str())?; + let state_db = self.state_db_for_materialized_thread(thread_id).await?; + self.reconcile_thread_goal_rollout(thread_id, &state_db) + .await?; + + let listener_command_tx = { + let thread_state = self.thread_state_manager.thread_state(thread_id).await; + let thread_state = thread_state.lock().await; + thread_state.listener_command_tx() + }; + let position = goal_plan_node_insert_position(params.position, params.reference_node_id)?; + let outcome = self + .goal_service + .insert_thread_goal_plan_node( + &state_db, + codex_state::ThreadGoalPlanNodeInsertParams { + thread_id, + plan_id: params.plan_id, + position, + key: params.key, + objective: params.objective, + title: params.title, + priority: params.priority.unwrap_or(0), + token_budget: params.token_budget, + depends_on: params.depends_on.unwrap_or_default(), + }, + ) + .await + .map_err(goal_service_error)?; + let plan = api_thread_goal_plan_from_state_for_thread(outcome.plan.clone(), thread_id); + let inserted_node = plan + .nodes + .iter() + .find(|node| node.node_id == outcome.inserted_node.node_id) + .cloned() + .ok_or_else(|| internal_error("inserted goal-plan node missing from snapshot"))?; + self.outgoing + .send_response( + request_id, + ThreadGoalPlanInsertNodeResponse { + plan: plan.clone(), + inserted_node, + }, + ) + .await; + self.emit_thread_goal_plan_snapshot_updated_ordered( + thread_id, + outcome.plan, + listener_command_tx, + ) + .await; + Ok(()) + } + + async fn thread_goal_plan_set_node_status_inner( + &self, + request_id: ConnectionRequestId, + params: ThreadGoalPlanSetNodeStatusParams, + ) -> Result<(), JSONRPCErrorError> { + if !self.config.features.enabled(Feature::Goals) { + return Err(invalid_request("goals feature is disabled")); + } + + let thread_id = parse_thread_id_for_request(params.thread_id.as_str())?; + let state_db = self.state_db_for_materialized_thread(thread_id).await?; + self.reconcile_thread_goal_rollout(thread_id, &state_db) + .await?; + + let listener_command_tx = { + let thread_state = self.thread_state_manager.thread_state(thread_id).await; + let thread_state = thread_state.lock().await; + thread_state.listener_command_tx() + }; + let outcome = self + .goal_service + .set_thread_goal_plan_node_status( + &state_db, + codex_state::ThreadGoalPlanNodeStatusUpdateParams { + thread_id, + node_id: params.node_id, + status: goal_plan_node_completion_status(params.status), + auto_execute: goal_auto_execute_from_config(&self.config), + }, + ) + .await + .map_err(goal_service_error)?; + let goal = outcome.goal.clone().map(ThreadGoal::from); + let activated_goal = outcome.activated_goal.clone().map(ThreadGoal::from); + let plan = api_thread_goal_plan_from_state_for_thread(outcome.plan.clone(), thread_id); + let node = plan + .nodes + .iter() + .find(|node| node.node_id == outcome.node.node_id) + .cloned() + .ok_or_else(|| internal_error("updated goal-plan node missing from snapshot"))?; + self.outgoing + .send_response( + request_id, + ThreadGoalPlanSetNodeStatusResponse { + goal: goal.clone(), + activated_goal: activated_goal.clone(), + cleared_goal: outcome.cleared_goal, + plan: plan.clone(), + node, + }, + ) + .await; + if let Some(goal) = goal { + self.emit_thread_goal_updated_ordered(thread_id, goal, listener_command_tx.clone()) + .await; + } + if outcome.cleared_goal { + self.emit_thread_goal_cleared_ordered(thread_id, listener_command_tx.clone()) + .await; + } + self.emit_thread_goal_plan_snapshot_updated_ordered( + thread_id, + outcome.plan.clone(), + listener_command_tx.clone(), + ) + .await; + if let Some(activated_goal) = activated_goal { + self.emit_thread_goal_updated_ordered(thread_id, activated_goal, listener_command_tx) + .await; + } + outcome.apply_runtime_effects(&self.goal_service).await; + Ok(()) + } + async fn state_db_for_materialized_thread( &self, thread_id: ThreadId, @@ -650,6 +902,44 @@ pub(super) fn goal_auto_execute_from_config( } } +fn goal_plan_node_insert_position( + position: ThreadGoalPlanNodeInsertPosition, + reference_node_id: Option, +) -> Result { + match position { + ThreadGoalPlanNodeInsertPosition::Before => { + let reference_node_id = reference_node_id + .ok_or_else(|| invalid_request("referenceNodeId is required for before"))?; + Ok(codex_state::ThreadGoalPlanNodeInsertPosition::Before( + reference_node_id, + )) + } + ThreadGoalPlanNodeInsertPosition::After => { + let reference_node_id = reference_node_id + .ok_or_else(|| invalid_request("referenceNodeId is required for after"))?; + Ok(codex_state::ThreadGoalPlanNodeInsertPosition::After( + reference_node_id, + )) + } + ThreadGoalPlanNodeInsertPosition::End => { + Ok(codex_state::ThreadGoalPlanNodeInsertPosition::End) + } + } +} + +fn goal_plan_node_completion_status( + status: ThreadGoalPlanNodeCompletionStatus, +) -> codex_state::ThreadGoalPlanNodeCompletionStatus { + match status { + ThreadGoalPlanNodeCompletionStatus::Complete => { + codex_state::ThreadGoalPlanNodeCompletionStatus::Complete + } + ThreadGoalPlanNodeCompletionStatus::Pending => { + codex_state::ThreadGoalPlanNodeCompletionStatus::Pending + } + } +} + pub(super) fn api_thread_goal_plan_from_state( snapshot: codex_state::ThreadGoalPlanSnapshot, ) -> ThreadGoalPlan { diff --git a/codex-rs/app-server/tests/suite/v2/thread_resume.rs b/codex-rs/app-server/tests/suite/v2/thread_resume.rs index 49315f826..4132419ec 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_resume.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_resume.rs @@ -37,8 +37,13 @@ use codex_app_server_protocol::ThreadGoalListResponse; use codex_app_server_protocol::ThreadGoalPlanActivateNodeResponse; use codex_app_server_protocol::ThreadGoalPlanAddGoalResponse; use codex_app_server_protocol::ThreadGoalPlanAutoExecute; +use codex_app_server_protocol::ThreadGoalPlanInsertNodeResponse; +use codex_app_server_protocol::ThreadGoalPlanNodeCompletionStatus; +use codex_app_server_protocol::ThreadGoalPlanNodeInsertPosition; use codex_app_server_protocol::ThreadGoalPlanNodeStatus; +use codex_app_server_protocol::ThreadGoalPlanSetNodeStatusResponse; use codex_app_server_protocol::ThreadGoalPlanStatus; +use codex_app_server_protocol::ThreadGoalPlanUpdateNodeResponse; use codex_app_server_protocol::ThreadGoalSetResponse; use codex_app_server_protocol::ThreadGoalStatus; use codex_app_server_protocol::ThreadItem; @@ -2586,6 +2591,223 @@ async fn thread_goal_plan_activate_node_rpc_updates_goal_plan_and_notifications( Ok(()) } +#[tokio::test] +async fn thread_goal_plan_editing_rpcs_update_insert_and_mark_done_undone() -> Result<()> { + let server = create_mock_responses_server_repeating_assistant("Done").await; + let codex_home = TempDir::new()?; + create_config_toml(codex_home.path(), &server.uri())?; + let config_path = codex_home.path().join("config.toml"); + let config = std::fs::read_to_string(&config_path)?; + std::fs::write( + &config_path, + config.replace("personality = true\n", "personality = true\ngoals = true\n"), + )?; + + let mut mcp = TestAppServer::new_without_managed_config(codex_home.path()).await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + + let start_id = mcp + .send_thread_start_request(ThreadStartParams { + model: Some("gpt-5.2-codex".to_string()), + ..Default::default() + }) + .await?; + let start_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(start_id)), + ) + .await??; + let ThreadStartResponse { thread, .. } = to_response::(start_resp)?; + + let turn_id = mcp + .send_turn_start_request(TurnStartParams { + thread_id: thread.id.clone(), + client_user_message_id: None, + input: vec![UserInput::Text { + text: "materialize this thread".to_string(), + text_elements: Vec::new(), + }], + ..Default::default() + }) + .await?; + let _turn_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(turn_id)), + ) + .await??; + timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_notification_message("turn/completed"), + ) + .await??; + mcp.clear_message_buffer(); + + let state_db = + StateRuntime::init(codex_home.path().to_path_buf(), "mock_provider".into()).await?; + let thread_id = ThreadId::from_string(&thread.id)?; + let create_outcome = state_db + .thread_goals() + .create_thread_goal_plan(codex_state::ThreadGoalPlanCreateParams { + thread_id, + auto_execute: codex_state::ThreadGoalPlanAutoExecute::Off, + max_tokens: None, + nodes: vec![ + codex_state::ThreadGoalPlanNodeCreateParams { + key: "first".to_string(), + objective: "First objective".to_string(), + assigned_thread_id: None, + title: None, + priority: 0, + token_budget: None, + depends_on: Vec::new(), + }, + codex_state::ThreadGoalPlanNodeCreateParams { + key: "second".to_string(), + objective: "Second objective".to_string(), + assigned_thread_id: None, + title: None, + priority: 0, + token_budget: None, + depends_on: vec!["first".to_string()], + }, + ], + }) + .await?; + let plan_id = create_outcome.snapshot.plan.plan_id.clone(); + let first_node_id = create_outcome.snapshot.nodes[0].node_id.clone(); + let second_node_id = create_outcome.snapshot.nodes[1].node_id.clone(); + drop(state_db); + + let update_id = mcp + .send_raw_request( + "thread/goalPlan/updateNode", + Some(json!({ + "threadId": thread.id, + "nodeId": first_node_id, + "key": "prep", + "objective": "Prepare edited objective", + "title": { "type": "set", "value": "Prepare" }, + "priority": 7, + "tokenBudget": { "type": "set", "value": 25 }, + })), + ) + .await?; + let update_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(update_id)), + ) + .await??; + let updated: ThreadGoalPlanUpdateNodeResponse = to_response(update_resp)?; + assert_eq!("prep", updated.node.key); + assert_eq!("Prepare edited objective", updated.node.objective); + assert_eq!(Some("Prepare".to_string()), updated.node.title); + assert_eq!(7, updated.node.priority); + assert_eq!(Some(25), updated.node.token_budget); + assert_eq!(None, updated.goal); + mcp.clear_message_buffer(); + + let insert_id = mcp + .send_raw_request( + "thread/goalPlan/insertNode", + Some(json!({ + "threadId": thread.id, + "planId": plan_id, + "position": ThreadGoalPlanNodeInsertPosition::Before, + "referenceNodeId": second_node_id, + "key": "middle", + "objective": "Middle objective", + "title": "Middle", + "priority": 3, + "tokenBudget": 30, + "dependsOn": ["prep"], + })), + ) + .await?; + let insert_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(insert_id)), + ) + .await??; + let inserted: ThreadGoalPlanInsertNodeResponse = to_response(insert_resp)?; + assert_eq!("middle", inserted.inserted_node.key); + assert_eq!(1, inserted.inserted_node.sequence); + assert_eq!( + vec!["prep", "middle", "second"], + inserted + .plan + .nodes + .iter() + .map(|node| node.key.as_str()) + .collect::>() + ); + mcp.clear_message_buffer(); + + let complete_id = mcp + .send_raw_request( + "thread/goalPlan/setNodeStatus", + Some(json!({ + "threadId": thread.id, + "nodeId": updated.node.node_id, + "status": ThreadGoalPlanNodeCompletionStatus::Complete, + })), + ) + .await?; + let complete_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(complete_id)), + ) + .await??; + let completed: ThreadGoalPlanSetNodeStatusResponse = to_response(complete_resp)?; + assert_eq!(ThreadGoalPlanNodeStatus::Complete, completed.node.status); + assert_eq!(1, completed.plan.completed_node_count); + mcp.clear_message_buffer(); + + let edit_complete_id = mcp + .send_raw_request( + "thread/goalPlan/updateNode", + Some(json!({ + "threadId": thread.id, + "nodeId": updated.node.node_id, + "objective": "Should fail", + })), + ) + .await?; + let edit_complete_err: JSONRPCError = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_error_message(RequestId::Integer(edit_complete_id)), + ) + .await??; + assert!( + edit_complete_err + .error + .message + .contains("mark it undone first"), + "unexpected error: {edit_complete_err:?}" + ); + + let pending_id = mcp + .send_raw_request( + "thread/goalPlan/setNodeStatus", + Some(json!({ + "threadId": thread.id, + "nodeId": updated.node.node_id, + "status": ThreadGoalPlanNodeCompletionStatus::Pending, + })), + ) + .await?; + let pending_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(pending_id)), + ) + .await??; + let pending: ThreadGoalPlanSetNodeStatusResponse = to_response(pending_resp)?; + assert_eq!(ThreadGoalPlanNodeStatus::Pending, pending.node.status); + assert_eq!(0, pending.plan.completed_node_count); + assert!(!pending.cleared_goal); + + Ok(()) +} + #[tokio::test] async fn thread_goal_set_complete_advances_ready_goal_plan_node_and_notifies() -> Result<()> { let server = create_mock_responses_server_repeating_assistant("Done").await; diff --git a/codex-rs/ext/goal/src/api.rs b/codex-rs/ext/goal/src/api.rs index e1e076703..d088c1ffd 100644 --- a/codex-rs/ext/goal/src/api.rs +++ b/codex-rs/ext/goal/src/api.rs @@ -137,6 +137,87 @@ impl GoalPlanAddOutcome { } } +#[derive(Clone, Debug)] +pub struct GoalPlanNodeUpdateOutcome { + pub goal: Option, + pub plan: codex_state::ThreadGoalPlanSnapshot, + pub node: codex_state::ThreadGoalPlanNode, + state_goal: Option, + previous_goal: Option, +} + +impl GoalPlanNodeUpdateOutcome { + pub async fn apply_runtime_effects(&self, goal_service: &GoalService) { + let Some(state_goal) = self.state_goal.clone() else { + return; + }; + if let Some(runtime) = goal_service.runtime_for_thread(state_goal.thread_id) + && let Err(err) = runtime + .apply_external_goal_set(state_goal, self.previous_goal.clone()) + .await + { + tracing::warn!("failed to apply external goal plan node update runtime effects: {err}"); + } + } +} + +#[derive(Clone, Debug)] +pub struct GoalPlanNodeInsertOutcome { + pub plan: codex_state::ThreadGoalPlanSnapshot, + pub inserted_node: codex_state::ThreadGoalPlanNode, +} + +#[derive(Clone, Debug)] +pub struct GoalPlanNodeStatusOutcome { + pub goal: Option, + pub activated_goal: Option, + pub cleared_goal: bool, + pub plan: codex_state::ThreadGoalPlanSnapshot, + pub node: codex_state::ThreadGoalPlanNode, + thread_id: ThreadId, + state_goal: Option, + state_activated_goal: Option, + previous_goal: Option, +} + +impl GoalPlanNodeStatusOutcome { + pub async fn apply_runtime_effects(&self, goal_service: &GoalService) { + let Some(runtime) = goal_service.runtime_for_thread(self.thread_id) else { + return; + }; + + if let Some(activated_goal) = self.state_activated_goal.clone() { + let previous_goal = self + .state_goal + .as_ref() + .map(PreviousGoalSnapshot::from) + .or_else(|| self.previous_goal.clone()); + if let Err(err) = runtime + .apply_external_goal_set(activated_goal, previous_goal) + .await + { + tracing::warn!( + "failed to apply external goal plan node activation runtime effects: {err}" + ); + } + return; + } + + if let Some(state_goal) = self.state_goal.clone() + && let Err(err) = runtime + .apply_external_goal_set(state_goal, self.previous_goal.clone()) + .await + { + tracing::warn!("failed to apply external goal plan node status runtime effects: {err}"); + } + if self.cleared_goal + && let Err(err) = runtime.apply_external_goal_clear().await + { + tracing::warn!("failed to apply external goal plan node undo runtime effects: {err}"); + } + } +} + #[derive(Clone, Debug)] pub struct GoalClearOutcome { pub cleared: bool, @@ -467,6 +548,141 @@ impl GoalService { }) } + pub async fn update_thread_goal_plan_node( + &self, + state_db: &codex_state::StateRuntime, + params: codex_state::ThreadGoalPlanNodeUpdateParams, + ) -> Result { + let thread_id = params.thread_id; + let runtime = self.runtime_for_thread(thread_id); + let _goal_state_permit = match runtime.as_ref() { + Some(runtime) => Some( + runtime + .goal_state_permit() + .await + .map_err(GoalServiceError::Internal)?, + ), + None => None, + }; + if let Some(runtime) = runtime.as_ref() + && let Err(err) = runtime.prepare_external_goal_mutation().await + { + tracing::warn!("failed to prepare external goal plan node update: {err}"); + } + + let previous_goal = state_db + .thread_goals() + .get_thread_goal(thread_id) + .await + .map_err(|err| { + GoalServiceError::Internal(format!("failed to read thread goal: {err}")) + })? + .as_ref() + .map(PreviousGoalSnapshot::from); + let outcome = state_db + .thread_goals() + .update_thread_goal_plan_node(params) + .await + .map_err(|err| { + GoalServiceError::InvalidRequest(format!("failed to update goal plan node: {err}")) + })?; + Ok(GoalPlanNodeUpdateOutcome { + goal: outcome.goal.clone().map(protocol_goal_from_state), + plan: outcome.snapshot, + node: outcome.node, + state_goal: outcome.goal, + previous_goal, + }) + } + + pub async fn insert_thread_goal_plan_node( + &self, + state_db: &codex_state::StateRuntime, + params: codex_state::ThreadGoalPlanNodeInsertParams, + ) -> Result { + let thread_id = params.thread_id; + let runtime = self.runtime_for_thread(thread_id); + let _goal_state_permit = match runtime.as_ref() { + Some(runtime) => Some( + runtime + .goal_state_permit() + .await + .map_err(GoalServiceError::Internal)?, + ), + None => None, + }; + if let Some(runtime) = runtime.as_ref() + && let Err(err) = runtime.prepare_external_goal_mutation().await + { + tracing::warn!("failed to prepare external goal plan node insert: {err}"); + } + + let outcome = state_db + .thread_goals() + .insert_thread_goal_plan_node(params) + .await + .map_err(|err| { + GoalServiceError::InvalidRequest(format!("failed to insert goal plan node: {err}")) + })?; + Ok(GoalPlanNodeInsertOutcome { + plan: outcome.snapshot, + inserted_node: outcome.inserted_node, + }) + } + + pub async fn set_thread_goal_plan_node_status( + &self, + state_db: &codex_state::StateRuntime, + params: codex_state::ThreadGoalPlanNodeStatusUpdateParams, + ) -> Result { + let thread_id = params.thread_id; + let runtime = self.runtime_for_thread(thread_id); + let _goal_state_permit = match runtime.as_ref() { + Some(runtime) => Some( + runtime + .goal_state_permit() + .await + .map_err(GoalServiceError::Internal)?, + ), + None => None, + }; + if let Some(runtime) = runtime.as_ref() + && let Err(err) = runtime.prepare_external_goal_mutation().await + { + tracing::warn!("failed to prepare external goal plan node status update: {err}"); + } + + let previous_goal = state_db + .thread_goals() + .get_thread_goal(thread_id) + .await + .map_err(|err| { + GoalServiceError::Internal(format!("failed to read thread goal: {err}")) + })? + .as_ref() + .map(PreviousGoalSnapshot::from); + let outcome = state_db + .thread_goals() + .set_thread_goal_plan_node_status(params) + .await + .map_err(|err| { + GoalServiceError::InvalidRequest(format!( + "failed to update goal plan node status: {err}" + )) + })?; + Ok(GoalPlanNodeStatusOutcome { + goal: outcome.goal.clone().map(protocol_goal_from_state), + activated_goal: outcome.activated_goal.clone().map(protocol_goal_from_state), + cleared_goal: outcome.cleared_goal, + plan: outcome.snapshot, + node: outcome.node, + thread_id, + state_goal: outcome.goal, + state_activated_goal: outcome.activated_goal, + previous_goal, + }) + } + pub fn suppress_next_idle_continuation(&self, thread_id: ThreadId, goal_id: &str) { if let Some(runtime) = self.runtime_for_thread(thread_id) { runtime.suppress_next_idle_continuation(goal_id); diff --git a/codex-rs/ext/goal/src/extension.rs b/codex-rs/ext/goal/src/extension.rs index e8db8fa93..392b1122c 100644 --- a/codex-rs/ext/goal/src/extension.rs +++ b/codex-rs/ext/goal/src/extension.rs @@ -42,8 +42,11 @@ use crate::runtime::GoalRuntimeHandle; use crate::spec::ACTIVATE_GOAL_PLAN_NODE_TOOL_NAME; use crate::spec::CREATE_GOAL_PLAN_TOOL_NAME; use crate::spec::GET_GOAL_PLAN_TOOL_NAME; +use crate::spec::INSERT_GOAL_PLAN_NODE_TOOL_NAME; use crate::spec::PAUSE_GOAL_TOOL_NAME; use crate::spec::RESUME_GOAL_TOOL_NAME; +use crate::spec::SET_GOAL_PLAN_NODE_STATUS_TOOL_NAME; +use crate::spec::UPDATE_GOAL_PLAN_NODE_TOOL_NAME; use crate::spec::UPDATE_GOAL_TOOL_NAME; use crate::steering::budget_limit_steering_item; use crate::tool::GoalToolExecutor; @@ -453,6 +456,9 @@ where | GET_GOAL_PLAN_TOOL_NAME | CREATE_GOAL_PLAN_TOOL_NAME | ACTIVATE_GOAL_PLAN_NODE_TOOL_NAME + | UPDATE_GOAL_PLAN_NODE_TOOL_NAME + | INSERT_GOAL_PLAN_NODE_TOOL_NAME + | SET_GOAL_PLAN_NODE_STATUS_TOOL_NAME )); if !should_count_for_goal_progress { return; @@ -547,6 +553,30 @@ where self.metrics.clone(), runtime.plan_config_handle(), )), + Arc::new(GoalToolExecutor::update_plan_node( + runtime.thread_id(), + Arc::clone(&self.state_dbs), + runtime.accounting_state(), + self.event_emitter.clone(), + self.metrics.clone(), + runtime.plan_config_handle(), + )), + Arc::new(GoalToolExecutor::insert_plan_node( + runtime.thread_id(), + Arc::clone(&self.state_dbs), + runtime.accounting_state(), + self.event_emitter.clone(), + self.metrics.clone(), + runtime.plan_config_handle(), + )), + Arc::new(GoalToolExecutor::set_plan_node_status( + runtime.thread_id(), + Arc::clone(&self.state_dbs), + runtime.accounting_state(), + self.event_emitter.clone(), + self.metrics.clone(), + runtime.plan_config_handle(), + )), Arc::new(GoalToolExecutor::update( runtime.thread_id(), Arc::clone(&self.state_dbs), diff --git a/codex-rs/ext/goal/src/lib.rs b/codex-rs/ext/goal/src/lib.rs index 05fa41c49..2de4ca97a 100644 --- a/codex-rs/ext/goal/src/lib.rs +++ b/codex-rs/ext/goal/src/lib.rs @@ -31,5 +31,8 @@ pub use spec::CREATE_GOAL_PLAN_TOOL_NAME; pub use spec::CREATE_GOAL_TOOL_NAME; pub use spec::GET_GOAL_PLAN_TOOL_NAME; pub use spec::GET_GOAL_TOOL_NAME; +pub use spec::INSERT_GOAL_PLAN_NODE_TOOL_NAME; +pub use spec::SET_GOAL_PLAN_NODE_STATUS_TOOL_NAME; +pub use spec::UPDATE_GOAL_PLAN_NODE_TOOL_NAME; pub use spec::UPDATE_GOAL_TOOL_NAME; pub use tool::CreateGoalRequest; diff --git a/codex-rs/ext/goal/src/spec.rs b/codex-rs/ext/goal/src/spec.rs index f527486a2..4a3ae7318 100644 --- a/codex-rs/ext/goal/src/spec.rs +++ b/codex-rs/ext/goal/src/spec.rs @@ -11,6 +11,9 @@ pub const CREATE_GOAL_TOOL_NAME: &str = "create_goal"; pub const GET_GOAL_PLAN_TOOL_NAME: &str = "get_goal_plan"; pub const CREATE_GOAL_PLAN_TOOL_NAME: &str = "create_goal_plan"; pub const ACTIVATE_GOAL_PLAN_NODE_TOOL_NAME: &str = "activate_goal_plan_node"; +pub const UPDATE_GOAL_PLAN_NODE_TOOL_NAME: &str = "update_goal_plan_node"; +pub const INSERT_GOAL_PLAN_NODE_TOOL_NAME: &str = "insert_goal_plan_node"; +pub const SET_GOAL_PLAN_NODE_STATUS_TOOL_NAME: &str = "set_goal_plan_node_status"; pub const UPDATE_GOAL_TOOL_NAME: &str = "update_goal"; pub const PAUSE_GOAL_TOOL_NAME: &str = "pause_goal"; pub const RESUME_GOAL_TOOL_NAME: &str = "resume_goal"; @@ -302,6 +305,230 @@ Do not activate a node whose dependencies are incomplete, and do not use this to }) } +pub fn create_update_goal_plan_node_tool() -> ToolSpec { + let properties = BTreeMap::from([ + ( + "node_id".to_string(), + JsonSchema::string(Some( + "Required stable node id from get_goal_plan for the node to edit.".to_string(), + )), + ), + ( + "key".to_string(), + JsonSchema::string(Some( + "Optional new stable short key. Keys must be unique within the plan and contain only ASCII letters, numbers, underscores, or hyphens." + .to_string(), + )), + ), + ( + "objective".to_string(), + JsonSchema::string(Some( + "Optional new concrete objective/prompt for this goal-plan node.".to_string(), + )), + ), + ( + "title".to_string(), + JsonSchema::string(Some( + "Optional compact display title for this node, at most 4-5 words. Omit to keep the current title." + .to_string(), + )), + ), + ( + "clear_title".to_string(), + JsonSchema::boolean(Some( + "Optional. Set true to clear the node title. Do not combine with title." + .to_string(), + )), + ), + ( + "priority".to_string(), + JsonSchema::integer(Some( + "Optional priority for choosing among independent ready goals. Higher runs first." + .to_string(), + )), + ), + ( + "token_budget".to_string(), + JsonSchema::integer(Some( + "Optional positive token budget for this node. Omit to keep the current budget." + .to_string(), + )), + ), + ( + "clear_token_budget".to_string(), + JsonSchema::boolean(Some( + "Optional. Set true to clear the node token budget. Do not combine with token_budget." + .to_string(), + )), + ), + ( + "depends_on".to_string(), + JsonSchema::array( + JsonSchema::string(Some( + "A goal-plan node key that must complete before this node is ready." + .to_string(), + )), + Some( + "Optional replacement dependency list. Omit to keep current dependencies." + .to_string(), + ), + ), + ), + ]); + + ToolSpec::Function(ResponsesApiTool { + name: UPDATE_GOAL_PLAN_NODE_TOOL_NAME.to_string(), + description: format!( + r#"Edit an existing goal-plan node's metadata or objective/prompt. +Completed nodes cannot be edited; mark the node pending with {SET_GOAL_PLAN_NODE_STATUS_TOOL_NAME} before editing it. +Changing a node key preserves existing dependency edges because dependencies are stored by node id. +{ADVERSARIAL_GOAL_COMPLETION_REQUIREMENT}"# + ), + strict: false, + defer_loading: None, + parameters: JsonSchema::object( + properties, + /*required*/ Some(vec!["node_id".to_string()]), + Some(false.into()), + ), + output_schema: None, + }) +} + +pub fn create_insert_goal_plan_node_tool() -> ToolSpec { + let properties = BTreeMap::from([ + ( + "plan_id".to_string(), + JsonSchema::string(Some( + "Required stable goal-plan id from get_goal_plan.".to_string(), + )), + ), + ( + "position".to_string(), + JsonSchema::string_enum( + vec![json!("before"), json!("after"), json!("end")], + Some( + "Required insertion position. Use before/after with reference_node_id, or end to append by display order." + .to_string(), + ), + ), + ), + ( + "reference_node_id".to_string(), + JsonSchema::string(Some( + "Required when position is before or after; ignored when position is end." + .to_string(), + )), + ), + ( + "key".to_string(), + JsonSchema::string(Some( + "Required stable short key unique inside this plan.".to_string(), + )), + ), + ( + "objective".to_string(), + JsonSchema::string(Some( + "Required concrete objective/prompt for the inserted node.".to_string(), + )), + ), + ( + "title".to_string(), + JsonSchema::string(Some( + "Optional compact display title for this node, at most 4-5 words.".to_string(), + )), + ), + ( + "priority".to_string(), + JsonSchema::integer(Some( + "Optional priority for choosing among independent ready goals. Higher runs first. Defaults to 0." + .to_string(), + )), + ), + ( + "token_budget".to_string(), + JsonSchema::integer(Some( + "Optional positive token budget for this node.".to_string(), + )), + ), + ( + "depends_on".to_string(), + JsonSchema::array( + JsonSchema::string(Some( + "A goal-plan node key that must complete before this node is ready." + .to_string(), + )), + Some( + "Optional dependency list for the inserted node. Position controls display order; dependencies control readiness." + .to_string(), + ), + ), + ), + ]); + + ToolSpec::Function(ResponsesApiTool { + name: INSERT_GOAL_PLAN_NODE_TOOL_NAME.to_string(), + description: format!( + r#"Insert a new pending node into an existing goal plan before, after, or at the end of the current node order. +Position controls display order; use depends_on to set readiness dependencies intentionally. +{ADVERSARIAL_GOAL_COMPLETION_REQUIREMENT}"# + ), + strict: false, + defer_loading: None, + parameters: JsonSchema::object( + properties, + /*required*/ + Some(vec![ + "plan_id".to_string(), + "position".to_string(), + "key".to_string(), + "objective".to_string(), + ]), + Some(false.into()), + ), + output_schema: None, + }) +} + +pub fn create_set_goal_plan_node_status_tool() -> ToolSpec { + let properties = BTreeMap::from([ + ( + "node_id".to_string(), + JsonSchema::string(Some( + "Required stable node id from get_goal_plan for the node to mark.".to_string(), + )), + ), + ( + "status".to_string(), + JsonSchema::string_enum( + vec![json!("complete"), json!("pending")], + Some( + "Required target status. Use complete to mark done and pending to mark undone." + .to_string(), + ), + ), + ), + ]); + + ToolSpec::Function(ResponsesApiTool { + name: SET_GOAL_PLAN_NODE_STATUS_TOOL_NAME.to_string(), + description: format!( + r#"Mark a goal-plan node done or undone. +Only complete and pending are supported here; use update_goal for the active goal's terminal lifecycle when you need normal completion accounting. +Completed nodes cannot be edited until they are marked pending with this tool. +{ADVERSARIAL_GOAL_COMPLETION_REQUIREMENT}"# + ), + strict: false, + defer_loading: None, + parameters: JsonSchema::object( + properties, + /*required*/ Some(vec!["node_id".to_string(), "status".to_string()]), + Some(false.into()), + ), + output_schema: None, + }) +} + pub fn create_update_goal_tool() -> ToolSpec { let properties = BTreeMap::from([( "status".to_string(), diff --git a/codex-rs/ext/goal/src/tool.rs b/codex-rs/ext/goal/src/tool.rs index 2a8c2a7bb..28f7649ef 100644 --- a/codex-rs/ext/goal/src/tool.rs +++ b/codex-rs/ext/goal/src/tool.rs @@ -27,16 +27,22 @@ use crate::spec::CREATE_GOAL_PLAN_TOOL_NAME; use crate::spec::CREATE_GOAL_TOOL_NAME; use crate::spec::GET_GOAL_PLAN_TOOL_NAME; use crate::spec::GET_GOAL_TOOL_NAME; +use crate::spec::INSERT_GOAL_PLAN_NODE_TOOL_NAME; use crate::spec::PAUSE_GOAL_TOOL_NAME; use crate::spec::RESUME_GOAL_TOOL_NAME; +use crate::spec::SET_GOAL_PLAN_NODE_STATUS_TOOL_NAME; +use crate::spec::UPDATE_GOAL_PLAN_NODE_TOOL_NAME; use crate::spec::UPDATE_GOAL_TOOL_NAME; use crate::spec::create_activate_goal_plan_node_tool; use crate::spec::create_create_goal_plan_tool; use crate::spec::create_create_goal_tool; use crate::spec::create_get_goal_plan_tool; use crate::spec::create_get_goal_tool; +use crate::spec::create_insert_goal_plan_node_tool; use crate::spec::create_pause_goal_tool; use crate::spec::create_resume_goal_tool; +use crate::spec::create_set_goal_plan_node_status_tool; +use crate::spec::create_update_goal_plan_node_tool; use crate::spec::create_update_goal_tool; use crate::tool_plan::GoalPlanCompletionReport; use crate::tool_plan::GoalPlanResponse; @@ -65,6 +71,9 @@ enum GoalToolKind { GetPlan, CreatePlan, ActivatePlanNode, + UpdatePlanNode, + InsertPlanNode, + SetPlanNodeStatus, Update, Pause, Resume, @@ -220,6 +229,63 @@ impl GoalToolExecutor { } } + pub(crate) fn update_plan_node( + thread_id: ThreadId, + state_db: Arc, + accounting_state: Arc, + event_emitter: GoalEventEmitter, + metrics: GoalMetrics, + plan_config: GoalPlanRuntimeConfigHandle, + ) -> Self { + Self { + kind: GoalToolKind::UpdatePlanNode, + thread_id, + state_db, + accounting_state, + event_emitter, + metrics, + plan_config: Some(plan_config), + } + } + + pub(crate) fn insert_plan_node( + thread_id: ThreadId, + state_db: Arc, + accounting_state: Arc, + event_emitter: GoalEventEmitter, + metrics: GoalMetrics, + plan_config: GoalPlanRuntimeConfigHandle, + ) -> Self { + Self { + kind: GoalToolKind::InsertPlanNode, + thread_id, + state_db, + accounting_state, + event_emitter, + metrics, + plan_config: Some(plan_config), + } + } + + pub(crate) fn set_plan_node_status( + thread_id: ThreadId, + state_db: Arc, + accounting_state: Arc, + event_emitter: GoalEventEmitter, + metrics: GoalMetrics, + plan_config: GoalPlanRuntimeConfigHandle, + ) -> Self { + Self { + kind: GoalToolKind::SetPlanNodeStatus, + thread_id, + state_db, + accounting_state, + event_emitter, + metrics, + plan_config: Some(plan_config), + } + } + pub(crate) fn update( thread_id: ThreadId, state_db: Arc, @@ -296,6 +362,9 @@ impl ToolExecutor for GoalToolExecutor { GoalToolKind::GetPlan => GET_GOAL_PLAN_TOOL_NAME, GoalToolKind::CreatePlan => CREATE_GOAL_PLAN_TOOL_NAME, GoalToolKind::ActivatePlanNode => ACTIVATE_GOAL_PLAN_NODE_TOOL_NAME, + GoalToolKind::UpdatePlanNode => UPDATE_GOAL_PLAN_NODE_TOOL_NAME, + GoalToolKind::InsertPlanNode => INSERT_GOAL_PLAN_NODE_TOOL_NAME, + GoalToolKind::SetPlanNodeStatus => SET_GOAL_PLAN_NODE_STATUS_TOOL_NAME, GoalToolKind::Update => UPDATE_GOAL_TOOL_NAME, GoalToolKind::Pause => PAUSE_GOAL_TOOL_NAME, GoalToolKind::Resume => RESUME_GOAL_TOOL_NAME, @@ -309,6 +378,9 @@ impl ToolExecutor for GoalToolExecutor { GoalToolKind::GetPlan => create_get_goal_plan_tool(), GoalToolKind::CreatePlan => create_create_goal_plan_tool(), GoalToolKind::ActivatePlanNode => create_activate_goal_plan_node_tool(), + GoalToolKind::UpdatePlanNode => create_update_goal_plan_node_tool(), + GoalToolKind::InsertPlanNode => create_insert_goal_plan_node_tool(), + GoalToolKind::SetPlanNodeStatus => create_set_goal_plan_node_status_tool(), GoalToolKind::Update => create_update_goal_tool(), GoalToolKind::Pause => create_pause_goal_tool(), GoalToolKind::Resume => create_resume_goal_tool(), @@ -322,6 +394,9 @@ impl ToolExecutor for GoalToolExecutor { GoalToolKind::GetPlan => self.handle_get_plan(invocation).await, GoalToolKind::CreatePlan => self.handle_create_plan(invocation).await, GoalToolKind::ActivatePlanNode => self.handle_activate_plan_node(invocation).await, + GoalToolKind::UpdatePlanNode => self.handle_update_plan_node(invocation).await, + GoalToolKind::InsertPlanNode => self.handle_insert_plan_node(invocation).await, + GoalToolKind::SetPlanNodeStatus => self.handle_set_plan_node_status(invocation).await, GoalToolKind::Update => self.handle_update(invocation).await, GoalToolKind::Pause => self.handle_pause(invocation).await, GoalToolKind::Resume => self.handle_resume(invocation).await, diff --git a/codex-rs/ext/goal/src/tool_plan.rs b/codex-rs/ext/goal/src/tool_plan.rs index b55b1589d..82ed96f75 100644 --- a/codex-rs/ext/goal/src/tool_plan.rs +++ b/codex-rs/ext/goal/src/tool_plan.rs @@ -76,6 +76,63 @@ struct ActivateGoalPlanNodeRequest { node_id: String, } +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +#[serde(rename_all = "snake_case")] +struct UpdateGoalPlanNodeRequest { + node_id: String, + key: Option, + objective: Option, + title: Option, + #[serde(default)] + clear_title: bool, + priority: Option, + token_budget: Option, + #[serde(default)] + clear_token_budget: bool, + depends_on: Option>, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +#[serde(rename_all = "snake_case")] +struct InsertGoalPlanNodeRequest { + plan_id: String, + position: InsertGoalPlanNodePosition, + reference_node_id: Option, + key: String, + objective: String, + title: Option, + #[serde(default)] + priority: Option, + token_budget: Option, + #[serde(default)] + depends_on: Vec, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "snake_case")] +enum InsertGoalPlanNodePosition { + Before, + After, + End, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +#[serde(rename_all = "snake_case")] +struct SetGoalPlanNodeStatusRequest { + node_id: String, + status: SetGoalPlanNodeStatus, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "snake_case")] +enum SetGoalPlanNodeStatus { + Complete, + Pending, +} + #[derive(Debug, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub(crate) struct GoalPlanResponse { @@ -432,6 +489,194 @@ impl GoalToolExecutor { ) } + pub(crate) async fn handle_update_plan_node( + &self, + invocation: ToolCall, + ) -> Result, FunctionCallError> { + let request: UpdateGoalPlanNodeRequest = parse_arguments(invocation.function_arguments()?)?; + if request.clear_title && request.title.is_some() { + return Err(FunctionCallError::RespondToModel( + "cannot set title and clear_title in the same update".to_string(), + )); + } + if request.clear_token_budget && request.token_budget.is_some() { + return Err(FunctionCallError::RespondToModel( + "cannot set token_budget and clear_token_budget in the same update".to_string(), + )); + } + let outcome = self + .state_db + .thread_goals() + .update_thread_goal_plan_node(codex_state::ThreadGoalPlanNodeUpdateParams { + thread_id: self.thread_id, + node_id: request.node_id, + key: request.key, + objective: request.objective, + title: match request.title { + Some(title) => codex_state::ThreadGoalPlanNodeTitleUpdate::Set(Some(title)), + None if request.clear_title => { + codex_state::ThreadGoalPlanNodeTitleUpdate::Set(None) + } + None => codex_state::ThreadGoalPlanNodeTitleUpdate::Keep, + }, + priority: request.priority, + token_budget: match request.token_budget { + Some(token_budget) => { + codex_state::ThreadGoalPlanNodeTokenBudgetUpdate::Set(Some(token_budget)) + } + None if request.clear_token_budget => { + codex_state::ThreadGoalPlanNodeTokenBudgetUpdate::Set(None) + } + None => codex_state::ThreadGoalPlanNodeTokenBudgetUpdate::Keep, + }, + depends_on: request.depends_on, + }) + .await + .map_err(|err| { + FunctionCallError::RespondToModel(format!("failed to update goal plan node: {err}")) + })?; + let updated_goal = outcome.goal.clone().map(protocol_goal_from_state); + if let Some(goal) = updated_goal.as_ref() { + self.event_emitter.thread_goal_updated( + format!("{}:updated-goal", invocation.call_id), + Some(invocation.turn_id.clone()), + goal.clone(), + ); + } + let goal = match updated_goal { + Some(goal) => Some(goal), + None => self.current_goal_response().await?, + }; + self.event_emitter.thread_goal_plan_updated( + format!("{}-goal-plan", invocation.call_id), + Some(invocation.turn_id.clone()), + outcome.snapshot.clone(), + ); + goal_response_with_plan( + goal, + /*activated_goal*/ None, + vec![GoalPlanResponse::from_snapshot_for_thread( + outcome.snapshot, + self.thread_id, + self.plan_node_objective_char_limit(), + )], + CompletionBudgetReport::Omit, + ) + } + + pub(crate) async fn handle_insert_plan_node( + &self, + invocation: ToolCall, + ) -> Result, FunctionCallError> { + let request: InsertGoalPlanNodeRequest = parse_arguments(invocation.function_arguments()?)?; + let position = insert_goal_plan_node_position(request.position, request.reference_node_id)?; + let outcome = self + .state_db + .thread_goals() + .insert_thread_goal_plan_node(codex_state::ThreadGoalPlanNodeInsertParams { + thread_id: self.thread_id, + plan_id: request.plan_id, + position, + key: request.key, + objective: request.objective, + title: request.title, + priority: request.priority.unwrap_or(0), + token_budget: request.token_budget, + depends_on: request.depends_on, + }) + .await + .map_err(|err| { + FunctionCallError::RespondToModel(format!("failed to insert goal plan node: {err}")) + })?; + self.event_emitter.thread_goal_plan_updated( + format!("{}-goal-plan", invocation.call_id), + Some(invocation.turn_id.clone()), + outcome.snapshot.clone(), + ); + let goal = self.current_goal_response().await?; + goal_response_with_plan( + goal, + /*activated_goal*/ None, + vec![GoalPlanResponse::from_snapshot_for_thread( + outcome.snapshot, + self.thread_id, + self.plan_node_objective_char_limit(), + )], + CompletionBudgetReport::Omit, + ) + } + + pub(crate) async fn handle_set_plan_node_status( + &self, + invocation: ToolCall, + ) -> Result, FunctionCallError> { + let request: SetGoalPlanNodeStatusRequest = + parse_arguments(invocation.function_arguments()?)?; + let outcome = self + .state_db + .thread_goals() + .set_thread_goal_plan_node_status(codex_state::ThreadGoalPlanNodeStatusUpdateParams { + thread_id: self.thread_id, + node_id: request.node_id, + status: set_goal_plan_node_status(request.status), + auto_execute: self + .plan_config + .as_ref() + .ok_or_else(|| { + FunctionCallError::Fatal( + "goal plan tool missing runtime config".to_string(), + ) + })? + .current() + .auto_execute, + }) + .await + .map_err(|err| { + FunctionCallError::RespondToModel(format!( + "failed to update goal plan node status: {err}" + )) + })?; + let goal = outcome.goal.clone().map(protocol_goal_from_state); + if let Some(goal) = goal.as_ref() { + self.event_emitter.thread_goal_updated( + format!("{}:updated-goal", invocation.call_id), + Some(invocation.turn_id.clone()), + goal.clone(), + ); + } + let activated_goal = self + .apply_activated_goal_from_plan(&invocation, outcome.activated_goal) + .await?; + self.event_emitter.thread_goal_plan_updated( + format!("{}-goal-plan", invocation.call_id), + Some(invocation.turn_id.clone()), + outcome.snapshot.clone(), + ); + let goal = match goal.or_else(|| activated_goal.clone()) { + Some(goal) => Some(goal), + None => self.current_goal_response().await?, + }; + goal_response_with_plan( + goal, + activated_goal.clone(), + vec![GoalPlanResponse::from_snapshot_for_thread( + outcome.snapshot, + self.thread_id, + self.plan_node_objective_char_limit(), + )], + CompletionBudgetReport::Omit, + ) + } + + async fn current_goal_response(&self) -> Result, FunctionCallError> { + self.state_db + .thread_goals() + .get_thread_goal(self.thread_id) + .await + .map(|goal| goal.map(protocol_goal_from_state)) + .map_err(|err| FunctionCallError::RespondToModel(format!("failed to read goal: {err}"))) + } + pub(crate) async fn apply_activated_goal_from_plan( &self, invocation: &ToolCall, @@ -547,6 +792,46 @@ impl GoalToolExecutor { } } +fn insert_goal_plan_node_position( + position: InsertGoalPlanNodePosition, + reference_node_id: Option, +) -> Result { + match position { + InsertGoalPlanNodePosition::Before => { + let reference_node_id = reference_node_id.ok_or_else(|| { + FunctionCallError::RespondToModel( + "reference_node_id is required when position is before".to_string(), + ) + })?; + Ok(codex_state::ThreadGoalPlanNodeInsertPosition::Before( + reference_node_id, + )) + } + InsertGoalPlanNodePosition::After => { + let reference_node_id = reference_node_id.ok_or_else(|| { + FunctionCallError::RespondToModel( + "reference_node_id is required when position is after".to_string(), + ) + })?; + Ok(codex_state::ThreadGoalPlanNodeInsertPosition::After( + reference_node_id, + )) + } + InsertGoalPlanNodePosition::End => Ok(codex_state::ThreadGoalPlanNodeInsertPosition::End), + } +} + +fn set_goal_plan_node_status( + status: SetGoalPlanNodeStatus, +) -> codex_state::ThreadGoalPlanNodeCompletionStatus { + match status { + SetGoalPlanNodeStatus::Complete => { + codex_state::ThreadGoalPlanNodeCompletionStatus::Complete + } + SetGoalPlanNodeStatus::Pending => codex_state::ThreadGoalPlanNodeCompletionStatus::Pending, + } +} + fn flatten_goal_plan_request( request: &mut CreateGoalPlanRequest, plan_config: GoalPlanRuntimeConfig, diff --git a/codex-rs/ext/goal/tests/goal_extension_backend.rs b/codex-rs/ext/goal/tests/goal_extension_backend.rs index 0eea812b7..98bd51b3d 100644 --- a/codex-rs/ext/goal/tests/goal_extension_backend.rs +++ b/codex-rs/ext/goal/tests/goal_extension_backend.rs @@ -149,6 +149,9 @@ async fn installed_goal_tools_include_resume_goal() -> anyhow::Result<()> { "create_goal".to_string(), "create_goal_plan".to_string(), "activate_goal_plan_node".to_string(), + "update_goal_plan_node".to_string(), + "insert_goal_plan_node".to_string(), + "set_goal_plan_node_status".to_string(), "update_goal".to_string(), "pause_goal".to_string(), "resume_goal".to_string(), @@ -584,6 +587,168 @@ async fn create_goal_plan_rejects_nesting_beyond_three_levels() -> anyhow::Resul Ok(()) } +#[tokio::test] +async fn goal_plan_edit_tools_update_insert_and_mark_done_undone() -> anyhow::Result<()> { + let runtime = test_runtime().await?; + let thread_id = test_thread_id()?; + seed_thread_metadata(runtime.as_ref(), thread_id).await?; + let harness = GoalExtensionHarness::new(runtime, thread_id).await?; + harness.start_turn("turn-1", &TokenUsage::default()).await; + let tools = harness.tools(); + + let create_plan_tool = tool_by_name(&tools, "create_goal_plan"); + let invocation = tool_call( + "create_goal_plan", + "call-create-goal-plan", + json!({ + "goals": [ + { + "key": "first", + "objective": "Run the first goal" + }, + { + "key": "second", + "objective": "Run the second goal", + "depends_on": ["first"] + } + ] + }), + ); + let output = create_plan_tool.handle(invocation.clone()).await?; + let result = output.code_mode_result(&invocation.payload); + let plan_id = result["goalPlans"][0]["planId"] + .as_str() + .ok_or_else(|| anyhow::anyhow!("created plan id should be returned"))? + .to_string(); + let first_node_id = result["goalPlans"][0]["nodes"][0]["nodeId"] + .as_str() + .ok_or_else(|| anyhow::anyhow!("first node id should be returned"))? + .to_string(); + let second_node_id = result["goalPlans"][0]["nodes"][1]["nodeId"] + .as_str() + .ok_or_else(|| anyhow::anyhow!("second node id should be returned"))? + .to_string(); + + let update_node_tool = tool_by_name(&tools, "update_goal_plan_node"); + let invocation = tool_call( + "update_goal_plan_node", + "call-update-node", + json!({ + "node_id": first_node_id, + "key": "prep", + "objective": "Prepare edited objective", + "title": "Prepare", + "priority": 7, + "token_budget": 25 + }), + ); + let output = update_node_tool.handle(invocation.clone()).await?; + let result = output.code_mode_result(&invocation.payload); + assert_eq!(result["goal"]["objective"], "Prepare edited objective"); + assert_eq!(result["goalPlans"][0]["nodes"][0]["key"], "prep"); + assert_eq!(result["goalPlans"][0]["nodes"][0]["title"], "Prepare"); + assert_eq!(result["goalPlans"][0]["nodes"][1]["dependsOn"][0], "prep"); + + let invocation = tool_call( + "update_goal_plan_node", + "call-clear-node-fields", + json!({ + "node_id": first_node_id, + "clear_title": true, + "clear_token_budget": true + }), + ); + let output = update_node_tool.handle(invocation.clone()).await?; + let result = output.code_mode_result(&invocation.payload); + assert_eq!(result["goal"]["title"], serde_json::Value::Null); + assert_eq!(result["goal"]["tokenBudget"], serde_json::Value::Null); + assert_eq!( + result["goalPlans"][0]["nodes"][0]["title"], + serde_json::Value::Null + ); + assert_eq!( + result["goalPlans"][0]["nodes"][0]["tokenBudget"], + serde_json::Value::Null + ); + + let insert_node_tool = tool_by_name(&tools, "insert_goal_plan_node"); + let invocation = tool_call( + "insert_goal_plan_node", + "call-insert-node", + json!({ + "plan_id": plan_id, + "position": "before", + "reference_node_id": second_node_id, + "key": "middle", + "objective": "Run the middle goal", + "title": "Middle", + "depends_on": ["prep"] + }), + ); + let output = insert_node_tool.handle(invocation.clone()).await?; + let result = output.code_mode_result(&invocation.payload); + assert_eq!(result["goal"]["objective"], "Prepare edited objective"); + assert_eq!( + vec!["prep", "middle", "second"], + result["goalPlans"][0]["nodes"] + .as_array() + .ok_or_else(|| anyhow::anyhow!("goal plan nodes should be an array"))? + .iter() + .map(|node| node["key"].as_str().unwrap_or_default()) + .collect::>() + ); + + let set_status_tool = tool_by_name(&tools, "set_goal_plan_node_status"); + let invocation = tool_call( + "set_goal_plan_node_status", + "call-complete-node", + json!({ + "node_id": first_node_id, + "status": "complete" + }), + ); + let output = set_status_tool.handle(invocation.clone()).await?; + let result = output.code_mode_result(&invocation.payload); + assert_eq!(result["goal"]["status"], "complete"); + assert_eq!(result["activatedGoal"]["objective"], "Run the middle goal"); + assert_eq!(result["goalPlans"][0]["completedNodeCount"], 1); + + let err = match update_node_tool + .handle(tool_call( + "update_goal_plan_node", + "call-edit-complete-node", + json!({ + "node_id": first_node_id, + "objective": "This edit should fail" + }), + )) + .await + { + Ok(_) => panic!("editing a complete node should fail"), + Err(err) => err, + }; + assert!( + err.to_string().contains("mark it undone first"), + "unexpected error: {err}" + ); + + let invocation = tool_call( + "set_goal_plan_node_status", + "call-mark-node-pending", + json!({ + "node_id": first_node_id, + "status": "pending" + }), + ); + let output = set_status_tool.handle(invocation.clone()).await?; + let result = output.code_mode_result(&invocation.payload); + assert_eq!(result["goal"]["objective"], "Run the middle goal"); + assert_eq!(result["goal"]["status"], "active"); + assert_eq!(result["goalPlans"][0]["nodes"][0]["status"], "pending"); + assert_eq!(result["goalPlans"][0]["completedNodeCount"], 0); + Ok(()) +} + #[tokio::test] async fn create_goal_tools_persist_context_lifecycle_actions() -> anyhow::Result<()> { let runtime = test_runtime().await?; @@ -1638,6 +1803,9 @@ async fn installed_goal_tools_require_adversarial_verification_before_completion "create_goal", "create_goal_plan", "activate_goal_plan_node", + "update_goal_plan_node", + "insert_goal_plan_node", + "set_goal_plan_node_status", "update_goal", "resume_goal", ] { diff --git a/codex-rs/state/src/lib.rs b/codex-rs/state/src/lib.rs index c28be23ef..36b7d5b7c 100644 --- a/codex-rs/state/src/lib.rs +++ b/codex-rs/state/src/lib.rs @@ -226,8 +226,17 @@ pub use runtime::ThreadGoalPlanAdvanceOutcome; pub use runtime::ThreadGoalPlanAppendParams; pub use runtime::ThreadGoalPlanCreateParams; pub use runtime::ThreadGoalPlanListPage; +pub use runtime::ThreadGoalPlanNodeCompletionStatus; pub use runtime::ThreadGoalPlanNodeCreateParams; pub use runtime::ThreadGoalPlanNodeHierarchyParams; +pub use runtime::ThreadGoalPlanNodeInsertOutcome; +pub use runtime::ThreadGoalPlanNodeInsertParams; +pub use runtime::ThreadGoalPlanNodeInsertPosition; +pub use runtime::ThreadGoalPlanNodeMutationOutcome; +pub use runtime::ThreadGoalPlanNodeStatusUpdateParams; +pub use runtime::ThreadGoalPlanNodeTitleUpdate; +pub use runtime::ThreadGoalPlanNodeTokenBudgetUpdate; +pub use runtime::ThreadGoalPlanNodeUpdateParams; pub use runtime::ThreadMonitorCreateParams; pub use runtime::ThreadMonitorEventCreateParams; pub use runtime::ThreadMonitorUpdate; diff --git a/codex-rs/state/src/runtime.rs b/codex-rs/state/src/runtime.rs index b91ef5608..111c2b1c1 100644 --- a/codex-rs/state/src/runtime.rs +++ b/codex-rs/state/src/runtime.rs @@ -101,6 +101,7 @@ use tracing::warn; mod agent_jobs; mod backfill; mod background_agents; +mod goal_plan_edits; mod goal_plans; mod goals; mod local_active_sessions; @@ -170,6 +171,15 @@ pub(crate) fn redact_state_json_string(value: &Value) -> anyhow::Result crate::redacted_local_state_json_string(value) } +pub use goal_plan_edits::ThreadGoalPlanNodeCompletionStatus; +pub use goal_plan_edits::ThreadGoalPlanNodeInsertOutcome; +pub use goal_plan_edits::ThreadGoalPlanNodeInsertParams; +pub use goal_plan_edits::ThreadGoalPlanNodeInsertPosition; +pub use goal_plan_edits::ThreadGoalPlanNodeMutationOutcome; +pub use goal_plan_edits::ThreadGoalPlanNodeStatusUpdateParams; +pub use goal_plan_edits::ThreadGoalPlanNodeTitleUpdate; +pub use goal_plan_edits::ThreadGoalPlanNodeTokenBudgetUpdate; +pub use goal_plan_edits::ThreadGoalPlanNodeUpdateParams; pub use goal_plans::DEFAULT_THREAD_GOAL_PLAN_LIST_LIMIT; pub use goal_plans::MAX_THREAD_GOAL_PLAN_LIST_LIMIT; pub use goal_plans::ThreadGoalPlanAddOutcome; diff --git a/codex-rs/state/src/runtime/goal_plan_edits.rs b/codex-rs/state/src/runtime/goal_plan_edits.rs new file mode 100644 index 000000000..be61f8ab3 --- /dev/null +++ b/codex-rs/state/src/runtime/goal_plan_edits.rs @@ -0,0 +1,1142 @@ +use super::*; +use crate::runtime::goal_plans::activate_next_ready_node_in_tx; +use crate::runtime::goal_plans::recalculate_goal_plan_status_in_tx; +use crate::runtime::goal_plans::snapshot_thread_goal_plan_in_tx; +use crate::runtime::goal_plans::thread_goal_from_row; +use crate::runtime::goal_plans::validate_plan_create_params; +use codex_protocol::protocol::normalize_thread_goal_title; +use std::collections::HashMap; +use uuid::Uuid; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum ThreadGoalPlanNodeTitleUpdate { + Keep, + Set(Option), +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum ThreadGoalPlanNodeTokenBudgetUpdate { + Keep, + Set(Option), +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ThreadGoalPlanNodeUpdateParams { + pub thread_id: ThreadId, + pub node_id: String, + pub key: Option, + pub objective: Option, + pub title: ThreadGoalPlanNodeTitleUpdate, + pub priority: Option, + pub token_budget: ThreadGoalPlanNodeTokenBudgetUpdate, + pub depends_on: Option>, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum ThreadGoalPlanNodeInsertPosition { + Before(String), + After(String), + End, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ThreadGoalPlanNodeInsertParams { + pub thread_id: ThreadId, + pub plan_id: String, + pub position: ThreadGoalPlanNodeInsertPosition, + pub key: String, + pub objective: String, + pub title: Option, + pub priority: i64, + pub token_budget: Option, + pub depends_on: Vec, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ThreadGoalPlanNodeCompletionStatus { + Complete, + Pending, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ThreadGoalPlanNodeStatusUpdateParams { + pub thread_id: ThreadId, + pub node_id: String, + pub status: ThreadGoalPlanNodeCompletionStatus, + pub auto_execute: crate::ThreadGoalPlanAutoExecute, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ThreadGoalPlanNodeMutationOutcome { + pub snapshot: crate::ThreadGoalPlanSnapshot, + pub node: crate::ThreadGoalPlanNode, + pub goal: Option, + pub activated_goal: Option, + pub cleared_goal: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ThreadGoalPlanNodeInsertOutcome { + pub snapshot: crate::ThreadGoalPlanSnapshot, + pub inserted_node: crate::ThreadGoalPlanNode, +} + +impl GoalStore { + pub async fn update_thread_goal_plan_node( + &self, + params: ThreadGoalPlanNodeUpdateParams, + ) -> anyhow::Result { + let now_ms = datetime_to_epoch_millis(Utc::now()); + let mut tx = self.pool.begin().await?; + let outcome = update_thread_goal_plan_node_in_tx(&mut tx, params, now_ms).await?; + tx.commit().await?; + Ok(outcome) + } + + pub async fn insert_thread_goal_plan_node( + &self, + params: ThreadGoalPlanNodeInsertParams, + ) -> anyhow::Result { + let now_ms = datetime_to_epoch_millis(Utc::now()); + let mut tx = self.pool.begin().await?; + let outcome = insert_thread_goal_plan_node_in_tx(&mut tx, params, now_ms).await?; + tx.commit().await?; + Ok(outcome) + } + + pub async fn set_thread_goal_plan_node_status( + &self, + params: ThreadGoalPlanNodeStatusUpdateParams, + ) -> anyhow::Result { + let now_ms = datetime_to_epoch_millis(Utc::now()); + let mut tx = self.pool.begin().await?; + let outcome = set_thread_goal_plan_node_status_in_tx(&mut tx, params, now_ms).await?; + tx.commit().await?; + Ok(outcome) + } +} + +async fn update_thread_goal_plan_node_in_tx( + tx: &mut sqlx::Transaction<'_, Sqlite>, + params: ThreadGoalPlanNodeUpdateParams, + now_ms: i64, +) -> anyhow::Result { + let node_id = require_non_empty_id(params.node_id.trim(), "goal plan node id")?; + let plan_id = plan_id_for_node_in_tx(tx, node_id).await?; + let snapshot = owned_plan_snapshot_in_tx(tx, params.thread_id, &plan_id).await?; + if snapshot.plan.status == crate::ThreadGoalPlanStatus::Cancelled { + anyhow::bail!("cannot edit a node in a cancelled goal plan"); + } + let node_index = snapshot_node_index(&snapshot, node_id)?; + let existing_node = snapshot.nodes[node_index].clone(); + if existing_node.status == crate::ThreadGoalPlanNodeStatus::Complete { + anyhow::bail!("cannot edit a completed goal-plan node; mark it undone first"); + } + if existing_node.status == crate::ThreadGoalPlanNodeStatus::Cancelled { + anyhow::bail!("cannot edit a cancelled goal-plan node"); + } + + let mut updated_node = existing_node.clone(); + if let Some(key) = params.key { + updated_node.key = key.trim().to_string(); + } + if let Some(objective) = params.objective { + updated_node.objective = objective.trim().to_string(); + } + if let ThreadGoalPlanNodeTitleUpdate::Set(title) = params.title { + updated_node.title = + normalize_thread_goal_title(title.as_deref()).map_err(anyhow::Error::msg)?; + } + if let Some(priority) = params.priority { + updated_node.priority = priority; + } + if let ThreadGoalPlanNodeTokenBudgetUpdate::Set(token_budget) = params.token_budget { + updated_node.token_budget = token_budget; + } + let updating_dependencies = params.depends_on.is_some(); + if let Some(depends_on) = params.depends_on { + updated_node.depends_on = normalize_dependency_keys(depends_on); + } + + let validation_nodes = + validation_nodes_with_replacement(&snapshot, node_index, &existing_node.key, &updated_node); + validate_plan_create_params(&ThreadGoalPlanCreateParams { + thread_id: snapshot.plan.thread_id, + auto_execute: snapshot.plan.auto_execute, + max_tokens: snapshot.plan.max_tokens, + nodes: validation_nodes, + })?; + + sqlx::query( + r#" +UPDATE thread_goal_plan_nodes +SET + key = ?, + objective = ?, + title = ?, + priority = ?, + token_budget = ?, + updated_at_ms = ? +WHERE node_id = ? + "#, + ) + .bind(&updated_node.key) + .bind(&updated_node.objective) + .bind(&updated_node.title) + .bind(updated_node.priority) + .bind(updated_node.token_budget) + .bind(now_ms) + .bind(&updated_node.node_id) + .execute(&mut **tx) + .await?; + + if updating_dependencies { + replace_node_dependencies_in_tx(tx, &updated_node, &snapshot.nodes).await?; + } + + let goal = update_projected_goal_metadata_in_tx(tx, &updated_node, now_ms).await?; + recalculate_goal_plan_status_in_tx(tx, &snapshot.plan.plan_id, now_ms).await?; + let snapshot = snapshot_thread_goal_plan_in_tx(tx, &snapshot.plan.plan_id).await?; + let node = snapshot_node(&snapshot, node_id)?; + Ok(ThreadGoalPlanNodeMutationOutcome { + snapshot, + node, + goal, + activated_goal: None, + cleared_goal: false, + }) +} + +async fn insert_thread_goal_plan_node_in_tx( + tx: &mut sqlx::Transaction<'_, Sqlite>, + params: ThreadGoalPlanNodeInsertParams, + now_ms: i64, +) -> anyhow::Result { + let plan_id = require_non_empty_id(params.plan_id.trim(), "goal plan id")?; + let snapshot = owned_plan_snapshot_in_tx(tx, params.thread_id, plan_id).await?; + if snapshot.plan.status == crate::ThreadGoalPlanStatus::Cancelled { + anyhow::bail!("cannot insert a node into a cancelled goal plan"); + } + if snapshot.nodes.len() >= super::goal_plans::MAX_GOAL_PLAN_NODES { + anyhow::bail!( + "goal plan contains {} goals but the maximum is {}", + snapshot.nodes.len(), + super::goal_plans::MAX_GOAL_PLAN_NODES + ); + } + + // An inserted node becomes a sibling of the node it is positioned against, + // so it inherits that node's place in the hierarchy introduced by nested + // goal plans. Appending at the end always produces a top-level node. + let (insert_sequence, parent_node_id, nesting_depth) = match ¶ms.position { + ThreadGoalPlanNodeInsertPosition::Before(reference_node_id) => { + let reference = reference_node(&snapshot, reference_node_id.trim())?; + ( + reference.sequence, + reference.parent_node_id, + reference.nesting_depth, + ) + } + ThreadGoalPlanNodeInsertPosition::After(reference_node_id) => { + let reference = reference_node(&snapshot, reference_node_id.trim())?; + ( + reference.sequence + 1, + reference.parent_node_id, + reference.nesting_depth, + ) + } + ThreadGoalPlanNodeInsertPosition::End => ( + snapshot + .nodes + .iter() + .map(|node| node.sequence) + .max() + .unwrap_or(-1) + + 1, + None, + 1, + ), + }; + + let title = normalize_thread_goal_title(params.title.as_deref()).map_err(anyhow::Error::msg)?; + let inserted_node_id = Uuid::new_v4().to_string(); + let inserted_node = ThreadGoalPlanNodeCreateParams { + key: params.key.trim().to_string(), + objective: params.objective.trim().to_string(), + assigned_thread_id: Some(params.thread_id), + title, + priority: params.priority, + token_budget: params.token_budget, + depends_on: normalize_dependency_keys(params.depends_on), + }; + let mut validation_nodes = snapshot_nodes_as_create_params(&snapshot.nodes); + validation_nodes.push(inserted_node.clone()); + validate_plan_create_params(&ThreadGoalPlanCreateParams { + thread_id: snapshot.plan.thread_id, + auto_execute: snapshot.plan.auto_execute, + max_tokens: snapshot.plan.max_tokens, + nodes: validation_nodes, + })?; + + sqlx::query( + r#" +UPDATE thread_goal_plan_nodes +SET sequence = sequence + 1, updated_at_ms = ? +WHERE plan_id = ? + AND sequence >= ? + "#, + ) + .bind(now_ms) + .bind(plan_id) + .bind(insert_sequence) + .execute(&mut **tx) + .await?; + + sqlx::query( + r#" +INSERT INTO thread_goal_plan_nodes ( + node_id, + plan_id, + thread_id, + assigned_thread_id, + parent_node_id, + nesting_depth, + key, + sequence, + priority, + objective, + title, + status, + token_budget, + created_at_ms, + updated_at_ms +) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + "#, + ) + .bind(&inserted_node_id) + .bind(plan_id) + .bind(snapshot.plan.thread_id.to_string()) + .bind(params.thread_id.to_string()) + .bind(parent_node_id.clone()) + .bind(nesting_depth) + .bind(&inserted_node.key) + .bind(insert_sequence) + .bind(inserted_node.priority) + .bind(&inserted_node.objective) + .bind(&inserted_node.title) + .bind(crate::ThreadGoalPlanNodeStatus::Pending.as_str()) + .bind(inserted_node.token_budget) + .bind(now_ms) + .bind(now_ms) + .execute(&mut **tx) + .await?; + + let inserted_state_node = crate::ThreadGoalPlanNode { + node_id: inserted_node_id.clone(), + plan_id: plan_id.to_string(), + thread_id: snapshot.plan.thread_id, + assigned_thread_id: params.thread_id, + parent_node_id, + nesting_depth, + key: inserted_node.key, + sequence: insert_sequence, + priority: inserted_node.priority, + objective: inserted_node.objective, + title: inserted_node.title, + status: crate::ThreadGoalPlanNodeStatus::Pending, + token_budget: inserted_node.token_budget, + tokens_used: 0, + time_used_seconds: 0, + projected_goal_id: None, + depends_on: inserted_node.depends_on, + created_at: Utc::now(), + updated_at: Utc::now(), + }; + replace_node_dependencies_in_tx(tx, &inserted_state_node, &snapshot.nodes).await?; + recalculate_goal_plan_status_in_tx(tx, plan_id, now_ms).await?; + let snapshot = snapshot_thread_goal_plan_in_tx(tx, plan_id).await?; + let inserted_node = snapshot_node(&snapshot, &inserted_node_id)?; + Ok(ThreadGoalPlanNodeInsertOutcome { + snapshot, + inserted_node, + }) +} + +async fn set_thread_goal_plan_node_status_in_tx( + tx: &mut sqlx::Transaction<'_, Sqlite>, + params: ThreadGoalPlanNodeStatusUpdateParams, + now_ms: i64, +) -> anyhow::Result { + let node_id = require_non_empty_id(params.node_id.trim(), "goal plan node id")?; + let plan_id = plan_id_for_node_in_tx(tx, node_id).await?; + let snapshot = owned_plan_snapshot_in_tx(tx, params.thread_id, &plan_id).await?; + if snapshot.plan.status == crate::ThreadGoalPlanStatus::Cancelled { + anyhow::bail!("cannot update a node in a cancelled goal plan"); + } + let node = snapshot_node(&snapshot, node_id)?; + match params.status { + ThreadGoalPlanNodeCompletionStatus::Complete => { + mark_goal_plan_node_complete_in_tx(tx, params, &snapshot, node, now_ms).await + } + ThreadGoalPlanNodeCompletionStatus::Pending => { + mark_goal_plan_node_pending_in_tx(tx, &snapshot, node, now_ms).await + } + } +} + +async fn mark_goal_plan_node_complete_in_tx( + tx: &mut sqlx::Transaction<'_, Sqlite>, + params: ThreadGoalPlanNodeStatusUpdateParams, + snapshot: &crate::ThreadGoalPlanSnapshot, + node: crate::ThreadGoalPlanNode, + now_ms: i64, +) -> anyhow::Result { + if node.status == crate::ThreadGoalPlanNodeStatus::Complete { + return Ok(ThreadGoalPlanNodeMutationOutcome { + snapshot: snapshot.clone(), + node, + goal: None, + activated_goal: None, + cleared_goal: false, + }); + } + if node.status == crate::ThreadGoalPlanNodeStatus::Cancelled { + anyhow::bail!("cannot mark a cancelled goal-plan node complete"); + } + + let goal = mark_projected_goal_complete_in_tx(tx, &node, now_ms).await?; + let tokens_used = goal + .as_ref() + .map_or(node.tokens_used, |goal| goal.tokens_used); + let time_used_seconds = goal + .as_ref() + .map_or(node.time_used_seconds, |goal| goal.time_used_seconds); + sqlx::query( + r#" +UPDATE thread_goal_plan_nodes +SET + status = ?, + tokens_used = ?, + time_used_seconds = ?, + updated_at_ms = ? +WHERE node_id = ? + "#, + ) + .bind(crate::ThreadGoalPlanNodeStatus::Complete.as_str()) + .bind(tokens_used) + .bind(time_used_seconds) + .bind(now_ms) + .bind(&node.node_id) + .execute(&mut **tx) + .await?; + + recalculate_goal_plan_status_in_tx(tx, &node.plan_id, now_ms).await?; + let should_auto_advance = matches!( + node.status, + crate::ThreadGoalPlanNodeStatus::Active + | crate::ThreadGoalPlanNodeStatus::Paused + | crate::ThreadGoalPlanNodeStatus::Blocked + | crate::ThreadGoalPlanNodeStatus::UsageLimited + | crate::ThreadGoalPlanNodeStatus::BudgetLimited + | crate::ThreadGoalPlanNodeStatus::Deferred + ); + let activated_goal = if should_auto_advance { + activate_next_ready_node_in_tx( + tx, + params.thread_id, + &node.plan_id, + params.auto_execute, + now_ms, + ) + .await? + } else { + None + }; + recalculate_goal_plan_status_in_tx(tx, &node.plan_id, now_ms).await?; + let snapshot = snapshot_thread_goal_plan_in_tx(tx, &node.plan_id).await?; + let node = snapshot_node(&snapshot, &node.node_id)?; + Ok(ThreadGoalPlanNodeMutationOutcome { + snapshot, + node, + goal, + activated_goal, + cleared_goal: false, + }) +} + +async fn mark_goal_plan_node_pending_in_tx( + tx: &mut sqlx::Transaction<'_, Sqlite>, + snapshot: &crate::ThreadGoalPlanSnapshot, + node: crate::ThreadGoalPlanNode, + now_ms: i64, +) -> anyhow::Result { + if node.status == crate::ThreadGoalPlanNodeStatus::Pending { + return Ok(ThreadGoalPlanNodeMutationOutcome { + snapshot: snapshot.clone(), + node, + goal: None, + activated_goal: None, + cleared_goal: false, + }); + } + if node.status != crate::ThreadGoalPlanNodeStatus::Complete { + anyhow::bail!("can only mark completed goal-plan nodes undone"); + } + + let cleared_goal = if let Some(projected_goal_id) = node.projected_goal_id.as_deref() { + sqlx::query( + r#" +DELETE FROM thread_goals +WHERE thread_id = ? + AND goal_id = ? + "#, + ) + .bind(node.assigned_thread_id.to_string()) + .bind(projected_goal_id) + .execute(&mut **tx) + .await? + .rows_affected() + > 0 + } else { + false + }; + + sqlx::query( + r#" +UPDATE thread_goal_plan_nodes +SET + status = ?, + projected_goal_id = NULL, + tokens_used = 0, + time_used_seconds = 0, + updated_at_ms = ? +WHERE node_id = ? + "#, + ) + .bind(crate::ThreadGoalPlanNodeStatus::Pending.as_str()) + .bind(now_ms) + .bind(&node.node_id) + .execute(&mut **tx) + .await?; + + recalculate_goal_plan_status_in_tx(tx, &node.plan_id, now_ms).await?; + let snapshot = snapshot_thread_goal_plan_in_tx(tx, &node.plan_id).await?; + let node = snapshot_node(&snapshot, &node.node_id)?; + Ok(ThreadGoalPlanNodeMutationOutcome { + snapshot, + node, + goal: None, + activated_goal: None, + cleared_goal, + }) +} + +async fn owned_plan_snapshot_in_tx( + tx: &mut sqlx::Transaction<'_, Sqlite>, + thread_id: ThreadId, + plan_id: &str, +) -> anyhow::Result { + let snapshot = snapshot_thread_goal_plan_in_tx(tx, plan_id).await?; + if snapshot.plan.thread_id != thread_id { + anyhow::bail!("goal plan {plan_id} does not belong to thread {thread_id}"); + } + Ok(snapshot) +} + +async fn plan_id_for_node_in_tx( + tx: &mut sqlx::Transaction<'_, Sqlite>, + node_id: &str, +) -> anyhow::Result { + sqlx::query_scalar( + r#" +SELECT plan_id +FROM thread_goal_plan_nodes +WHERE node_id = ? + "#, + ) + .bind(node_id) + .fetch_optional(&mut **tx) + .await? + .ok_or_else(|| anyhow::anyhow!("goal plan node `{node_id}` does not exist")) +} + +fn snapshot_node_index( + snapshot: &crate::ThreadGoalPlanSnapshot, + node_id: &str, +) -> anyhow::Result { + snapshot + .nodes + .iter() + .position(|node| node.node_id == node_id) + .ok_or_else(|| anyhow::anyhow!("goal plan node `{node_id}` does not exist")) +} + +fn snapshot_node( + snapshot: &crate::ThreadGoalPlanSnapshot, + node_id: &str, +) -> anyhow::Result { + snapshot + .nodes + .iter() + .find(|node| node.node_id == node_id) + .cloned() + .ok_or_else(|| anyhow::anyhow!("goal plan node `{node_id}` does not exist")) +} + +fn reference_node( + snapshot: &crate::ThreadGoalPlanSnapshot, + reference_node_id: &str, +) -> anyhow::Result { + require_non_empty_id(reference_node_id, "reference goal plan node id")?; + snapshot_node(snapshot, reference_node_id) +} + +fn require_non_empty_id<'a>(value: &'a str, label: &str) -> anyhow::Result<&'a str> { + if value.is_empty() { + anyhow::bail!("{label} must not be empty"); + } + Ok(value) +} + +fn normalize_dependency_keys(depends_on: Vec) -> Vec { + depends_on + .into_iter() + .map(|dependency| dependency.trim().to_string()) + .collect() +} + +fn validation_nodes_with_replacement( + snapshot: &crate::ThreadGoalPlanSnapshot, + node_index: usize, + old_key: &str, + updated_node: &crate::ThreadGoalPlanNode, +) -> Vec { + snapshot + .nodes + .iter() + .enumerate() + .map(|(index, node)| { + let source = if index == node_index { + updated_node + } else { + node + }; + let depends_on = source + .depends_on + .iter() + .map(|dependency| { + if dependency == old_key { + updated_node.key.clone() + } else { + dependency.clone() + } + }) + .collect(); + ThreadGoalPlanNodeCreateParams { + key: source.key.clone(), + objective: source.objective.clone(), + assigned_thread_id: Some(source.assigned_thread_id), + title: source.title.clone(), + priority: source.priority, + token_budget: source.token_budget, + depends_on, + } + }) + .collect() +} + +fn snapshot_nodes_as_create_params( + nodes: &[crate::ThreadGoalPlanNode], +) -> Vec { + nodes + .iter() + .map(|node| ThreadGoalPlanNodeCreateParams { + key: node.key.clone(), + objective: node.objective.clone(), + assigned_thread_id: Some(node.assigned_thread_id), + title: node.title.clone(), + priority: node.priority, + token_budget: node.token_budget, + depends_on: node.depends_on.clone(), + }) + .collect() +} + +async fn replace_node_dependencies_in_tx( + tx: &mut sqlx::Transaction<'_, Sqlite>, + node: &crate::ThreadGoalPlanNode, + existing_nodes: &[crate::ThreadGoalPlanNode], +) -> anyhow::Result<()> { + sqlx::query( + r#" +DELETE FROM thread_goal_plan_dependencies +WHERE node_id = ? + "#, + ) + .bind(&node.node_id) + .execute(&mut **tx) + .await?; + + let node_ids_by_key = existing_nodes + .iter() + .map(|node| (node.key.as_str(), node.node_id.as_str())) + .chain(std::iter::once((node.key.as_str(), node.node_id.as_str()))) + .collect::>(); + for dependency_key in &node.depends_on { + let dependency_id = node_ids_by_key + .get(dependency_key.as_str()) + .ok_or_else(|| { + anyhow::anyhow!( + "goal node {} depends on unknown goal node {dependency_key}", + node.key + ) + })?; + sqlx::query( + r#" +INSERT INTO thread_goal_plan_dependencies (node_id, depends_on_node_id) +VALUES (?, ?) + "#, + ) + .bind(&node.node_id) + .bind(dependency_id) + .execute(&mut **tx) + .await?; + } + Ok(()) +} + +async fn update_projected_goal_metadata_in_tx( + tx: &mut sqlx::Transaction<'_, Sqlite>, + node: &crate::ThreadGoalPlanNode, + now_ms: i64, +) -> anyhow::Result> { + let Some(projected_goal_id) = node.projected_goal_id.as_deref() else { + return Ok(None); + }; + let row = sqlx::query( + r#" +UPDATE thread_goals +SET + objective = ?, + title = ?, + token_budget = ?, + updated_at_ms = ? +WHERE thread_id = ? + AND goal_id = ? +RETURNING + thread_id, + goal_id, + objective, + title, + status, + token_budget, + tokens_used, + time_used_seconds, + created_at_ms, + updated_at_ms + "#, + ) + .bind(&node.objective) + .bind(&node.title) + .bind(node.token_budget) + .bind(now_ms) + .bind(node.assigned_thread_id.to_string()) + .bind(projected_goal_id) + .fetch_optional(&mut **tx) + .await?; + row.map(|row| thread_goal_from_row(&row)).transpose() +} + +async fn mark_projected_goal_complete_in_tx( + tx: &mut sqlx::Transaction<'_, Sqlite>, + node: &crate::ThreadGoalPlanNode, + now_ms: i64, +) -> anyhow::Result> { + let Some(projected_goal_id) = node.projected_goal_id.as_deref() else { + return Ok(None); + }; + let row = sqlx::query( + r#" +UPDATE thread_goals +SET status = ?, updated_at_ms = ? +WHERE thread_id = ? + AND goal_id = ? +RETURNING + thread_id, + goal_id, + objective, + title, + status, + token_budget, + tokens_used, + time_used_seconds, + created_at_ms, + updated_at_ms + "#, + ) + .bind(crate::ThreadGoalStatus::Complete.as_str()) + .bind(now_ms) + .bind(node.assigned_thread_id.to_string()) + .bind(projected_goal_id) + .fetch_optional(&mut **tx) + .await?; + row.map(|row| thread_goal_from_row(&row)).transpose() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::runtime::test_support::unique_temp_dir; + use pretty_assertions::assert_eq; + use std::sync::Arc; + + async fn test_runtime() -> Arc { + StateRuntime::init(unique_temp_dir(), "test-provider".to_string()) + .await + .expect("state db should initialize") + } + + fn test_thread_id() -> ThreadId { + ThreadId::from_string("00000000-0000-0000-0000-000000000456").expect("valid thread id") + } + + #[tokio::test] + async fn completed_goal_plan_node_must_be_marked_pending_before_editing() { + let runtime = test_runtime().await; + let thread_id = test_thread_id(); + let created = runtime + .thread_goals() + .create_thread_goal_plan(ThreadGoalPlanCreateParams { + thread_id, + auto_execute: crate::ThreadGoalPlanAutoExecute::Off, + max_tokens: None, + nodes: vec![ + ThreadGoalPlanNodeCreateParams { + key: "first".to_string(), + objective: "First objective".to_string(), + assigned_thread_id: None, + title: Some("First".to_string()), + priority: 0, + token_budget: None, + depends_on: Vec::new(), + }, + ThreadGoalPlanNodeCreateParams { + key: "second".to_string(), + objective: "Second objective".to_string(), + assigned_thread_id: None, + title: None, + priority: 0, + token_budget: None, + depends_on: vec!["first".to_string()], + }, + ], + }) + .await + .expect("goal plan should be created"); + let first_node_id = created.snapshot.nodes[0].node_id.clone(); + + runtime + .thread_goals() + .set_thread_goal_plan_node_status(ThreadGoalPlanNodeStatusUpdateParams { + thread_id, + node_id: first_node_id.clone(), + status: ThreadGoalPlanNodeCompletionStatus::Complete, + auto_execute: crate::ThreadGoalPlanAutoExecute::Off, + }) + .await + .expect("node should be marked complete"); + + let edit_complete = runtime + .thread_goals() + .update_thread_goal_plan_node(ThreadGoalPlanNodeUpdateParams { + thread_id, + node_id: first_node_id.clone(), + key: Some("renamed".to_string()), + objective: None, + title: ThreadGoalPlanNodeTitleUpdate::Keep, + priority: None, + token_budget: ThreadGoalPlanNodeTokenBudgetUpdate::Keep, + depends_on: None, + }) + .await; + assert!( + edit_complete + .expect_err("editing a complete node should fail") + .to_string() + .contains("mark it undone first") + ); + + runtime + .thread_goals() + .set_thread_goal_plan_node_status(ThreadGoalPlanNodeStatusUpdateParams { + thread_id, + node_id: first_node_id.clone(), + status: ThreadGoalPlanNodeCompletionStatus::Pending, + auto_execute: crate::ThreadGoalPlanAutoExecute::Off, + }) + .await + .expect("complete node should be marked pending"); + let edited = runtime + .thread_goals() + .update_thread_goal_plan_node(ThreadGoalPlanNodeUpdateParams { + thread_id, + node_id: first_node_id.clone(), + key: Some("renamed".to_string()), + objective: Some("Edited first objective".to_string()), + title: ThreadGoalPlanNodeTitleUpdate::Set(Some("Edited".to_string())), + priority: Some(5), + token_budget: ThreadGoalPlanNodeTokenBudgetUpdate::Set(Some(123)), + depends_on: None, + }) + .await + .expect("pending node should be editable"); + + assert_eq!("renamed", edited.node.key); + assert_eq!("Edited first objective", edited.node.objective); + assert_eq!(Some("Edited".to_string()), edited.node.title); + assert_eq!(5, edited.node.priority); + assert_eq!(Some(123), edited.node.token_budget); + assert_eq!( + vec!["renamed".to_string()], + edited.snapshot.nodes[1].depends_on + ); + } + + #[tokio::test] + async fn insert_goal_plan_node_between_existing_nodes_updates_sequence() { + let runtime = test_runtime().await; + let thread_id = test_thread_id(); + let created = runtime + .thread_goals() + .create_thread_goal_plan(ThreadGoalPlanCreateParams { + thread_id, + auto_execute: crate::ThreadGoalPlanAutoExecute::Off, + max_tokens: None, + nodes: vec![ + ThreadGoalPlanNodeCreateParams { + key: "first".to_string(), + objective: "First objective".to_string(), + assigned_thread_id: None, + title: None, + priority: 0, + token_budget: None, + depends_on: Vec::new(), + }, + ThreadGoalPlanNodeCreateParams { + key: "second".to_string(), + objective: "Second objective".to_string(), + assigned_thread_id: None, + title: None, + priority: 0, + token_budget: None, + depends_on: vec!["first".to_string()], + }, + ], + }) + .await + .expect("goal plan should be created"); + let second_node_id = created.snapshot.nodes[1].node_id.clone(); + + let inserted = runtime + .thread_goals() + .insert_thread_goal_plan_node(ThreadGoalPlanNodeInsertParams { + thread_id, + plan_id: created.snapshot.plan.plan_id, + position: ThreadGoalPlanNodeInsertPosition::Before(second_node_id.clone()), + key: "middle".to_string(), + objective: "Middle objective".to_string(), + title: Some("Middle".to_string()), + priority: 2, + token_budget: Some(50), + depends_on: vec!["first".to_string()], + }) + .await + .expect("node should insert before existing node"); + + assert_eq!( + vec!["first", "middle", "second"], + inserted + .snapshot + .nodes + .iter() + .map(|node| node.key.as_str()) + .collect::>() + ); + assert_eq!(1, inserted.inserted_node.sequence); + assert_eq!(2, inserted.snapshot.nodes[2].sequence); + assert_eq!(vec!["first".to_string()], inserted.inserted_node.depends_on); + assert_eq!(None, inserted.inserted_node.parent_node_id); + assert_eq!(1, inserted.inserted_node.nesting_depth); + } + + #[tokio::test] + async fn insert_goal_plan_node_inherits_reference_node_hierarchy() { + let runtime = test_runtime().await; + let thread_id = test_thread_id(); + let created = runtime + .thread_goals() + .create_thread_goal_plan(ThreadGoalPlanCreateParams { + thread_id, + auto_execute: crate::ThreadGoalPlanAutoExecute::Off, + max_tokens: None, + nodes: vec![ + ThreadGoalPlanNodeCreateParams { + key: "parent".to_string(), + objective: "Parent objective".to_string(), + assigned_thread_id: None, + title: None, + priority: 0, + token_budget: None, + depends_on: Vec::new(), + }, + ThreadGoalPlanNodeCreateParams { + key: "child".to_string(), + objective: "Child objective".to_string(), + assigned_thread_id: None, + title: None, + priority: 0, + token_budget: None, + depends_on: vec!["parent".to_string()], + }, + ], + }) + .await + .expect("goal plan should be created"); + let plan_id = created.snapshot.plan.plan_id.clone(); + let snapshot = runtime + .thread_goals() + .update_thread_goal_plan_node_hierarchy( + thread_id, + plan_id.as_str(), + vec![ + crate::ThreadGoalPlanNodeHierarchyParams { + key: "parent".to_string(), + parent_key: None, + nesting_depth: 1, + }, + crate::ThreadGoalPlanNodeHierarchyParams { + key: "child".to_string(), + parent_key: Some("parent".to_string()), + nesting_depth: 2, + }, + ], + ) + .await + .expect("hierarchy should apply"); + let parent_node_id = snapshot.nodes[0].node_id.clone(); + let child_node_id = snapshot.nodes[1].node_id.clone(); + + let inserted = runtime + .thread_goals() + .insert_thread_goal_plan_node(ThreadGoalPlanNodeInsertParams { + thread_id, + plan_id: plan_id.clone(), + position: ThreadGoalPlanNodeInsertPosition::After(child_node_id), + key: "sibling".to_string(), + objective: "Sibling objective".to_string(), + title: None, + priority: 0, + token_budget: None, + depends_on: vec!["parent".to_string()], + }) + .await + .expect("node should insert after the nested node"); + + assert_eq!( + Some(parent_node_id), + inserted.inserted_node.parent_node_id, + "an inserted sibling inherits the reference node's parent" + ); + assert_eq!(2, inserted.inserted_node.nesting_depth); + + let appended = runtime + .thread_goals() + .insert_thread_goal_plan_node(ThreadGoalPlanNodeInsertParams { + thread_id, + plan_id, + position: ThreadGoalPlanNodeInsertPosition::End, + key: "tail".to_string(), + objective: "Tail objective".to_string(), + title: None, + priority: 0, + token_budget: None, + depends_on: Vec::new(), + }) + .await + .expect("node should append at the end"); + + assert_eq!(None, appended.inserted_node.parent_node_id); + assert_eq!(1, appended.inserted_node.nesting_depth); + } + + #[tokio::test] + async fn marking_active_goal_plan_node_complete_advances_ready_node() { + let runtime = test_runtime().await; + let thread_id = test_thread_id(); + let created = runtime + .thread_goals() + .create_thread_goal_plan(ThreadGoalPlanCreateParams { + thread_id, + auto_execute: crate::ThreadGoalPlanAutoExecute::ReadyOnly, + max_tokens: None, + nodes: vec![ + ThreadGoalPlanNodeCreateParams { + key: "first".to_string(), + objective: "First objective".to_string(), + assigned_thread_id: None, + title: None, + priority: 0, + token_budget: None, + depends_on: Vec::new(), + }, + ThreadGoalPlanNodeCreateParams { + key: "second".to_string(), + objective: "Second objective".to_string(), + assigned_thread_id: None, + title: None, + priority: 0, + token_budget: None, + depends_on: vec!["first".to_string()], + }, + ], + }) + .await + .expect("goal plan should be created"); + let first_node_id = created.snapshot.nodes[0].node_id.clone(); + + let completed = runtime + .thread_goals() + .set_thread_goal_plan_node_status(ThreadGoalPlanNodeStatusUpdateParams { + thread_id, + node_id: first_node_id, + status: ThreadGoalPlanNodeCompletionStatus::Complete, + auto_execute: crate::ThreadGoalPlanAutoExecute::ReadyOnly, + }) + .await + .expect("active node should complete"); + + assert_eq!( + Some(crate::ThreadGoalStatus::Complete), + completed.goal.as_ref().map(|goal| goal.status) + ); + assert_eq!( + Some("Second objective"), + completed + .activated_goal + .as_ref() + .map(|goal| goal.objective.as_str()) + ); + assert_eq!( + vec![ + crate::ThreadGoalPlanNodeStatus::Complete, + crate::ThreadGoalPlanNodeStatus::Active, + ], + completed + .snapshot + .nodes + .iter() + .map(|node| node.status) + .collect::>() + ); + } +} diff --git a/codex-rs/state/src/runtime/goal_plans.rs b/codex-rs/state/src/runtime/goal_plans.rs index 9d87ea48e..0aef2183e 100644 --- a/codex-rs/state/src/runtime/goal_plans.rs +++ b/codex-rs/state/src/runtime/goal_plans.rs @@ -11,7 +11,7 @@ use uuid::Uuid; pub const DEFAULT_THREAD_GOAL_PLAN_LIST_LIMIT: u32 = 20; pub const MAX_THREAD_GOAL_PLAN_LIST_LIMIT: u32 = 50; -const MAX_GOAL_PLAN_NODES: usize = 128; +pub(super) const MAX_GOAL_PLAN_NODES: usize = 128; const MAX_GOAL_PLAN_NODE_KEY_LEN: usize = 128; #[derive(Clone, Debug, PartialEq, Eq)] @@ -920,7 +920,7 @@ WHERE plan_id = ? } } -async fn activate_next_ready_node_in_tx( +pub(super) async fn activate_next_ready_node_in_tx( tx: &mut sqlx::Transaction<'_, Sqlite>, thread_id: ThreadId, plan_id: &str, @@ -2218,7 +2218,9 @@ WHERE dependency.node_id IN ( Ok(dependencies_by_node_id) } -fn thread_goal_from_row(row: &sqlx::sqlite::SqliteRow) -> anyhow::Result { +pub(super) fn thread_goal_from_row( + row: &sqlx::sqlite::SqliteRow, +) -> anyhow::Result { crate::model::ThreadGoalRow::try_from_row(row).and_then(crate::ThreadGoal::try_from) } @@ -2242,7 +2244,9 @@ fn status_after_budget_limit( } } -fn validate_plan_create_params(params: &ThreadGoalPlanCreateParams) -> anyhow::Result<()> { +pub(super) fn validate_plan_create_params( + params: &ThreadGoalPlanCreateParams, +) -> anyhow::Result<()> { if params.nodes.is_empty() { anyhow::bail!("goal plan must contain at least one goal"); }