feat (acp): custom methods for managing scheduler (#9972)

This commit is contained in:
Lifei Zhou 2026-06-25 09:11:35 +10:00 committed by GitHub
parent 09998b12cf
commit eea698945c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 2050 additions and 484 deletions

View file

@ -6,6 +6,8 @@ use std::collections::HashMap;
mod recipe;
pub use recipe::*;
mod schedule;
pub use schedule::*;
/// Schema descriptor for a single custom method, produced by the
/// `#[custom_methods]` macro's generated `custom_method_schemas()` function.

View file

@ -0,0 +1,169 @@
use agent_client_protocol::schema::SessionInfo;
use agent_client_protocol::{JsonRpcRequest, JsonRpcResponse};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use super::{EmptyResponse, RecipeDto};
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct ScheduledJobDto {
pub id: String,
pub source: String,
pub cron: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub last_run: Option<String>,
pub currently_running: bool,
pub paused: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub current_session_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub job_start_time: Option<String>,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
#[request(
method = "_goose/unstable/schedules/list",
response = ListSchedulesResponse
)]
pub struct ListSchedulesRequest {}
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)]
pub struct ListSchedulesResponse {
pub jobs: Vec<ScheduledJobDto>,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
#[request(
method = "_goose/unstable/schedules/create",
response = CreateScheduleResponse
)]
pub struct CreateScheduleRequest {
pub id: String,
pub recipe: RecipeDto,
pub cron: String,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)]
pub struct CreateScheduleResponse {
pub job: ScheduledJobDto,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
#[request(method = "_goose/unstable/schedules/delete", response = EmptyResponse)]
#[serde(rename_all = "camelCase")]
pub struct DeleteScheduleRequest {
pub schedule_id: String,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
#[request(
method = "_goose/unstable/schedules/update",
response = UpdateScheduleResponse
)]
#[serde(rename_all = "camelCase")]
pub struct UpdateScheduleRequest {
pub schedule_id: String,
pub cron: String,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)]
pub struct UpdateScheduleResponse {
pub job: ScheduledJobDto,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
#[request(
method = "_goose/unstable/schedules/run-now",
response = RunScheduleNowResponse
)]
#[serde(rename_all = "camelCase")]
pub struct RunScheduleNowRequest {
pub schedule_id: String,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)]
#[serde(rename_all = "camelCase")]
pub struct RunScheduleNowResponse {
pub status: RunScheduleNowStatus,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub session_id: Option<String>,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub enum RunScheduleNowStatus {
#[default]
Completed,
Cancelled,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
#[request(
method = "_goose/unstable/schedules/sessions/list",
response = ListScheduleSessionsResponse
)]
#[serde(rename_all = "camelCase")]
pub struct ListScheduleSessionsRequest {
pub schedule_id: String,
pub limit: usize,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)]
pub struct ListScheduleSessionsResponse {
pub sessions: Vec<SessionInfo>,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
#[request(method = "_goose/unstable/schedules/pause", response = EmptyResponse)]
#[serde(rename_all = "camelCase")]
pub struct PauseScheduleRequest {
pub schedule_id: String,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
#[request(
method = "_goose/unstable/schedules/unpause",
response = EmptyResponse
)]
#[serde(rename_all = "camelCase")]
pub struct UnpauseScheduleRequest {
pub schedule_id: String,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
#[request(
method = "_goose/unstable/schedules/running-job/kill",
response = KillRunningJobResponse
)]
#[serde(rename_all = "camelCase")]
pub struct KillRunningJobRequest {
pub job_id: String,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)]
pub struct KillRunningJobResponse {
pub message: String,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
#[request(
method = "_goose/unstable/schedules/running-job/inspect",
response = InspectRunningJobResponse
)]
#[serde(rename_all = "camelCase")]
pub struct InspectRunningJobRequest {
pub job_id: String,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)]
#[serde(rename_all = "camelCase")]
pub struct InspectRunningJobResponse {
pub running: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub session_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub job_start_time: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub running_duration_seconds: Option<i64>,
}

View file

