pactive 0.0.3-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 +84 -10
- package/bin/pactive.mjs +650 -18
- package/dsh/index.js +170 -14
- package/mcp/tools/definitions.mjs +159 -13
- package/package.json +1 -1
- package/pi/extension.ts +191 -15
- package/pi/skills/pactive/SKILL.md +16 -2
- package/skills/pactive/SKILL.md +16 -2
- 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 +10 -2
- 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/dsh/index.js
CHANGED
|
@@ -65,12 +65,20 @@ 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 };
|
|
69
75
|
const CONFIG_PARAMETERS = {
|
|
70
76
|
type: "object",
|
|
71
77
|
properties: {
|
|
72
|
-
key: { type: "string", description: "配置键;缺省返回全部(idle.enabled / idle.thresholdMs / policy
|
|
78
|
+
key: { type: "string", description: "配置键;缺省返回全部(idle.enabled / idle.thresholdMs / policy.* / globalGuard.hourlyLimit)" },
|
|
73
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 时用名字)" },
|
|
74
82
|
},
|
|
75
83
|
additionalProperties: false,
|
|
76
84
|
};
|
|
@@ -79,11 +87,60 @@ const SCHEDULE_PARAMETERS = {
|
|
|
79
87
|
properties: {
|
|
80
88
|
prompt: { type: "string", description: "任务正文(到点注入给会话的内容)" },
|
|
81
89
|
when: { type: "string", description: "触发时间:时长(10m)/ 时刻(18:30)/ ISO 8601 / every 1h" },
|
|
90
|
+
assistant: { type: "string", description: "记在哪个助理名下(名字或 id);不传 = 默认助理" },
|
|
82
91
|
},
|
|
83
92
|
required: ["prompt"],
|
|
84
93
|
additionalProperties: false,
|
|
85
94
|
};
|
|
86
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
|
+
|
|
87
144
|
/**
|
|
88
145
|
* @param {any} ctx Cordis plugin context
|
|
89
146
|
* @param {Record<string, any>} config
|
|
@@ -97,8 +154,8 @@ export function apply(ctx, config) {
|
|
|
97
154
|
ctx.tools.register({
|
|
98
155
|
name: "pactive_status",
|
|
99
156
|
description:
|
|
100
|
-
"
|
|
101
|
-
"→
|
|
157
|
+
"当用户问「我现在闲不闲」「主动提醒开启了吗」「为什么没触发」「刚才主动做了什么」「盯盘那个助理怎么样了」时用。" +
|
|
158
|
+
"→ 读空闲判定、reason、护栏闸门与任务数;给 assistant 看该助理,不给看全部助理概览。只读,不落盘。",
|
|
102
159
|
parameters: STATUS_PARAMETERS,
|
|
103
160
|
async execute(args = {}) {
|
|
104
161
|
try {
|
|
@@ -106,19 +163,48 @@ export function apply(ctx, config) {
|
|
|
106
163
|
const now = Date.now();
|
|
107
164
|
const paths = core.resolvePaths(HOST_KIND, process.env);
|
|
108
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
|
+
}
|
|
109
192
|
const idle = core.evaluateIdle
|
|
110
193
|
? core.evaluateIdle(state, now, state.policy ?? {})
|
|
111
194
|
: { idle: false, reason: "requires-action" };
|
|
112
195
|
const guard = core.checkGuard ? core.checkGuard(state, now) : { pass: false, reason: "requires-action" };
|
|
113
196
|
const tasks = core.schedule?.list ? core.schedule.list(state) : (state.tasks ?? []);
|
|
114
|
-
const
|
|
197
|
+
const fleet = assistantOverview(core, state);
|
|
115
198
|
return {
|
|
116
199
|
ok: true,
|
|
117
200
|
host: HOST_KIND,
|
|
118
201
|
now,
|
|
119
202
|
enabled: state.idle?.enabled ?? false,
|
|
203
|
+
assistants: fleet,
|
|
204
|
+
assistantCount: fleet.length,
|
|
120
205
|
idle,
|
|
121
206
|
guard,
|
|
207
|
+
globalGuard,
|
|
122
208
|
tasks: Array.isArray(tasks) ? tasks.length : 0,
|
|
123
209
|
activity,
|
|
124
210
|
};
|
|
@@ -128,11 +214,49 @@ export function apply(ctx, config) {
|
|
|
128
214
|
},
|
|
129
215
|
});
|
|
130
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
|
+
|
|
131
255
|
ctx.tools.register({
|
|
132
256
|
name: "pactive_schedule",
|
|
133
257
|
description:
|
|
134
258
|
"当用户说「10 分钟后提醒我喝水」「18:30 提醒我开会」「每小时检查一次」时用。" +
|
|
135
|
-
"→ 登记一条定时任务并立即落盘;when 支持 10m / 18:30 / ISO 8601 / every 1h
|
|
259
|
+
"→ 登记一条定时任务并立即落盘;when 支持 10m / 18:30 / ISO 8601 / every 1h;assistant 不填就记在默认助理名下。" +
|
|
136
260
|
"会写盘,需要人工确认(只在用户明确要求时调用)。",
|
|
137
261
|
parameters: SCHEDULE_PARAMETERS,
|
|
138
262
|
async execute(args = {}) {
|
|
@@ -144,19 +268,33 @@ export function apply(ctx, config) {
|
|
|
144
268
|
if (!spec) return { ok: false, error: `无法解析 when:${String(args.when)}` };
|
|
145
269
|
const core = await loadCore();
|
|
146
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
|
+
}
|
|
147
282
|
const next = await mutateState(core, paths, (state) => {
|
|
283
|
+
const targetId =
|
|
284
|
+
assistantId ?? (core.assistants?.ensureDefault ? core.assistants.ensureDefault(state).assistantId : null);
|
|
148
285
|
const out = core.schedule.add(
|
|
149
286
|
state,
|
|
150
|
-
{ ...spec, prompt, content: prompt, owner: state.identity?.agentId },
|
|
287
|
+
{ ...spec, prompt, content: prompt, owner: state.identity?.agentId, assistantId: targetId },
|
|
151
288
|
now,
|
|
152
289
|
);
|
|
153
290
|
return pickState(out, state);
|
|
154
291
|
});
|
|
155
|
-
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 ?? []);
|
|
156
293
|
const created = Array.isArray(tasks) ? tasks[tasks.length - 1] : null;
|
|
157
294
|
return {
|
|
158
295
|
ok: true,
|
|
159
296
|
taskId: created?.id ?? created?.taskId ?? null,
|
|
297
|
+
assistantId: created?.assistantId ?? assistantId,
|
|
160
298
|
dueAt: created?.dueAt ?? spec.at ?? null,
|
|
161
299
|
kind: spec.kind,
|
|
162
300
|
when: spec.when,
|
|
@@ -168,12 +306,12 @@ export function apply(ctx, config) {
|
|
|
168
306
|
},
|
|
169
307
|
});
|
|
170
308
|
|
|
171
|
-
// Read-only by construction: a write request returns the command, never a write (contract §2.3).
|
|
309
|
+
// Read-only by construction: a write request returns the command, never a write (contract §2.3 / I66).
|
|
172
310
|
ctx.tools.register({
|
|
173
311
|
name: "pactive_config",
|
|
174
312
|
description:
|
|
175
|
-
"
|
|
176
|
-
"→
|
|
313
|
+
"当用户问「主动提醒的开关开着吗」「阈值是多少」「安静时间是几点」「帮我把主动提醒打开」「再给我加一个盯盘的助理」时用。" +
|
|
314
|
+
"→ 读配置值与默认值;要改配置或增删 / 开关助理时,只返回一条需要人工确认的可复制命令。只读,不落盘。",
|
|
177
315
|
parameters: CONFIG_PARAMETERS,
|
|
178
316
|
async execute(args = {}) {
|
|
179
317
|
try {
|
|
@@ -182,18 +320,36 @@ export function apply(ctx, config) {
|
|
|
182
320
|
const known = Object.keys(described.defaults);
|
|
183
321
|
const key = typeof args.key === "string" ? args.key.trim() : "";
|
|
184
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
|
+
|
|
185
343
|
if (key !== "" && !known.includes(key)) {
|
|
186
344
|
return { ok: false, error: `未知配置键(${key});已知:${known.join(", ")}` };
|
|
187
345
|
}
|
|
188
|
-
const paths = core.resolvePaths(HOST_KIND, process.env);
|
|
189
|
-
const state = (await readState(core, paths)) ?? core.defaultState();
|
|
190
346
|
const values = (key === "" ? known : [key]).map((k) => ({
|
|
191
347
|
key: k,
|
|
192
348
|
value: core.config?.get ? core.config.get(state, k) : null,
|
|
193
349
|
default: described.defaults[k] ?? null,
|
|
194
350
|
humanGated: described.schema?.[k]?.humanGated === true,
|
|
195
351
|
}));
|
|
196
|
-
if (rawValue === "") return { ok: true, readOnly: true, wrote: false, values };
|
|
352
|
+
if (rawValue === "") return { ok: true, readOnly: true, wrote: false, values, assistants: fleet.length };
|
|
197
353
|
return {
|
|
198
354
|
ok: false,
|
|
199
355
|
wrote: false,
|
|
@@ -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,19 +36,35 @@ export const TOOL_DEFINITIONS = [
|
|
|
35
36
|
{
|
|
36
37
|
name: "pactive_status",
|
|
37
38
|
description:
|
|
38
|
-
"
|
|
39
|
-
"→
|
|
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。只读,不落盘。",
|
|
40
55
|
inputSchema: obj({}, []),
|
|
41
56
|
},
|
|
42
57
|
{
|
|
43
58
|
name: "pactive_config",
|
|
44
59
|
description:
|
|
45
|
-
"
|
|
46
|
-
"→
|
|
60
|
+
"当用户问「主动提醒的开关开着吗」「阈值是多少」「安静时间是几点」「帮我把主动提醒打开」「再给我加一个盯盘的助理」时用。" +
|
|
61
|
+
"→ 读配置值与默认值;要改配置或增删 / 开关助理时,只返回一条需要人工确认的可复制命令。只读,不落盘。",
|
|
47
62
|
inputSchema: obj(
|
|
48
63
|
{
|
|
49
|
-
key: str("配置键;缺省返回全部(idle.enabled / idle.thresholdMs / policy
|
|
64
|
+
key: str("配置键;缺省返回全部(idle.enabled / idle.thresholdMs / policy.* / globalGuard.hourlyLimit)"),
|
|
50
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 时用名字)"),
|
|
51
68
|
},
|
|
52
69
|
[],
|
|
53
70
|
),
|
|
@@ -56,12 +73,13 @@ export const TOOL_DEFINITIONS = [
|
|
|
56
73
|
name: "pactive_schedule",
|
|
57
74
|
description:
|
|
58
75
|
"当用户说「10 分钟后提醒我喝水」「18:30 提醒我开会」「每小时检查一次」时用。" +
|
|
59
|
-
"→ 登记一条定时任务并立即落盘;when 支持 10m / 18:30 / ISO 8601 / every 1h
|
|
76
|
+
"→ 登记一条定时任务并立即落盘;when 支持 10m / 18:30 / ISO 8601 / every 1h;assistant 不填就记在默认助理名下。" +
|
|
60
77
|
"会写盘,需要人工确认(只在用户明确要求时调用)。",
|
|
61
78
|
inputSchema: obj(
|
|
62
79
|
{
|
|
63
80
|
prompt: str("任务正文(到点注入给会话的内容)"),
|
|
64
81
|
when: str("触发时间:时长 / 时刻 / ISO 8601 / every <时长>;缺省 10m"),
|
|
82
|
+
assistant: str("记在哪个助理名下(名字或 id);不传 = 默认助理"),
|
|
65
83
|
},
|
|
66
84
|
["prompt"],
|
|
67
85
|
),
|
|
@@ -99,6 +117,54 @@ function json(text, isError = false) {
|
|
|
99
117
|
return { content: [{ type: "text", text: typeof text === "string" ? text : JSON.stringify(text, null, 2) }], isError };
|
|
100
118
|
}
|
|
101
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
|
+
|
|
102
168
|
/**
|
|
103
169
|
* Run one tool.
|
|
104
170
|
* @param {string} name
|
|
@@ -121,33 +187,94 @@ export async function runTool(name, args = {}, options = {}) {
|
|
|
121
187
|
case "pactive_status": {
|
|
122
188
|
const state = (await readState(core, paths)) ?? core.defaultState();
|
|
123
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
|
+
}
|
|
124
215
|
const idle = core.evaluateIdle ? core.evaluateIdle(state, now, policy) : { idle: false, reason: "requires-action" };
|
|
125
216
|
const guard = core.checkGuard ? core.checkGuard(state, now) : { pass: false, reason: "requires-action" };
|
|
126
217
|
const tasks = core.schedule?.list ? core.schedule.list(state) : (state.tasks ?? []);
|
|
127
218
|
const activity = core.getActivity ? core.getActivity(state) : null;
|
|
219
|
+
const fleet = assistantOverview(core, state);
|
|
128
220
|
return json({
|
|
129
221
|
host: HOST_KIND,
|
|
130
222
|
now,
|
|
131
223
|
enabled: state.idle?.enabled ?? false,
|
|
224
|
+
assistants: fleet,
|
|
225
|
+
assistantCount: fleet.length,
|
|
132
226
|
idle,
|
|
133
227
|
guard,
|
|
228
|
+
globalGuard,
|
|
134
229
|
tasks: Array.isArray(tasks) ? tasks.length : 0,
|
|
135
230
|
activity,
|
|
136
231
|
stateFile: paths.stateFile,
|
|
137
232
|
});
|
|
138
233
|
}
|
|
139
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
|
+
|
|
140
247
|
case "pactive_config": {
|
|
141
248
|
// Read-only. A write request never lands: the model gets a copy-pasteable command
|
|
142
|
-
// whose --confirm only a human can type (contract §2.3 / I59).
|
|
249
|
+
// whose --confirm only a human can type (contract §2.3 / I59 / I66).
|
|
143
250
|
const described = core.config?.describe ? core.config.describe() : { schema: {}, defaults: {}, humanGatedKeys: [] };
|
|
144
251
|
const known = Object.keys(described.defaults);
|
|
145
252
|
const key = typeof args.key === "string" ? args.key.trim() : "";
|
|
146
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
|
+
|
|
147
275
|
if (key !== "" && !known.includes(key)) {
|
|
148
276
|
return json(`ERROR INVALID_ARGUMENT: 未知配置键(${key});已知:${known.join(", ")}`, true);
|
|
149
277
|
}
|
|
150
|
-
const state = (await readState(core, paths)) ?? core.defaultState();
|
|
151
278
|
const values = (key === "" ? known : [key]).map((k) => ({
|
|
152
279
|
key: k,
|
|
153
280
|
value: core.config?.get ? core.config.get(state, k) : null,
|
|
@@ -155,7 +282,7 @@ export async function runTool(name, args = {}, options = {}) {
|
|
|
155
282
|
humanGated: described.schema?.[k]?.humanGated === true,
|
|
156
283
|
}));
|
|
157
284
|
if (rawValue === "") {
|
|
158
|
-
return json({ ok: true, readOnly: true, wrote: false, values, stateFile: paths.stateFile });
|
|
285
|
+
return json({ ok: true, readOnly: true, wrote: false, values, assistants: fleet.length, stateFile: paths.stateFile });
|
|
159
286
|
}
|
|
160
287
|
return json({
|
|
161
288
|
ok: false,
|
|
@@ -173,15 +300,34 @@ export async function runTool(name, args = {}, options = {}) {
|
|
|
173
300
|
if (prompt === "") return json("ERROR INVALID_ARGUMENT: prompt 必填", true);
|
|
174
301
|
const spec = parseWhen(args.when, now);
|
|
175
302
|
if (!spec) return json(`ERROR INVALID_ARGUMENT: 无法解析 when(${String(args.when)})`, true);
|
|
176
|
-
const
|
|
177
|
-
|
|
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
|
+
);
|
|
178
323
|
return pickState(out, state);
|
|
179
324
|
});
|
|
180
|
-
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 ?? []);
|
|
181
326
|
const created = Array.isArray(tasks) ? tasks[tasks.length - 1] : null;
|
|
182
327
|
return json({
|
|
183
328
|
ok: true,
|
|
184
329
|
taskId: created?.id ?? created?.taskId ?? null,
|
|
330
|
+
assistantId: created?.assistantId ?? assistantId,
|
|
185
331
|
dueAt: created?.dueAt ?? spec.at ?? null,
|
|
186
332
|
kind: spec.kind,
|
|
187
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",
|