personal-bot 0.1.2 → 0.2.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.
Files changed (44) hide show
  1. package/CONTEXT.md +6 -2
  2. package/README.md +9 -7
  3. package/README.zh-CN.md +9 -7
  4. package/docs/README.md +3 -1
  5. package/docs/agent-collaboration.md +98 -40
  6. package/docs/file-storage.md +25 -4
  7. package/docs/release.md +2 -2
  8. package/docs/subagent-upgrade-proposal.md +357 -0
  9. package/docs/technical-design.md +23 -4
  10. package/docs/validation.md +35 -5
  11. package/npm-shrinkwrap.json +2253 -0
  12. package/package.json +5 -4
  13. package/resources/agents/coordinator.md +8 -1
  14. package/resources/agents/general-purpose.md +10 -4
  15. package/resources/prompts/tool-guidelines.md +2 -2
  16. package/resources/prompts/workspace.md +2 -2
  17. package/resources/references/compute.md +3 -3
  18. package/resources/references/task-conductor.md +4 -4
  19. package/resources/skills/project-maintenance/SKILL.md +1 -1
  20. package/resources/skills/provider-connections/SKILL.md +2 -2
  21. package/src/common/markdown.ts +26 -0
  22. package/src/extension/agent-events.ts +116 -14
  23. package/src/extension/agents-command.ts +15 -17
  24. package/src/extension/command.ts +5 -3
  25. package/src/extension/lifecycle.ts +7 -2
  26. package/src/extension/subagent-entry.ts +144 -31
  27. package/src/extension/teams-command.ts +60 -0
  28. package/src/integrations/pi/agent-context.ts +1 -1
  29. package/src/integrations/pi/agent-input.ts +19 -0
  30. package/src/integrations/pi/pi-agents.ts +22 -9
  31. package/src/integrations/pi/team-context.ts +78 -0
  32. package/src/model/agent.ts +2 -0
  33. package/src/model/team.ts +29 -0
  34. package/src/pbot.ts +5 -3
  35. package/src/services/agents/agent-service.ts +91 -21
  36. package/src/services/agents/team-service.ts +274 -0
  37. package/src/storage/agent-files.ts +43 -10
  38. package/src/storage/team-files.ts +93 -0
  39. package/src/tools/agents/contract.ts +8 -6
  40. package/src/tools/agents/tool.ts +10 -9
  41. package/src/tools/register.ts +6 -3
  42. package/src/tools/result.ts +5 -0
  43. package/src/tools/teams/contract.ts +67 -0
  44. package/src/tools/teams/tool.ts +63 -0
