Skip to content

Commit 4cfe1ca

Browse files
Zexiclaude
authored andcommitted
fix: flatten schedule tool parameters to a single object schema
Some providers (e.g. DeepSeek) reject discriminated-union tool parameters with "schema must be a JSON Schema of 'type: \"object\"', got 'type: null'". Replace the union of CreateAction/DeleteAction/ListAction with one Struct that has `action` plus optional `expression`/`message`/`id` fields, then validate required combinations at runtime and return an actionable error back to the model when fields are missing. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent db306c8 commit 4cfe1ca

1 file changed

Lines changed: 36 additions & 26 deletions

File tree

packages/opencode/src/tool/schedule.ts

Lines changed: 36 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -21,29 +21,22 @@ Examples:
2121
2222
Minimum interval is 60 seconds. Maximum 10 schedules per session.`
2323

24-
const CreateAction = Schema.Struct({
25-
action: Schema.Literal("create").annotate({ description: "Create a new scheduled task." }),
26-
expression: Schema.String.annotate({
27-
description: "Standard 5-field cron expression. Example: '*/10 * * * *'. Minimum interval 60s.",
24+
export const Parameters = Schema.Struct({
25+
action: Schema.Literals(["create", "delete", "list"]).annotate({
26+
description:
27+
"Which operation to perform. 'create' requires expression+message; 'delete' requires id; 'list' takes no extra fields.",
2828
}),
29-
message: Schema.String.annotate({
30-
description: "Message content to inject into the session when the cron fires.",
29+
expression: Schema.optional(Schema.String).annotate({
30+
description:
31+
"Required for action='create'. Standard 5-field cron expression. Example: '*/10 * * * *'. Minimum interval 60s.",
3132
}),
32-
})
33-
34-
const DeleteAction = Schema.Struct({
35-
action: Schema.Literal("delete").annotate({ description: "Delete an existing scheduled task by id." }),
36-
id: Schema.String.annotate({ description: "Schedule id from create or list." }),
37-
})
38-
39-
const ListAction = Schema.Struct({
40-
action: Schema.Literal("list").annotate({
41-
description: "List all scheduled tasks for this session.",
33+
message: Schema.optional(Schema.String).annotate({
34+
description:
35+
"Required for action='create'. Message content to inject into the session when the cron fires.",
36+
}),
37+
id: Schema.optional(Schema.String).annotate({
38+
description: "Required for action='delete'. Schedule id from create or list.",
4239
}),
43-
})
44-
45-
export const Parameters = Schema.Union([CreateAction, DeleteAction, ListAction]).annotate({
46-
discriminator: "action",
4740
})
4841

4942
type Metadata = {
@@ -63,8 +56,17 @@ export const ScheduleTool = Tool.define<typeof Parameters, Metadata, Schedule.Se
6356
Effect.gen(function* () {
6457
switch (params.action) {
6558
case "create": {
59+
if (!params.expression || !params.message) {
60+
return {
61+
title: "Missing fields",
62+
output:
63+
"action='create' requires both 'expression' (5-field cron) and 'message'. Re-call with both fields.",
64+
metadata: { action: "create" } satisfies Metadata,
65+
}
66+
}
6667
const expression = params.expression.trim()
67-
return yield* schedule.create({ sessionID: ctx.sessionID, expression, message: params.message }).pipe(
68+
const message = params.message
69+
return yield* schedule.create({ sessionID: ctx.sessionID, expression, message }).pipe(
6870
Effect.map((info) => ({
6971
title: `Scheduled: ${expression}`,
7072
output: JSON.stringify(
@@ -77,7 +79,7 @@ export const ScheduleTool = Tool.define<typeof Parameters, Metadata, Schedule.Se
7779
null,
7880
2,
7981
),
80-
metadata: { action: "create", scheduleID: info.id } satisfies Metadata,
82+
metadata: { action: "create" as const, scheduleID: info.id } satisfies Metadata,
8183
})),
8284
Effect.catchTag("ScheduleInvalidExpression", (e) =>
8385
Effect.succeed({
@@ -103,17 +105,25 @@ export const ScheduleTool = Tool.define<typeof Parameters, Metadata, Schedule.Se
103105
)
104106
}
105107
case "delete": {
106-
return yield* schedule.delete(params.id as Schedule.ID).pipe(
108+
const id = params.id
109+
if (!id) {
110+
return {
111+
title: "Missing id",
112+
output: "action='delete' requires 'id'. Use schedule({action:'list'}) to find current ids.",
113+
metadata: { action: "delete" as const } satisfies Metadata,
114+
}
115+
}
116+
return yield* schedule.delete(id as Schedule.ID).pipe(
107117
Effect.map(() => ({
108118
title: "Schedule deleted",
109-
output: `Deleted schedule ${params.id}.`,
110-
metadata: { action: "delete", scheduleID: params.id } satisfies Metadata,
119+
output: `Deleted schedule ${id}.`,
120+
metadata: { action: "delete" as const, scheduleID: id } satisfies Metadata,
111121
})),
112122
Effect.catchTag("ScheduleNotFound", (e) =>
113123
Effect.succeed({
114124
title: "Schedule not found",
115125
output: `No schedule with id "${e.scheduleID}". Use schedule({action:"list"}) to see current ids.`,
116-
metadata: { action: "delete" } satisfies Metadata,
126+
metadata: { action: "delete" as const } satisfies Metadata,
117127
}),
118128
),
119129
)

0 commit comments

Comments
 (0)