pactive 0.0.2-alpha.1 → 0.0.4-alpha.1
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/.opcteam-plugin/plugin.json +8 -4
- package/BREAKING-CHANGES.md +14 -3
- package/README.md +86 -6
- package/bin/pactive.mjs +917 -4
- package/commands/pactive.md +4 -1
- package/dsh/index.js +214 -6
- package/mcp/tools/definitions.mjs +205 -8
- package/package.json +1 -1
- package/pi/extension.ts +246 -8
- package/pi/skills/pactive/SKILL.md +57 -20
- package/skills/pactive/SKILL.md +57 -20
- package/src/core/assistants.mjs +237 -0
- package/src/core/config.mjs +40 -3
- package/src/core/fleet.mjs +80 -0
- package/src/core/guard.mjs +117 -32
- package/src/core/idle.mjs +11 -8
- package/src/core/index.mjs +3 -0
- package/src/core/scheduler.mjs +16 -4
- package/src/core/state.mjs +134 -12
- package/src/core/tick.mjs +128 -0
- package/src/hosts/herdr/fleet-view.mjs +31 -0
- package/src/hosts/herdr/index.mjs +429 -0
package/commands/pactive.md
CHANGED
|
@@ -22,6 +22,7 @@ mcp__plugin_pactive_pactive__<tool>
|
|
|
22
22
|
- `mcp__plugin_pactive_pactive__pactive_schedule`
|
|
23
23
|
- `mcp__plugin_pactive_pactive__pactive_tell`
|
|
24
24
|
- `mcp__plugin_pactive_pactive__pactive_history`
|
|
25
|
+
- `mcp__plugin_pactive_pactive__pactive_config`(只读;要改配置时只返回可复制命令)
|
|
25
26
|
|
|
26
27
|
裸名(`pactive_status`)不一定能被 ToolSearch 发现;若命名空间工具没加载,用
|
|
27
28
|
`select:mcp__plugin_pactive_pactive__pactive_status` 搜一次。
|
|
@@ -36,11 +37,13 @@ mcp__plugin_pactive_pactive__<tool>
|
|
|
36
37
|
| 每小时 | `pactive_schedule` `when:"every 1h"` |
|
|
37
38
|
| 告诉另一个会话 | `pactive_tell` `to` `body`(可选 `kind:"ask"`) |
|
|
38
39
|
| 最近发生过什么 | `pactive_history` `limit` |
|
|
40
|
+
| 开关 / 阈值 / 安静时间是多少 | `pactive_config` `key`(只读,报当前值与默认值) |
|
|
41
|
+
| 帮我开启主动提醒 / 改阈值 | `pactive_config` `key` `value` → 只拿到 `pactive config set … --confirm`,交给用户执行 |
|
|
39
42
|
|
|
40
43
|
## 硬规则
|
|
41
44
|
|
|
42
45
|
1. **不要替用户改配置**:pactive 的开关与配置是人工闸门(CLI `--confirm`)。用户要求改配置时,
|
|
43
|
-
告诉他用 `pactive config set … --confirm
|
|
46
|
+
告诉他用 `pactive config set … --confirm`,不要自行绕过;用户执行后下一轮用 `pactive_status` 复核。
|
|
44
47
|
2. **回执如实转述**:`queued` 就是**还没送到**(对端离线),不要说成已送达。
|
|
45
48
|
3. **默认不打扰**:空闲触发默认关闭;用户没明确要「空闲自动推进」时,不要假设它已开启。
|
|
46
49
|
4. **只报新事实**:状态类输出要给出 `idle.reason` 与护栏结论,不要泛泛说「一切正常」。
|
package/dsh/index.js
CHANGED
|
@@ -65,17 +65,82 @@ async function markControlReturned(ctx) {
|
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
const STATUS_PARAMETERS = {
|
|
68
|
+
const STATUS_PARAMETERS = {
|
|
69
|
+
type: "object",
|
|
70
|
+
properties: { assistant: { type: "string", description: "只读:助理名或 id;不传 = 全部助理概览" } },
|
|
71
|
+
additionalProperties: false,
|
|
72
|
+
};
|
|
73
|
+
const ASSISTANTS_PARAMETERS = { type: "object", properties: {}, additionalProperties: false };
|
|
74
|
+
const FLEET_PARAMETERS = { type: "object", properties: {}, additionalProperties: false };
|
|
75
|
+
const CONFIG_PARAMETERS = {
|
|
76
|
+
type: "object",
|
|
77
|
+
properties: {
|
|
78
|
+
key: { type: "string", description: "配置键;缺省返回全部(idle.enabled / idle.thresholdMs / policy.* / globalGuard.hourlyLimit)" },
|
|
79
|
+
value: { type: "string", description: "想改成的新值;给了就只返回 `pactive config set <key> <value> --confirm`,不落盘" },
|
|
80
|
+
kind: { type: "string", description: "提议哪种写操作:assistant.add | assistant.remove | assistant.enable(只返回命令,不落盘)" },
|
|
81
|
+
name: { type: "string", description: "kind=assistant.* 时要操作的助理名或 id(缺 id 时用名字)" },
|
|
82
|
+
},
|
|
83
|
+
additionalProperties: false,
|
|
84
|
+
};
|
|
69
85
|
const SCHEDULE_PARAMETERS = {
|
|
70
86
|
type: "object",
|
|
71
87
|
properties: {
|
|
72
88
|
prompt: { type: "string", description: "任务正文(到点注入给会话的内容)" },
|
|
73
89
|
when: { type: "string", description: "触发时间:时长(10m)/ 时刻(18:30)/ ISO 8601 / every 1h" },
|
|
90
|
+
assistant: { type: "string", description: "记在哪个助理名下(名字或 id);不传 = 默认助理" },
|
|
74
91
|
},
|
|
75
92
|
required: ["prompt"],
|
|
76
93
|
additionalProperties: false,
|
|
77
94
|
};
|
|
78
95
|
|
|
96
|
+
/** One assistant as the model needs to see it: switch, pending work, next action. */
|
|
97
|
+
function assistantView(core, state, record) {
|
|
98
|
+
const all = core.schedule?.list ? core.schedule.list(state, record.assistantId) : [];
|
|
99
|
+
const active = (Array.isArray(all) ? all : []).filter((t) => t && t.active !== false);
|
|
100
|
+
const dueAt = active.map((t) => t.dueAt).filter((n) => Number.isFinite(n));
|
|
101
|
+
return {
|
|
102
|
+
assistantId: record.assistantId,
|
|
103
|
+
name: record.name,
|
|
104
|
+
title: record.title,
|
|
105
|
+
enabled: record.enabled === true,
|
|
106
|
+
role: record.role ?? "",
|
|
107
|
+
tasks: active.length,
|
|
108
|
+
nextDueAt: dueAt.length > 0 ? Math.min(...dueAt) : null,
|
|
109
|
+
lastFiredAt: record.lastFiredAt ?? 0,
|
|
110
|
+
firedCount: record.firedCount ?? 0,
|
|
111
|
+
policy: record.policy ?? {},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function assistantOverview(core, state) {
|
|
116
|
+
const list = core.assistants?.list ? core.assistants.list(state) : [];
|
|
117
|
+
return list.map((record) => assistantView(core, state, record));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* I66: the model only *proposes* — every assistant write comes back as one copy-pasteable
|
|
122
|
+
* command whose `--confirm` a human has to type. Nothing here writes.
|
|
123
|
+
*/
|
|
124
|
+
function assistCommandFor(kind, name, value, fleet) {
|
|
125
|
+
const known = fleet.map((a) => a.name);
|
|
126
|
+
if (kind === "assistant.add") {
|
|
127
|
+
if (name === "") return { ok: false, reason: "kind=assistant.add 需要 name" };
|
|
128
|
+
if (known.includes(name)) return { ok: false, reason: `助理已经存在(${name}),已有:${known.join(", ")}` };
|
|
129
|
+
return { ok: true, command: `pactive assistant add ${name} --confirm` };
|
|
130
|
+
}
|
|
131
|
+
if (kind === "assistant.remove" || kind === "assistant.enable") {
|
|
132
|
+
if (name === "") return { ok: false, reason: `kind=${kind} 需要 name` };
|
|
133
|
+
const hit = fleet.find((a) => a.name === name || a.assistantId === name);
|
|
134
|
+
if (!hit) return { ok: false, reason: `未知助理(${name});已有:${known.join(", ") || "(none yet)"}` };
|
|
135
|
+
if (kind === "assistant.enable") {
|
|
136
|
+
const on = value === "" ? !hit.enabled : value === "true" || value === "1" || value === "on";
|
|
137
|
+
return { ok: true, command: `pactive assistant ${on ? "enable" : "disable"} ${hit.name} --confirm` };
|
|
138
|
+
}
|
|
139
|
+
return { ok: true, command: `pactive assistant rm ${hit.name} --confirm` };
|
|
140
|
+
}
|
|
141
|
+
return { ok: false, reason: `未知 kind(${kind});支持 assistant.add / assistant.remove / assistant.enable` };
|
|
142
|
+
}
|
|
143
|
+
|
|
79
144
|
/**
|
|
80
145
|
* @param {any} ctx Cordis plugin context
|
|
81
146
|
* @param {Record<string, any>} config
|
|
@@ -89,7 +154,8 @@ export function apply(ctx, config) {
|
|
|
89
154
|
ctx.tools.register({
|
|
90
155
|
name: "pactive_status",
|
|
91
156
|
description:
|
|
92
|
-
"
|
|
157
|
+
"当用户问「我现在闲不闲」「主动提醒开启了吗」「为什么没触发」「刚才主动做了什么」「盯盘那个助理怎么样了」时用。" +
|
|
158
|
+
"→ 读空闲判定、reason、护栏闸门与任务数;给 assistant 看该助理,不给看全部助理概览。只读,不落盘。",
|
|
93
159
|
parameters: STATUS_PARAMETERS,
|
|
94
160
|
async execute(args = {}) {
|
|
95
161
|
try {
|
|
@@ -97,19 +163,48 @@ export function apply(ctx, config) {
|
|
|
97
163
|
const now = Date.now();
|
|
98
164
|
const paths = core.resolvePaths(HOST_KIND, process.env);
|
|
99
165
|
const state = (await readState(core, paths)) ?? core.defaultState();
|
|
166
|
+
const globalGuard = state.globalGuard ?? null;
|
|
167
|
+
const activity = core.getActivity ? core.getActivity(state) : null;
|
|
168
|
+
const requested = typeof args.assistant === "string" ? args.assistant.trim() : "";
|
|
169
|
+
if (requested !== "") {
|
|
170
|
+
const record = core.assistants?.resolve ? core.assistants.resolve(state, requested) : null;
|
|
171
|
+
if (!record) {
|
|
172
|
+
const names = assistantOverview(core, state).map((a) => a.name);
|
|
173
|
+
return { ok: false, error: `未知助理(${requested});已知:${names.join(", ") || "(none yet)"}` };
|
|
174
|
+
}
|
|
175
|
+
const idle = core.evaluateIdle
|
|
176
|
+
? core.evaluateIdle(state, now, {}, record.assistantId)
|
|
177
|
+
: { idle: false, reason: "requires-action" };
|
|
178
|
+
const guard = core.checkGuard
|
|
179
|
+
? core.checkGuard(state, now, record.assistantId)
|
|
180
|
+
: { pass: false, reason: "requires-action" };
|
|
181
|
+
return {
|
|
182
|
+
ok: true,
|
|
183
|
+
host: HOST_KIND,
|
|
184
|
+
now,
|
|
185
|
+
assistant: assistantView(core, state, record),
|
|
186
|
+
idle,
|
|
187
|
+
guard,
|
|
188
|
+
globalGuard,
|
|
189
|
+
activity,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
100
192
|
const idle = core.evaluateIdle
|
|
101
193
|
? core.evaluateIdle(state, now, state.policy ?? {})
|
|
102
194
|
: { idle: false, reason: "requires-action" };
|
|
103
195
|
const guard = core.checkGuard ? core.checkGuard(state, now) : { pass: false, reason: "requires-action" };
|
|
104
196
|
const tasks = core.schedule?.list ? core.schedule.list(state) : (state.tasks ?? []);
|
|
105
|
-
const
|
|
197
|
+
const fleet = assistantOverview(core, state);
|
|
106
198
|
return {
|
|
107
199
|
ok: true,
|
|
108
200
|
host: HOST_KIND,
|
|
109
201
|
now,
|
|
110
202
|
enabled: state.idle?.enabled ?? false,
|
|
203
|
+
assistants: fleet,
|
|
204
|
+
assistantCount: fleet.length,
|
|
111
205
|
idle,
|
|
112
206
|
guard,
|
|
207
|
+
globalGuard,
|
|
113
208
|
tasks: Array.isArray(tasks) ? tasks.length : 0,
|
|
114
209
|
activity,
|
|
115
210
|
};
|
|
@@ -119,9 +214,50 @@ export function apply(ctx, config) {
|
|
|
119
214
|
},
|
|
120
215
|
});
|
|
121
216
|
|
|
217
|
+
ctx.tools.register({
|
|
218
|
+
name: "pactive_assistants",
|
|
219
|
+
description:
|
|
220
|
+
"当用户问「我有几个助理」「有哪些助理」「盯盘助理开着吗」「各自下次什么时候动作」时用。" +
|
|
221
|
+
"→ 列出全部助理的名称、开关、任务数、下次到期与触发次数。只读,不落盘。",
|
|
222
|
+
parameters: ASSISTANTS_PARAMETERS,
|
|
223
|
+
async execute() {
|
|
224
|
+
try {
|
|
225
|
+
const core = await loadCore();
|
|
226
|
+
const paths = core.resolvePaths(HOST_KIND, process.env);
|
|
227
|
+
const state = (await readState(core, paths)) ?? core.defaultState();
|
|
228
|
+
const fleet = assistantOverview(core, state);
|
|
229
|
+
return { ok: true, count: fleet.length, assistants: fleet };
|
|
230
|
+
} catch (err) {
|
|
231
|
+
return { ok: false, error: errorText(err) };
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
ctx.tools.register({
|
|
237
|
+
name: "pactive_fleet",
|
|
238
|
+
description:
|
|
239
|
+
"当用户问「我有几个助理在跑」「盯盘那个助理在哪个 pane」「助理的窗口还活着吗」「上次主动投递送达了吗」时用。" +
|
|
240
|
+
"→ 读助理 ↔ herdr pane 绑定、agent 状态与最近一次投递;pane 状态来自 herdr,未观测到就写 unobserved。只读,不落盘。",
|
|
241
|
+
parameters: FLEET_PARAMETERS,
|
|
242
|
+
async execute() {
|
|
243
|
+
try {
|
|
244
|
+
const core = await loadCore();
|
|
245
|
+
const fleetView = await import("../src/hosts/herdr/fleet-view.mjs");
|
|
246
|
+
const paths = core.resolvePaths(HOST_KIND, process.env);
|
|
247
|
+
const state = (await readState(core, paths)) ?? core.defaultState();
|
|
248
|
+
return { ...fleetView.fleetPayload(core, state, paths, process.env) };
|
|
249
|
+
} catch (err) {
|
|
250
|
+
return { ok: false, error: errorText(err) };
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
|
|
122
255
|
ctx.tools.register({
|
|
123
256
|
name: "pactive_schedule",
|
|
124
|
-
description:
|
|
257
|
+
description:
|
|
258
|
+
"当用户说「10 分钟后提醒我喝水」「18:30 提醒我开会」「每小时检查一次」时用。" +
|
|
259
|
+
"→ 登记一条定时任务并立即落盘;when 支持 10m / 18:30 / ISO 8601 / every 1h;assistant 不填就记在默认助理名下。" +
|
|
260
|
+
"会写盘,需要人工确认(只在用户明确要求时调用)。",
|
|
125
261
|
parameters: SCHEDULE_PARAMETERS,
|
|
126
262
|
async execute(args = {}) {
|
|
127
263
|
try {
|
|
@@ -132,19 +268,33 @@ export function apply(ctx, config) {
|
|
|
132
268
|
if (!spec) return { ok: false, error: `无法解析 when:${String(args.when)}` };
|
|
133
269
|
const core = await loadCore();
|
|
134
270
|
const paths = core.resolvePaths(HOST_KIND, process.env);
|
|
271
|
+
const requested = typeof args.assistant === "string" ? args.assistant.trim() : "";
|
|
272
|
+
let assistantId = null;
|
|
273
|
+
if (requested !== "") {
|
|
274
|
+
const before = (await readState(core, paths)) ?? core.defaultState();
|
|
275
|
+
const record = core.assistants?.resolve ? core.assistants.resolve(before, requested) : null;
|
|
276
|
+
if (!record) {
|
|
277
|
+
const names = assistantOverview(core, before).map((a) => a.name);
|
|
278
|
+
return { ok: false, error: `未知助理(${requested});已知:${names.join(", ") || "(none yet)"}` };
|
|
279
|
+
}
|
|
280
|
+
assistantId = record.assistantId;
|
|
281
|
+
}
|
|
135
282
|
const next = await mutateState(core, paths, (state) => {
|
|
283
|
+
const targetId =
|
|
284
|
+
assistantId ?? (core.assistants?.ensureDefault ? core.assistants.ensureDefault(state).assistantId : null);
|
|
136
285
|
const out = core.schedule.add(
|
|
137
286
|
state,
|
|
138
|
-
{ ...spec, prompt, content: prompt, owner: state.identity?.agentId },
|
|
287
|
+
{ ...spec, prompt, content: prompt, owner: state.identity?.agentId, assistantId: targetId },
|
|
139
288
|
now,
|
|
140
289
|
);
|
|
141
290
|
return pickState(out, state);
|
|
142
291
|
});
|
|
143
|
-
const tasks = core.schedule?.list ? core.schedule.list(next) : (next.tasks ?? []);
|
|
292
|
+
const tasks = core.schedule?.list ? core.schedule.list(next, assistantId ?? undefined) : (next.tasks ?? []);
|
|
144
293
|
const created = Array.isArray(tasks) ? tasks[tasks.length - 1] : null;
|
|
145
294
|
return {
|
|
146
295
|
ok: true,
|
|
147
296
|
taskId: created?.id ?? created?.taskId ?? null,
|
|
297
|
+
assistantId: created?.assistantId ?? assistantId,
|
|
148
298
|
dueAt: created?.dueAt ?? spec.at ?? null,
|
|
149
299
|
kind: spec.kind,
|
|
150
300
|
when: spec.when,
|
|
@@ -155,6 +305,64 @@ export function apply(ctx, config) {
|
|
|
155
305
|
}
|
|
156
306
|
},
|
|
157
307
|
});
|
|
308
|
+
|
|
309
|
+
// Read-only by construction: a write request returns the command, never a write (contract §2.3 / I66).
|
|
310
|
+
ctx.tools.register({
|
|
311
|
+
name: "pactive_config",
|
|
312
|
+
description:
|
|
313
|
+
"当用户问「主动提醒的开关开着吗」「阈值是多少」「安静时间是几点」「帮我把主动提醒打开」「再给我加一个盯盘的助理」时用。" +
|
|
314
|
+
"→ 读配置值与默认值;要改配置或增删 / 开关助理时,只返回一条需要人工确认的可复制命令。只读,不落盘。",
|
|
315
|
+
parameters: CONFIG_PARAMETERS,
|
|
316
|
+
async execute(args = {}) {
|
|
317
|
+
try {
|
|
318
|
+
const core = await loadCore();
|
|
319
|
+
const described = core.config?.describe ? core.config.describe() : { schema: {}, defaults: {} };
|
|
320
|
+
const known = Object.keys(described.defaults);
|
|
321
|
+
const key = typeof args.key === "string" ? args.key.trim() : "";
|
|
322
|
+
const rawValue = args.value === undefined || args.value === null ? "" : String(args.value).trim();
|
|
323
|
+
const kind = typeof args.kind === "string" ? args.kind.trim() : "";
|
|
324
|
+
const name = typeof args.name === "string" ? args.name.trim() : "";
|
|
325
|
+
const paths = core.resolvePaths(HOST_KIND, process.env);
|
|
326
|
+
const state = (await readState(core, paths)) ?? core.defaultState();
|
|
327
|
+
const fleet = assistantOverview(core, state);
|
|
328
|
+
|
|
329
|
+
if (kind !== "") {
|
|
330
|
+
const command = assistCommandFor(kind, name, rawValue, fleet);
|
|
331
|
+
if (!command.ok) return { ok: false, error: command.reason };
|
|
332
|
+
return {
|
|
333
|
+
ok: false,
|
|
334
|
+
wrote: false,
|
|
335
|
+
reason: "need_human_confirmation",
|
|
336
|
+
command: command.command,
|
|
337
|
+
kind,
|
|
338
|
+
before: { assistants: fleet.length, names: fleet.map((a) => a.name) },
|
|
339
|
+
hint: "不要自己改状态文件;把 command 原样给用户,等他执行后下一轮用 pactive_assistants 复核。",
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (key !== "" && !known.includes(key)) {
|
|
344
|
+
return { ok: false, error: `未知配置键(${key});已知:${known.join(", ")}` };
|
|
345
|
+
}
|
|
346
|
+
const values = (key === "" ? known : [key]).map((k) => ({
|
|
347
|
+
key: k,
|
|
348
|
+
value: core.config?.get ? core.config.get(state, k) : null,
|
|
349
|
+
default: described.defaults[k] ?? null,
|
|
350
|
+
humanGated: described.schema?.[k]?.humanGated === true,
|
|
351
|
+
}));
|
|
352
|
+
if (rawValue === "") return { ok: true, readOnly: true, wrote: false, values, assistants: fleet.length };
|
|
353
|
+
return {
|
|
354
|
+
ok: false,
|
|
355
|
+
wrote: false,
|
|
356
|
+
reason: "need_human_confirmation",
|
|
357
|
+
command: `pactive config set ${key} ${rawValue} --confirm`,
|
|
358
|
+
hint: "不要自己改状态文件;把 command 原样给用户,等他执行后下一轮用 pactive_status 复核。",
|
|
359
|
+
values,
|
|
360
|
+
};
|
|
361
|
+
} catch (err) {
|
|
362
|
+
return { ok: false, error: errorText(err) };
|
|
363
|
+
}
|
|
364
|
+
},
|
|
365
|
+
});
|
|
158
366
|
};
|
|
159
367
|
|
|
160
368
|
const registerCommands = () => {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* MCP tool surface of the `pactive` OPCteam plugin (stdio JSON-RPC server).
|
|
3
3
|
*
|
|
4
|
-
* Tools: pactive_status /
|
|
4
|
+
* Tools: pactive_status / pactive_assistants / pactive_fleet / pactive_config / pactive_schedule /
|
|
5
|
+
* pactive_tell / pactive_history.
|
|
5
6
|
* Zero third-party dependencies — the schemas are plain JSON Schema.
|
|
6
7
|
*
|
|
7
8
|
* unverified: mailbox.send / ledger.tail argument shapes come from the frozen
|
|
@@ -35,17 +36,50 @@ export const TOOL_DEFINITIONS = [
|
|
|
35
36
|
{
|
|
36
37
|
name: "pactive_status",
|
|
37
38
|
description:
|
|
38
|
-
"
|
|
39
|
+
"当用户问「我现在闲不闲」「主动提醒开启了吗」「为什么没触发」「刚才主动做了什么」「盯盘那个助理怎么样了」时用。" +
|
|
40
|
+
"→ 读空闲判定、reason、护栏闸门与任务数;给 assistant 看该助理,不给看全部助理概览。只读,不落盘。",
|
|
41
|
+
inputSchema: obj({ assistant: str("只读:助理名或 id;不传 = 全部助理概览") }, []),
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: "pactive_assistants",
|
|
45
|
+
description:
|
|
46
|
+
"当用户问「我有几个助理」「有哪些助理」「盯盘助理开着吗」「各自下次什么时候动作」时用。" +
|
|
47
|
+
"→ 列出全部助理的名称、开关、任务数、下次到期与触发次数。只读,不落盘。",
|
|
48
|
+
inputSchema: obj({}, []),
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: "pactive_fleet",
|
|
52
|
+
description:
|
|
53
|
+
"当用户问「我有几个助理在跑」「盯盘那个助理在哪个 pane」「助理的窗口还活着吗」「上次主动投递送达了吗」时用。" +
|
|
54
|
+
"→ 读助理 ↔ herdr pane 绑定、agent 状态与最近一次投递;pane 状态来自 herdr,未观测到就写 unobserved。只读,不落盘。",
|
|
39
55
|
inputSchema: obj({}, []),
|
|
40
56
|
},
|
|
57
|
+
{
|
|
58
|
+
name: "pactive_config",
|
|
59
|
+
description:
|
|
60
|
+
"当用户问「主动提醒的开关开着吗」「阈值是多少」「安静时间是几点」「帮我把主动提醒打开」「再给我加一个盯盘的助理」时用。" +
|
|
61
|
+
"→ 读配置值与默认值;要改配置或增删 / 开关助理时,只返回一条需要人工确认的可复制命令。只读,不落盘。",
|
|
62
|
+
inputSchema: obj(
|
|
63
|
+
{
|
|
64
|
+
key: str("配置键;缺省返回全部(idle.enabled / idle.thresholdMs / policy.* / globalGuard.hourlyLimit)"),
|
|
65
|
+
value: str("想改成的新值;给了就只返回 `pactive config set <key> <value> --confirm`,不落盘"),
|
|
66
|
+
kind: str("提议哪种写操作:assistant.add | assistant.remove | assistant.enable(只返回命令,不落盘)"),
|
|
67
|
+
name: str("kind=assistant.* 时要操作的助理名或 id(缺 id 时用名字)"),
|
|
68
|
+
},
|
|
69
|
+
[],
|
|
70
|
+
),
|
|
71
|
+
},
|
|
41
72
|
{
|
|
42
73
|
name: "pactive_schedule",
|
|
43
74
|
description:
|
|
44
|
-
"
|
|
75
|
+
"当用户说「10 分钟后提醒我喝水」「18:30 提醒我开会」「每小时检查一次」时用。" +
|
|
76
|
+
"→ 登记一条定时任务并立即落盘;when 支持 10m / 18:30 / ISO 8601 / every 1h;assistant 不填就记在默认助理名下。" +
|
|
77
|
+
"会写盘,需要人工确认(只在用户明确要求时调用)。",
|
|
45
78
|
inputSchema: obj(
|
|
46
79
|
{
|
|
47
80
|
prompt: str("任务正文(到点注入给会话的内容)"),
|
|
48
81
|
when: str("触发时间:时长 / 时刻 / ISO 8601 / every <时长>;缺省 10m"),
|
|
82
|
+
assistant: str("记在哪个助理名下(名字或 id);不传 = 默认助理"),
|
|
49
83
|
},
|
|
50
84
|
["prompt"],
|
|
51
85
|
),
|
|
@@ -53,7 +87,9 @@ export const TOOL_DEFINITIONS = [
|
|
|
53
87
|
{
|
|
54
88
|
name: "pactive_tell",
|
|
55
89
|
description:
|
|
56
|
-
"
|
|
90
|
+
"当用户说「告诉另一个会话我有空」「问一下隔壁 agent 跑完了吗」时用。" +
|
|
91
|
+
"→ 给同机另一个会话投递消息;对端离线时进入 outbox 排队,不伪装成功。" +
|
|
92
|
+
"会写盘,需要人工确认(只在用户明确要求时调用)。",
|
|
57
93
|
inputSchema: obj(
|
|
58
94
|
{
|
|
59
95
|
to: str("目标会话的 agentId 或名字"),
|
|
@@ -65,7 +101,9 @@ export const TOOL_DEFINITIONS = [
|
|
|
65
101
|
},
|
|
66
102
|
{
|
|
67
103
|
name: "pactive_history",
|
|
68
|
-
description:
|
|
104
|
+
description:
|
|
105
|
+
"当用户问「刚才那几次主动提醒都发生了什么」「有没有真的触发过」时用。" +
|
|
106
|
+
"→ 读事件账本尾部(触发→投递→回应→结账)。只读。",
|
|
69
107
|
inputSchema: obj({ limit: { type: "integer", description: "返回条数,默认 20" } }, []),
|
|
70
108
|
},
|
|
71
109
|
];
|
|
@@ -79,6 +117,54 @@ function json(text, isError = false) {
|
|
|
79
117
|
return { content: [{ type: "text", text: typeof text === "string" ? text : JSON.stringify(text, null, 2) }], isError };
|
|
80
118
|
}
|
|
81
119
|
|
|
120
|
+
/** One assistant as the model (and the user) needs to see it: switch, pending work, next action. */
|
|
121
|
+
function assistantView(core, state, record) {
|
|
122
|
+
const all = core.schedule?.list ? core.schedule.list(state, record.assistantId) : [];
|
|
123
|
+
const active = (Array.isArray(all) ? all : []).filter((t) => t && t.active !== false);
|
|
124
|
+
const dueAt = active.map((t) => t.dueAt).filter((n) => Number.isFinite(n));
|
|
125
|
+
return {
|
|
126
|
+
assistantId: record.assistantId,
|
|
127
|
+
name: record.name,
|
|
128
|
+
title: record.title,
|
|
129
|
+
enabled: record.enabled === true,
|
|
130
|
+
role: record.role ?? "",
|
|
131
|
+
tasks: active.length,
|
|
132
|
+
nextDueAt: dueAt.length > 0 ? Math.min(...dueAt) : null,
|
|
133
|
+
lastFiredAt: record.lastFiredAt ?? 0,
|
|
134
|
+
firedCount: record.firedCount ?? 0,
|
|
135
|
+
policy: record.policy ?? {},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function assistantOverview(core, state) {
|
|
140
|
+
const list = core.assistants?.list ? core.assistants.list(state) : [];
|
|
141
|
+
return list.map((record) => assistantView(core, state, record));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* I66: the model only *proposes* — every assistant write comes back as one copy-pasteable
|
|
146
|
+
* command whose `--confirm` a human has to type. Nothing above writes.
|
|
147
|
+
*/
|
|
148
|
+
function assistCommandFor(kind, name, value, fleet) {
|
|
149
|
+
const known = fleet.map((a) => a.name);
|
|
150
|
+
if (kind === "assistant.add") {
|
|
151
|
+
if (name === "") return { ok: false, reason: "kind=assistant.add 需要 name" };
|
|
152
|
+
if (known.includes(name)) return { ok: false, reason: `助理已经存在(${name}),已有:${known.join(", ")}` };
|
|
153
|
+
return { ok: true, command: `pactive assistant add ${name} --confirm` };
|
|
154
|
+
}
|
|
155
|
+
if (kind === "assistant.remove" || kind === "assistant.enable") {
|
|
156
|
+
if (name === "") return { ok: false, reason: `kind=${kind} 需要 name` };
|
|
157
|
+
const hit = fleet.find((a) => a.name === name || a.assistantId === name);
|
|
158
|
+
if (!hit) return { ok: false, reason: `未知助理(${name});已有:${known.join(", ") || "(none yet)"}` };
|
|
159
|
+
if (kind === "assistant.enable") {
|
|
160
|
+
const on = value === "" ? !hit.enabled : value === "true" || value === "1" || value === "on";
|
|
161
|
+
return { ok: true, command: `pactive assistant ${on ? "enable" : "disable"} ${hit.name} --confirm` };
|
|
162
|
+
}
|
|
163
|
+
return { ok: true, command: `pactive assistant rm ${hit.name} --confirm` };
|
|
164
|
+
}
|
|
165
|
+
return { ok: false, reason: `未知 kind(${kind});支持 assistant.add / assistant.remove / assistant.enable` };
|
|
166
|
+
}
|
|
167
|
+
|
|
82
168
|
/**
|
|
83
169
|
* Run one tool.
|
|
84
170
|
* @param {string} name
|
|
@@ -101,36 +187,147 @@ export async function runTool(name, args = {}, options = {}) {
|
|
|
101
187
|
case "pactive_status": {
|
|
102
188
|
const state = (await readState(core, paths)) ?? core.defaultState();
|
|
103
189
|
const policy = state.policy ?? {};
|
|
190
|
+
const globalGuard = state.globalGuard ?? null;
|
|
191
|
+
const requested = typeof args.assistant === "string" ? args.assistant.trim() : "";
|
|
192
|
+
if (requested !== "") {
|
|
193
|
+
const record = core.assistants?.resolve ? core.assistants.resolve(state, requested) : null;
|
|
194
|
+
if (!record) {
|
|
195
|
+
const names = core.assistants?.list ? core.assistants.list(state).map((a) => a.name) : [];
|
|
196
|
+
return json(`ERROR INVALID_ARGUMENT: 未知助理(${requested});已知:${names.join(", ") || "(none yet)"}`, true);
|
|
197
|
+
}
|
|
198
|
+
const idle = core.evaluateIdle
|
|
199
|
+
? core.evaluateIdle(state, now, {}, record.assistantId)
|
|
200
|
+
: { idle: false, reason: "requires-action" };
|
|
201
|
+
const guard = core.checkGuard
|
|
202
|
+
? core.checkGuard(state, now, record.assistantId)
|
|
203
|
+
: { pass: false, reason: "requires-action" };
|
|
204
|
+
return json({
|
|
205
|
+
host: HOST_KIND,
|
|
206
|
+
now,
|
|
207
|
+
assistant: assistantView(core, state, record),
|
|
208
|
+
idle,
|
|
209
|
+
guard,
|
|
210
|
+
globalGuard,
|
|
211
|
+
activity: core.getActivity ? core.getActivity(state) : null,
|
|
212
|
+
stateFile: paths.stateFile,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
104
215
|
const idle = core.evaluateIdle ? core.evaluateIdle(state, now, policy) : { idle: false, reason: "requires-action" };
|
|
105
216
|
const guard = core.checkGuard ? core.checkGuard(state, now) : { pass: false, reason: "requires-action" };
|
|
106
217
|
const tasks = core.schedule?.list ? core.schedule.list(state) : (state.tasks ?? []);
|
|
107
218
|
const activity = core.getActivity ? core.getActivity(state) : null;
|
|
219
|
+
const fleet = assistantOverview(core, state);
|
|
108
220
|
return json({
|
|
109
221
|
host: HOST_KIND,
|
|
110
222
|
now,
|
|
111
223
|
enabled: state.idle?.enabled ?? false,
|
|
224
|
+
assistants: fleet,
|
|
225
|
+
assistantCount: fleet.length,
|
|
112
226
|
idle,
|
|
113
227
|
guard,
|
|
228
|
+
globalGuard,
|
|
114
229
|
tasks: Array.isArray(tasks) ? tasks.length : 0,
|
|
115
230
|
activity,
|
|
116
231
|
stateFile: paths.stateFile,
|
|
117
232
|
});
|
|
118
233
|
}
|
|
119
234
|
|
|
235
|
+
case "pactive_assistants": {
|
|
236
|
+
const state = (await readState(core, paths)) ?? core.defaultState();
|
|
237
|
+
const fleet = assistantOverview(core, state);
|
|
238
|
+
return json({ ok: true, count: fleet.length, assistants: fleet, stateFile: paths.stateFile });
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
case "pactive_fleet": {
|
|
242
|
+
const state = (await readState(core, paths)) ?? core.defaultState();
|
|
243
|
+
const { fleetPayload } = await import("../../src/hosts/herdr/fleet-view.mjs");
|
|
244
|
+
return json({ ...fleetPayload(core, state, paths, env), stateFile: paths.stateFile });
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
case "pactive_config": {
|
|
248
|
+
// Read-only. A write request never lands: the model gets a copy-pasteable command
|
|
249
|
+
// whose --confirm only a human can type (contract §2.3 / I59 / I66).
|
|
250
|
+
const described = core.config?.describe ? core.config.describe() : { schema: {}, defaults: {}, humanGatedKeys: [] };
|
|
251
|
+
const known = Object.keys(described.defaults);
|
|
252
|
+
const key = typeof args.key === "string" ? args.key.trim() : "";
|
|
253
|
+
const rawValue = args.value === undefined || args.value === null ? "" : String(args.value).trim();
|
|
254
|
+
const kind = typeof args.kind === "string" ? args.kind.trim() : "";
|
|
255
|
+
const name = typeof args.name === "string" ? args.name.trim() : "";
|
|
256
|
+
const state = (await readState(core, paths)) ?? core.defaultState();
|
|
257
|
+
const fleet = assistantOverview(core, state);
|
|
258
|
+
const names = fleet.map((a) => a.name);
|
|
259
|
+
|
|
260
|
+
if (kind !== "") {
|
|
261
|
+
const command = assistCommandFor(kind, name, rawValue, fleet);
|
|
262
|
+
if (!command.ok) return json(`ERROR INVALID_ARGUMENT: ${command.reason}`, true);
|
|
263
|
+
return json({
|
|
264
|
+
ok: false,
|
|
265
|
+
readOnly: false,
|
|
266
|
+
wrote: false,
|
|
267
|
+
reason: "need_human_confirmation",
|
|
268
|
+
command: command.command,
|
|
269
|
+
kind,
|
|
270
|
+
before: { assistants: fleet.length, names },
|
|
271
|
+
hint: "不要自己改状态文件;把 command 原样给用户,等他执行后下一轮用 pactive_assistants 复核。",
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (key !== "" && !known.includes(key)) {
|
|
276
|
+
return json(`ERROR INVALID_ARGUMENT: 未知配置键(${key});已知:${known.join(", ")}`, true);
|
|
277
|
+
}
|
|
278
|
+
const values = (key === "" ? known : [key]).map((k) => ({
|
|
279
|
+
key: k,
|
|
280
|
+
value: core.config?.get ? core.config.get(state, k) : null,
|
|
281
|
+
default: described.defaults[k] ?? null,
|
|
282
|
+
humanGated: described.schema?.[k]?.humanGated === true,
|
|
283
|
+
}));
|
|
284
|
+
if (rawValue === "") {
|
|
285
|
+
return json({ ok: true, readOnly: true, wrote: false, values, assistants: fleet.length, stateFile: paths.stateFile });
|
|
286
|
+
}
|
|
287
|
+
return json({
|
|
288
|
+
ok: false,
|
|
289
|
+
readOnly: false,
|
|
290
|
+
wrote: false,
|
|
291
|
+
reason: "need_human_confirmation",
|
|
292
|
+
command: `pactive config set ${key} ${rawValue} --confirm`,
|
|
293
|
+
hint: "不要自己改状态文件;把 command 原样给用户,等他执行后下一轮用 pactive_status 复核。",
|
|
294
|
+
values,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
120
298
|
case "pactive_schedule": {
|
|
121
299
|
const prompt = typeof args.prompt === "string" ? args.prompt.trim() : "";
|
|
122
300
|
if (prompt === "") return json("ERROR INVALID_ARGUMENT: prompt 必填", true);
|
|
123
301
|
const spec = parseWhen(args.when, now);
|
|
124
302
|
if (!spec) return json(`ERROR INVALID_ARGUMENT: 无法解析 when(${String(args.when)})`, true);
|
|
125
|
-
const
|
|
126
|
-
|
|
303
|
+
const requested = typeof args.assistant === "string" ? args.assistant.trim() : "";
|
|
304
|
+
const state0 = (await readState(core, paths)) ?? core.defaultState();
|
|
305
|
+
let assistantId = null;
|
|
306
|
+
if (requested !== "") {
|
|
307
|
+
const record = core.assistants?.resolve ? core.assistants.resolve(state0, requested) : null;
|
|
308
|
+
if (!record) {
|
|
309
|
+
const names = assistantOverview(core, state0).map((a) => a.name);
|
|
310
|
+
return json(`ERROR INVALID_ARGUMENT: 未知助理(${requested});已知:${names.join(", ") || "(none yet)"}`, true);
|
|
311
|
+
}
|
|
312
|
+
assistantId = record.assistantId;
|
|
313
|
+
}
|
|
314
|
+
const next = await mutateState(core, paths, (state) => {
|
|
315
|
+
const targetId = assistantId ?? (core.assistants?.ensureDefault
|
|
316
|
+
? core.assistants.ensureDefault(state).assistantId
|
|
317
|
+
: null);
|
|
318
|
+
const out = core.schedule.add(
|
|
319
|
+
state,
|
|
320
|
+
{ ...spec, prompt, content: prompt, owner: state.identity?.agentId, assistantId: targetId },
|
|
321
|
+
now,
|
|
322
|
+
);
|
|
127
323
|
return pickState(out, state);
|
|
128
324
|
});
|
|
129
|
-
const tasks = core.schedule?.list ? core.schedule.list(next) : (next.tasks ?? []);
|
|
325
|
+
const tasks = core.schedule?.list ? core.schedule.list(next, assistantId ?? undefined) : (next.tasks ?? []);
|
|
130
326
|
const created = Array.isArray(tasks) ? tasks[tasks.length - 1] : null;
|
|
131
327
|
return json({
|
|
132
328
|
ok: true,
|
|
133
329
|
taskId: created?.id ?? created?.taskId ?? null,
|
|
330
|
+
assistantId: created?.assistantId ?? assistantId,
|
|
134
331
|
dueAt: created?.dueAt ?? spec.at ?? null,
|
|
135
332
|
kind: spec.kind,
|
|
136
333
|
when: spec.when,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pactive",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4-alpha.1",
|
|
4
4
|
"description": "Pactive:跨 Pi / OPCteam / DSH 的主动能力运行时(idle 判定默认关闭、人工闸门、事件账本)。专有软件:使用条款见包内 LICENSE(发行物含 Pi 扩展源码 pi/extension.ts)。",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"type": "module",
|