@ -250,6 +250,56 @@
"requestType": "RecipeToYamlRequest_unstable",
"responseType": "RecipeToYamlResponse_unstable"
},
{
"method": "_goose/unstable/schedules/list",
"requestType": "ListSchedulesRequest_unstable",
"responseType": "ListSchedulesResponse_unstable"
},
{
"method": "_goose/unstable/schedules/sessions/list",
"requestType": "ListScheduleSessionsRequest_unstable",
"responseType": "ListScheduleSessionsResponse_unstable"
},
{
"method": "_goose/unstable/schedules/create",
"requestType": "CreateScheduleRequest_unstable",
"responseType": "CreateScheduleResponse_unstable"
},
{
"method": "_goose/unstable/schedules/delete",
"requestType": "DeleteScheduleRequest_unstable",
"responseType": "EmptyResponse"
},
{
"method": "_goose/unstable/schedules/pause",
"requestType": "PauseScheduleRequest_unstable",
"responseType": "EmptyResponse"
},
{
"method": "_goose/unstable/schedules/unpause",
"requestType": "UnpauseScheduleRequest_unstable",
"responseType": "EmptyResponse"
},
{
"method": "_goose/unstable/schedules/update",
"requestType": "UpdateScheduleRequest_unstable",
"responseType": "UpdateScheduleResponse_unstable"
},
{
"method": "_goose/unstable/schedules/run-now",
"requestType": "RunScheduleNowRequest_unstable",
"responseType": "RunScheduleNowResponse_unstable"
},
{
"method": "_goose/unstable/schedules/running-job/kill",
"requestType": "KillRunningJobRequest_unstable",
"responseType": "KillRunningJobResponse_unstable"
},
{
"method": "_goose/unstable/schedules/running-job/inspect",
"requestType": "InspectRunningJobRequest_unstable",
"responseType": "InspectRunningJobResponse_unstable"
},
{
"method": "_goose/unstable/session/info",
"requestType": "GetSessionInfoRequest_unstable",

View file

@ -3520,32 +3520,105 @@
"x-side": "agent",
"x-method": "_goose/unstable/recipes/to-yaml"
},
"GetSessionInfoRequest_unstable": {
"ListSchedulesRequest_unstable": {
"type": "object",
"properties": {
"sessionId": {
"type": "string"
}
},
"required": [
"sessionId"
],
"description": "Return list-style metadata for a single session without loading the conversation.",
"x-side": "agent",
"x-method": "_goose/unstable/session/info"
"x-method": "_goose/unstable/schedules/list"
},
"GetSessionInfoResponse_unstable": {
"ListSchedulesResponse_unstable": {
"type": "object",
"properties": {
"session": {
"$ref": "#/$defs/SessionInfo"
"jobs": {
"type": "array",
"items": {
"$ref": "#/$defs/ScheduledJobDto"
}
}
},
"required": [
"session"
"jobs"
],
"x-side": "agent",
"x-method": "_goose/unstable/session/info"
"x-method": "_goose/unstable/schedules/list"
},
"ScheduledJobDto": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"source": {
"type": "string"
},
"cron": {
"type": "string"
},
"lastRun": {
"type": [
"string",
"null"
]
},
"currentlyRunning": {
"type": "boolean"
},
"paused": {
"type": "boolean"
},
"currentSessionId": {
"type": [
"string",
"null"
]
},
"jobStartTime": {
"type": [
"string",
"null"
]
}
},
"required": [
"id",
"source",
"cron",
"currentlyRunning",
"paused"
]
},
"ListScheduleSessionsRequest_unstable": {
"type": "object",
"properties": {
"scheduleId": {
"type": "string"
},
"limit": {
"type": "integer",
"minimum": 0
}
},
"required": [
"scheduleId",
"limit"
],
"x-side": "agent",
"x-method": "_goose/unstable/schedules/sessions/list"
},
"ListScheduleSessionsResponse_unstable": {
"type": "object",
"properties": {
"sessions": {
"type": "array",
"items": {
"$ref": "#/$defs/SessionInfo"
}
}
},
"required": [
"sessions"
],
"x-side": "agent",
"x-method": "_goose/unstable/schedules/sessions/list"
},
"SessionInfo": {
"type": "object",
@ -3598,6 +3671,245 @@
"type": "string",
"description": "A unique identifier for a conversation session between a client and agent.\n\nSessions maintain their own context, conversation history, and state,\nallowing multiple independent interactions with the same agent.\n\nSee protocol docs: [Session ID](https://agentclientprotocol.com/protocol/session-setup#session-id)"
},
"CreateScheduleRequest_unstable": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"recipe": {
"$ref": "#/$defs/RecipeDto"
},
"cron": {
"type": "string"
}
},
"required": [
"id",
"recipe",
"cron"
],
"x-side": "agent",
"x-method": "_goose/unstable/schedules/create"
},
"CreateScheduleResponse_unstable": {
"type": "object",
"properties": {
"job": {
"$ref": "#/$defs/ScheduledJobDto"
}
},
"required": [
"job"
],
"x-side": "agent",
"x-method": "_goose/unstable/schedules/create"
},
"DeleteScheduleRequest_unstable": {
"type": "object",
"properties": {
"scheduleId": {
"type": "string"
}
},
"required": [
"scheduleId"
],
"x-side": "agent",
"x-method": "_goose/unstable/schedules/delete"
},
"PauseScheduleRequest_unstable": {
"type": "object",
"properties": {
"scheduleId": {
"type": "string"
}
},
"required": [
"scheduleId"
],
"x-side": "agent",
"x-method": "_goose/unstable/schedules/pause"
},
"UnpauseScheduleRequest_unstable": {
"type": "object",
"properties": {
"scheduleId": {
"type": "string"
}
},
"required": [
"scheduleId"
],
"x-side": "agent",
"x-method": "_goose/unstable/schedules/unpause"
},
"UpdateScheduleRequest_unstable": {
"type": "object",
"properties": {
"scheduleId": {
"type": "string"
},
"cron": {
"type": "string"
}
},
"required": [
"scheduleId",
"cron"
],
"x-side": "agent",
"x-method": "_goose/unstable/schedules/update"
},
"UpdateScheduleResponse_unstable": {
"type": "object",
"properties": {
"job": {
"$ref": "#/$defs/ScheduledJobDto"
}
},
"required": [
"job"
],
"x-side": "agent",
"x-method": "_goose/unstable/schedules/update"
},
"RunScheduleNowRequest_unstable": {
"type": "object",
"properties": {
"scheduleId": {
"type": "string"
}
},
"required": [
"scheduleId"
],
"x-side": "agent",
"x-method": "_goose/unstable/schedules/run-now"
},
"RunScheduleNowResponse_unstable": {
"type": "object",
"properties": {
"status": {
"$ref": "#/$defs/RunScheduleNowStatus"
},
"sessionId": {
"type": [
"string",
"null"
]
}
},
"required": [
"status"
],
"x-side": "agent",
"x-method": "_goose/unstable/schedules/run-now"
},
"RunScheduleNowStatus": {
"type": "string",
"enum": [
"completed",
"cancelled"
]
},
"KillRunningJobRequest_unstable": {
"type": "object",
"properties": {
"jobId": {
"type": "string"
}
},
"required": [
"jobId"
],
"x-side": "agent",
"x-method": "_goose/unstable/schedules/running-job/kill"
},
"KillRunningJobResponse_unstable": {
"type": "object",
"properties": {
"message": {
"type": "string"
}
},
"required": [
"message"
],
"x-side": "agent",
"x-method": "_goose/unstable/schedules/running-job/kill"
},
"InspectRunningJobRequest_unstable": {
"type": "object",
"properties": {
"jobId": {
"type": "string"
}
},
"required": [
"jobId"
],
"x-side": "agent",
"x-method": "_goose/unstable/schedules/running-job/inspect"
},
"InspectRunningJobResponse_unstable": {
"type": "object",
"properties": {
"running": {
"type": "boolean"
},
"sessionId": {
"type": [
"string",
"null"
]
},
"jobStartTime": {
"type": [
"string",
"null"
]
},
"runningDurationSeconds": {
"type": [
"integer",
"null"
]
}
},
"required": [
"running"
],
"x-side": "agent",
"x-method": "_goose/unstable/schedules/running-job/inspect"
},
"GetSessionInfoRequest_unstable": {
"type": "object",
"properties": {
"sessionId": {
"type": "string"
}
},
"required": [
"sessionId"
],
"description": "Return list-style metadata for a single session without loading the conversation.",
"x-side": "agent",
"x-method": "_goose/unstable/session/info"
},
"GetSessionInfoResponse_unstable": {
"type": "object",
"properties": {
"session": {
"$ref": "#/$defs/SessionInfo"
}
},
"required": [
"session"
],
"x-side": "agent",
"x-method": "_goose/unstable/session/info"
},
"TruncateSessionConversationRequest_unstable": {
"type": "object",
"properties": {
@ -5195,6 +5507,96 @@
"description": "Params for _goose/unstable/recipes/to-yaml",
"title": "RecipeToYamlRequest_unstable"
},
{
"allOf": [
{
"$ref": "#/$defs/ListSchedulesRequest_unstable"
}
],
"description": "Params for _goose/unstable/schedules/list",
"title": "ListSchedulesRequest_unstable"
},
{
"allOf": [
{
"$ref": "#/$defs/ListScheduleSessionsRequest_unstable"
}
],
"description": "Params for _goose/unstable/schedules/sessions/list",
"title": "ListScheduleSessionsRequest_unstable"
},
{
"allOf": [
{
"$ref": "#/$defs/CreateScheduleRequest_unstable"
}
],
"description": "Params for _goose/unstable/schedules/create",
"title": "CreateScheduleRequest_unstable"
},
{
"allOf": [
{
"$ref": "#/$defs/DeleteScheduleRequest_unstable"
}
],
"description": "Params for _goose/unstable/schedules/delete",
"title": "DeleteScheduleRequest_unstable"
},
{
"allOf": [
{
"$ref": "#/$defs/PauseScheduleRequest_unstable"
}
],
"description": "Params for _goose/unstable/schedules/pause",
"title": "PauseScheduleRequest_unstable"
},
{
"allOf": [
{
"$ref": "#/$defs/UnpauseScheduleRequest_unstable"
}
],
"description": "Params for _goose/unstable/schedules/unpause",
"title": "UnpauseScheduleRequest_unstable"
},
{
"allOf": [
{
"$ref": "#/$defs/UpdateScheduleRequest_unstable"
}
],
"description": "Params for _goose/unstable/schedules/update",
"title": "UpdateScheduleRequest_unstable"
},
{
"allOf": [
{
"$ref": "#/$defs/RunScheduleNowRequest_unstable"
}
],
"description": "Params for _goose/unstable/schedules/run-now",
"title": "RunScheduleNowRequest_unstable"
},
{
"allOf": [
{
"$ref": "#/$defs/KillRunningJobRequest_unstable"
}
],
"description": "Params for _goose/unstable/schedules/running-job/kill",
"title": "KillRunningJobRequest_unstable"
},
{
"allOf": [
{
"$ref": "#/$defs/InspectRunningJobRequest_unstable"
}
],
"description": "Params for _goose/unstable/schedules/running-job/inspect",
"title": "InspectRunningJobRequest_unstable"
},
{
"allOf": [
{
@ -5721,6 +6123,62 @@
],
"title": "RecipeToYamlResponse_unstable"
},
{
"allOf": [
{
"$ref": "#/$defs/ListSchedulesResponse_unstable"
}
],
"title": "ListSchedulesResponse_unstable"
},
{
"allOf": [
{
"$ref": "#/$defs/ListScheduleSessionsResponse_unstable"
}
],
"title": "ListScheduleSessionsResponse_unstable"
},
{
"allOf": [
{
"$ref": "#/$defs/CreateScheduleResponse_unstable"
}
],
"title": "CreateScheduleResponse_unstable"
},
{
"allOf": [
{
"$ref": "#/$defs/UpdateScheduleResponse_unstable"
}
],
"title": "UpdateScheduleResponse_unstable"
},
{
"allOf": [
{
"$ref": "#/$defs/RunScheduleNowResponse_unstable"
}
],
"title": "RunScheduleNowResponse_unstable"
},
{
"allOf": [
{
"$ref": "#/$defs/KillRunningJobResponse_unstable"
}
],
"title": "KillRunningJobResponse_unstable"
},
{
"allOf": [
{
"$ref": "#/$defs/InspectRunningJobResponse_unstable"
}
],
"title": "InspectRunningJobResponse_unstable"
},
{
"allOf": [
{

View file

@ -99,6 +99,7 @@ mod onboarding;
mod providers;
mod recipe;
mod resources;
mod schedule;
mod slash_commands;
mod sources;
mod tool_notifications;

View file

@ -416,6 +416,86 @@ impl GooseAcpAgent {
self.on_recipe_to_yaml(req).await
}
#[custom_method(ListSchedulesRequest)]
async fn dispatch_list_schedules(
&self,
req: ListSchedulesRequest,
) -> Result<ListSchedulesResponse, agent_client_protocol::Error> {
self.on_list_schedules(req).await
}
#[custom_method(ListScheduleSessionsRequest)]
async fn dispatch_list_schedule_sessions(
&self,
req: ListScheduleSessionsRequest,
) -> Result<ListScheduleSessionsResponse, agent_client_protocol::Error> {
self.on_list_schedule_sessions(req).await
}
#[custom_method(CreateScheduleRequest)]
async fn dispatch_create_schedule(
&self,
req: CreateScheduleRequest,
) -> Result<CreateScheduleResponse, agent_client_protocol::Error> {
self.on_create_schedule(req).await
}
#[custom_method(DeleteScheduleRequest)]
async fn dispatch_delete_schedule(
&self,
req: DeleteScheduleRequest,
) -> Result<EmptyResponse, agent_client_protocol::Error> {
self.on_delete_schedule(req).await
}
#[custom_method(PauseScheduleRequest)]
async fn dispatch_pause_schedule(
&self,
req: PauseScheduleRequest,
) -> Result<EmptyResponse, agent_client_protocol::Error> {
self.on_pause_schedule(req).await
}
#[custom_method(UnpauseScheduleRequest)]
async fn dispatch_unpause_schedule(
&self,
req: UnpauseScheduleRequest,
) -> Result<EmptyResponse, agent_client_protocol::Error> {
self.on_unpause_schedule(req).await
}
#[custom_method(UpdateScheduleRequest)]
async fn dispatch_update_schedule(
&self,
req: UpdateScheduleRequest,
) -> Result<UpdateScheduleResponse, agent_client_protocol::Error> {
self.on_update_schedule(req).await
}
#[custom_method(RunScheduleNowRequest)]
async fn dispatch_run_schedule_now(
&self,
req: RunScheduleNowRequest,
) -> Result<RunScheduleNowResponse, agent_client_protocol::Error> {
self.on_run_schedule_now(req).await
}
#[custom_method(KillRunningJobRequest)]
async fn dispatch_kill_running_job(
&self,
req: KillRunningJobRequest,
) -> Result<KillRunningJobResponse, agent_client_protocol::Error> {
self.on_kill_running_job(req).await
}
#[custom_method(InspectRunningJobRequest)]
async fn dispatch_inspect_running_job(
&self,
req: InspectRunningJobRequest,
) -> Result<InspectRunningJobResponse, agent_client_protocol::Error> {
self.on_inspect_running_job(req).await
}
#[custom_method(GetSessionInfoRequest)]
async fn dispatch_get_session_info(
&self,

View file

@ -0,0 +1,343 @@
use goose_sdk_types::custom_requests::{
CreateScheduleRequest, CreateScheduleResponse, DeleteScheduleRequest, EmptyResponse,
InspectRunningJobRequest, InspectRunningJobResponse, KillRunningJobRequest,
KillRunningJobResponse, ListScheduleSessionsRequest, ListScheduleSessionsResponse,
ListSchedulesRequest, ListSchedulesResponse, PauseScheduleRequest, RunScheduleNowRequest,
RunScheduleNowResponse, RunScheduleNowStatus, ScheduledJobDto, UnpauseScheduleRequest,
UpdateScheduleRequest, UpdateScheduleResponse,
};
use tokio::fs;
use super::{build_session_info, GooseAcpAgent, ResultExt};
use crate::recipe::validate_recipe::validate_recipe_template_from_content;
use crate::recipe::Recipe;
use crate::scheduler::{get_default_scheduled_recipes_dir, ScheduledJob, SchedulerError};
fn validate_schedule_id(id: &str) -> Result<(), agent_client_protocol::Error> {
let is_valid = !id.is_empty()
&& id
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == ' ');
if !is_valid {
return Err(agent_client_protocol::Error::invalid_params().data(
"Schedule name must use only alphanumeric characters, hyphens, underscores, or spaces",
));
}
Ok(())
}
fn validate_schedule_recipe(recipe: &Recipe) -> Result<(), agent_client_protocol::Error> {
let recipe_yaml = recipe
.to_yaml()
.map_err(|e| agent_client_protocol::Error::invalid_params().data(e.to_string()))?;
validate_recipe_template_from_content(&recipe_yaml, None)
.map_err(|e| agent_client_protocol::Error::invalid_params().data(e.to_string()))?;
Ok(())
}
fn schedule_not_found_or_internal(error: SchedulerError) -> agent_client_protocol::Error {
match error {
SchedulerError::JobNotFound(id) => {
agent_client_protocol::Error::resource_not_found(Some(id))
}
error => agent_client_protocol::Error::internal_error().data(error.to_string()),
}
}
fn create_schedule_error(error: SchedulerError) -> agent_client_protocol::Error {
match error {
SchedulerError::CronParseError(message) => agent_client_protocol::Error::invalid_params()
.data(format!("Invalid cron expression: {message}")),
SchedulerError::RecipeLoadError(message) => agent_client_protocol::Error::invalid_params()
.data(format!("Recipe load error: {message}")),
SchedulerError::JobIdExists(id) => agent_client_protocol::Error::invalid_params()
.data(format!("Job ID already exists: {id}")),
error => agent_client_protocol::Error::internal_error()
.data(format!("Error creating schedule: {error}")),
}
}
fn schedule_state_error(error: SchedulerError) -> agent_client_protocol::Error {
match error {
SchedulerError::JobNotFound(id) => {
agent_client_protocol::Error::resource_not_found(Some(id))
}
SchedulerError::AnyhowError(error) => {
agent_client_protocol::Error::invalid_params().data(error.to_string())
}
error => agent_client_protocol::Error::internal_error().data(error.to_string()),
}
}
fn update_schedule_error(error: SchedulerError) -> agent_client_protocol::Error {
match error {
SchedulerError::JobNotFound(id) => {
agent_client_protocol::Error::resource_not_found(Some(id))
}
SchedulerError::AnyhowError(error) => {
agent_client_protocol::Error::invalid_params().data(error.to_string())
}
SchedulerError::CronParseError(message) => agent_client_protocol::Error::invalid_params()
.data(format!("Invalid cron expression: {message}")),
error => agent_client_protocol::Error::internal_error().data(error.to_string()),
}
}
fn run_schedule_now_error(
error: SchedulerError,
) -> Result<RunScheduleNowResponse, agent_client_protocol::Error> {
match error {
SchedulerError::JobNotFound(id) => {
Err(agent_client_protocol::Error::resource_not_found(Some(id)))
}
SchedulerError::AnyhowError(error)
if error.to_string().contains("was successfully cancelled") =>
{
Ok(RunScheduleNowResponse {
status: RunScheduleNowStatus::Cancelled,
session_id: None,
})
}
error => Err(agent_client_protocol::Error::internal_error()
.data(format!("Error running schedule: {error}"))),
}
}
fn scheduled_job_to_dto(job: ScheduledJob) -> ScheduledJobDto {
ScheduledJobDto {
id: job.id,
source: job.source,
cron: job.cron,
last_run: job.last_run.map(|value| value.to_rfc3339()),
currently_running: job.currently_running,
paused: job.paused,
current_session_id: job.current_session_id,
job_start_time: job.process_start_time.map(|value| value.to_rfc3339()),
}
}
impl GooseAcpAgent {
pub(super) async fn on_list_schedules(
&self,
_req: ListSchedulesRequest,
) -> Result<ListSchedulesResponse, agent_client_protocol::Error> {
let jobs = self
.agent_manager
.scheduler()
.list_scheduled_jobs()
.await
.into_iter()
.map(scheduled_job_to_dto)
.collect();
Ok(ListSchedulesResponse { jobs })
}
pub(super) async fn on_list_schedule_sessions(
&self,
req: ListScheduleSessionsRequest,
) -> Result<ListScheduleSessionsResponse, agent_client_protocol::Error> {
let sessions = self
.agent_manager
.scheduler()
.sessions(&req.schedule_id, req.limit)
.await
.internal_err_ctx("Failed to fetch schedule sessions")?
.into_iter()
.map(|(_, session)| build_session_info(session))
.collect();
Ok(ListScheduleSessionsResponse { sessions })
}
pub(super) async fn on_create_schedule(
&self,
req: CreateScheduleRequest,
) -> Result<CreateScheduleResponse, agent_client_protocol::Error> {
let id = req.id.trim().to_string();
validate_schedule_id(&id)?;
let recipe = Recipe::try_from(req.recipe).map_err(|e| {
agent_client_protocol::Error::invalid_params().data(format!("recipe: {e}"))
})?;
if recipe.check_for_security_warnings() {
return Err(agent_client_protocol::Error::invalid_params().data(
"This recipe contains hidden characters that could be malicious. Please remove them before trying to save.",
));
}
validate_schedule_recipe(&recipe)?;
let scheduled_recipes_dir = get_default_scheduled_recipes_dir().map_err(|e| {
agent_client_protocol::Error::internal_error()
.data(format!("Failed to get scheduled recipes directory: {e}"))
})?;
let recipe_path = scheduled_recipes_dir.join(format!("{id}.yaml"));
let yaml_content = recipe.to_yaml().map_err(|e| {
agent_client_protocol::Error::internal_error()
.data(format!("Failed to convert recipe to YAML: {e}"))
})?;
fs::write(&recipe_path, yaml_content).await.map_err(|e| {
agent_client_protocol::Error::internal_error()
.data(format!("Failed to save recipe file: {e}"))
})?;
let job = ScheduledJob {
id,
source: recipe_path.to_string_lossy().into_owned(),
cron: req.cron,
last_run: None,
currently_running: false,
paused: false,
current_session_id: None,
process_start_time: None,
parameters: vec![],
recipe_base_dir: None,
};
self.agent_manager
.scheduler()
.add_scheduled_job(job.clone(), false)
.await
.map_err(create_schedule_error)?;
Ok(CreateScheduleResponse {
job: scheduled_job_to_dto(job),
})
}
pub(super) async fn on_delete_schedule(
&self,
req: DeleteScheduleRequest,
) -> Result<EmptyResponse, agent_client_protocol::Error> {
self.agent_manager
.scheduler()
.remove_scheduled_job(&req.schedule_id, false)
.await
.map_err(schedule_not_found_or_internal)?;
Ok(EmptyResponse {})
}
pub(super) async fn on_pause_schedule(
&self,
req: PauseScheduleRequest,
) -> Result<EmptyResponse, agent_client_protocol::Error> {
self.agent_manager
.scheduler()
.pause_schedule(&req.schedule_id)
.await
.map_err(schedule_state_error)?;
Ok(EmptyResponse {})
}
pub(super) async fn on_unpause_schedule(
&self,
req: UnpauseScheduleRequest,
) -> Result<EmptyResponse, agent_client_protocol::Error> {
self.agent_manager
.scheduler()
.unpause_schedule(&req.schedule_id)
.await
.map_err(schedule_not_found_or_internal)?;
Ok(EmptyResponse {})
}
pub(super) async fn on_update_schedule(
&self,
req: UpdateScheduleRequest,
) -> Result<UpdateScheduleResponse, agent_client_protocol::Error> {
let schedule_id = req.schedule_id;
let cron = req.cron;
let scheduler = self.agent_manager.scheduler();
scheduler
.update_schedule(&schedule_id, cron)
.await
.map_err(update_schedule_error)?;
let job = scheduler
.list_scheduled_jobs()
.await
.into_iter()
.find(|job| job.id == schedule_id)
.ok_or_else(|| {
agent_client_protocol::Error::internal_error()
.data("Schedule not found after update")
})?;
Ok(UpdateScheduleResponse {
job: scheduled_job_to_dto(job),
})
}
pub(super) async fn on_run_schedule_now(
&self,
req: RunScheduleNowRequest,
) -> Result<RunScheduleNowResponse, agent_client_protocol::Error> {
match self
.agent_manager
.scheduler()
.run_now(&req.schedule_id)
.await
{
Ok(session_id) => Ok(RunScheduleNowResponse {
status: RunScheduleNowStatus::Completed,
session_id: Some(session_id),
}),
Err(error) => run_schedule_now_error(error),
}
}
pub(super) async fn on_kill_running_job(
&self,
req: KillRunningJobRequest,
) -> Result<KillRunningJobResponse, agent_client_protocol::Error> {
self.agent_manager
.scheduler()
.kill_running_job(&req.job_id)
.await
.map_err(schedule_state_error)?;
Ok(KillRunningJobResponse {
message: format!("Successfully killed running job '{}'", req.job_id),
})
}
pub(super) async fn on_inspect_running_job(
&self,
req: InspectRunningJobRequest,
) -> Result<InspectRunningJobResponse, agent_client_protocol::Error> {
let job = self
.agent_manager
.scheduler()
.list_scheduled_jobs()
.await
.into_iter()
.find(|job| job.id == req.job_id)
.ok_or_else(|| agent_client_protocol::Error::resource_not_found(Some(req.job_id)))?;
if !job.currently_running {
return Ok(InspectRunningJobResponse::default());
}
let running_duration_seconds = job.process_start_time.map(|start_time| {
chrono::Utc::now()
.signed_duration_since(start_time)
.num_seconds()
});
Ok(InspectRunningJobResponse {
running: true,
session_id: job.current_session_id,
job_start_time: job.process_start_time.map(|value| value.to_rfc3339()),
running_duration_seconds,
})
}
}

View file

@ -139,6 +139,16 @@ async fn persist_jobs(
Ok(())
}
fn clear_running_state(job: &mut ScheduledJob) -> bool {
let changed = job.currently_running
|| job.current_session_id.is_some()
|| job.process_start_time.is_some();
job.currently_running = false;
job.current_session_id = None;
job.process_start_time = None;
changed
}
pub struct Scheduler {
tokio_scheduler: TokioJobScheduler,
jobs: Arc<Mutex<JobsMap>>,
@ -432,7 +442,7 @@ impl Scheduler {
return;
}
let list: Vec<ScheduledJob> = match serde_json::from_str(&data) {
let mut list: Vec<ScheduledJob> = match serde_json::from_str(&data) {
Ok(jobs) => jobs,
Err(e) => {
tracing::error!(
@ -444,6 +454,20 @@ impl Scheduler {
}
};
let reset_stale_running_state = list
.iter_mut()
.fold(false, |changed, job| clear_running_state(job) || changed);
if reset_stale_running_state {
match serde_json::to_string_pretty(&list) {
Ok(data) => {
if let Err(e) = fs::write(&self.storage_path, data) {
tracing::error!("Failed to persist scheduler startup state: {}", e);
}
}
Err(e) => tracing::error!("Failed to serialize scheduler startup state: {}", e),
}
}
for job_to_load in list {
if !Path::new(&job_to_load.source).exists() {
tracing::warn!(
@ -554,12 +578,15 @@ impl Scheduler {
pub async fn list_scheduled_jobs(&self) -> Vec<ScheduledJob> {
self.sync_from_storage().await;
self.jobs
let mut jobs: Vec<ScheduledJob> = self
.jobs
.lock()
.await
.values()
.map(|(_, j)| j.clone())
.collect()
.collect();
jobs.sort_by(|a, b| a.id.cmp(&b.id));
jobs
}
pub async fn remove_scheduled_job(
@ -648,6 +675,7 @@ impl Scheduler {
cancel_token.clone(),
)
.await;
let was_cancelled = cancel_token.is_cancelled();
{
let mut tasks = self.running_tasks.lock().await;
@ -667,6 +695,10 @@ impl Scheduler {
persist_jobs(&self.storage_path, &self.jobs).await?;
match result {
_ if was_cancelled => Err(SchedulerError::AnyhowError(anyhow!(
"Job '{}' was successfully cancelled",
sched_id
))),
Ok(session_id) => Ok(session_id),
Err(e) => Err(SchedulerError::AnyhowError(anyhow!(
"Job '{}' failed: {}",
@ -770,14 +802,25 @@ impl Scheduler {
}
}
let token = {
let mut tasks = self.running_tasks.lock().await;
tasks.remove(sched_id)
};
if let Some(token) = token {
token.cancel();
}
{
let tasks = self.running_tasks.lock().await;
if let Some(token) = tasks.get(sched_id) {
token.cancel();
let mut jobs_guard = self.jobs.lock().await;
match jobs_guard.get_mut(sched_id) {
Some((_, job)) => {
clear_running_state(job);
}
None => return Err(SchedulerError::JobNotFound(sched_id.to_string())),
}
}
Ok(())
persist_jobs(&self.storage_path, &self.jobs).await
}
pub async fn get_running_job_info(
@ -1227,6 +1270,112 @@ mod tests {
);
}
#[tokio::test]
async fn test_kill_running_job_clears_state_and_persists() {
let temp_dir = tempdir().unwrap();
let storage_path = temp_dir.path().join("schedule.json");
let recipe_path = create_test_recipe(temp_dir.path(), "running_job");
let session_manager = Arc::new(SessionManager::new(temp_dir.path().to_path_buf()));
let scheduler = Scheduler::new(storage_path.clone(), session_manager)
.await
.unwrap();
let job = ScheduledJob {
id: "running_job".to_string(),
source: recipe_path.to_string_lossy().to_string(),
cron: "0 0 0 1 1 *".to_string(),
last_run: None,
currently_running: false,
paused: false,
current_session_id: None,
process_start_time: None,
parameters: vec![],
recipe_base_dir: None,
};
scheduler.add_scheduled_job(job, false).await.unwrap();
{
let mut jobs_guard = scheduler.jobs.lock().await;
let (_, job) = jobs_guard.get_mut("running_job").unwrap();
job.currently_running = true;
job.current_session_id = Some("session-id".to_string());
job.process_start_time = Some(Utc::now());
}
{
let mut tasks = scheduler.running_tasks.lock().await;
tasks.insert("running_job".to_string(), CancellationToken::new());
}
persist_jobs(&storage_path, &scheduler.jobs).await.unwrap();
scheduler.kill_running_job("running_job").await.unwrap();
let jobs = scheduler.list_scheduled_jobs().await;
let killed_job = jobs.iter().find(|job| job.id == "running_job").unwrap();
assert!(!killed_job.currently_running);
assert!(killed_job.current_session_id.is_none());
assert!(killed_job.process_start_time.is_none());
assert!(scheduler.running_tasks.lock().await.is_empty());
let persisted_jobs: Vec<ScheduledJob> =
serde_json::from_str(&fs::read_to_string(storage_path).unwrap()).unwrap();
let persisted_job = persisted_jobs
.iter()
.find(|job| job.id == "running_job")
.unwrap();
assert!(!persisted_job.currently_running);
assert!(persisted_job.current_session_id.is_none());
assert!(persisted_job.process_start_time.is_none());
}
#[tokio::test]
async fn test_load_jobs_from_storage_clears_stale_running_state() {
let temp_dir = tempdir().unwrap();
let storage_path = temp_dir.path().join("schedule.json");
let recipe_path = create_test_recipe(temp_dir.path(), "stale_running_job");
let started_at = Utc::now();
let stale_job = ScheduledJob {
id: "stale_running_job".to_string(),
source: recipe_path.to_string_lossy().to_string(),
cron: "0 0 0 1 1 *".to_string(),
last_run: None,
currently_running: true,
paused: false,
current_session_id: Some("stale-session-id".to_string()),
process_start_time: Some(started_at),
parameters: vec![],
recipe_base_dir: None,
};
fs::write(
&storage_path,
serde_json::to_string_pretty(&vec![stale_job]).unwrap(),
)
.unwrap();
let session_manager = Arc::new(SessionManager::new(temp_dir.path().to_path_buf()));
let scheduler = Scheduler::new(storage_path.clone(), session_manager)
.await
.unwrap();
let jobs = scheduler.list_scheduled_jobs().await;
let loaded_job = jobs
.iter()
.find(|job| job.id == "stale_running_job")
.unwrap();
assert!(!loaded_job.currently_running);
assert!(loaded_job.current_session_id.is_none());
assert!(loaded_job.process_start_time.is_none());
let persisted_jobs: Vec<ScheduledJob> =
serde_json::from_str(&fs::read_to_string(storage_path).unwrap()).unwrap();
let persisted_job = persisted_jobs
.iter()
.find(|job| job.id == "stale_running_job")
.unwrap();
assert!(!persisted_job.currently_running);
assert!(persisted_job.current_session_id.is_none());
assert!(persisted_job.process_start_time.is_none());
}
#[tokio::test]
async fn test_job_with_no_prompt_does_not_panic() {
let _guard = env_lock::lock_env([

View file

@ -0,0 +1,193 @@
import type {
CreateScheduleRequest_unstable,
InspectRunningJobResponse_unstable,
KillRunningJobResponse_unstable,
RunScheduleNowResponse_unstable,
ScheduledJobDto,
SessionInfo,
} from '@aaif/goose-sdk';
import { getAcpClient } from './acpConnection';
let inFlightListSchedules: Promise<ScheduledJobDto[]> | null = null;
const inFlightListScheduleSessions = new Map<string, Promise<SessionInfo[]>>();
function acpErrorMessage(error: unknown): string | null {
if (typeof error !== 'object' || error === null) {
return null;
}
const candidate = 'error' in error && isRecord(error.error) ? error.error : error;
if (!isRecord(candidate)) {
return null;
}
if (typeof candidate.data === 'string') {
return candidate.data;
}
return typeof candidate.message === 'string' ? candidate.message : null;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null;
}
function normalizeAcpError(error: unknown, fallback: string): Error {
const message = acpErrorMessage(error);
if (message) {
return new Error(message);
}
if (error instanceof Error) {
return error;
}
return new Error(fallback);
}
function clearInFlightScheduleReads(): void {
inFlightListSchedules = null;
inFlightListScheduleSessions.clear();
}
export async function acpListSchedules(): Promise<ScheduledJobDto[]> {
const pending = inFlightListSchedules;
if (pending) {
return pending;
}
const listPromise = (async () => {
const client = await getAcpClient();
const response = await client.goose.schedulesList_unstable({});
return response.jobs;
})().catch((error) => {
throw normalizeAcpError(error, 'Failed to list schedules');
});
inFlightListSchedules = listPromise;
try {
return await listPromise;
} finally {
if (inFlightListSchedules === listPromise) {
inFlightListSchedules = null;
}
}
}
export async function acpCreateSchedule(
request: CreateScheduleRequest_unstable
): Promise<ScheduledJobDto> {
try {
const client = await getAcpClient();
const response = await client.goose.schedulesCreate_unstable(request);
clearInFlightScheduleReads();
return response.job;
} catch (error) {
throw normalizeAcpError(error, 'Failed to create schedule');
}
}
export async function acpDeleteSchedule(scheduleId: string): Promise<void> {
try {
const client = await getAcpClient();
await client.goose.schedulesDelete_unstable({ scheduleId });
clearInFlightScheduleReads();
} catch (error) {
throw normalizeAcpError(error, 'Failed to delete schedule');
}
}
export async function acpListScheduleSessions(
scheduleId: string,
limit: number
): Promise<SessionInfo[]> {
const key = `${scheduleId}:${limit}`;
const pending = inFlightListScheduleSessions.get(key);
if (pending) {
return pending;
}
const listPromise = (async () => {
const client = await getAcpClient();
const response = await client.goose.schedulesSessionsList_unstable({ scheduleId, limit });
return response.sessions;
})().catch((error) => {
throw normalizeAcpError(error, 'Failed to list schedule sessions');
});
inFlightListScheduleSessions.set(key, listPromise);
try {
return await listPromise;
} finally {
if (inFlightListScheduleSessions.get(key) === listPromise) {
inFlightListScheduleSessions.delete(key);
}
}
}
export async function acpRunScheduleNow(
scheduleId: string
): Promise<RunScheduleNowResponse_unstable> {
try {
const client = await getAcpClient();
const response = await client.goose.schedulesRunNow_unstable({ scheduleId });
clearInFlightScheduleReads();
return response;
} catch (error) {
throw normalizeAcpError(error, 'Failed to run schedule now');
}
}
export async function acpPauseSchedule(scheduleId: string): Promise<void> {
try {
const client = await getAcpClient();
await client.goose.schedulesPause_unstable({ scheduleId });
clearInFlightScheduleReads();
} catch (error) {
throw normalizeAcpError(error, 'Failed to pause schedule');
}
}
export async function acpUnpauseSchedule(scheduleId: string): Promise<void> {
try {
const client = await getAcpClient();
await client.goose.schedulesUnpause_unstable({ scheduleId });
clearInFlightScheduleReads();
} catch (error) {
throw normalizeAcpError(error, 'Failed to unpause schedule');
}
}
export async function acpUpdateSchedule(
scheduleId: string,
cron: string
): Promise<ScheduledJobDto> {
try {
const client = await getAcpClient();
const response = await client.goose.schedulesUpdate_unstable({ scheduleId, cron });
clearInFlightScheduleReads();
return response.job;
} catch (error) {
throw normalizeAcpError(error, 'Failed to update schedule');
}
}
export async function acpKillRunningJob(jobId: string): Promise<KillRunningJobResponse_unstable> {
try {
const client = await getAcpClient();
const response = await client.goose.schedulesRunningJobKill_unstable({ jobId });
clearInFlightScheduleReads();
return response;
} catch (error) {
throw normalizeAcpError(error, 'Failed to kill running job');
}
}
export async function acpInspectRunningJob(
jobId: string
): Promise<InspectRunningJobResponse_unstable> {
try {
const client = await getAcpClient();
return await client.goose.schedulesRunningJobInspect_unstable({ jobId });
} catch (error) {
throw normalizeAcpError(error, 'Failed to inspect running job');
}
}

View file

@ -991,8 +991,8 @@ export default function RecipesView() {
id: scheduleRecipeManifest.id,
source: '',
cron: scheduleRecipeManifest.schedule_cron,
last_run: null,
currently_running: false,
lastRun: null,
currentlyRunning: false,
paused: false,
}
: null

View file

@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react';
import { ScheduledJob } from '../../schedule';
import type { ScheduledJobDto } from '@aaif/goose-sdk';
import { errorMessage } from '../../utils/conversionUtils';
import { defineMessages, useIntl } from '../../i18n';
import {
@ -61,7 +61,7 @@ const i18n = defineMessages({
});
interface CronPickerProps {
schedule: ScheduledJob | null;
schedule: ScheduledJobDto | null;
onChange: (cron: string) => void;
isValid: (valid: boolean) => void;
}

View file

@ -1,29 +1,28 @@
import React, { useState, useEffect, useCallback } from 'react';
import type { ScheduledJobDto, SessionInfo } from '@aaif/goose-sdk';
import { Button } from '../ui/button';
import { ScrollArea } from '../ui/scroll-area';
import BackButton from '../ui/BackButton';
import { Card } from '../ui/card';
import {
getScheduleSessions,
runScheduleNow,
pauseSchedule,
unpauseSchedule,
updateSchedule,
listSchedules,
killRunningJob,
inspectRunningJob,
ScheduledJob,
} from '../../schedule';
import SessionHistoryView from '../sessions/SessionHistoryView';
acpListScheduleSessions,
acpRunScheduleNow,
acpPauseSchedule,
acpUnpauseSchedule,
acpUpdateSchedule,
acpListSchedules,
acpKillRunningJob,
acpInspectRunningJob,
} from '../../acp/schedules';
import { ScheduleModal, NewSchedulePayload } from './ScheduleModal';
import { toastError, toastSuccess } from '../../toasts';
import { Loader2, Pause, Play, Edit, Square, Eye } from 'lucide-react';
import cronstrue from 'cronstrue';
import { formatToLocalDateWithTimezone } from '../../utils/date';
import { getSession, Session } from '../../api';
import { trackScheduleRunNow, getErrorType } from '../../utils/analytics';
import { errorMessage } from '../../utils/conversionUtils';
import { defineMessages, useIntl } from '../../i18n';
import { useNavigation } from '../../hooks/useNavigation';
const i18n = defineMessages({
scheduleNotFound: { id: 'scheduleDetailView.scheduleNotFound', defaultMessage: 'Schedule Not Found' },
@ -57,12 +56,11 @@ const i18n = defineMessages({
created: { id: 'scheduleDetailView.created', defaultMessage: 'Created: {date}' },
messages: { id: 'scheduleDetailView.messages', defaultMessage: 'Messages: {count}' },
dir: { id: 'scheduleDetailView.dir', defaultMessage: 'Dir: {path}' },
tokens: { id: 'scheduleDetailView.tokens', defaultMessage: 'Tokens: {count}' },
idLabel: { id: 'scheduleDetailView.idLabel', defaultMessage: 'ID:' },
jobCancelled: { id: 'scheduleDetailView.jobCancelled', defaultMessage: 'Job Cancelled' },
jobCancelledMsg: { id: 'scheduleDetailView.jobCancelledMsg', defaultMessage: 'The job was cancelled while starting up.' },
scheduleTriggered: { id: 'scheduleDetailView.scheduleTriggered', defaultMessage: 'Schedule Triggered' },
newSession: { id: 'scheduleDetailView.newSession', defaultMessage: 'New session: {sessionId}' },
scheduleCompleted: { id: 'scheduleDetailView.scheduleCompleted', defaultMessage: 'Run completed' },
completedSession: { id: 'scheduleDetailView.completedSession', defaultMessage: 'Session: {sessionId}' },
runScheduleError: { id: 'scheduleDetailView.runScheduleError', defaultMessage: 'Run Schedule Error' },
scheduleUnpaused: { id: 'scheduleDetailView.scheduleUnpaused', defaultMessage: 'Schedule Unpaused' },
unpausedMsg: { id: 'scheduleDetailView.unpausedMsg', defaultMessage: 'Unpaused "{id}"' },
@ -77,53 +75,48 @@ const i18n = defineMessages({
scheduleUpdated: { id: 'scheduleDetailView.scheduleUpdated', defaultMessage: 'Schedule Updated' },
updatedMsg: { id: 'scheduleDetailView.updatedMsg', defaultMessage: 'Updated "{id}"' },
updateScheduleError: { id: 'scheduleDetailView.updateScheduleError', defaultMessage: 'Update Schedule Error' },
failedToLoadSession: { id: 'scheduleDetailView.failedToLoadSession', defaultMessage: 'Failed to load session' },
scheduleNotFoundError: { id: 'scheduleDetailView.scheduleNotFoundError', defaultMessage: 'Schedule not found' },
});
interface ScheduleSessionMeta {
id: string;
name: string;
createdAt: string;
workingDir?: string;
scheduleId?: string | null;
messageCount?: number;
totalTokens?: number | null;
inputTokens?: number | null;
outputTokens?: number | null;
accumulatedTotalTokens?: number | null;
accumulatedInputTokens?: number | null;
accumulatedOutputTokens?: number | null;
}
interface ScheduleDetailViewProps {
scheduleId: string | null;
onNavigateBack: () => void;
}
function sessionMeta(session: SessionInfo): Record<string, unknown> {
return typeof session._meta === 'object' && session._meta !== null ? session._meta : {};
}
function metaString(session: SessionInfo, key: string): string | undefined {
const value = sessionMeta(session)[key];
return typeof value === 'string' ? value : undefined;
}
function metaNumber(session: SessionInfo, key: string): number | undefined {
const value = sessionMeta(session)[key];
return typeof value === 'number' ? value : undefined;
}
const ScheduleDetailView: React.FC<ScheduleDetailViewProps> = ({ scheduleId, onNavigateBack }) => {
const intl = useIntl();
const [sessions, setSessions] = useState<ScheduleSessionMeta[]>([]);
const setView = useNavigation();
const [sessions, setSessions] = useState<SessionInfo[]>([]);
const [isLoadingSessions, setIsLoadingSessions] = useState(false);
const [sessionsError, setSessionsError] = useState<string | null>(null);
const [scheduleDetails, setScheduleDetails] = useState<ScheduledJob | null>(null);
const [scheduleDetails, setScheduleDetails] = useState<ScheduledJobDto | null>(null);
const [isLoadingSchedule, setIsLoadingSchedule] = useState(false);
const [scheduleError, setScheduleError] = useState<string | null>(null);
const [isActionLoading, setIsActionLoading] = useState(false);
const [selectedSession, setSelectedSession] = useState<Session | null>(null);
const [isLoadingSession, setIsLoadingSession] = useState(false);
const [sessionError, setSessionError] = useState<string | null>(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const fetchSessions = async (sId: string) => {
setIsLoadingSessions(true);
setSessionsError(null);
try {
const data = await getScheduleSessions(sId, 20);
const data = await acpListScheduleSessions(sId, 20);
setSessions(data);
} catch (err) {
setSessionsError(errorMessage(err, 'Failed to fetch sessions'));
@ -136,7 +129,7 @@ const ScheduleDetailView: React.FC<ScheduleDetailViewProps> = ({ scheduleId, onN
setIsLoadingSchedule(true);
setScheduleError(null);
try {
const allSchedules = await listSchedules();
const allSchedules = await acpListSchedules();
const schedule = allSchedules.find((s) => s.id === sId);
if (schedule) {
setScheduleDetails(schedule);
@ -151,22 +144,27 @@ const ScheduleDetailView: React.FC<ScheduleDetailViewProps> = ({ scheduleId, onN
}, [intl]);
useEffect(() => {
if (scheduleId && !selectedSession) {
if (scheduleId) {
fetchSessions(scheduleId);
fetchSchedule(scheduleId);
}
}, [scheduleId, selectedSession, fetchSchedule]);
}, [scheduleId, fetchSchedule]);
const openSession = useCallback((sessionId: string) => {
setView('pair', {
disableAnimation: true,
resumeSessionId: sessionId,
});
}, [setView]);
const handleRunNow = async () => {
if (!scheduleId) return;
setIsActionLoading(true);
try {
const newSessionId = await runScheduleNow(scheduleId);
const result = await acpRunScheduleNow(scheduleId);
trackScheduleRunNow(true);
if (newSessionId === 'CANCELLED') {
toastSuccess({ title: intl.formatMessage(i18n.jobCancelled), msg: intl.formatMessage(i18n.jobCancelledMsg) });
} else {
toastSuccess({ title: intl.formatMessage(i18n.scheduleTriggered), msg: intl.formatMessage(i18n.newSession, { sessionId: newSessionId }) });
if (result.status === 'completed' && result.sessionId) {
toastSuccess({ title: intl.formatMessage(i18n.scheduleCompleted), msg: intl.formatMessage(i18n.completedSession, { sessionId: result.sessionId }) });
}
await fetchSessions(scheduleId);
await fetchSchedule(scheduleId);
@ -187,10 +185,10 @@ const ScheduleDetailView: React.FC<ScheduleDetailViewProps> = ({ scheduleId, onN
setIsActionLoading(true);
try {
if (scheduleDetails.paused) {
await unpauseSchedule(scheduleId);
await acpUnpauseSchedule(scheduleId);
toastSuccess({ title: intl.formatMessage(i18n.scheduleUnpaused), msg: intl.formatMessage(i18n.unpausedMsg, { id: scheduleId }) });
} else {
await pauseSchedule(scheduleId);
await acpPauseSchedule(scheduleId);
toastSuccess({ title: intl.formatMessage(i18n.schedulePaused), msg: intl.formatMessage(i18n.pausedMsg, { id: scheduleId }) });
}
await fetchSchedule(scheduleId);
@ -209,7 +207,7 @@ const ScheduleDetailView: React.FC<ScheduleDetailViewProps> = ({ scheduleId, onN
if (!scheduleId) return;
setIsActionLoading(true);
try {
const result = await killRunningJob(scheduleId);
const result = await acpKillRunningJob(scheduleId);
toastSuccess({ title: intl.formatMessage(i18n.jobKilled), msg: result.message });
await fetchSchedule(scheduleId);
} catch (err) {
@ -227,7 +225,7 @@ const ScheduleDetailView: React.FC<ScheduleDetailViewProps> = ({ scheduleId, onN
if (!scheduleId) return;
setIsActionLoading(true);
try {
const result = await inspectRunningJob(scheduleId);
const result = await acpInspectRunningJob(scheduleId);
if (result.sessionId) {
const duration = result.runningDurationSeconds
? `${Math.floor(result.runningDurationSeconds / 60)}m ${result.runningDurationSeconds % 60}s`
@ -254,7 +252,7 @@ const ScheduleDetailView: React.FC<ScheduleDetailViewProps> = ({ scheduleId, onN
if (!scheduleId) return;
setIsActionLoading(true);
try {
await updateSchedule(scheduleId, payload as string);
await acpUpdateSchedule(scheduleId, payload as string);
toastSuccess({ title: intl.formatMessage(i18n.scheduleUpdated), msg: intl.formatMessage(i18n.updatedMsg, { id: scheduleId }) });
await fetchSchedule(scheduleId);
setIsModalOpen(false);
@ -269,37 +267,6 @@ const ScheduleDetailView: React.FC<ScheduleDetailViewProps> = ({ scheduleId, onN
}
};
const loadSession = async (sessionId: string) => {
setIsLoadingSession(true);
setSessionError(null);
try {
const response = await getSession<true>({
path: { session_id: sessionId },
throwOnError: true,
});
setSelectedSession(response.data);
} catch (err) {
const msg = errorMessage(err, 'Failed to load session');
setSessionError(msg);
toastError({ title: intl.formatMessage(i18n.failedToLoadSession), msg });
} finally {
setIsLoadingSession(false);
}
};
if (selectedSession) {
return (
<SessionHistoryView
session={selectedSession}
isLoading={isLoadingSession}
error={sessionError}
onBack={() => setSelectedSession(null)}
onRetry={() => loadSession(selectedSession.id)}
showActionButtons={true}
/>
);
}
if (!scheduleId) {
return (
<div className="h-screen w-full flex flex-col items-center justify-center bg-white dark:bg-gray-900 text-text-primary p-8">
@ -352,7 +319,7 @@ const ScheduleDetailView: React.FC<ScheduleDetailViewProps> = ({ scheduleId, onN
{scheduleDetails.id}
</h3>
<div className="mt-2 md:mt-0 flex items-center gap-2">
{scheduleDetails.currently_running && (
{scheduleDetails.currentlyRunning && (
<div className="text-sm text-green-500 dark:text-green-400 font-semibold flex items-center">
<span className="inline-block w-2 h-2 bg-green-500 dark:bg-green-400 rounded-full mr-1 animate-pulse"></span>
{intl.formatMessage(i18n.currentlyRunning)}
@ -377,18 +344,18 @@ const ScheduleDetailView: React.FC<ScheduleDetailViewProps> = ({ scheduleId, onN
</p>
<p className="text-sm text-text-primary">
<span className="font-semibold">{intl.formatMessage(i18n.lastRun)}</span>{' '}
{formatToLocalDateWithTimezone(scheduleDetails.last_run)}
{formatToLocalDateWithTimezone(scheduleDetails.lastRun)}
</p>
{scheduleDetails.currently_running && scheduleDetails.current_session_id && (
{scheduleDetails.currentlyRunning && scheduleDetails.currentSessionId && (
<p className="text-sm text-text-primary">
<span className="font-semibold">{intl.formatMessage(i18n.currentSession)}</span>{' '}
{scheduleDetails.current_session_id}
{scheduleDetails.currentSessionId}
</p>
)}
{scheduleDetails.currently_running && scheduleDetails.process_start_time && (
{scheduleDetails.currentlyRunning && scheduleDetails.jobStartTime && (
<p className="text-sm text-text-primary">
<span className="font-semibold">{intl.formatMessage(i18n.processStarted)}</span>{' '}
{formatToLocalDateWithTimezone(scheduleDetails.process_start_time)}
{formatToLocalDateWithTimezone(scheduleDetails.jobStartTime)}
</p>
)}
</div>
@ -401,13 +368,13 @@ const ScheduleDetailView: React.FC<ScheduleDetailViewProps> = ({ scheduleId, onN
<div className="flex flex-col md:flex-row gap-2">
<Button
onClick={handleRunNow}
disabled={isActionLoading || scheduleDetails?.currently_running}
disabled={isActionLoading || scheduleDetails?.currentlyRunning}
className="w-full md:w-auto"
>
{intl.formatMessage(i18n.runScheduleNow)}
</Button>
{scheduleDetails && !scheduleDetails.currently_running && (
{scheduleDetails && !scheduleDetails.currentlyRunning && (
<>
<Button
onClick={() => setIsModalOpen(true)}
@ -443,7 +410,7 @@ const ScheduleDetailView: React.FC<ScheduleDetailViewProps> = ({ scheduleId, onN
</>
)}
{scheduleDetails?.currently_running && (
{scheduleDetails?.currentlyRunning && (
<>
<Button
onClick={handleInspect}
@ -467,7 +434,7 @@ const ScheduleDetailView: React.FC<ScheduleDetailViewProps> = ({ scheduleId, onN
)}
</div>
{scheduleDetails?.currently_running && (
{scheduleDetails?.currentlyRunning && (
<p className="text-sm text-amber-600 dark:text-amber-400 mt-2">
{intl.formatMessage(i18n.cannotModifyRunning)}
</p>
@ -496,45 +463,46 @@ const ScheduleDetailView: React.FC<ScheduleDetailViewProps> = ({ scheduleId, onN
{sessions.length > 0 && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{sessions.map((session) => (
<Card
key={session.id}
className="p-4 bg-background-primary shadow cursor-pointer hover:shadow-lg transition-shadow duration-200"
onClick={() => loadSession(session.id)}
>
<h3
className="text-sm font-semibold text-text-primary truncate"
title={session.name || session.id}
{sessions.map((session) => {
const sessionId = String(session.sessionId);
const sessionName = session.title ?? sessionId;
const createdAt = metaString(session, 'createdAt') ?? session.updatedAt;
const messageCount = metaNumber(session, 'messageCount');
return (
<Card
key={sessionId}
className="p-4 bg-background-primary shadow cursor-pointer hover:shadow-lg transition-shadow duration-200"
onClick={() => openSession(sessionId)}
>
{session.name || intl.formatMessage(i18n.sessionId, { id: session.id })}
</h3>
<p className="text-xs text-text-secondary mt-1">
{intl.formatMessage(i18n.created, { date: session.createdAt ? formatToLocalDateWithTimezone(session.createdAt) : 'N/A' })}
</p>
{session.messageCount !== undefined && (
<p className="text-xs text-text-secondary mt-1">
{intl.formatMessage(i18n.messages, { count: session.messageCount })}
</p>
)}
{session.workingDir && (
<p
className="text-xs text-text-secondary mt-1 truncate"
title={session.workingDir}
<h3
className="text-sm font-semibold text-text-primary truncate"
title={sessionName}
>
{intl.formatMessage(i18n.dir, { path: session.workingDir })}
{sessionName || intl.formatMessage(i18n.sessionId, { id: sessionId })}
</h3>
<p className="text-xs text-text-secondary mt-1">
{intl.formatMessage(i18n.created, { date: createdAt ? formatToLocalDateWithTimezone(createdAt) : 'N/A' })}
</p>
)}
{session.accumulatedTotalTokens !== undefined &&
session.accumulatedTotalTokens !== null && (
{messageCount !== undefined && (
<p className="text-xs text-text-secondary mt-1">
{intl.formatMessage(i18n.tokens, { count: session.accumulatedTotalTokens })}
{intl.formatMessage(i18n.messages, { count: messageCount })}
</p>
)}
<p className="text-xs text-text-secondary mt-1">
{intl.formatMessage(i18n.idLabel)} <span className="font-mono">{session.id}</span>
</p>
</Card>
))}
{session.cwd && (
<p
className="text-xs text-text-secondary mt-1 truncate"
title={session.cwd}
>
{intl.formatMessage(i18n.dir, { path: session.cwd })}
</p>
)}
<p className="text-xs text-text-secondary mt-1">
{intl.formatMessage(i18n.idLabel)} <span className="font-mono">{sessionId}</span>
</p>
</Card>
);
})}
</div>
)}
</section>

View file

@ -1,8 +1,8 @@
import React, { useState, useEffect, FormEvent, useCallback } from 'react';
import type { ScheduledJobDto } from '@aaif/goose-sdk';
import { Card } from '../ui/card';
import { Button } from '../ui/button';
import { Input } from '../ui/input';
import { ScheduledJob } from '../../schedule';
import { CronPicker } from './CronPicker';
import { Recipe, parseDeeplink, parseRecipeFromFile } from '../../recipe';
import { getStorageDirectory } from '../../recipe/recipe_management';
@ -47,7 +47,7 @@ interface ScheduleModalProps {
isOpen: boolean;
onClose: () => void;
onSubmit: (payload: NewSchedulePayload | string) => Promise<void>;
schedule: ScheduledJob | null;
schedule: ScheduledJobDto | null;
isLoadingExternally: boolean;
apiErrorExternally: string | null;
initialDeepLink: string | null;

View file

@ -1,16 +1,16 @@
import React, { useState, useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import type { ScheduledJobDto } from '@aaif/goose-sdk';
import {
listSchedules,
createSchedule,
deleteSchedule,
pauseSchedule,
unpauseSchedule,
updateSchedule,
killRunningJob,
inspectRunningJob,
ScheduledJob,
} from '../../schedule';
acpListSchedules,
acpCreateSchedule,
acpDeleteSchedule,
acpPauseSchedule,
acpUnpauseSchedule,
acpUpdateSchedule,
acpKillRunningJob,
acpInspectRunningJob,
} from '../../acp/schedules';
import { ScrollArea } from '../ui/scroll-area';
import { Card } from '../ui/card';
import { Button } from '../ui/button';
@ -67,9 +67,9 @@ interface SchedulesViewProps {
}
const ScheduleCard: React.FC<{
job: ScheduledJob;
job: ScheduledJobDto;
onNavigateToDetail: (id: string) => void;
onEdit: (job: ScheduledJob) => void;
onEdit: (job: ScheduledJobDto) => void;
onPause: (id: string) => void;
onUnpause: (id: string) => void;
onKill: (id: string) => void;
@ -95,7 +95,7 @@ const ScheduleCard: React.FC<{
readableCron = job.cron;
}
const formattedLastRun = formatToLocalDateWithTimezone(job.last_run);
const formattedLastRun = formatToLocalDateWithTimezone(job.lastRun);
return (
<Card
@ -108,7 +108,7 @@ const ScheduleCard: React.FC<{
<h3 className="text-base truncate max-w-[50vw]" title={job.id}>
{job.id}
</h3>
{job.currently_running && (
{job.currentlyRunning && (
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300">
<span className="inline-block w-2 h-2 bg-green-500 rounded-full mr-1 animate-pulse"></span>
{intl.formatMessage(i18n.running)}
@ -130,7 +130,7 @@ const ScheduleCard: React.FC<{
</div>
<div className="flex items-center gap-2 shrink-0">
{!job.currently_running && (
{!job.currentlyRunning && (
<>
<Button
onClick={(e) => {
@ -173,7 +173,7 @@ const ScheduleCard: React.FC<{
</Button>
</>
)}
{job.currently_running && (
{job.currentlyRunning && (
<>
<Button
onClick={(e) => {
@ -224,13 +224,13 @@ const ScheduleCard: React.FC<{
const SchedulesView: React.FC<SchedulesViewProps> = ({ onClose: _onClose }) => {
const intl = useIntl();
const location = useLocation();
const [schedules, setSchedules] = useState<ScheduledJob[]>([]);
const [schedules, setSchedules] = useState<ScheduledJobDto[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [apiError, setApiError] = useState<string | null>(null);
const [submitApiError, setSubmitApiError] = useState<string | null>(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingSchedule, setEditingSchedule] = useState<ScheduledJob | null>(null);
const [editingSchedule, setEditingSchedule] = useState<ScheduledJobDto | null>(null);
const [isRefreshing, setIsRefreshing] = useState(false);
const [pendingDeepLink, setPendingDeepLink] = useState<string | null>(null);
const [actionsInProgress, setActionsInProgress] = useState<Set<string>>(new Set());
@ -240,7 +240,7 @@ const SchedulesView: React.FC<SchedulesViewProps> = ({ onClose: _onClose }) => {
setIsLoading(true);
setApiError(null);
try {
const fetchedSchedules = await listSchedules();
const fetchedSchedules = await acpListSchedules();
setSchedules(fetchedSchedules);
} catch (error) {
console.error('Failed to fetch schedules:', error);
@ -289,14 +289,14 @@ const SchedulesView: React.FC<SchedulesViewProps> = ({ onClose: _onClose }) => {
setSubmitApiError(null);
try {
if (editingSchedule) {
await updateSchedule(editingSchedule.id, payload as string);
await acpUpdateSchedule(editingSchedule.id, payload as string);
toastSuccess({
title: intl.formatMessage(i18n.scheduleUpdated),
msg: intl.formatMessage(i18n.scheduleUpdatedMsg, { id: editingSchedule.id }),
});
} else {
const newPayload = payload as NewSchedulePayload;
await createSchedule(newPayload);
await acpCreateSchedule(newPayload);
const sourceType = pendingDeepLink ? 'deeplink' : 'file';
trackScheduleCreated(sourceType, true);
}
@ -325,7 +325,7 @@ const SchedulesView: React.FC<SchedulesViewProps> = ({ onClose: _onClose }) => {
setApiError(null);
try {
await deleteSchedule(id);
await acpDeleteSchedule(id);
trackScheduleDeleted(true);
await fetchSchedules();
} catch (error) {
@ -347,7 +347,7 @@ const SchedulesView: React.FC<SchedulesViewProps> = ({ onClose: _onClose }) => {
setApiError(null);
try {
await pauseSchedule(id);
await acpPauseSchedule(id);
toastSuccess({
title: intl.formatMessage(i18n.schedulePaused),
msg: intl.formatMessage(i18n.schedulePausedMsg, { id }),
@ -375,7 +375,7 @@ const SchedulesView: React.FC<SchedulesViewProps> = ({ onClose: _onClose }) => {
setApiError(null);
try {
await unpauseSchedule(id);
await acpUnpauseSchedule(id);
toastSuccess({
title: intl.formatMessage(i18n.scheduleUnpaused),
msg: intl.formatMessage(i18n.scheduleUnpausedMsg, { id }),
@ -403,7 +403,7 @@ const SchedulesView: React.FC<SchedulesViewProps> = ({ onClose: _onClose }) => {
setApiError(null);
try {
const result = await killRunningJob(id);
const result = await acpKillRunningJob(id);
toastSuccess({
title: intl.formatMessage(i18n.jobKilled),
msg: result.message,
@ -431,7 +431,7 @@ const SchedulesView: React.FC<SchedulesViewProps> = ({ onClose: _onClose }) => {
setApiError(null);
try {
const result = await inspectRunningJob(id);
const result = await acpInspectRunningJob(id);
if (result.sessionId) {
const duration = result.runningDurationSeconds
? `${Math.floor(result.runningDurationSeconds / 60)}m ${result.runningDurationSeconds % 60}s`

View file

@ -1,9 +1,9 @@
import { render, screen, type RenderOptions, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, expect, it, vi } from 'vitest';
import type { ScheduledJobDto } from '@aaif/goose-sdk';
import { IntlTestWrapper } from '../../../i18n/test-utils';
import { CronPicker } from '../CronPicker';
import type { ScheduledJob } from '../../../schedule';
const renderWithIntl = (ui: React.ReactElement, options?: RenderOptions) =>
render(ui, { wrapper: IntlTestWrapper, ...options });
@ -13,10 +13,12 @@ const getLastCron = (onChange: ReturnType<typeof vi.fn>) => {
return calls[calls.length - 1]?.[0];
};
const scheduledJob = (cron: string): ScheduledJob => ({
const scheduledJob = (cron: string): ScheduledJobDto => ({
id: 'quarterly-report',
source: 'dummy.yaml',
cron,
currentlyRunning: false,
paused: false,
});
describe('CronPicker', () => {

View file

@ -3296,6 +3296,9 @@
"scheduleDetailView.cannotModifyRunning": {
"defaultMessage": "Cannot trigger or modify a schedule while it's already running."
},
"scheduleDetailView.completedSession": {
"defaultMessage": "Session: {sessionId}"
},
"scheduleDetailView.created": {
"defaultMessage": "Created: {date}"
},
@ -3317,9 +3320,6 @@
"scheduleDetailView.errorPrefix": {
"defaultMessage": "Error: {error}"
},
"scheduleDetailView.failedToLoadSession": {
"defaultMessage": "Failed to load session"
},
"scheduleDetailView.idLabel": {
"defaultMessage": "ID:"
},
@ -3362,9 +3362,6 @@
"scheduleDetailView.messages": {
"defaultMessage": "Messages: {count}"
},
"scheduleDetailView.newSession": {
"defaultMessage": "New session: {sessionId}"
},
"scheduleDetailView.noScheduleId": {
"defaultMessage": "No schedule ID provided. Return to schedules list."
},
@ -3401,6 +3398,9 @@
"scheduleDetailView.runScheduleNow": {
"defaultMessage": "Run Schedule Now"
},
"scheduleDetailView.scheduleCompleted": {
"defaultMessage": "Run completed"
},
"scheduleDetailView.scheduleDetails": {
"defaultMessage": "Schedule Details"
},
@ -3419,9 +3419,6 @@
"scheduleDetailView.schedulePaused": {
"defaultMessage": "Schedule Paused"
},
"scheduleDetailView.scheduleTriggered": {
"defaultMessage": "Schedule Triggered"
},
"scheduleDetailView.scheduleUnpaused": {
"defaultMessage": "Schedule Unpaused"
},
@ -3431,9 +3428,6 @@
"scheduleDetailView.sessionId": {
"defaultMessage": "Session ID: {id}"
},
"scheduleDetailView.tokens": {
"defaultMessage": "Tokens: {count}"
},
"scheduleDetailView.unpauseSchedule": {
"defaultMessage": "Unpause Schedule"
},

View file

@ -3296,6 +3296,9 @@
"scheduleDetailView.cannotModifyRunning": {
"defaultMessage": "No se puede activar ni modificar una programación mientras se está ejecutando."
},
"scheduleDetailView.completedSession": {
"defaultMessage": "Sesión: {sessionId}"
},
"scheduleDetailView.created": {
"defaultMessage": "Creada: {date}"
},
@ -3317,9 +3320,6 @@
"scheduleDetailView.errorPrefix": {
"defaultMessage": "Error: {error}"
},
"scheduleDetailView.failedToLoadSession": {
"defaultMessage": "No se pudo cargar la sesión"
},
"scheduleDetailView.idLabel": {
"defaultMessage": "ID:"
},
@ -3362,9 +3362,6 @@
"scheduleDetailView.messages": {
"defaultMessage": "Mensajes: {count}"
},
"scheduleDetailView.newSession": {
"defaultMessage": "Nueva sesión: {sessionId}"
},
"scheduleDetailView.noScheduleId": {
"defaultMessage": "No se proporcionó ningún ID de programación. Vuelve a la lista de programaciones."
},
@ -3401,6 +3398,9 @@
"scheduleDetailView.runScheduleNow": {
"defaultMessage": "Ejecutar programación ahora"
},
"scheduleDetailView.scheduleCompleted": {
"defaultMessage": "Ejecución completada"
},
"scheduleDetailView.scheduleDetails": {
"defaultMessage": "Detalles de la programación"
},
@ -3419,9 +3419,6 @@
"scheduleDetailView.schedulePaused": {
"defaultMessage": "Programación en pausa"
},
"scheduleDetailView.scheduleTriggered": {
"defaultMessage": "Programación activada"
},
"scheduleDetailView.scheduleUnpaused": {
"defaultMessage": "Programación reanudada"
},
@ -3431,9 +3428,6 @@
"scheduleDetailView.sessionId": {
"defaultMessage": "ID de sesión: {id}"
},
"scheduleDetailView.tokens": {
"defaultMessage": "Tokens: {count}"
},
"scheduleDetailView.unpauseSchedule": {
"defaultMessage": "Reanudar programación"
},

View file

@ -3296,6 +3296,9 @@
"scheduleDetailView.cannotModifyRunning": {
"defaultMessage": "जब कोई शेड्यूल पहले से चल रहा हो तो उसे ट्रिगर या संशोधित नहीं किया जा सकता।"
},
"scheduleDetailView.completedSession": {
"defaultMessage": "सत्र: {sessionId}"
},
"scheduleDetailView.created": {
"defaultMessage": "बनाया गया: {date}"
},
@ -3317,9 +3320,6 @@
"scheduleDetailView.errorPrefix": {
"defaultMessage": "त्रुटि: {error}"
},
"scheduleDetailView.failedToLoadSession": {
"defaultMessage": "सत्र लोड करने में विफल"
},
"scheduleDetailView.idLabel": {
"defaultMessage": "आईडी:"
},
@ -3362,9 +3362,6 @@
"scheduleDetailView.messages": {
"defaultMessage": "संदेश: {count}"
},
"scheduleDetailView.newSession": {
"defaultMessage": "नया सत्र: {sessionId}"
},
"scheduleDetailView.noScheduleId": {
"defaultMessage": "कोई शेड्यूल आईडी प्रदान नहीं की गई. शेड्यूल सूची पर लौटें."
},
@ -3401,6 +3398,9 @@
"scheduleDetailView.runScheduleNow": {
"defaultMessage": "अभी शेड्यूल चलाएँ"
},
"scheduleDetailView.scheduleCompleted": {
"defaultMessage": "रन पूरा हुआ"
},
"scheduleDetailView.scheduleDetails": {
"defaultMessage": "अनुसूची विवरण"
},
@ -3419,9 +3419,6 @@
"scheduleDetailView.schedulePaused": {
"defaultMessage": "शेड्यूल रोका गया"
},
"scheduleDetailView.scheduleTriggered": {
"defaultMessage": "शेड्यूल ट्रिगर हुआ"
},
"scheduleDetailView.scheduleUnpaused": {
"defaultMessage": "शेड्यूल अनरोका"
},
@ -3431,9 +3428,6 @@
"scheduleDetailView.sessionId": {
"defaultMessage": "सत्र आईडी: {id}"
},
"scheduleDetailView.tokens": {
"defaultMessage": "टोकन: {count}"
},
"scheduleDetailView.unpauseSchedule": {
"defaultMessage": "शेड्यूल को फिर से रोकें"
},

View file

@ -3296,6 +3296,9 @@
"scheduleDetailView.cannotModifyRunning": {
"defaultMessage": "既に実行中のため、スケジュールを実行または変更できません。"
},
"scheduleDetailView.completedSession": {
"defaultMessage": "セッション: {sessionId}"
},
"scheduleDetailView.created": {
"defaultMessage": "作成: {date}"
},
@ -3317,9 +3320,6 @@
"scheduleDetailView.errorPrefix": {
"defaultMessage": "エラー: {error}"
},
"scheduleDetailView.failedToLoadSession": {
"defaultMessage": "セッションの読み込みに失敗しました"
},
"scheduleDetailView.idLabel": {
"defaultMessage": "ID:"
},
@ -3362,9 +3362,6 @@
"scheduleDetailView.messages": {
"defaultMessage": "メッセージ: {count}"
},
"scheduleDetailView.newSession": {
"defaultMessage": "新しいセッション: {sessionId}"
},
"scheduleDetailView.noScheduleId": {
"defaultMessage": "スケジュールIDが指定されていません。スケジュール一覧に戻ってください。"
},
@ -3401,6 +3398,9 @@
"scheduleDetailView.runScheduleNow": {
"defaultMessage": "スケジュールを今すぐ実行"
},
"scheduleDetailView.scheduleCompleted": {
"defaultMessage": "実行が完了しました"
},
"scheduleDetailView.scheduleDetails": {
"defaultMessage": "スケジュール詳細"
},
@ -3419,9 +3419,6 @@
"scheduleDetailView.schedulePaused": {
"defaultMessage": "スケジュールを一時停止しました"
},
"scheduleDetailView.scheduleTriggered": {
"defaultMessage": "スケジュールを実行しました"
},
"scheduleDetailView.scheduleUnpaused": {
"defaultMessage": "スケジュールを再開しました"
},
@ -3431,9 +3428,6 @@
"scheduleDetailView.sessionId": {
"defaultMessage": "セッションID: {id}"
},
"scheduleDetailView.tokens": {
"defaultMessage": "トークン: {count}"
},
"scheduleDetailView.unpauseSchedule": {
"defaultMessage": "スケジュールを再開"
},

View file

@ -3296,6 +3296,9 @@
"scheduleDetailView.cannotModifyRunning": {
"defaultMessage": "이미 실행 중인 일정은 트리거하거나 수정할 수 없습니다."
},
"scheduleDetailView.completedSession": {
"defaultMessage": "세션: {sessionId}"
},
"scheduleDetailView.created": {
"defaultMessage": "생성일: {date}"
},
@ -3317,9 +3320,6 @@
"scheduleDetailView.errorPrefix": {
"defaultMessage": "오류: {error}"
},
"scheduleDetailView.failedToLoadSession": {
"defaultMessage": "세션을 로드하지 못했습니다."
},
"scheduleDetailView.idLabel": {
"defaultMessage": "ID:"
},
@ -3362,9 +3362,6 @@
"scheduleDetailView.messages": {
"defaultMessage": "메시지: {count}"
},
"scheduleDetailView.newSession": {
"defaultMessage": "새 세션: {sessionId}"
},
"scheduleDetailView.noScheduleId": {
"defaultMessage": "제공된 일정 ID가 없습니다. 일정 목록으로 돌아갑니다."
},
@ -3401,6 +3398,9 @@
"scheduleDetailView.runScheduleNow": {
"defaultMessage": "지금 일정 실행"
},
"scheduleDetailView.scheduleCompleted": {
"defaultMessage": "실행 완료"
},
"scheduleDetailView.scheduleDetails": {
"defaultMessage": "일정 세부정보"
},
@ -3419,9 +3419,6 @@
"scheduleDetailView.schedulePaused": {
"defaultMessage": "일정이 일시 중지됨"
},
"scheduleDetailView.scheduleTriggered": {
"defaultMessage": "일정이 실행됨"
},
"scheduleDetailView.scheduleUnpaused": {
"defaultMessage": "일정 일시중지 해제됨"
},
@ -3431,9 +3428,6 @@
"scheduleDetailView.sessionId": {
"defaultMessage": "세션 ID: {id}"
},
"scheduleDetailView.tokens": {
"defaultMessage": "토큰: {count}"
},
"scheduleDetailView.unpauseSchedule": {
"defaultMessage": "일정 일시중지 해제"
},

View file

@ -3296,6 +3296,9 @@
"scheduleDetailView.cannotModifyRunning": {
"defaultMessage": "Нельзя запускать или изменять расписание, пока оно выполняется."
},
"scheduleDetailView.completedSession": {
"defaultMessage": "Сеанс: {sessionId}"
},
"scheduleDetailView.created": {
"defaultMessage": "Создано: {date}"
},
@ -3317,9 +3320,6 @@
"scheduleDetailView.errorPrefix": {
"defaultMessage": "Ошибка: {error}"
},
"scheduleDetailView.failedToLoadSession": {
"defaultMessage": "Не удалось загрузить сеанс"
},
"scheduleDetailView.idLabel": {
"defaultMessage": "ID:"
},
@ -3362,9 +3362,6 @@
"scheduleDetailView.messages": {
"defaultMessage": "Сообщений: {count}"
},
"scheduleDetailView.newSession": {
"defaultMessage": "Новый сеанс: {sessionId}"
},
"scheduleDetailView.noScheduleId": {
"defaultMessage": "ID расписания не указан. Вернитесь к списку расписаний."
},
@ -3401,6 +3398,9 @@
"scheduleDetailView.runScheduleNow": {
"defaultMessage": "Запустить расписание сейчас"
},
"scheduleDetailView.scheduleCompleted": {
"defaultMessage": "Запуск завершен"
},
"scheduleDetailView.scheduleDetails": {
"defaultMessage": "Детали расписания"
},
@ -3419,9 +3419,6 @@
"scheduleDetailView.schedulePaused": {
"defaultMessage": "Расписание приостановлено"
},
"scheduleDetailView.scheduleTriggered": {
"defaultMessage": "Расписание запущено"
},
"scheduleDetailView.scheduleUnpaused": {
"defaultMessage": "Расписание возобновлено"
},
@ -3431,9 +3428,6 @@
"scheduleDetailView.sessionId": {
"defaultMessage": "ID сеанса: {id}"
},
"scheduleDetailView.tokens": {
"defaultMessage": "Токенов: {count}"
},
"scheduleDetailView.unpauseSchedule": {
"defaultMessage": "Возобновить расписание"
},

View file

@ -3296,6 +3296,9 @@
"scheduleDetailView.cannotModifyRunning": {
"defaultMessage": "Zaten çalışmakta olan bir program tetiklenemez veya değiştirilemez."
},
"scheduleDetailView.completedSession": {
"defaultMessage": "Oturum: {sessionId}"
},
"scheduleDetailView.created": {
"defaultMessage": "Oluşturuldu: {date}"
},
@ -3317,9 +3320,6 @@
"scheduleDetailView.errorPrefix": {
"defaultMessage": "Hata: {error}"
},
"scheduleDetailView.failedToLoadSession": {
"defaultMessage": "Oturum yüklenemedi"
},
"scheduleDetailView.idLabel": {
"defaultMessage": "Kimlik:"
},
@ -3362,9 +3362,6 @@
"scheduleDetailView.messages": {
"defaultMessage": "Mesajlar: {count}"
},
"scheduleDetailView.newSession": {
"defaultMessage": "Yeni oturum: {sessionId}"
},
"scheduleDetailView.noScheduleId": {
"defaultMessage": "Program kimliği sağlanmadı. Program listesine geri dönün."
},
@ -3401,6 +3398,9 @@
"scheduleDetailView.runScheduleNow": {
"defaultMessage": "Şimdi Planla'yı Çalıştır"
},
"scheduleDetailView.scheduleCompleted": {
"defaultMessage": "Çalıştırma tamamlandı"
},
"scheduleDetailView.scheduleDetails": {
"defaultMessage": "Program Ayrıntıları"
},
@ -3419,9 +3419,6 @@
"scheduleDetailView.schedulePaused": {
"defaultMessage": "Plan Duraklatıldı"
},
"scheduleDetailView.scheduleTriggered": {
"defaultMessage": "Zamanlama Tetiklendi"
},
"scheduleDetailView.scheduleUnpaused": {
"defaultMessage": "Duraklatmayı Kaldırmayı Planla"
},
@ -3431,9 +3428,6 @@
"scheduleDetailView.sessionId": {
"defaultMessage": "Oturum Kimliği: {id}"
},
"scheduleDetailView.tokens": {
"defaultMessage": "Jetonlar: {count}"
},
"scheduleDetailView.unpauseSchedule": {
"defaultMessage": "Programı Duraklatmayı Kaldır"
},

View file

@ -3296,6 +3296,9 @@
"scheduleDetailView.cannotModifyRunning": {
"defaultMessage": "计划正在运行中,无法触发或修改。"
},
"scheduleDetailView.completedSession": {
"defaultMessage": "会话:{sessionId}"
},
"scheduleDetailView.created": {
"defaultMessage": "创建于:{date}"
},
@ -3317,9 +3320,6 @@
"scheduleDetailView.errorPrefix": {
"defaultMessage": "错误:{error}"
},
"scheduleDetailView.failedToLoadSession": {
"defaultMessage": "加载会话失败"
},
"scheduleDetailView.idLabel": {
"defaultMessage": "ID"
},
@ -3362,9 +3362,6 @@
"scheduleDetailView.messages": {
"defaultMessage": "消息:{count}"
},
"scheduleDetailView.newSession": {
"defaultMessage": "新会话:{sessionId}"
},
"scheduleDetailView.noScheduleId": {
"defaultMessage": "未提供计划 ID。返回计划列表。"
},
@ -3401,6 +3398,9 @@
"scheduleDetailView.runScheduleNow": {
"defaultMessage": "立即运行计划"
},
"scheduleDetailView.scheduleCompleted": {
"defaultMessage": "运行已完成"
},
"scheduleDetailView.scheduleDetails": {
"defaultMessage": "计划详情"
},
@ -3419,9 +3419,6 @@
"scheduleDetailView.schedulePaused": {
"defaultMessage": "计划已暂停"
},
"scheduleDetailView.scheduleTriggered": {
"defaultMessage": "计划已触发"
},
"scheduleDetailView.scheduleUnpaused": {
"defaultMessage": "计划已恢复"
},
@ -3431,9 +3428,6 @@
"scheduleDetailView.sessionId": {
"defaultMessage": "会话 ID{id}"
},
"scheduleDetailView.tokens": {
"defaultMessage": "令牌:{count}"
},
"scheduleDetailView.unpauseSchedule": {
"defaultMessage": "恢复计划"
},

View file

@ -1,191 +0,0 @@
import {
listSchedules as apiListSchedules,
createSchedule as apiCreateSchedule,
deleteSchedule as apiDeleteSchedule,
pauseSchedule as apiPauseSchedule,
unpauseSchedule as apiUnpauseSchedule,
updateSchedule as apiUpdateSchedule,
sessionsHandler as apiGetScheduleSessions,
runNowHandler as apiRunScheduleNow,
killRunningJob as apiKillRunningJob,
inspectRunningJob as apiInspectRunningJob,
SessionDisplayInfo,
} from './api';
import type { Recipe } from './recipe';
export interface ScheduledJob {
id: string;
source: string;
cron: string;
last_run?: string | null;
currently_running?: boolean;
paused?: boolean;
current_session_id?: string | null;
process_start_time?: string | null;
}
export interface ScheduleSession {
id: string;
name: string;
createdAt: string; // ISO 8601 date string
workingDir: string;
scheduleId: string;
messageCount: number;
totalTokens: number;
inputTokens: number;
outputTokens: number;
accumulatedTotalTokens: number;
accumulatedInputTokens: number;
accumulatedOutputTokens: number;
}
export async function listSchedules(): Promise<ScheduledJob[]> {
try {
const response = await apiListSchedules<true>();
if (response && response.data && Array.isArray(response.data.jobs)) {
return response.data.jobs as ScheduledJob[];
}
console.error('Unexpected response format from apiListSchedules', response);
throw new Error('Failed to list schedules: Unexpected response format');
} catch (error) {
console.error('Error listing schedules:', error);
throw error;
}
}
export async function createSchedule(request: {
id: string;
recipe: Recipe;
cron: string;
}): Promise<ScheduledJob> {
type ApiCreateScheduleBody = Parameters<typeof apiCreateSchedule>[0]['body'];
const response = await apiCreateSchedule({ body: request as unknown as ApiCreateScheduleBody });
if (response.data) {
return response.data as ScheduledJob;
}
const err = response.error as { message?: string } | undefined;
throw new Error(err?.message || 'Failed to create schedule');
}
export async function deleteSchedule(id: string): Promise<void> {
try {
await apiDeleteSchedule<true>({ path: { id } });
} catch (error) {
console.error(`Error deleting schedule ${id}:`, error);
throw error;
}
}
export async function getScheduleSessions(
scheduleId: string,
limit: number
): Promise<Array<SessionDisplayInfo>> {
const response = await apiGetScheduleSessions<true>({
path: { id: scheduleId },
query: { limit },
throwOnError: true,
});
return response.data;
}
export async function runScheduleNow(scheduleId: string): Promise<string> {
try {
const response = await apiRunScheduleNow<true>({
path: { id: scheduleId },
});
if (response && response.data && response.data.session_id) {
return response.data.session_id;
}
console.error('Unexpected response format from apiRunScheduleNow', response);
throw new Error('Failed to run schedule now: Unexpected response format');
} catch (error) {
console.error(`Error running schedule ${scheduleId} now:`, error);
throw error;
}
}
export async function pauseSchedule(scheduleId: string): Promise<void> {
try {
await apiPauseSchedule<true>({
path: { id: scheduleId },
});
} catch (error) {
console.error(`Error pausing schedule ${scheduleId}:`, error);
throw error;
}
}
export async function unpauseSchedule(scheduleId: string): Promise<void> {
try {
await apiUnpauseSchedule<true>({
path: { id: scheduleId },
});
} catch (error) {
console.error(`Error unpausing schedule ${scheduleId}:`, error);
throw error;
}
}
export async function updateSchedule(scheduleId: string, cron: string): Promise<ScheduledJob> {
try {
const response = await apiUpdateSchedule<true>({
path: { id: scheduleId },
body: { cron },
});
if (response && response.data) {
return response.data as ScheduledJob;
}
console.error('Unexpected response format from apiUpdateSchedule', response);
throw new Error('Failed to update schedule: Unexpected response format');
} catch (error) {
console.error(`Error updating schedule ${scheduleId}:`, error);
throw error;
}
}
export interface KillJobResponse {
message: string;
}
export interface InspectJobResponse {
sessionId?: string | null;
processStartTime?: string | null;
runningDurationSeconds?: number | null;
}
export async function killRunningJob(scheduleId: string): Promise<KillJobResponse> {
try {
const response = await apiKillRunningJob<true>({
path: { id: scheduleId },
});
if (response && response.data) {
return response.data as KillJobResponse;
}
console.error('Unexpected response format from apiKillRunningJob', response);
throw new Error('Failed to kill running job: Unexpected response format');
} catch (error) {
console.error(`Error killing running job ${scheduleId}:`, error);
throw error;
}
}
export async function inspectRunningJob(scheduleId: string): Promise<InspectJobResponse> {
try {
const response = await apiInspectRunningJob<true>({
path: { id: scheduleId },
});
if (response && response.data) {
return response.data as InspectJobResponse;
}
console.error('Unexpected response format from apiInspectRunningJob', response);
throw new Error('Failed to inspect running job: Unexpected response format');
} catch (error) {
console.error(`Error inspecting running job ${scheduleId}:`, error);
throw error;
}
}

View file

@ -12,6 +12,8 @@ import type {
AddConfigExtensionRequest_unstable,
AddSessionExtensionRequest_unstable,
ArchiveSessionRequest_unstable,
CreateScheduleRequest_unstable,
CreateScheduleResponse_unstable,
CreateSourceRequest_unstable,
CreateSourceResponse_unstable,
CustomProviderCreateRequest_unstable,
@ -28,6 +30,7 @@ import type {
DefaultsReadResponse_unstable,
DefaultsSaveRequest_unstable,
DeleteRecipeRequest_unstable,
DeleteScheduleRequest_unstable,
DeleteSessionRequest,
DeleteSourceRequest_unstable,
DiagnosticsGetRequest_unstable,
@ -69,12 +72,20 @@ import type {
ImportSessionResponse_unstable,
ImportSourcesRequest_unstable,
ImportSourcesResponse_unstable,
InspectRunningJobRequest_unstable,
InspectRunningJobResponse_unstable,
KillRunningJobRequest_unstable,
KillRunningJobResponse_unstable,
ListAgentMentionsRequest_unstable,
ListAgentMentionsResponse_unstable,
ListProvidersRequest_unstable,
ListProvidersResponse_unstable,
ListRecipesRequest_unstable,
ListRecipesResponse_unstable,
ListScheduleSessionsRequest_unstable,
ListScheduleSessionsResponse_unstable,
ListSchedulesRequest_unstable,
ListSchedulesResponse_unstable,
ListSlashCommandsRequest_unstable,
ListSlashCommandsResponse_unstable,
ListSourcesRequest_unstable,
@ -85,6 +96,7 @@ import type {
OnboardingImportScanResponse_unstable,
ParseRecipeRequest_unstable,
ParseRecipeResponse_unstable,
PauseScheduleRequest_unstable,
PreferencesReadRequest_unstable,
PreferencesReadResponse_unstable,
PreferencesRemoveRequest_unstable,
@ -116,6 +128,8 @@ import type {
RemoveSessionExtensionRequest_unstable,
RenameSessionRequest_unstable,
RequestRecipeParams_unstable,
RunScheduleNowRequest_unstable,
RunScheduleNowResponse_unstable,
SaveRecipeRequest_unstable,
SaveRecipeResponse_unstable,
ScanRecipeRequest_unstable,
@ -128,12 +142,16 @@ import type {
SteerSessionResponse_unstable,
TruncateSessionConversationRequest_unstable,
UnarchiveSessionRequest_unstable,
UnpauseScheduleRequest_unstable,
UpdateScheduleRequest_unstable,
UpdateScheduleResponse_unstable,
UpdateSessionProjectRequest_unstable,
UpdateSourceRequest_unstable,
UpdateSourceResponse_unstable,
UpdateWorkingDirRequest_unstable,
} from './types.gen.js';
import {
zCreateScheduleResponse_unstable,
zCreateSourceResponse_unstable,
zCustomProviderCreateResponse_unstable,
zCustomProviderDeleteResponse_unstable,
@ -158,9 +176,13 @@ import {
zGooseToolCallResponse_unstable,
zImportSessionResponse_unstable,
zImportSourcesResponse_unstable,
zInspectRunningJobResponse_unstable,
zKillRunningJobResponse_unstable,
zListAgentMentionsResponse_unstable,
zListProvidersResponse_unstable,
zListRecipesResponse_unstable,
zListScheduleSessionsResponse_unstable,
zListSchedulesResponse_unstable,
zListSlashCommandsResponse_unstable,
zListSourcesResponse_unstable,
zOnboardingImportApplyResponse_unstable,
@ -178,9 +200,11 @@ import {
zRecipeToYamlResponse_unstable,
zRefreshProviderInventoryResponse_unstable,
zRequestRecipeParams_unstable,
zRunScheduleNowResponse_unstable,
zSaveRecipeResponse_unstable,
zScanRecipeResponse_unstable,
zSteerSessionResponse_unstable,
zUpdateScheduleResponse_unstable,
zUpdateSourceResponse_unstable,
} from './zod.gen.js';
@ -714,6 +738,108 @@ export class GooseExtClient {
) as RecipeToYamlResponse_unstable;
}
async schedulesList_unstable(
params: ListSchedulesRequest_unstable,
): Promise<ListSchedulesResponse_unstable> {
const raw = await this.conn.extMethod(
"_goose/unstable/schedules/list",
params,
);
return zListSchedulesResponse_unstable.parse(
raw,
) as ListSchedulesResponse_unstable;
}
async schedulesSessionsList_unstable(
params: ListScheduleSessionsRequest_unstable,
): Promise<ListScheduleSessionsResponse_unstable> {
const raw = await this.conn.extMethod(
"_goose/unstable/schedules/sessions/list",
params,
);
return zListScheduleSessionsResponse_unstable.parse(
raw,
) as ListScheduleSessionsResponse_unstable;
}
async schedulesCreate_unstable(
params: CreateScheduleRequest_unstable,
): Promise<CreateScheduleResponse_unstable> {
const raw = await this.conn.extMethod(
"_goose/unstable/schedules/create",
params,
);
return zCreateScheduleResponse_unstable.parse(
raw,
) as CreateScheduleResponse_unstable;
}
async schedulesDelete_unstable(
params: DeleteScheduleRequest_unstable,
): Promise<void> {
await this.conn.extMethod("_goose/unstable/schedules/delete", params);
}
async schedulesPause_unstable(
params: PauseScheduleRequest_unstable,
): Promise<void> {
await this.conn.extMethod("_goose/unstable/schedules/pause", params);
}
async schedulesUnpause_unstable(
params: UnpauseScheduleRequest_unstable,
): Promise<void> {
await this.conn.extMethod("_goose/unstable/schedules/unpause", params);
}
async schedulesUpdate_unstable(
params: UpdateScheduleRequest_unstable,
): Promise<UpdateScheduleResponse_unstable> {
const raw = await this.conn.extMethod(
"_goose/unstable/schedules/update",
params,
);
return zUpdateScheduleResponse_unstable.parse(
raw,
) as UpdateScheduleResponse_unstable;
}
async schedulesRunNow_unstable(
params: RunScheduleNowRequest_unstable,
): Promise<RunScheduleNowResponse_unstable> {
const raw = await this.conn.extMethod(
"_goose/unstable/schedules/run-now",
params,
);
return zRunScheduleNowResponse_unstable.parse(
raw,
) as RunScheduleNowResponse_unstable;
}
async schedulesRunningJobKill_unstable(
params: KillRunningJobRequest_unstable,
): Promise<KillRunningJobResponse_unstable> {
const raw = await this.conn.extMethod(
"_goose/unstable/schedules/running-job/kill",
params,
);
return zKillRunningJobResponse_unstable.parse(
raw,
) as KillRunningJobResponse_unstable;
}
async schedulesRunningJobInspect_unstable(
params: InspectRunningJobRequest_unstable,
): Promise<InspectRunningJobResponse_unstable> {
const raw = await this.conn.extMethod(
"_goose/unstable/schedules/running-job/inspect",
params,
);
return zInspectRunningJobResponse_unstable.parse(
raw,
) as InspectRunningJobResponse_unstable;
}
async sessionInfo_unstable(
params: GetSessionInfoRequest_unstable,
): Promise<GetSessionInfoResponse_unstable> {

File diff suppressed because one or more lines are too long

View file

@ -1335,15 +1335,32 @@ export type RecipeToYamlResponse_unstable = {
yaml: string;
};
/**
* Return list-style metadata for a single session without loading the conversation.
*/
export type GetSessionInfoRequest_unstable = {
sessionId: string;
export type ListSchedulesRequest_unstable = {
[key: string]: unknown;
};
export type GetSessionInfoResponse_unstable = {
session: SessionInfo;
export type ListSchedulesResponse_unstable = {
jobs: Array<ScheduledJobDto>;
};
export type ScheduledJobDto = {
id: string;
source: string;
cron: string;
lastRun?: string | null;
currentlyRunning: boolean;
paused: boolean;
currentSessionId?: string | null;
jobStartTime?: string | null;
};
export type ListScheduleSessionsRequest_unstable = {
scheduleId: string;
limit: number;
};
export type ListScheduleSessionsResponse_unstable = {
sessions: Array<SessionInfo>;
};
/**
@ -1398,6 +1415,78 @@ export type SessionInfo = {
*/
export type SessionId = string;
export type CreateScheduleRequest_unstable = {
id: string;
recipe: RecipeDto;
cron: string;
};
export type CreateScheduleResponse_unstable = {
job: ScheduledJobDto;
};
export type DeleteScheduleRequest_unstable = {
scheduleId: string;
};
export type PauseScheduleRequest_unstable = {
scheduleId: string;
};
export type UnpauseScheduleRequest_unstable = {
scheduleId: string;
};
export type UpdateScheduleRequest_unstable = {
scheduleId: string;
cron: string;
};
export type UpdateScheduleResponse_unstable = {
job: ScheduledJobDto;
};
export type RunScheduleNowRequest_unstable = {
scheduleId: string;
};
export type RunScheduleNowResponse_unstable = {
status: RunScheduleNowStatus;
sessionId?: string | null;
};
export type RunScheduleNowStatus = 'completed' | 'cancelled';
export type KillRunningJobRequest_unstable = {
jobId: string;
};
export type KillRunningJobResponse_unstable = {
message: string;
};
export type InspectRunningJobRequest_unstable = {
jobId: string;
};
export type InspectRunningJobResponse_unstable = {
running: boolean;
sessionId?: string | null;
jobStartTime?: string | null;
runningDurationSeconds?: number | null;
};
/**
* Return list-style metadata for a single session without loading the conversation.
*/
export type GetSessionInfoRequest_unstable = {
sessionId: string;
};
export type GetSessionInfoResponse_unstable = {
session: SessionInfo;
};
/**
* Truncate a session conversation from the given message timestamp onward.
*/
@ -1905,14 +1994,14 @@ export type RecipeParamsAction = 'submit' | 'cancel';
export type ExtRequest = {
id: string;
method: string;
params?: AddSessionExtensionRequest_unstable | RemoveSessionExtensionRequest_unstable | GetToolsRequest_unstable | GooseToolCallRequest_unstable | ReadResourceRequest_unstable | UpdateWorkingDirRequest_unstable | SetSessionSystemPromptRequest_unstable | SteerSessionRequest_unstable | DiagnosticsGetRequest_unstable | DeleteSessionRequest | GetConfigExtensionsRequest_unstable | GetAvailableExtensionsRequest_unstable | AddConfigExtensionRequest_unstable | RemoveConfigExtensionRequest_unstable | SetConfigExtensionEnabledRequest_unstable | GetSessionExtensionsRequest_unstable | ListProvidersRequest_unstable | ProviderSupportedModelsListRequest_unstable | ProviderCatalogListRequest_unstable | ProviderSetupCatalogListRequest_unstable | ProviderCatalogTemplateRequest_unstable | CustomProviderCreateRequest_unstable | CustomProviderReadRequest_unstable | CustomProviderUpdateRequest_unstable | CustomProviderDeleteRequest_unstable | RefreshProviderInventoryRequest_unstable | ProviderConfigReadRequest_unstable | ProviderConfigStatusRequest_unstable | ProviderConfigSaveRequest_unstable | ProviderConfigDeleteRequest_unstable | ProviderConfigAuthenticateRequest_unstable | PreferencesReadRequest_unstable | PreferencesSaveRequest_unstable | PreferencesRemoveRequest_unstable | DefaultsReadRequest_unstable | DefaultsSaveRequest_unstable | OnboardingImportScanRequest_unstable | OnboardingImportApplyRequest_unstable | ExportSessionRequest_unstable | ImportSessionRequest_unstable | EncodeRecipeRequest_unstable | DecodeRecipeRequest_unstable | ScanRecipeRequest_unstable | ListRecipesRequest_unstable | DeleteRecipeRequest_unstable | ScheduleRecipeRequest_unstable | SetRecipeSlashCommandRequest_unstable | SaveRecipeRequest_unstable | ParseRecipeRequest_unstable | RecipeToYamlRequest_unstable | GetSessionInfoRequest_unstable | TruncateSessionConversationRequest_unstable | UpdateSessionProjectRequest_unstable | RenameSessionRequest_unstable | ArchiveSessionRequest_unstable | UnarchiveSessionRequest_unstable | CreateSourceRequest_unstable | ListSourcesRequest_unstable | ListAgentMentionsRequest_unstable | ListSlashCommandsRequest_unstable | UpdateSourceRequest_unstable | DeleteSourceRequest_unstable | ExportSourceRequest_unstable | ImportSourcesRequest_unstable | DictationTranscribeRequest_unstable | DictationConfigRequest_unstable | DictationSecretSaveRequest_unstable | DictationSecretDeleteRequest_unstable | DictationModelsListRequest_unstable | DictationModelDownloadRequest_unstable | DictationModelDownloadProgressRequest_unstable | DictationModelCancelRequest_unstable | DictationModelDeleteRequest_unstable | DictationModelSelectRequest_unstable | {
params?: AddSessionExtensionRequest_unstable | RemoveSessionExtensionRequest_unstable | GetToolsRequest_unstable | GooseToolCallRequest_unstable | ReadResourceRequest_unstable | UpdateWorkingDirRequest_unstable | SetSessionSystemPromptRequest_unstable | SteerSessionRequest_unstable | DiagnosticsGetRequest_unstable | DeleteSessionRequest | GetConfigExtensionsRequest_unstable | GetAvailableExtensionsRequest_unstable | AddConfigExtensionRequest_unstable | RemoveConfigExtensionRequest_unstable | SetConfigExtensionEnabledRequest_unstable | GetSessionExtensionsRequest_unstable | ListProvidersRequest_unstable | ProviderSupportedModelsListRequest_unstable | ProviderCatalogListRequest_unstable | ProviderSetupCatalogListRequest_unstable | ProviderCatalogTemplateRequest_unstable | CustomProviderCreateRequest_unstable | CustomProviderReadRequest_unstable | CustomProviderUpdateRequest_unstable | CustomProviderDeleteRequest_unstable | RefreshProviderInventoryRequest_unstable | ProviderConfigReadRequest_unstable | ProviderConfigStatusRequest_unstable | ProviderConfigSaveRequest_unstable | ProviderConfigDeleteRequest_unstable | ProviderConfigAuthenticateRequest_unstable | PreferencesReadRequest_unstable | PreferencesSaveRequest_unstable | PreferencesRemoveRequest_unstable | DefaultsReadRequest_unstable | DefaultsSaveRequest_unstable | OnboardingImportScanRequest_unstable | OnboardingImportApplyRequest_unstable | ExportSessionRequest_unstable | ImportSessionRequest_unstable | EncodeRecipeRequest_unstable | DecodeRecipeRequest_unstable | ScanRecipeRequest_unstable | ListRecipesRequest_unstable | DeleteRecipeRequest_unstable | ScheduleRecipeRequest_unstable | SetRecipeSlashCommandRequest_unstable | SaveRecipeRequest_unstable | ParseRecipeRequest_unstable | RecipeToYamlRequest_unstable | ListSchedulesRequest_unstable | ListScheduleSessionsRequest_unstable | CreateScheduleRequest_unstable | DeleteScheduleRequest_unstable | PauseScheduleRequest_unstable | UnpauseScheduleRequest_unstable | UpdateScheduleRequest_unstable | RunScheduleNowRequest_unstable | KillRunningJobRequest_unstable | InspectRunningJobRequest_unstable | GetSessionInfoRequest_unstable | TruncateSessionConversationRequest_unstable | UpdateSessionProjectRequest_unstable | RenameSessionRequest_unstable | ArchiveSessionRequest_unstable | UnarchiveSessionRequest_unstable | CreateSourceRequest_unstable | ListSourcesRequest_unstable | ListAgentMentionsRequest_unstable | ListSlashCommandsRequest_unstable | UpdateSourceRequest_unstable | DeleteSourceRequest_unstable | ExportSourceRequest_unstable | ImportSourcesRequest_unstable | DictationTranscribeRequest_unstable | DictationConfigRequest_unstable | DictationSecretSaveRequest_unstable | DictationSecretDeleteRequest_unstable | DictationModelsListRequest_unstable | DictationModelDownloadRequest_unstable | DictationModelDownloadProgressRequest_unstable | DictationModelCancelRequest_unstable | DictationModelDeleteRequest_unstable | DictationModelSelectRequest_unstable | {
[key: string]: unknown;
} | null;
};
export type ExtResponse = {
id: string;
result?: EmptyResponse | GetToolsResponse_unstable | GooseToolCallResponse_unstable | ReadResourceResponse_unstable | SteerSessionResponse_unstable | DiagnosticsGetResponse_unstable | GetConfigExtensionsResponse_unstable | GetAvailableExtensionsResponse_unstable | GetSessionExtensionsResponse_unstable | ListProvidersResponse_unstable | ProviderSupportedModelsListResponse_unstable | ProviderCatalogListResponse_unstable | ProviderSetupCatalogListResponse_unstable | ProviderCatalogTemplateResponse_unstable | CustomProviderCreateResponse_unstable | CustomProviderReadResponse_unstable | CustomProviderUpdateResponse_unstable | CustomProviderDeleteResponse_unstable | RefreshProviderInventoryResponse_unstable | ProviderConfigReadResponse_unstable | ProviderConfigStatusResponse_unstable | ProviderConfigChangeResponse_unstable | PreferencesReadResponse_unstable | DefaultsReadResponse_unstable | OnboardingImportScanResponse_unstable | OnboardingImportApplyResponse_unstable | ExportSessionResponse_unstable | ImportSessionResponse_unstable | EncodeRecipeResponse_unstable | DecodeRecipeResponse_unstable | ScanRecipeResponse_unstable | ListRecipesResponse_unstable | SaveRecipeResponse_unstable | ParseRecipeResponse_unstable | RecipeToYamlResponse_unstable | GetSessionInfoResponse_unstable | CreateSourceResponse_unstable | ListSourcesResponse_unstable | ListAgentMentionsResponse_unstable | ListSlashCommandsResponse_unstable | UpdateSourceResponse_unstable | ExportSourceResponse_unstable | ImportSourcesResponse_unstable | DictationTranscribeResponse_unstable | DictationConfigResponse_unstable | DictationModelsListResponse_unstable | DictationModelDownloadProgressResponse_unstable | unknown;
result?: EmptyResponse | GetToolsResponse_unstable | GooseToolCallResponse_unstable | ReadResourceResponse_unstable | SteerSessionResponse_unstable | DiagnosticsGetResponse_unstable | GetConfigExtensionsResponse_unstable | GetAvailableExtensionsResponse_unstable | GetSessionExtensionsResponse_unstable | ListProvidersResponse_unstable | ProviderSupportedModelsListResponse_unstable | ProviderCatalogListResponse_unstable | ProviderSetupCatalogListResponse_unstable | ProviderCatalogTemplateResponse_unstable | CustomProviderCreateResponse_unstable | CustomProviderReadResponse_unstable | CustomProviderUpdateResponse_unstable | CustomProviderDeleteResponse_unstable | RefreshProviderInventoryResponse_unstable | ProviderConfigReadResponse_unstable | ProviderConfigStatusResponse_unstable | ProviderConfigChangeResponse_unstable | PreferencesReadResponse_unstable | DefaultsReadResponse_unstable | OnboardingImportScanResponse_unstable | OnboardingImportApplyResponse_unstable | ExportSessionResponse_unstable | ImportSessionResponse_unstable | EncodeRecipeResponse_unstable | DecodeRecipeResponse_unstable | ScanRecipeResponse_unstable | ListRecipesResponse_unstable | SaveRecipeResponse_unstable | ParseRecipeResponse_unstable | RecipeToYamlResponse_unstable | ListSchedulesResponse_unstable | ListScheduleSessionsResponse_unstable | CreateScheduleResponse_unstable | UpdateScheduleResponse_unstable | RunScheduleNowResponse_unstable | KillRunningJobResponse_unstable | InspectRunningJobResponse_unstable | GetSessionInfoResponse_unstable | CreateSourceResponse_unstable | ListSourcesResponse_unstable | ListAgentMentionsResponse_unstable | ListSlashCommandsResponse_unstable | UpdateSourceResponse_unstable | ExportSourceResponse_unstable | ImportSourcesResponse_unstable | DictationTranscribeResponse_unstable | DictationConfigResponse_unstable | DictationModelsListResponse_unstable | DictationModelDownloadProgressResponse_unstable | unknown;
} | {
error: {
code: number;

View file

@ -1470,11 +1470,35 @@ export const zRecipeToYamlResponse_unstable = z.object({
yaml: z.string()
});
/**
* Return list-style metadata for a single session without loading the conversation.
*/
export const zGetSessionInfoRequest_unstable = z.object({
sessionId: z.string()
export const zListSchedulesRequest_unstable = z.record(z.unknown());
export const zScheduledJobDto = z.object({
id: z.string(),
source: z.string(),
cron: z.string(),
lastRun: z.union([
z.string(),
z.null()
]).optional(),
currentlyRunning: z.boolean(),
paused: z.boolean(),
currentSessionId: z.union([
z.string(),
z.null()
]).optional(),
jobStartTime: z.union([
z.string(),
z.null()
]).optional()
});
export const zListSchedulesResponse_unstable = z.object({
jobs: z.array(zScheduledJobDto)
});
export const zListScheduleSessionsRequest_unstable = z.object({
scheduleId: z.string(),
limit: z.number().int().gte(0)
});
/**
@ -1508,6 +1532,90 @@ export const zSessionInfo = z.object({
]).optional()
});
export const zListScheduleSessionsResponse_unstable = z.object({
sessions: z.array(zSessionInfo)
});
export const zCreateScheduleRequest_unstable = z.object({
id: z.string(),
recipe: zRecipeDto,
cron: z.string()
});
export const zCreateScheduleResponse_unstable = z.object({
job: zScheduledJobDto
});
export const zDeleteScheduleRequest_unstable = z.object({
scheduleId: z.string()
});
export const zPauseScheduleRequest_unstable = z.object({
scheduleId: z.string()
});
export const zUnpauseScheduleRequest_unstable = z.object({
scheduleId: z.string()
});
export const zUpdateScheduleRequest_unstable = z.object({
scheduleId: z.string(),
cron: z.string()
});
export const zUpdateScheduleResponse_unstable = z.object({
job: zScheduledJobDto
});
export const zRunScheduleNowRequest_unstable = z.object({
scheduleId: z.string()
});
export const zRunScheduleNowStatus = z.enum(['completed', 'cancelled']);
export const zRunScheduleNowResponse_unstable = z.object({
status: zRunScheduleNowStatus,
sessionId: z.union([
z.string(),
z.null()
]).optional()
});
export const zKillRunningJobRequest_unstable = z.object({
jobId: z.string()
});
export const zKillRunningJobResponse_unstable = z.object({
message: z.string()
});
export const zInspectRunningJobRequest_unstable = z.object({
jobId: z.string()
});
export const zInspectRunningJobResponse_unstable = z.object({
running: z.boolean(),
sessionId: z.union([
z.string(),
z.null()
]).optional(),
jobStartTime: z.union([
z.string(),
z.null()
]).optional(),
runningDurationSeconds: z.union([
z.number().int(),
z.null()
]).optional()
});
/**
* Return list-style metadata for a single session without loading the conversation.
*/
export const zGetSessionInfoRequest_unstable = z.object({
sessionId: z.string()
});
export const zGetSessionInfoResponse_unstable = z.object({
session: zSessionInfo
});
@ -2056,6 +2164,16 @@ export const zExtRequest = z.object({
zSaveRecipeRequest_unstable,
zParseRecipeRequest_unstable,
zRecipeToYamlRequest_unstable,
zListSchedulesRequest_unstable,
zListScheduleSessionsRequest_unstable,
zCreateScheduleRequest_unstable,
zDeleteScheduleRequest_unstable,
zPauseScheduleRequest_unstable,
zUnpauseScheduleRequest_unstable,
zUpdateScheduleRequest_unstable,
zRunScheduleNowRequest_unstable,
zKillRunningJobRequest_unstable,
zInspectRunningJobRequest_unstable,
zGetSessionInfoRequest_unstable,
zTruncateSessionConversationRequest_unstable,
zUpdateSessionProjectRequest_unstable,
@ -2128,6 +2246,13 @@ export const zExtResponse = z.union([
zSaveRecipeResponse_unstable,
zParseRecipeResponse_unstable,
zRecipeToYamlResponse_unstable,
zListSchedulesResponse_unstable,
zListScheduleSessionsResponse_unstable,
zCreateScheduleResponse_unstable,
zUpdateScheduleResponse_unstable,
zRunScheduleNowResponse_unstable,
zKillRunningJobResponse_unstable,
zInspectRunningJobResponse_unstable,
zGetSessionInfoResponse_unstable,
zCreateSourceResponse_unstable,
zListSourcesResponse_unstable,