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/dsh/index.js CHANGED
@@ -65,12 +65,20 @@ 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 };
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
- "→ 读当前空闲判定、reason、护栏四闸门、任务数与最近活动。只读,不落盘。",
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 activity = core.getActivity ? core.getActivity(state) : null;
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 / 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,19 +36,35 @@ export const TOOL_DEFINITIONS = [
35
36
  {
36
37
  name: "pactive_status",
37
38
  description:
38
- "当用户问「我现在闲不闲」「主动提醒开启了吗」「为什么没触发」「刚才主动做了什么」时用。" +
39
- "→ 读当前空闲判定、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。只读,不落盘。",
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 next = await mutateState(core, paths, (state) => {
177
- 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
+ );
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-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",