@@ -0,0 +1,93 @@
1
+ import { existsSync, lstatSync, mkdirSync, readdirSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { object } from "../common/json.js";
4
+ import { requireThat } from "../common/validation.js";
5
+ import type { TeamRecord, TeamReady, TeamLink } from "../model/team.js";
6
+ import { AgentFiles } from "./agent-files.js";
7
+ import { readJsonFile, writeJsonFile } from "./atomic-json.js";
8
+ import { fileId, privateDirectory } from "./file-access.js";
9
+
10
+ const text = (v: unknown): v is string => typeof v === "string" && v.length > 0;
11
+ const integer = (v: unknown, min = 0): v is number => Number.isSafeInteger(v) && (v as number) >= min;
12
+ export function teamReady(v: unknown): TeamReady {
13
+ requireThat(object(v) && text(v.generation) && typeof v.ready === "boolean", "团队就绪标记无效。");
14
+ return v as unknown as TeamReady;
15
+ }
16
+ export function teamRecord(v: unknown): TeamRecord {
17
+ requireThat(object(v) && text(v.teamId) && text(v.release) && text(v.workspace) && text(v.parentSessionId) &&
18
+ (v.parentLeaf === null || text(v.parentLeaf)) && text(v.name) && text(v.brief) && typeof v.closed === "boolean" &&
19
+ Array.isArray(v.members) && v.members.length > 0 && v.members.length <= 50 && Array.isArray(v.runs) && Array.isArray(v.messages), "团队记录无效。");
20
+ fileId(v.teamId);
21
+ const members = new Set<string>(), runs = new Set<string>(), requests = new Map<string, { agentId: string; runId: string }>(), operations = new Set<string>();
22
+ for (const m of v.members) {
23
+ requireThat(object(m) && text(m.agentId) && text(m.role) && !members.has(m.agentId), "团队成员无效或重复。");
24
+ fileId(m.agentId); members.add(m.agentId);
25
+ }
26
+ let running = 0;
27
+ for (const r of v.runs) {
28
+ requireThat(object(r) && text(r.runId) && !runs.has(r.runId) && text(r.instruction) &&
29
+ ["roundRobin", "parallel"].includes(String(r.mode)) && integer(r.rounds, 1) && r.rounds <= 10 &&
30
+ integer(r.maxConcurrent, 1) && r.maxConcurrent <= members.size && (r.mode !== "roundRobin" || r.maxConcurrent === 1) &&
31
+ integer(r.deadlineAt, 1) && ["running", "paused", "finished"].includes(String(r.state)) &&
32
+ (r.reason === undefined || text(r.reason)) && object(r.generations) && Array.isArray(r.issued), "团队批次无效。");
33
+ fileId(r.runId); runs.add(r.runId);
34
+ requireThat(Object.keys(r.generations).length === members.size && [...members].every(id => text((r.generations as Record<string, unknown>)[id])), "团队代次无效。");
35
+ if (r.state === "running") running++;
36
+ const turns = new Set<string>();
37
+ for (const q of r.issued) {
38
+ requireThat(object(q) && text(q.requestId) && !requests.has(q.requestId) && text(q.agentId) && members.has(q.agentId) &&
39
+ q.generation === r.generations[q.agentId] && integer(q.round, 1) && q.round <= r.rounds &&
40
+ integer(q.throughSeq) && q.throughSeq <= v.messages.length && !turns.has(q.agentId + ":" + q.round), "团队请求关联无效。");
41
+ fileId(q.requestId); requests.set(q.requestId, { agentId: q.agentId, runId: r.runId }); turns.add(q.agentId + ":" + q.round);
42
+ }
43
+ }
44
+ requireThat(running <= 1 && (!v.closed || running === 0), "团队运行状态冲突。");
45
+ for (const [i, m] of v.messages.entries()) {
46
+ requireThat(object(m) && m.seq === i + 1 && text(m.operationId) && !operations.has(m.operationId) && typeof m.text === "string" &&
47
+ object(m.author) && ["user", "coordinator", "member"].includes(String(m.author.kind)) && text(m.author.name) &&
48
+ (m.author.kind === "member" ? text(m.author.agentId) && members.has(m.author.agentId) : m.author.agentId === undefined) &&
49
+ (m.replyTo === undefined || (integer(m.replyTo, 1) && m.replyTo <= i)) &&
50
+ (m.runId === undefined || (text(m.runId) && runs.has(m.runId))), "团队消息无效。");
51
+ if (m.source !== undefined) requireThat(object(m.source) && text(m.source.requestId) && text(m.source.cycleId) &&
52
+ requests.get(m.source.requestId)?.agentId === m.author.agentId && requests.get(m.source.requestId)?.runId === m.runId, "团队报告关联无效。");
53
+ operations.add(m.operationId);
54
+ }
55
+ return v as unknown as TeamRecord;
56
+ }
57
+ export class TeamFiles {
58
+ readonly root: string;
59
+ constructor(readonly agents: AgentFiles) { this.root = join(dirname(agents.root), "teams"); }
60
+ check() { this.agents.check(); }
61
+ read(id: string) {
62
+ const value = readJsonFile(this.root, fileId(id) + "/team.json", teamRecord);
63
+ requireThat(value.teamId === id, "团队路径与身份不匹配。"); return value;
64
+ }
65
+ list() {
66
+ if (!existsSync(this.root)) return [];
67
+ return readdirSync(this.root, { withFileTypes: true }).filter(e => !e.name.startsWith(".")).map(e => this.read(e.name));
68
+ }
69
+ save(value: TeamRecord, create = false) {
70
+ this.check(); teamRecord(value);
71
+ let published: boolean;
72
+ try {
73
+ mkdirSync(this.root, { recursive: true, mode: 0o700 });
74
+ const directory = lstatSync(this.root);
75
+ requireThat(directory.isDirectory() && !directory.isSymbolicLink(), "团队状态必须使用普通目录。");
76
+ if (create) privateDirectory(this.root, fileId(value.teamId));
77
+ published = writeJsonFile(this.root, fileId(value.teamId) + "/team.json", value, teamRecord, create);
78
+ } catch (error) { this.agents.fail(error); throw error; }
79
+ requireThat(published, "团队已存在。");
80
+ }
81
+ ready(id: string) { return this.agents.optional(id, "team-ready.json", teamReady); }
82
+ setReady(id: string, generation: string, ready: boolean) {
83
+ this.agents.write(id, "team-ready.json", { generation, ready }, teamReady);
84
+ }
85
+ links(records = this.list()): Map<string, TeamLink> {
86
+ const result = new Map<string, TeamLink>();
87
+ for (const team of records) for (const run of team.runs) for (const request of run.issued) {
88
+ requireThat(!result.has(request.requestId), "团队请求 ID 重复。");
89
+ result.set(request.requestId, { team, run, request });
90
+ }
91
+ return result;
92
+ }
93
+ }
@@ -17,14 +17,14 @@ const options = {
17
17
  noContextFiles: Type.Optional(Type.Boolean({ description: "关闭 Pi 原生上下文文件加载,默认 false。" })),
18
18
  };
19
19
  const page = {
20
- cycleId: Type.Optional(Type.String({ ...id, description: "read 的历史工作周期;省略时读取最近周期。" })),
21
- offset: Type.Optional(Type.Integer({ minimum: 0, description: "报告字符偏移,默认 0。" })),
22
- limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 16000, description: "最多返回字符数,默认 16000。" })),
20
+ cycleId: Type.Optional(Type.String({ ...id, description: "仅 read:省略时读取最近周期;按 nextOffset 续读时须固定为上页返回的 cycle.cycleId,避免切换到新周期。" })),
21
+ offset: Type.Optional(Type.Integer({ minimum: 0, description: "仅 read:报告正文的 UTF-16 字符偏移,默认 0;续读使用上页 nextOffset。" })),
22
+ limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 16000, description: "仅 read:最多返回的正文 UTF-16 字符数,默认 16000,不是消息数或 token 数。" })),
23
23
  };
