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.
@@ -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 = { type: "object", properties: {}, additionalProperties: false };
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
- "Pactive 主动机制状态:空闲判定与 reason、护栏四闸门、定时任务数、最近活动。只读。",
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 activity = core.getActivity ? core.getActivity(state) : null;
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: "登记一个定时任务(真正落盘)。when 支持时长(10m)、时刻(18:30)、ISO 时间、every 1h。",
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 / pactive_schedule / pactive_tell / pactive_history.
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
- "Pactive 主动机制状态:空闲判定与 reason、护栏四闸门、定时任务数、最近活动。只读。",
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
- "登记一个定时任务(真正落盘)。when 支持时长(10m)、时刻(18:30)、ISO 时间、every 1h。",
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
- "给同机另一个会话投递一条消息。对端离线时进入 outbox 排队,不伪装成功。",
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: "读取 Pactive 事件账本尾部(触发→投递→回应→结账),用于追溯主动行为。",
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 next = await mutateState(core, paths, (state) => {
126
- const out = core.schedule.add(state, { ...spec, prompt, content: prompt, owner: state.identity?.agentId }, now);
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.2-alpha.1",
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",