@zhushanwen/pi-scheduler 0.3.2 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -2
- package/src/__tests__/sdk-contract.test.ts +20 -21
- package/src/__tests__/tool.test.ts +15 -17
- package/src/index.ts +6 -13
- package/src/tool.ts +7 -9
package/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhushanwen/pi-scheduler",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "index.ts",
|
|
6
|
+
"xyz-agent": {
|
|
7
|
+
"role": "universal"
|
|
8
|
+
},
|
|
6
9
|
"pi": {
|
|
7
10
|
"extensions": [
|
|
8
11
|
"./index.ts"
|
|
@@ -15,6 +18,7 @@
|
|
|
15
18
|
"pi-package"
|
|
16
19
|
],
|
|
17
20
|
"devDependencies": {
|
|
21
|
+
"@vitest/coverage-v8": "^4.1.9",
|
|
18
22
|
"fast-check": "^4.9.0",
|
|
19
23
|
"vitest": "^4.1.8"
|
|
20
24
|
},
|
|
@@ -25,7 +29,7 @@
|
|
|
25
29
|
"vitest.config.ts"
|
|
26
30
|
],
|
|
27
31
|
"peerDependencies": {
|
|
28
|
-
"@earendil-works/pi-coding-agent": "
|
|
32
|
+
"@earendil-works/pi-coding-agent": "^0.84.1",
|
|
29
33
|
"croner": "^9.0.0",
|
|
30
34
|
"typebox": "*"
|
|
31
35
|
},
|
|
@@ -5,14 +5,16 @@
|
|
|
5
5
|
// 1. tool 注册名为 schedule / schedule_control
|
|
6
6
|
// 2. tool 有 execute 函数字段(不是 handler/fn)
|
|
7
7
|
// 3. execute 是 async function(SDK 期望返回 Promise)
|
|
8
|
-
// 4.
|
|
8
|
+
// 4. 错误路径 throw(W4:pi-agent-core 只对 execute throw 置 isError:true,
|
|
9
|
+
// 返回值里的 isError 字段被 agent-loop 丢弃——错误轮曾被标成功;
|
|
10
|
+
// 锚点 agent-loop.js:453-483/525-547)
|
|
9
11
|
//
|
|
10
12
|
// 不导入 SchedulerRuntime 的内部:只通过 index.ts 的 default export 测,
|
|
11
13
|
// 保证 tool 注册逻辑的入口契约。
|
|
12
14
|
//
|
|
13
15
|
// 关键回归点:runtime 在 session_start 前为 null。execute 通过 getRuntime() 延迟
|
|
14
16
|
// 读取——若在 factory 顶层捕获 runtime! 非空断言,注册时 runtime 为 null,
|
|
15
|
-
// execute 调用会 NPE。此套件验证 session_start 前 execute
|
|
17
|
+
// execute 调用会 NPE。此套件验证 session_start 前 execute 优雅 throw 而非 crash。
|
|
16
18
|
|
|
17
19
|
import type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent'
|
|
18
20
|
import { describe, expect, it, vi } from 'vitest'
|
|
@@ -111,7 +113,7 @@ describe('pi-scheduler SDK contract', () => {
|
|
|
111
113
|
}
|
|
112
114
|
})
|
|
113
115
|
|
|
114
|
-
it('session_start 前 execute
|
|
116
|
+
it('session_start 前 execute throw(runtime 未初始化;W4:pi 只采信 throw)', async () => {
|
|
115
117
|
const { pi, tools, events } = createMockPi()
|
|
116
118
|
schedulerExtension(pi)
|
|
117
119
|
const fakeCtx = createFakeCtx()
|
|
@@ -119,12 +121,10 @@ describe('pi-scheduler SDK contract', () => {
|
|
|
119
121
|
// 不触发 session_start,runtime 仍为 null
|
|
120
122
|
expect(events.get('session_start')).toBeDefined()
|
|
121
123
|
|
|
122
|
-
|
|
123
|
-
expect(
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
isError: true,
|
|
127
|
-
})
|
|
124
|
+
// W4:错误 throw 传播——pi catch 后置 isError:true,文案(含 Error: 前缀格式,R3)进 toolResult
|
|
125
|
+
await expect(
|
|
126
|
+
tools[0]!.execute('call-1', { prompt: 'x', schedule: '5m' }, undefined, undefined, fakeCtx),
|
|
127
|
+
).rejects.toThrow('Error: Scheduler not initialized: session not started')
|
|
128
128
|
})
|
|
129
129
|
|
|
130
130
|
it('session_start 后 execute 正常返回结果', async () => {
|
|
@@ -153,31 +153,30 @@ describe('pi-scheduler SDK contract', () => {
|
|
|
153
153
|
expect(result.isError).toBeFalsy()
|
|
154
154
|
})
|
|
155
155
|
|
|
156
|
-
it('handler
|
|
156
|
+
it('handler 业务失败 throw(W4:返回值 isError 被 pi 丢弃,throw 才被采信)', async () => {
|
|
157
157
|
const { pi, tools, events } = createMockPi()
|
|
158
158
|
schedulerExtension(pi)
|
|
159
159
|
const fakeCtx = createFakeCtx()
|
|
160
160
|
await events.get('session_start')!({ type: 'session_start', reason: 'startup' }, fakeCtx)
|
|
161
161
|
|
|
162
|
-
// 非法 cron 表达式:service.create
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
expect(result.content[0].text).not.toContain('Error:')
|
|
162
|
+
// 非法 cron 表达式:service.create 失败 → toToolResult throw service message 本体
|
|
163
|
+
//(无 'Error:' 前缀——前缀格式仅 index.ts 的初始化异常兜底使用)
|
|
164
|
+
await expect(
|
|
165
|
+
tools[0]!.execute('call-err', { prompt: 'x', schedule: 'invalid-cron-expr-xxx' }, undefined, undefined, fakeCtx),
|
|
166
|
+
).rejects.toThrow('Invalid schedule')
|
|
168
167
|
})
|
|
169
168
|
|
|
170
|
-
it('schedule_control tool
|
|
169
|
+
it('schedule_control tool 业务失败同样 throw(W4)', async () => {
|
|
171
170
|
const { pi, tools, events } = createMockPi()
|
|
172
171
|
schedulerExtension(pi)
|
|
173
172
|
const fakeCtx = createFakeCtx()
|
|
174
173
|
await events.get('session_start')!({ type: 'session_start', reason: 'startup' }, fakeCtx)
|
|
175
174
|
|
|
176
|
-
// toggle 不存在的 task id:service 返回 TASK_NOT_FOUND
|
|
175
|
+
// toggle 不存在的 task id:service 返回 TASK_NOT_FOUND → throw message 本体
|
|
177
176
|
const controlTool = tools.find(t => t.name === 'schedule_control')!
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
177
|
+
await expect(
|
|
178
|
+
controlTool.execute('call-ctrl', { action: 'toggle', id: 'deadbeef', enabled: false }, undefined, undefined, fakeCtx),
|
|
179
|
+
).rejects.toThrow('Task deadbeef not found.')
|
|
181
180
|
})
|
|
182
181
|
|
|
183
182
|
it('registerCommand 注册了名为 schedule 的命令', () => {
|
|
@@ -25,12 +25,12 @@ describe('schedule tool', () => {
|
|
|
25
25
|
expect(details.task.schedule).toEqual({ mode: 'interval', intervalMs: 300000 })
|
|
26
26
|
})
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
expect(
|
|
32
|
-
|
|
33
|
-
|
|
28
|
+
// W4:业务失败 throw(pi 只对 execute throw 置 isError:true,返回值里的 isError
|
|
29
|
+
// 被 agent-loop 丢弃——错误轮曾被标成功)。原 errorCode details 随 throw 不再产出。
|
|
30
|
+
it('invalid schedule throws with message (W4: pi 采信 throw)', async () => {
|
|
31
|
+
await expect(handler({ prompt: 'test', schedule: 'invalid' })).rejects.toThrow(
|
|
32
|
+
'Invalid schedule',
|
|
33
|
+
)
|
|
34
34
|
})
|
|
35
35
|
})
|
|
36
36
|
|
|
@@ -62,19 +62,17 @@ describe('schedule_control tool', () => {
|
|
|
62
62
|
expect(result.content[0]!.text).toContain('disabled')
|
|
63
63
|
})
|
|
64
64
|
|
|
65
|
-
it('missing id on toggle
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
65
|
+
it('missing id on toggle throws (W4)', async () => {
|
|
66
|
+
await expect(handler({ action: 'toggle', enabled: true })).rejects.toThrow(
|
|
67
|
+
'id is required',
|
|
68
|
+
)
|
|
69
69
|
})
|
|
70
70
|
|
|
71
|
-
// TC5 tool 侧:toggle 不存在 id →
|
|
72
|
-
it('TC5: toggle unknown id
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
expect(details.errorCode).toBe('TASK_NOT_FOUND')
|
|
77
|
-
expect(result.content[0]!.text).toBe('Task deadbeef not found.')
|
|
71
|
+
// TC5 tool 侧:toggle 不存在 id → throw(service message 原文即 TASK_NOT_FOUND 文案)
|
|
72
|
+
it('TC5: toggle unknown id throws with TASK_NOT_FOUND message (W4)', async () => {
|
|
73
|
+
await expect(handler({ action: 'toggle', id: 'deadbeef', enabled: false })).rejects.toThrow(
|
|
74
|
+
'Task deadbeef not found.',
|
|
75
|
+
)
|
|
78
76
|
})
|
|
79
77
|
|
|
80
78
|
it('deletes task', async () => {
|
package/src/index.ts
CHANGED
|
@@ -113,8 +113,9 @@ export default function schedulerExtension(pi: ExtensionAPI): void {
|
|
|
113
113
|
|
|
114
114
|
// 注册 schedule tool
|
|
115
115
|
// execute 内联闭包:从 SDK 全签名 (toolCallId, params, signal, onUpdate, ctx) 提取 params 转调
|
|
116
|
-
// handler
|
|
117
|
-
//
|
|
116
|
+
// handler。错误路径 throw(W4):pi 只对 execute throw 置 isError:true(返回值里的
|
|
117
|
+
// isError 被 agent-loop 丢弃);getService() 未初始化异常穿透到这里,包装
|
|
118
|
+
// 'Error: Scheduler not initialized' 格式(R3 格式保持)。
|
|
118
119
|
pi.registerTool({
|
|
119
120
|
name: 'schedule',
|
|
120
121
|
label: 'Schedule',
|
|
@@ -131,16 +132,12 @@ export default function schedulerExtension(pi: ExtensionAPI): void {
|
|
|
131
132
|
try {
|
|
132
133
|
return await createScheduleHandler(getService())(params)
|
|
133
134
|
} catch (err) {
|
|
134
|
-
|
|
135
|
-
content: [{ type: 'text' as const, text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
|
|
136
|
-
details: {},
|
|
137
|
-
isError: true,
|
|
138
|
-
}
|
|
135
|
+
throw new Error(`Error: ${err instanceof Error ? err.message : String(err)}`)
|
|
139
136
|
}
|
|
140
137
|
},
|
|
141
138
|
})
|
|
142
139
|
|
|
143
|
-
// 注册 schedule_control tool
|
|
140
|
+
// 注册 schedule_control tool(错误路径同上:throw 让 pi 置 isError)
|
|
144
141
|
pi.registerTool({
|
|
145
142
|
name: 'schedule_control',
|
|
146
143
|
label: 'Schedule Control',
|
|
@@ -157,11 +154,7 @@ export default function schedulerExtension(pi: ExtensionAPI): void {
|
|
|
157
154
|
try {
|
|
158
155
|
return await createScheduleControlHandler(getService())(params)
|
|
159
156
|
} catch (err) {
|
|
160
|
-
|
|
161
|
-
content: [{ type: 'text' as const, text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
|
|
162
|
-
details: {},
|
|
163
|
-
isError: true,
|
|
164
|
-
}
|
|
157
|
+
throw new Error(`Error: ${err instanceof Error ? err.message : String(err)}`)
|
|
165
158
|
}
|
|
166
159
|
},
|
|
167
160
|
})
|
package/src/tool.ts
CHANGED
|
@@ -27,8 +27,9 @@ export const scheduleGuidelines = [
|
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
29
|
* schedule tool handler(SchedulerService 瘦壳,无独立业务逻辑)。
|
|
30
|
-
* 业务失败 →
|
|
31
|
-
*
|
|
30
|
+
* 业务失败 → throw(pi 只对 execute throw 置 isError:true,返回值里的 isError
|
|
31
|
+
* 被 agent-loop 丢弃——W4 修复,锚点 agent-loop.js:453-483);service 未初始化等
|
|
32
|
+
* 初始化异常不在此 catch——穿透到 index.ts execute 的 catch 兜底(R3)。
|
|
32
33
|
*/
|
|
33
34
|
export function createScheduleHandler(service: SchedulerService) {
|
|
34
35
|
return async (params: ScheduleParamsT) => {
|
|
@@ -86,19 +87,16 @@ export function createScheduleControlHandler(service: SchedulerService) {
|
|
|
86
87
|
|
|
87
88
|
/**
|
|
88
89
|
* ServiceResult → tool execute 返回。
|
|
89
|
-
* 成功 → {content: [message], details: data};失败 →
|
|
90
|
+
* 成功 → {content: [message], details: data};失败 → throw(W4:pi 契约只有
|
|
91
|
+
* execute throw 才置 isError:true,pi catch 后 message 原样成为 toolResult content,
|
|
92
|
+
* 错误轮不再被标成功)。
|
|
90
93
|
*/
|
|
91
94
|
function toToolResult(result: ServiceResult): {
|
|
92
95
|
content: { type: 'text'; text: string }[]
|
|
93
96
|
details: unknown
|
|
94
|
-
isError?: boolean
|
|
95
97
|
} {
|
|
96
98
|
if (!result.success) {
|
|
97
|
-
|
|
98
|
-
content: [{ type: 'text' as const, text: result.message }],
|
|
99
|
-
details: { errorCode: result.errorCode },
|
|
100
|
-
isError: true,
|
|
101
|
-
}
|
|
99
|
+
throw new Error(result.message)
|
|
102
100
|
}
|
|
103
101
|
return {
|
|
104
102
|
content: [{ type: 'text' as const, text: result.message }],
|