24
24
  const delivery = Type.Optional(Type.Union([Type.Literal("steer"), Type.Literal("followUp")], { description: "send 默认 steer 补充当前工作;followUp 排到当前工作之后。" }));
25
25
  export const agentParametersSchema = Type.Object({
26
26
  action: Type.Union([Type.Literal("start"), Type.Literal("send"), Type.Literal("rename"), Type.Literal("status"), Type.Literal("list"), Type.Literal("read"), Type.Literal("stop"), Type.Literal("resume")], {
27
- description: "start 至少提供 message/prompts/files 之一,其余启动选项仅用于 start。list 不带其他字段;其余操作须 agentId。send/resume 须 message,rename 须 name;分页仅用于 read。",
27
+ description: "start 仅接受启动选项,可省略 message/prompts/files 创建待命进程,禁止 agentId/delivery/分页字段;list 只接受 action;status/stop 只接受并必填 agentId;send 必填 agentId/message,仅可选 delivery;resume 必填 agentId/message;rename 必填 agentId/name;read 必填 agentId,仅可选 cycleId/offset/limit。禁止携带其他 action 的字段。",
28
28
  }),
29
29
  agentId: Type.Optional(id), ...options, ...page, delivery,
30
30
  }, { additionalProperties: false });
@@ -48,7 +48,9 @@ export const agentViewSchema = Type.Object({ agentId: Type.String(), name: Type.
48
48
  state: Type.Union([Type.Literal("starting"), Type.Literal("busy"), Type.Literal("idle"), Type.Literal("stopping"), Type.Literal("stopped"), Type.Literal("error")], { description: "进程状态;idle 不代表上轮工作成功,结果见 cycle.state。" }),
49
49
  activity: Type.String(), cycle: Type.Optional(cycle), error: Type.Optional(Type.String()),
50
50
  requestId: Type.Optional(Type.String()), messageStatus: Type.Optional(Type.Literal("queued", { description: "消息已入队,不代表模型已阅读或完成。" })),
51
- cycles: Type.Optional(Type.Array(Type.Object({ cycleId: Type.String(), state: Type.String() }))),
52
- report: Type.Optional(Type.String()), totalLength: Type.Optional(Type.Integer()), nextOffset: Type.Optional(Type.Union([Type.Integer(), Type.Null()])),
51
+ cycles: Type.Optional(Type.Array(Type.Object({ cycleId: Type.String(), state: Type.String() }), { description: "仅 read 返回:可读取的周期索引。" })),
52
+ report: Type.Optional(Type.String({ description: "仅 read 返回:选定周期的报告正文片段。已结束周期的读取结果持久化后领取该周期通知,即使本页未读完。" })),
53
+ totalLength: Type.Optional(Type.Integer({ description: "仅 read 返回:完整报告的 UTF-16 字符数。" })),
54
+ nextOffset: Type.Optional(Type.Union([Type.Integer(), Type.Null()], { description: "仅 read 返回:null 表示本报告已读完;否则以相同 agentId、cycle.cycleId 和此 offset 继续。" })),
53
55
  }, { additionalProperties: false });
54
56
  export const agentResultSchema = Type.Union([agentViewSchema, Type.Object({ agents: Type.Array(agentViewSchema) }, { additionalProperties: false })]);
@@ -5,7 +5,7 @@ import { agentOwner } from "../../integrations/pi/agent-context.js";
5
5
  import type { AgentOptions, AgentView } from "../../model/agent.js";
6
6
  import type { CallAccess } from "../access.js";
7
7
  import { requireThat, schema } from "../../common/validation.js";
8
- import { result } from "../result.js";
8
+ import { markdownResult } from "../result.js";
9
9
  import { agentActionSchema, agentParametersSchema, agentResultSchema, agentViewSchema } from "./contract.js";
10
10
 
11
11
  type AgentOutput = Static<typeof agentViewSchema>;
@@ -27,34 +27,35 @@ function agentOutput(view: AgentView & Partial<AgentOutput>): AgentOutput {
27
27
  }
28
28
 
29
29
  export function agentTool(service: AgentService, access: CallAccess) {
30
- const valid = schema(agentActionSchema);
30
+ const validators = new Map(agentActionSchema.anyOf.map(action => [action.properties.action.const, schema(action)]));
31
31
  return defineTool({ name: "pbot_agent", label: "子会话",
32
- description: "独立 Pi 子会话,不继承父聊天历史。start 立即返回,完成自动通知,无需轮询。send 继续对话,rename 改名,status/list 查看状态,read 非阻塞读取报告。state 是进程状态,cycle.state 是工作结果,queued 仅表示入队。stop 停止并保留历史,resume 携新 message 恢复,不重放旧工作。独立复核须 start 新上下文。子会话使用原生 CLI/MCP,按委派范围工作,不递归委派。每个父会话最多 50 个存活子会话,空闲也计入。",
32
+ description: "独立 Pi 子会话,不继承父聊天历史。start 立即返回;未提供 message/prompts/files 时只启动待命进程,不调用模型。完成自动通知,无需轮询。团队占用期间不能私人 send。send 继续对话,rename 改名,status/list 查看状态,status 返回最近周期且不读取报告正文。read 非阻塞读取报告;读取已结束周期会领取该周期的完成通知(包括部分读取),后续不再重复通知。nextOffset 非空时以返回的 cycle.cycleId 固定周期续读。state 是进程状态,cycle.state 是工作结果,queued 仅表示入队。stop 停止并保留历史,resume 携新 message 恢复,不重放旧工作。独立复核须 start 新上下文。子会话使用原生 CLI/MCP,按委派范围工作,不递归委派。每个父会话最多 50 个存活子会话,空闲也计入。",
33
33
  parameters: agentParametersSchema, outputSchema: agentResultSchema,
34
34
  async execute(_id, args, signal, _update, ctx) {
35
35
  access.check(); signal?.throwIfAborted();
36
- requireThat(valid(args), "子会话参数不符合所选 action;请核对必要字段。");
36
+ const valid = validators.get(args.action);
37
+ requireThat(valid, "未知的子会话 action:" + args.action);
38
+ requireThat(valid(args), "子会话 " + args.action + " 参数无效:" + JSON.stringify(valid.errors));
37
39
  const input = args as Static<typeof agentParametersSchema>, owner = agentOwner(ctx);
38
40
  let value: AgentView & Partial<AgentOutput>;
39
41
  switch (input.action) {
40
42
  case "start": {
41
- requireThat(input.message || input.prompts?.length || input.files?.length, "start 需要 message、prompts 或 files。");
42
43
  const { action: _action, ...supplied } = input;
43
44
  value = service.start({ provider: ctx.model?.provider, model: ctx.model?.id, thinking: ctx.thinkingLevel, ...supplied } as AgentOptions, owner); break;
44
45
  }
45
- case "list": return result({ agents: service.list(owner).map(agentOutput) });
46
+ case "list": return markdownResult({ agents: service.list(owner).map(agentOutput) }, "子会话列表");
46
47
  case "send": value = service.send(input.agentId!, input.message!, input.delivery ?? "steer", owner); break;
47
48
  case "rename": value = service.rename(input.agentId!, input.name!, owner); break;
48
49
  case "resume": value = service.resume(input.agentId!, input.message!, owner); break;
49
50
  case "stop": value = service.stop(input.agentId!, owner); break;
50
- case "status": value = service.read(input.agentId!, owner, undefined, 0, 1); break;
51
+ case "status": value = service.status(input.agentId!, owner); break;
51
52
  case "read": {
52
53
  const page = service.read(input.agentId!, owner, input.cycleId, input.offset, input.limit);
53
- const data = agentOutput(page), output = result(data);
54
+ const data = agentOutput(page), output = markdownResult(data, "子会话报告");
54
55
  return { ...output, details: { ...data, ...(page.cycle && page.cycle.state !== "running" ? { agentReadKeys: [page.agentId + ":" + page.cycle.cycleId] } : {}) } };
55
56
  }
56
57
  }
57
- return result(agentOutput(value));
58
+ return markdownResult(agentOutput(value), "子会话状态");
58
59
  },
59
60
  });
60
61
  }
@@ -5,9 +5,12 @@ import { resourceRoot } from "../common/paths.js";
5
5
  import type { AgentService } from "../services/agents/agent-service.js";
6
6
  import type { CallAccess } from "./access.js";
7
7
  import { agentTool } from "./agents/tool.js";
8
+ import type { TeamService } from "../services/agents/team-service.js";
9
+ import { teamTool, teamMessageTool } from "./teams/tool.js";
10
+ import { agentOwner } from "../integrations/pi/agent-context.js";
8
11
 
9
- export function mainTools(agents: AgentService, access: CallAccess): ToolDefinition[] {
10
- return [agentTool(agents, access)];
12
+ export function mainTools(agents: AgentService, teams: TeamService, access: CallAccess): ToolDefinition[] {
13
+ return [agentTool(agents, access), teamTool(teams, agents, access), teamMessageTool(teams, access, ctx => ({ kind: "coordinator", owner: agentOwner(ctx) }))];
11
14
  }
12
15
  export class ToolRegistry {
13
16
  readonly #registered = new Map<string, ToolDefinition>();
@@ -16,7 +19,7 @@ export class ToolRegistry {
16
19
  const promptGuidelines = readFileSync(join(resourceRoot, "prompts/tool-guidelines.md"), "utf8")
17
20
  .trim().split(/\r?\n\s*\r?\n/);
18
21
  for (const definition of tools) {
19
- const tool = { ...definition, namespace: { name: "pbot", description: "Personal Bot 独立子会话工具。" },
22
+ const tool = { ...definition, namespace: { name: "pbot", description: "Personal Bot 独立子会话与团队协作工具。" },
20
23
  promptGuidelines };
21
24
  this.#registered.set(tool.name, tool); this.pi.registerTool(tool);
22
25
  }
@@ -1,4 +1,9 @@
1
1
  import { json } from "../common/json.js";
2
+ import { markdownFields } from "../common/markdown.js";
3
+
4
+ export function markdownResult(value: unknown, title: string) {
5
+ return result(value, false, "# " + title + "\n\n" + markdownFields(json(value)));
6
+ }
2
7
 
3
8
  export function result(value: unknown, isError = false, text?: string) {
4
9
  const data = json(value);
@@ -0,0 +1,67 @@
1
+ import { Type } from "typebox";
2
+ import type { TSchema } from "typebox";
3
+
4
+ const id = Type.String({ minLength: 1, maxLength: 128 });
5
+ const text = Type.String({ minLength: 1, maxLength: 24000 });
6
+ const name = Type.String({ minLength: 1, maxLength: 64, pattern: "^[^\\r\\n\\x00-\\x1f]+$" });
7
+ const mode = Type.Union([Type.Literal("roundRobin"), Type.Literal("parallel")], { description: "run 必填:roundRobin 按成员顺序轮流发言;parallel 在并发上限内发言,全员本轮完成后才进入下一轮。" });
8
+ const members = Type.Array(Type.Object({ agentId: id, role: Type.String({ minLength: 1, maxLength: 4000 }) }, { additionalProperties: false }), {
9
+ minItems: 1, maxItems: 50, description: "仅 create,必填:固定成员及职责。成员须属于当前父会话分支、版本兼容且未加入其他未关闭团队。create 不启动模型,也不等待成员就绪。",
10
+ });
11
+ const run = {
12
+ instruction: Type.String({ ...text, description: "仅 run,必填:本次新批次的明确任务与交付要求;不恢复或重放旧批次。" }), mode,
13
+ rounds: Type.Optional(Type.Integer({ minimum: 1, maximum: 10, description: "仅 run:每位成员的轮数,默认 2。" })),
14
+ maxConcurrent: Type.Optional(Type.Integer({ minimum: 1, maximum: 50, description: "仅 run:roundRobin 默认且只允许 1;parallel 默认 min(2, 成员数)。不得超过成员数;已排队但未完成的请求也占用并发名额。" })),
15
+ timeoutSeconds: Type.Optional(Type.Integer({ minimum: 1, maximum: 86400, description: "仅 run:整批期限,默认 900 秒,包含所有轮次和等待。到期停止新安排和领取,子入口尽力中止未被人工接管的在途工作;pause 则允许在途工作收尾。" })),
16
+ };
17
+ const page = {
18
+ afterSeq: Type.Optional(Type.Integer({ minimum: 0, description: "仅 read:读取序号大于此值的消息,默认 0,不得超过 lastSeq。返回 nextAfterSeq 用于继续消息列表;单条续读使用 seq/offset,省略 afterSeq。先补完被截断的正文,再继续列表。" })),
19
+ seq: Type.Optional(Type.Integer({ minimum: 1, description: "仅 read:指定一条消息,配合该消息的 nextOffset 续读。未指定时从 afterSeq 后顺序读取。" })),
20
+ offset: Type.Optional(Type.Integer({ minimum: 0, description: "仅 read:指定 seq 的正文 UTF-16 字符偏移,默认 0;非零时必须同时提供 seq。" })),
21
+ limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 16000, description: "仅 read:本页所有消息正文合计的 UTF-16 字符上限,默认 16000,不是消息条数或 token 数;不含元数据和 Markdown 格式开销。" })),
22
+ };
23
+ const object = <T extends Record<string, TSchema>>(fields: T) => Type.Object(fields, { additionalProperties: false });
24
+ export const teamParametersSchema = object({
25
+ action: Type.Union(["create", "list", "status", "read", "run", "pause", "close"].map(v => Type.Literal(v)), {
26
+ description: "create 只接受并必填 name/brief/members;list 只接受 action;status/pause/close 只接受并必填 teamId;read 必填 teamId,可选 afterSeq/seq/offset/limit;run 必填 teamId/instruction/mode,可选 rounds/maxConcurrent/timeoutSeconds。禁止携带其他 action 的字段。run 要求所有成员存活、idle、当前代次团队接入就绪、无待处理私人输入,且团队未关闭、无运行批次或在途工作。pause 仅停止新安排和未领取请求;close 要求先暂停并等待在途工作结束,不停止成员进程。",
27
+ }),
28
+ teamId: Type.Optional(id), name: Type.Optional(name), brief: Type.Optional(text), members: Type.Optional(members),
29
+ instruction: Type.Optional(run.instruction), mode: Type.Optional(mode), rounds: run.rounds, maxConcurrent: run.maxConcurrent, timeoutSeconds: run.timeoutSeconds, ...page,
30
+ });
31
+ export const teamActionSchema = Type.Union([
32
+ object({ action: Type.Literal("create"), name, brief: text, members }),
33
+ object({ action: Type.Literal("list") }),
34
+ ...["status", "pause", "close"].map(v => object({ action: Type.Literal(v), teamId: id })),
35
+ object({ action: Type.Literal("run"), teamId: id, ...run }),
36
+ object({ action: Type.Literal("read"), teamId: id, ...page }),
37
+ ]);
38
+ export const teamMessageParametersSchema = object({
39
+ action: Type.Union([Type.Literal("read"), Type.Literal("post")], {
40
+ description: "read 必填 teamId,只可选 afterSeq/seq/offset/limit,禁止 text/replyTo;post 必填 teamId/text,只可选 replyTo,禁止所有分页字段。",
41
+ }), teamId: id, text: Type.Optional(Type.String({ ...text, description: "仅 post,必填:向全体成员公开的原文,不扩大任务授权。" })),
42
+ replyTo: Type.Optional(Type.Integer({ minimum: 1, description: "仅 post:回复已有消息的 seq。" })), ...page,
43
+ });
44
+ export const teamMessageActionSchema = Type.Union([
45
+ object({ action: Type.Literal("read"), teamId: id, ...page }),
46
+ object({ action: Type.Literal("post"), teamId: id, text, replyTo: Type.Optional(Type.Integer({ minimum: 1 })) }),
47
+ ]);
48
+ const message = object({ seq: Type.Integer(), operationId: Type.String(),
49
+ author: object({ kind: Type.Union([Type.Literal("user"), Type.Literal("coordinator"), Type.Literal("member")]), agentId: Type.Optional(id), name: Type.String() }),
50
+ text: Type.String(), replyTo: Type.Optional(Type.Integer()), runId: Type.Optional(id),
51
+ source: Type.Optional(object({ requestId: id, cycleId: id })),
52
+ offset: Type.Integer(), totalLength: Type.Integer({ description: "完整正文的 UTF-16 字符数。" }),
53
+ nextOffset: Type.Union([Type.Integer(), Type.Null()], { description: "非 null 表示本条正文尚未读完;使用相同 teamId、seq 和此 offset 续读。" }) });
54
+ export const teamPageSchema = object({ teamId: id, messages: Type.Array(message), lastSeq: Type.Integer(),
55
+ nextAfterSeq: Type.Union([Type.Integer(), Type.Null()], { description: "后续消息列表的 afterSeq;null 只表示列表已到末尾,仍须检查每条消息的 nextOffset。指定 seq 时始终为 null。" }) });
56
+ const ownedView = object({ teamId: id, name: Type.String(), brief: Type.String(), closed: Type.Boolean(), owned: Type.Literal(true),
57
+ members: Type.Array(object({ agentId: id, role: Type.String(), name: Type.String(), state: Type.String() })),
58
+ lastSeq: Type.Integer(), activeCount: Type.Integer({ description: "所有批次中已领取、进程仍存活且尚无完成记录的请求数;paused 仍可能大于 0。" }),
59
+ unresolved: Type.Array(object({ requestId: id, agentId: id, runId: id, round: Type.Integer(), active: Type.Boolean(),
60
+ state: Type.String({ description: "queued=运行批次中尚未领取;accepted=已领取且仍在途;uncertain=已领取但进程/代次不再匹配、无完成记录;cancelled=批次已停止且未领取,或周期取消;unpublished=已完成但尚未公开有效原文;mixed/interactive=含人工或私人工作,不自动公开;failed/interrupted=失败或中断。不能将 unresolved 当作成功或自动重试许可。" }), cycleId: Type.Optional(id) }), {
61
+ description: "所有批次中尚未确认完成且公开的请求,包含排队、在途、失败、结果不明和不宜公开的工作;不是仅列出运行中的请求。先核对原请求和业务结果,避免重复执行。",
62
+ }),
63
+ run: Type.Optional(object({ runId: id, instruction: Type.String(), mode, state: Type.Union([Type.Literal("running"), Type.Literal("paused"), Type.Literal("finished")]),
64
+ rounds: Type.Integer(), maxConcurrent: Type.Integer(), deadlineAt: Type.Number(), scheduled: Type.Integer(), published: Type.Integer(), reason: Type.Optional(Type.String()) })) });
65
+ const view = Type.Union([ownedView, object({ teamId: id, name: Type.String(), closed: Type.Boolean(), owned: Type.Literal(false) })]);
66
+ export const teamResultSchema = Type.Union([view, object({ teams: Type.Array(view) }), teamPageSchema]);
67
+ export const teamMessageResultSchema = Type.Union([teamPageSchema, object({ teamId: id, seq: Type.Integer(), status: Type.Literal("stored") })]);
@@ -0,0 +1,63 @@
1
+ import type { Static } from "typebox";
2
+ import { defineTool, type ExtensionContext } from "@earendil-works/pi-coding-agent";
3
+ import type { TeamService } from "../../services/agents/team-service.js";
4
+ import type { AgentService } from "../../services/agents/agent-service.js";
5
+ import type { TeamActor } from "../../model/team.js";
6
+ import { agentOwner } from "../../integrations/pi/agent-context.js";
7
+ import { teamMessagesMarkdown } from "../../integrations/pi/team-context.js";
8
+ import { requireThat, schema } from "../../common/validation.js";
9
+ import type { CallAccess } from "../access.js";
10
+ import { markdownResult, result } from "../result.js";
11
+ import { teamParametersSchema, teamActionSchema, teamResultSchema, teamMessageParametersSchema, teamMessageActionSchema, teamMessageResultSchema } from "./contract.js";
12
+
13
+ export function teamTool(service: TeamService, agents: AgentService, access: Pick<CallAccess, "check">) {
14
+ const validators = new Map(teamActionSchema.anyOf.map(action => [action.properties.action.const, schema(action)]));
15
+ return defineTool({ name: "pbot_team", label: "团队",
16
+ description: "主会话管理固定成员团队。create 不启动模型;run 开展有限 roundRobin 轮流讨论或 parallel 并行轮次,默认 2 轮、整批 15 分钟,成员直接取得公共原文。团队运行时占用成员;pause 停止新安排和领取,允许在途工作收尾,之后可私人 send;需要中止成员用 pbot_agent stop。到期时子入口尽力中止未被人工接管的团队工作。close 保留成员进程和历史;换成员须关闭旧团队再创建。继续须明确 run 新批次,不重放旧请求。完成仍需业务验收。",
17
+ parameters: teamParametersSchema, outputSchema: teamResultSchema,
18
+ async execute(_id, args, signal, _update, ctx) {
19
+ access.check(); signal?.throwIfAborted();
20
+ const valid = validators.get(args.action);
21
+ requireThat(valid, "未知的团队 action:" + args.action);
22
+ requireThat(valid(args), "团队 " + args.action + " 参数无效:" + JSON.stringify(valid.errors));
23
+ const input = args as Static<typeof teamParametersSchema>, owner = agentOwner(ctx), views = agents.list(owner);
24
+ service.recover(owner);
25
+ let id = input.teamId!;
26
+ switch (input.action) {
27
+ case "create": id = service.create(owner, { name: input.name!, brief: input.brief!, members: input.members! }, views).teamId; break;
28
+ case "list": return markdownResult({ teams: service.list(owner, views) }, "团队列表");
29
+ case "read": {
30
+ const page = service.read(id, { kind: "coordinator", owner }, input);
31
+ return result(page, false, teamMessagesMarkdown(page));
32
+ }
33
+ case "run": service.run(id, owner, { instruction: input.instruction!, mode: input.mode!,
34
+ ...(input.rounds === undefined ? {} : { rounds: input.rounds }),
35
+ ...(input.maxConcurrent === undefined ? {} : { maxConcurrent: input.maxConcurrent }),
36
+ ...(input.timeoutSeconds === undefined ? {} : { timeoutSeconds: input.timeoutSeconds }) }, views); break;
37
+ case "pause": service.pause(id, owner); break;
38
+ case "close": service.close(id, owner, views); break;
39
+ }
40
+ return markdownResult(service.status(id, owner, views), "团队状态");
41
+ },
42
+ });
43
+ }
44
+ export function teamMessageTool(service: TeamService, access: Pick<CallAccess, "check">, actor: (ctx: ExtensionContext) => TeamActor) {
45
+ const validators = new Map(teamMessageActionSchema.anyOf.map(action => [action.properties.action.const, schema(action)]));
46
+ return defineTool({ name: "pbot_team_message", label: "团队消息",
47
+ description: "直接读取/发布团队公共原文。post 不唤醒模型、不增加轮次,所有成员可见,不提供私信或递归委派。成员只能在自己的团队工作中 post。read 用 afterSeq 分页;长消息用 seq/offset/limit 继续,nextOffset 非空表示该条正文不完整。保存成功不是模型已读或业务完成。",
48
+ parameters: teamMessageParametersSchema, outputSchema: teamMessageResultSchema,
49
+ async execute(id, args, signal, _update, ctx) {
50
+ access.check(); signal?.throwIfAborted();
51
+ const valid = validators.get(args.action);
52
+ requireThat(valid, "未知的团队消息 action:" + args.action);
53
+ requireThat(valid(args), "团队消息 " + args.action + " 参数无效:" + JSON.stringify(valid.errors));
54
+ const input = args as Static<typeof teamMessageParametersSchema>, sender = actor(ctx);
55
+ if (input.action === "read") {
56
+ const page = service.read(input.teamId, sender, input);
57
+ return result(page, false, teamMessagesMarkdown(page));
58
+ }
59
+ const message = service.post(input.teamId, sender, id, input.text!, input.replyTo);
60
+ return markdownResult({ teamId: input.teamId, seq: message.seq, status: "stored" }, "团队消息已保存");
61
+ },
62
+ });
63
+ }