personal-bot 0.2.4 → 0.2.5
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/CONTEXT.md +9 -7
- package/README.md +13 -6
- package/README.zh-CN.md +13 -6
- package/docs/README.md +3 -1
- package/docs/agent-collaboration.md +26 -12
- package/docs/architecture-guidelines.md +2 -2
- package/docs/capability-design.md +6 -4
- package/docs/dsh-implementation-plan.md +158 -0
- package/docs/dsh-support-proposal.md +314 -0
- package/docs/dsh.md +103 -0
- package/docs/file-storage.md +14 -9
- package/docs/history.md +29 -18
- package/docs/installation-design.md +3 -1
- package/docs/interaction-design.md +13 -0
- package/docs/release.md +3 -3
- package/docs/technical-design.md +29 -15
- package/docs/validation.md +38 -2
- package/lib/client.js +1071 -0
- package/lib/common/files.d.ts +2 -0
- package/lib/common/files.js +44 -0
- package/lib/common/json.d.ts +6 -0
- package/lib/common/json.js +16 -0
- package/lib/common/markdown.d.ts +5 -0
- package/lib/common/markdown.js +26 -0
- package/lib/common/paths.d.ts +1 -0
- package/lib/common/paths.js +2 -0
- package/lib/common/validation.d.ts +3 -0
- package/lib/common/validation.js +14 -0
- package/lib/common/version.d.ts +3 -0
- package/lib/common/version.js +13 -0
- package/lib/dsh.d.ts +4 -0
- package/lib/dsh.js +22 -0
- package/lib/extension/dsh/agent-events.d.ts +24 -0
- package/lib/extension/dsh/agent-events.js +242 -0
- package/lib/extension/dsh/agent-runtime.d.ts +49 -0
- package/lib/extension/dsh/agent-runtime.js +348 -0
- package/lib/extension/dsh/command.d.ts +4 -0
- package/lib/extension/dsh/command.js +83 -0
- package/lib/extension/dsh/files.d.ts +5 -0
- package/lib/extension/dsh/files.js +67 -0
- package/lib/extension/dsh/history.d.ts +15 -0
- package/lib/extension/dsh/history.js +88 -0
- package/lib/extension/dsh/management.d.ts +21 -0
- package/lib/extension/dsh/management.js +217 -0
- package/lib/extension/dsh/workspace.d.ts +48 -0
- package/lib/extension/dsh/workspace.js +290 -0
- package/lib/integrations/agent-resources.d.ts +1 -0
- package/lib/integrations/agent-resources.js +18 -0
- package/lib/integrations/dsh/dsh-agents.d.ts +30 -0
- package/lib/integrations/dsh/dsh-agents.js +104 -0
- package/lib/integrations/dsh/history-summary.d.ts +11 -0
- package/lib/integrations/dsh/history-summary.js +40 -0
- package/lib/integrations/dsh/history.d.ts +16 -0
- package/lib/integrations/dsh/history.js +156 -0
- package/lib/integrations/dsh/input.d.ts +21 -0
- package/lib/integrations/dsh/input.js +62 -0
- package/lib/integrations/dsh/messages.d.ts +36 -0
- package/lib/integrations/dsh/messages.js +11 -0
- package/lib/integrations/dsh/notifications.d.ts +8 -0
- package/lib/integrations/dsh/notifications.js +55 -0
- package/lib/integrations/dsh/resources.d.ts +10 -0
- package/lib/integrations/dsh/resources.js +41 -0
- package/lib/integrations/dsh/schema.d.ts +7 -0
- package/lib/integrations/dsh/schema.js +28 -0
- package/lib/integrations/dsh/session.d.ts +14 -0
- package/lib/integrations/dsh/session.js +59 -0
- package/lib/integrations/history-summary.d.ts +2 -0
- package/lib/integrations/history-summary.js +21 -0
- package/lib/integrations/process.d.ts +14 -0
- package/lib/integrations/process.js +48 -0
- package/lib/integrations/python-activation.d.ts +2 -0
- package/lib/integrations/python-activation.js +29 -0
- package/lib/integrations/team-context.d.ts +18 -0
- package/lib/integrations/team-context.js +57 -0
- package/lib/model/agent.d.ts +92 -0
- package/lib/model/agent.js +1 -0
- package/lib/model/history.d.ts +43 -0
- package/lib/model/history.js +1 -0
- package/lib/model/team.d.ts +85 -0
- package/lib/model/team.js +1 -0
- package/lib/services/agents/agent-notices.d.ts +13 -0
- package/lib/services/agents/agent-notices.js +25 -0
- package/lib/services/agents/agent-run.d.ts +39 -0
- package/lib/services/agents/agent-run.js +138 -0
- package/lib/services/agents/agent-service.d.ts +166 -0
- package/lib/services/agents/agent-service.js +325 -0
- package/lib/services/agents/team-service.d.ts +168 -0
- package/lib/services/agents/team-service.js +343 -0
- package/lib/services/history/history-service.d.ts +103 -0
- package/lib/services/history/history-service.js +269 -0
- package/lib/services/workspace/python-environment-service.d.ts +15 -0
- package/lib/services/workspace/python-environment-service.js +50 -0
- package/lib/services/workspace/workspace-files.d.ts +2 -0
- package/lib/services/workspace/workspace-files.js +17 -0
- package/lib/services/workspace/workspace-preparation.d.ts +2 -0
- package/lib/services/workspace/workspace-preparation.js +18 -0
- package/lib/storage/agent-files.d.ts +30 -0
- package/lib/storage/agent-files.js +166 -0
- package/lib/storage/atomic-json.d.ts +11 -0
- package/lib/storage/atomic-json.js +101 -0
- package/lib/storage/file-access.d.ts +2 -0
- package/lib/storage/file-access.js +26 -0
- package/lib/storage/history-files.d.ts +10 -0
- package/lib/storage/history-files.js +63 -0
- package/lib/storage/team-files.d.ts +16 -0
- package/lib/storage/team-files.js +111 -0
- package/lib/tools/agents/actions.d.ts +52 -0
- package/lib/tools/agents/actions.js +49 -0
- package/lib/tools/agents/contract.d.ts +163 -0
- package/lib/tools/agents/contract.js +56 -0
- package/lib/tools/agents/dsh-contract.d.ts +52 -0
- package/lib/tools/agents/dsh-contract.js +15 -0
- package/lib/tools/agents/dsh-tool.d.ts +5 -0
- package/lib/tools/agents/dsh-tool.js +38 -0
- package/lib/tools/history/actions.d.ts +70 -0
- package/lib/tools/history/actions.js +7 -0
- package/lib/tools/history/contract.d.ts +81 -0
- package/lib/tools/history/contract.js +32 -0
- package/lib/tools/history/dsh-tool.d.ts +4 -0
- package/lib/tools/history/dsh-tool.js +22 -0
- package/lib/tools/result.d.ts +18 -0
- package/lib/tools/result.js +10 -0
- package/lib/tools/teams/actions.d.ts +24 -0
- package/lib/tools/teams/actions.js +38 -0
- package/lib/tools/teams/contract.d.ts +232 -0
- package/lib/tools/teams/contract.js +67 -0
- package/lib/tools/teams/dsh-tool.d.ts +8 -0
- package/lib/tools/teams/dsh-tool.js +49 -0
- package/lib/ui/dsh/attachments.d.ts +5 -0
- package/lib/ui/dsh/attachments.js +39 -0
- package/lib/ui/dsh/elements.d.ts +17 -0
- package/lib/ui/dsh/elements.js +79 -0
- package/lib/ui/dsh/index.d.ts +4 -0
- package/lib/ui/dsh/index.js +13 -0
- package/lib/ui/dsh/management.d.ts +4 -0
- package/lib/ui/dsh/management.js +711 -0
- package/lib/ui/dsh/remote.d.ts +15 -0
- package/lib/ui/dsh/remote.js +11 -0
- package/lib/ui/dsh/styles.d.ts +1 -0
- package/lib/ui/dsh/styles.js +132 -0
- package/lib/ui/presentation.d.ts +3 -0
- package/lib/ui/presentation.js +12 -0
- package/npm-shrinkwrap.json +11140 -830
- package/package.json +94 -12
- package/python/README.md +6 -6
- package/resources/agents/coordinator.md +1 -1
- package/resources/dsh/cordis.patch.yml +3 -0
- package/resources/prompts/workspace.md +1 -1
- package/resources/references/compute.md +1 -1
- package/resources/references/task-conductor.md +6 -4
- package/resources/skills/provider-connections/SKILL.md +10 -0
- package/scripts/README.md +9 -1
- package/src/dsh.ts +22 -0
- package/src/extension/agent-events.ts +51 -46
- package/src/extension/agents-command.ts +48 -34
- package/src/extension/command.ts +42 -23
- package/src/extension/dsh/agent-events.ts +148 -0
- package/src/extension/dsh/agent-runtime.ts +251 -0
- package/src/extension/dsh/command.ts +78 -0
- package/src/extension/dsh/files.ts +61 -0
- package/src/extension/dsh/history.ts +69 -0
- package/src/extension/dsh/management.ts +108 -0
- package/src/extension/dsh/workspace.ts +206 -0
- package/src/extension/events.ts +6 -4
- package/src/extension/history.ts +15 -12
- package/src/extension/subagent-entry.ts +34 -63
- package/src/extension/teams-command.ts +53 -46
- package/src/integrations/{pi/agent-resources.ts → agent-resources.ts} +2 -2
- package/src/integrations/dsh/dsh-agents.ts +97 -0
- package/src/integrations/dsh/history-summary.ts +43 -0
- package/src/integrations/dsh/history.ts +76 -0
- package/src/integrations/dsh/input.ts +66 -0
- package/src/integrations/dsh/messages.ts +22 -0
- package/src/integrations/dsh/notifications.ts +46 -0
- package/src/integrations/dsh/resources.ts +44 -0
- package/src/integrations/dsh/schema.ts +26 -0
- package/src/integrations/dsh/session.ts +67 -0
- package/src/integrations/history-summary.ts +20 -0
- package/src/integrations/pi/history.ts +4 -14
- package/src/integrations/pi/pi-agents.ts +17 -6
- package/src/integrations/pi/team-context.ts +6 -58
- package/src/integrations/team-context.ts +60 -0
- package/src/model/agent.ts +2 -0
- package/src/model/history.ts +3 -1
- package/src/pbot.ts +1 -1
- package/src/services/agents/agent-notices.ts +28 -0
- package/src/services/agents/agent-run.ts +114 -0
- package/src/services/agents/agent-service.ts +127 -64
- package/src/services/history/history-service.ts +120 -59
- package/src/storage/agent-files.ts +17 -4
- package/src/storage/history-files.ts +30 -5
- package/src/tools/agents/actions.ts +47 -0
- package/src/tools/agents/contract.ts +8 -5
- package/src/tools/agents/dsh-contract.ts +16 -0
- package/src/tools/agents/dsh-tool.ts +41 -0
- package/src/tools/agents/tool.ts +7 -43
- package/src/tools/history/actions.ts +9 -0
- package/src/tools/history/contract.ts +34 -0
- package/src/tools/history/dsh-tool.ts +22 -0
- package/src/tools/history/tool.ts +7 -30
- package/src/tools/teams/actions.ts +38 -0
- package/src/tools/teams/contract.ts +7 -3
- package/src/tools/teams/dsh-tool.ts +55 -0
- package/src/tools/teams/tool.ts +6 -31
- package/src/ui/agent-display.ts +7 -4
- package/src/ui/dsh/attachments.ts +42 -0
- package/src/ui/dsh/elements.ts +50 -0
- package/src/ui/dsh/index.ts +18 -0
- package/src/ui/dsh/management.ts +412 -0
- package/src/ui/dsh/remote.ts +20 -0
- package/src/ui/dsh/styles.ts +132 -0
- package/src/ui/pi/menu.ts +37 -0
- package/src/ui/pi/reader.ts +46 -0
- package/src/ui/presentation.ts +12 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import type { HistoryExchange, HistoryLocation, HistoryNode, HistoryRecord, HistorySession, HistorySource, HistorySummarizer } from "../../model/history.js";
|
|
2
|
+
import type { HistoryExchange, HistoryHost, HistoryLocation, HistoryNode, HistoryRecord, HistorySession, HistorySource, HistorySummarizer } from "../../model/history.js";
|
|
3
3
|
import type { HistoryFiles } from "../../storage/history-files.js";
|
|
4
4
|
import { requireThat } from "../../common/validation.js";
|
|
5
5
|
|
|
6
6
|
/** Shared prefixes reuse nodes; only consecutive exchanges on one native branch can merge. */
|
|
7
|
-
export function indexHistory(session: HistorySession, previous?: HistoryRecord): HistoryRecord {
|
|
8
|
-
const id = previous?.id ?? randomUUID(), nodes = new Map<string, HistoryNode>();
|
|
7
|
+
export function indexHistory(session: HistorySession, previous?: HistoryRecord, host: HistoryHost = "pi"): HistoryRecord {
|
|
8
|
+
const id = previous?.id ?? (host === "dsh" ? "dsh-" : "") + randomUUID(), nodes = new Map<string, HistoryNode>();
|
|
9
9
|
const old = new Map(previous?.nodes.map(node => [node.id, node])), changed = new Set<string>();
|
|
10
10
|
const add = (value: HistoryExchange, level: number, children: string[] = []): HistoryNode => {
|
|
11
11
|
const key = `${id}:${level}:${value.entryIds.at(-1)}`, found = nodes.get(key);
|
|
@@ -40,106 +40,167 @@ export function indexHistory(session: HistorySession, previous?: HistoryRecord):
|
|
|
40
40
|
return { id, sessionId: session.sessionId, name: session.name, nodes: [...nodes.values()] };
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
export interface HistoryQuery { query?: string; sessionId?: string; from?: string; to?: string; offset?: number; limit?: number }
|
|
43
|
+
export interface HistoryQuery { host?: HistoryHost; query?: string; sessionId?: string; from?: string; to?: string; offset?: number; limit?: number }
|
|
44
|
+
type LocatedRecord = { host: HistoryHost; record: HistoryRecord };
|
|
44
45
|
const page = <T>(values: T[], offset: number, limit: number) => ({ values: values.slice(offset, offset + limit),
|
|
45
46
|
nextOffset: offset + limit < values.length ? offset + limit : null });
|
|
46
|
-
const brief = (record:
|
|
47
|
+
const brief = ({ host, record }: LocatedRecord, node: HistoryNode) => ({ host, id: node.id, kind: node.kind, sessionId: record.sessionId,
|
|
47
48
|
name: record.name, preview: node.text.replace(/\s+/g, " ").slice(0, 100), from: node.from, to: node.to, ready: node.ready, completed: node.completed });
|
|
49
|
+
const rawSource = (record?: HistoryRecord) => record && JSON.stringify({ name: record.name,
|
|
50
|
+
exchanges: record.nodes.filter(node => node.kind === "exchange").map(node => [node.id, node.entryIds, node.text, node.from, node.to, node.completed]) });
|
|
51
|
+
const summarySource = (node: HistoryNode) => [node.id, node.entryIds, node.children, node.text, node.from, node.to, node.completed, node.ready];
|
|
52
|
+
|
|
53
|
+
/** Validate the exact inputs again under the source lock; another writer's ready summary wins. */
|
|
54
|
+
export function completeHistorySummary(record: HistoryRecord, id: string, children: HistoryNode[], text: string) {
|
|
55
|
+
const node = record.nodes.find(value => value.id === id);
|
|
56
|
+
if (!node || node.kind !== "summary" || node.ready || JSON.stringify(node.children) !== JSON.stringify(children.map(child => child.id))) return false;
|
|
57
|
+
const current = node.children.map(id => record.nodes.find(value => value.id === id));
|
|
58
|
+
if (current.some(value => !value) || JSON.stringify(current.map(value => summarySource(value!))) !== JSON.stringify(children.map(summarySource))) return false;
|
|
59
|
+
requireThat(text.trim(), "历史摘要为空。");
|
|
60
|
+
node.text = text.trim(); node.ready = true; return true;
|
|
61
|
+
}
|
|
48
62
|
|
|
49
63
|
export class HistoryService {
|
|
50
|
-
#records:
|
|
64
|
+
#records: LocatedRecord[] = [];
|
|
51
65
|
#closed = false;
|
|
66
|
+
#loaded = false;
|
|
52
67
|
#refresh: Promise<void> | undefined;
|
|
53
68
|
#work: Promise<void> | undefined;
|
|
54
|
-
|
|
69
|
+
#sourceWarnings: string[] = [];
|
|
70
|
+
#summaryError: string | undefined;
|
|
55
71
|
constructor(readonly root: string, private readonly files: HistoryFiles, private readonly source: HistorySource,
|
|
56
|
-
private previousFiles?: HistoryFiles) {
|
|
72
|
+
private previousFiles?: HistoryFiles, private readonly otherFiles?: HistoryFiles) {
|
|
73
|
+
requireThat(source.host === files.host && (!otherFiles || otherFiles.host !== files.host), "历史来源与归档目录不匹配。");
|
|
74
|
+
requireThat(files.host === "pi" || !previousFiles, "dsh 不导入 Pi 旧归档。");
|
|
75
|
+
}
|
|
57
76
|
check() { requireThat(!this.#closed, "历史服务已关闭。"); }
|
|
77
|
+
get warnings() { return [...this.#sourceWarnings, ...(this.#summaryError ? [this.files.host + " 摘要:" + this.#summaryError] : [])]; }
|
|
78
|
+
get status() { return { loaded: this.#loaded, organizing: Boolean(this.#work), pending: this.#loaded ? this.#records.filter(value => value.host === this.files.host)
|
|
79
|
+
.reduce((total, value) => total + value.record.nodes.filter(node => node.kind === "summary" && !node.ready).length, 0) : null, warnings: this.warnings }; }
|
|
58
80
|
refresh(location: HistoryLocation, signal?: AbortSignal): Promise<void> {
|
|
59
81
|
this.check();
|
|
60
|
-
|
|
61
|
-
this.#refresh
|
|
62
|
-
|
|
82
|
+
// Each caller keeps its own cancellation. A closed page cannot cancel a server-owned refresh.
|
|
83
|
+
const work = (this.#refresh ?? Promise.resolve()).catch(() => {}).then(() => this.load(location, signal));
|
|
84
|
+
this.#refresh = work;
|
|
85
|
+
return work.finally(() => { if (this.#refresh === work) this.#refresh = undefined; });
|
|
63
86
|
}
|
|
64
87
|
private async load(location: HistoryLocation, signal?: AbortSignal) {
|
|
65
|
-
const previous = this.files.list();
|
|
66
|
-
for (const value of previous) if ("file" in value) {
|
|
67
|
-
const { id, sessionId, name, nodes } = value;
|
|
68
|
-
this.files.save({ id, sessionId, name, nodes });
|
|
69
|
-
}
|
|
70
|
-
for (const { id, sessionId, name, nodes } of this.previousFiles?.list(this.root) ?? []) {
|
|
71
|
-
if (previous.some(record => record.sessionId === sessionId)) continue;
|
|
72
|
-
const value = { id, sessionId, name, nodes };
|
|
73
|
-
this.files.save(value); previous.push(value);
|
|
74
|
-
}
|
|
75
|
-
this.previousFiles = undefined;
|
|
76
|
-
const { sessions, warnings } = await this.source.scan(location, signal);
|
|
77
88
|
this.check(); signal?.throwIfAborted();
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
89
|
+
const warnings: string[] = [], records: LocatedRecord[] = [];
|
|
90
|
+
const warning = (host: HistoryHost, error: unknown) => { this.check(); signal?.throwIfAborted(); warnings.push(host + ": " + String(error)); };
|
|
91
|
+
let known: HistoryRecord[] = [];
|
|
92
|
+
try {
|
|
93
|
+
const before = this.files.list(); known = before;
|
|
94
|
+
let imports: HistoryRecord[] = [], importedPrevious = !this.previousFiles;
|
|
95
|
+
if (this.previousFiles) {
|
|
96
|
+
try { imports = this.previousFiles.list(this.root); importedPrevious = true; } catch (error) { warning("pi", error); }
|
|
97
|
+
}
|
|
98
|
+
let sessions: HistorySession[] = [];
|
|
99
|
+
try {
|
|
100
|
+
const scanned = await this.source.scan(location, signal); sessions = scanned.sessions;
|
|
101
|
+
warnings.push(...scanned.warnings.map(value => this.files.host + ": " + value));
|
|
102
|
+
} catch (error) { warning(this.files.host, error); }
|
|
103
|
+
this.check(); signal?.throwIfAborted();
|
|
104
|
+
if (sessions.length || imports.length || (this.files.host === "pi" && before.some(value => "file" in value))) {
|
|
105
|
+
known = this.files.control(() => {
|
|
106
|
+
this.check(); signal?.throwIfAborted();
|
|
107
|
+
const latest = this.files.list(), current = new Map(latest.map(record => [record.sessionId, record])), imported = new Set<string>();
|
|
108
|
+
if (this.files.host === "pi") {
|
|
109
|
+
for (const value of latest) if ("file" in value) {
|
|
110
|
+
const { id, sessionId, name, nodes } = value, clean = { id, sessionId, name, nodes };
|
|
111
|
+
this.files.save(clean); current.set(sessionId, clean);
|
|
112
|
+
}
|
|
113
|
+
for (const { id, sessionId, name, nodes } of imports) if (!current.has(sessionId)) {
|
|
114
|
+
const value = { id, sessionId, name, nodes }; this.files.save(value); current.set(sessionId, value); imported.add(sessionId);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
for (const session of sessions) {
|
|
118
|
+
const previous = current.get(session.sessionId), captured = before.find(record => record.sessionId === session.sessionId);
|
|
119
|
+
// A concurrent source update must not be overwritten by this older scan. Summary-only updates remain reusable.
|
|
120
|
+
if (captured && rawSource(captured) !== rawSource(previous)) continue;
|
|
121
|
+
if (!captured && previous && !imported.has(session.sessionId)) continue;
|
|
122
|
+
const value = indexHistory(session, previous, this.files.host);
|
|
123
|
+
if (JSON.stringify(previous) !== JSON.stringify(value)) this.files.save(value);
|
|
124
|
+
current.set(session.sessionId, value);
|
|
125
|
+
}
|
|
126
|
+
return [...current.values()];
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
if (importedPrevious) this.previousFiles = undefined;
|
|
130
|
+
} catch (error) { warning(this.files.host, error); }
|
|
131
|
+
records.push(...known.map(record => ({ host: this.files.host, record })));
|
|
132
|
+
if (this.otherFiles) {
|
|
133
|
+
try { records.push(...this.otherFiles.list().map(record => ({ host: this.otherFiles!.host, record }))); }
|
|
134
|
+
catch (error) { warning(this.otherFiles.host, error); }
|
|
83
135
|
}
|
|
84
|
-
this.#records =
|
|
85
|
-
this.warnings = warnings;
|
|
136
|
+
this.check(); signal?.throwIfAborted(); this.#records = records; this.#sourceWarnings = warnings; this.#loaded = true;
|
|
86
137
|
}
|
|
87
|
-
search({ query = "", sessionId, from, to, offset = 0, limit = 20 }: HistoryQuery = {}) {
|
|
138
|
+
search({ host, query = "", sessionId, from, to, offset = 0, limit = 20 }: HistoryQuery = {}) {
|
|
88
139
|
this.check();
|
|
89
140
|
const needle = query.trim().toLowerCase(), after = from === undefined ? -Infinity : Date.parse(from), before = to === undefined ? Infinity : Date.parse(to);
|
|
90
141
|
requireThat(!Number.isNaN(after) && !Number.isNaN(before) && after <= before, "时间范围无效,请使用 ISO 日期或时间。");
|
|
91
|
-
const matches = this.#records.filter(
|
|
142
|
+
const matches = this.#records.filter(value => (!host || value.host === host) && (!sessionId || value.record.sessionId === sessionId)).flatMap(value => value.record.nodes
|
|
92
143
|
.filter(node => (needle ? node.kind !== "session" && node.ready && node.text.toLowerCase().includes(needle) : node.kind === "session") &&
|
|
93
144
|
Date.parse(node.to) >= after && Date.parse(node.from) <= before)
|
|
94
145
|
.map(node => {
|
|
95
146
|
const start = Math.max(0, node.text.toLowerCase().indexOf(needle) - 80);
|
|
96
|
-
return { ...brief(
|
|
147
|
+
return { ...brief(value, node), parents: node.parents, snippet: node.text.slice(start, start + 400) };
|
|
97
148
|
})).sort((a, b) => b.to.localeCompare(a.to) || a.id.localeCompare(b.id));
|
|
98
149
|
const result = page(matches, offset, limit);
|
|
99
150
|
return { matches: result.values, total: matches.length, nextOffset: result.nextOffset, warnings: this.warnings };
|
|
100
151
|
}
|
|
101
|
-
private find(id: string) {
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
requireThat(
|
|
105
|
-
|
|
152
|
+
private find(id: string, host?: HistoryHost) {
|
|
153
|
+
const values = this.#records.filter(value => (!host || value.host === host) && value.record.nodes.some(node => node.id === id));
|
|
154
|
+
requireThat(values.length, "历史节点不存在,请重新搜索。");
|
|
155
|
+
requireThat(values.length === 1, "历史节点来源不明确,请指定 host。");
|
|
156
|
+
const value = values[0]!, node = value.record.nodes.find(node => node.id === id)!;
|
|
157
|
+
return { value, node };
|
|
106
158
|
}
|
|
107
|
-
read(id: string, offset = 0, limit = 12000, childrenOffset = 0) {
|
|
159
|
+
read(id: string, offset = 0, limit = 12000, childrenOffset = 0, host?: HistoryHost) {
|
|
108
160
|
this.check();
|
|
109
|
-
const {
|
|
161
|
+
const { value, node } = this.find(id, host), lookup = new Map(value.record.nodes.map(node => [node.id, node]));
|
|
110
162
|
requireThat(offset <= node.text.length, "读取位置超出正文。");
|
|
111
163
|
if (offset && /[\uDC00-\uDFFF]/.test(node.text[offset] ?? "")) offset--;
|
|
112
164
|
let end = Math.min(node.text.length, offset + limit);
|
|
113
165
|
if (end < node.text.length && /[\uD800-\uDBFF]/.test(node.text[end - 1] ?? "")) end += end - offset === 1 ? 1 : -1;
|
|
114
166
|
const children = page(node.children, childrenOffset, 20);
|
|
115
|
-
return { ...brief(
|
|
167
|
+
return { ...brief(value, node), text: node.text.slice(offset, end), totalLength: node.text.length,
|
|
116
168
|
nextOffset: end < node.text.length ? end : null,
|
|
117
|
-
parents: node.parents.map(id => brief(
|
|
118
|
-
nextChildrenOffset: children.nextOffset, source: { sessionId: record.sessionId, entryIds: node.entryIds }, warnings: this.warnings };
|
|
169
|
+
parents: node.parents.map(id => brief(value, lookup.get(id)!)), children: children.values.map(id => brief(value, lookup.get(id)!)),
|
|
170
|
+
nextChildrenOffset: children.nextOffset, source: { host: value.host, sessionId: value.record.sessionId, entryIds: node.entryIds }, warnings: this.warnings };
|
|
119
171
|
}
|
|
120
|
-
/**
|
|
172
|
+
/** Only this source is organized. Model calls run outside the write lock. */
|
|
121
173
|
organize(summarize: HistorySummarizer, signal: AbortSignal): Promise<void> {
|
|
122
174
|
this.check();
|
|
123
175
|
const run = async () => {
|
|
124
|
-
for (
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const
|
|
130
|
-
if (children.
|
|
131
|
-
const text = await summarize(children.map(child => `[${child.from} — ${child.to}${child.completed ? "" : ";未完成"}]\n${child.text}`).join("\n\n"), signal);
|
|
132
|
-
this.check(); signal.throwIfAborted();
|
|
133
|
-
// A query may have refreshed the source while this model request was running.
|
|
134
|
-
if (!this.#records.includes(record)) return;
|
|
135
|
-
requireThat(text.trim(), "历史摘要为空。");
|
|
136
|
-
node.text = text.trim(); node.ready = true; this.files.save(record);
|
|
176
|
+
for (;;) {
|
|
177
|
+
this.check(); signal.throwIfAborted();
|
|
178
|
+
let pending: { record: HistoryRecord; node: HistoryNode; children: HistoryNode[] } | undefined;
|
|
179
|
+
for (const record of this.files.list()) {
|
|
180
|
+
const lookup = new Map(record.nodes.map(node => [node.id, node]));
|
|
181
|
+
const node = [...record.nodes].sort((a, b) => a.level - b.level).find(node => node.kind === "summary" && !node.ready && node.children.every(id => lookup.get(id)!.ready));
|
|
182
|
+
if (node) { pending = { record, node, children: node.children.map(id => lookup.get(id)!) }; break; }
|
|
137
183
|
}
|
|
184
|
+
if (!pending) return;
|
|
185
|
+
const { record, node, children } = pending;
|
|
186
|
+
const text = await summarize(children.map(child => `[${child.from} — ${child.to}${child.completed ? "" : ";未完成"}]\n${child.text}`).join("\n\n"), signal);
|
|
187
|
+
this.check(); signal.throwIfAborted();
|
|
188
|
+
const committed = this.files.control(() => {
|
|
189
|
+
this.check(); signal.throwIfAborted();
|
|
190
|
+
const current = this.files.list().find(value => value.id === record.id);
|
|
191
|
+
if (!current || !completeHistorySummary(current, node.id, children, text)) return;
|
|
192
|
+
this.files.save(current); return current;
|
|
193
|
+
});
|
|
194
|
+
if (!committed) return;
|
|
195
|
+
this.#records = this.#records.map(value => value.host === this.files.host && value.record.id === committed.id ? { host: value.host, record: committed } : value);
|
|
138
196
|
}
|
|
139
197
|
};
|
|
140
|
-
const work = (this.#work ?? Promise.resolve()).catch(() => {}).then(run);
|
|
198
|
+
const work = (this.#work ?? Promise.resolve()).catch(() => {}).then(run).then(() => { this.#summaryError = undefined; }).catch(error => {
|
|
199
|
+
if (!signal.aborted && !this.#closed) this.#summaryError = String(error);
|
|
200
|
+
throw error;
|
|
201
|
+
});
|
|
141
202
|
this.#work = work;
|
|
142
203
|
return work.finally(() => { if (this.#work === work) this.#work = undefined; });
|
|
143
204
|
}
|
|
144
|
-
async close() { this.#closed = true; await Promise.allSettled([this.#work, this.#refresh]); }
|
|
205
|
+
async close() { this.#closed = true; await Promise.allSettled([this.#work, this.#refresh]); this.#records = []; this.#loaded = false; }
|
|
145
206
|
}
|
|
@@ -5,7 +5,7 @@ import { canonical, object } from "../common/json.js";
|
|
|
5
5
|
import { requireThat } from "../common/validation.js";
|
|
6
6
|
import { readJsonFile, writeJsonFile } from "./atomic-json.js";
|
|
7
7
|
import { fileId, privateDirectory } from "./file-access.js";
|
|
8
|
-
import type { AgentDescriptor, AgentLaunch, AgentMessage, AgentStatus, AgentCycle, AgentSessionRef } from "../model/agent.js";
|
|
8
|
+
import type { AgentDescriptor, AgentLaunch, AgentMessage, AgentStatus, AgentCycle, AgentSessionRef, AgentSessionReference } from "../model/agent.js";
|
|
9
9
|
|
|
10
10
|
const text = (v: unknown): v is string => typeof v === "string" && v.length > 0;
|
|
11
11
|
const number = (v: unknown): v is number => typeof v === "number" && Number.isFinite(v);
|
|
@@ -18,6 +18,11 @@ export function sessionRef(v: unknown): AgentSessionRef {
|
|
|
18
18
|
requireThat(object(v) && (v.file === null || (text(v.file) && isAbsolute(v.file) && !v.file.includes("\0"))), "子会话引用无效。");
|
|
19
19
|
return v as unknown as AgentSessionRef;
|
|
20
20
|
}
|
|
21
|
+
function dshSessionRef(v: unknown): Extract<AgentSessionReference, { host: "dsh" }> {
|
|
22
|
+
requireThat(object(v) && v.host === "dsh" && text(v.sessionId), "dsh 原生会话引用无效。");
|
|
23
|
+
fileId(v.sessionId as string);
|
|
24
|
+
return v as unknown as Extract<AgentSessionReference, { host: "dsh" }>;
|
|
25
|
+
}
|
|
21
26
|
export function launch(v: unknown): AgentLaunch {
|
|
22
27
|
requireThat(object(v) && text(v.generation) && text(v.name) && Number.isSafeInteger(v.sequence) && typeof v.stop === "boolean" && number(v.startedAt), "子会话启动状态无效。");
|
|
23
28
|
return v as unknown as AgentLaunch;
|
|
@@ -41,7 +46,7 @@ export function status(v: unknown): AgentStatus {
|
|
|
41
46
|
}
|
|
42
47
|
export class AgentFiles {
|
|
43
48
|
#failure: unknown;
|
|
44
|
-
constructor(readonly root: string) {}
|
|
49
|
+
constructor(readonly root: string, readonly host: AgentSessionReference["host"] = "pi") {}
|
|
45
50
|
check() { if (this.#failure) throw this.#failure; }
|
|
46
51
|
fail(error: unknown) { this.#failure = error; }
|
|
47
52
|
directory(id: string) { return join(this.root, fileId(id)); }
|
|
@@ -63,7 +68,8 @@ export class AgentFiles {
|
|
|
63
68
|
for (const dir of ["inbox", "completions"]) privateDirectory(this.directory(value.agentId), dir);
|
|
64
69
|
this.write(value.agentId, "descriptor.json", value, descriptor, true);
|
|
65
70
|
this.write(value.agentId, "launch.json", run, launch, true);
|
|
66
|
-
this.write(value.agentId, "session-ref.json", { file: null }, sessionRef, true);
|
|
71
|
+
if (this.host === "pi") this.write(value.agentId, "session-ref.json", { file: null }, sessionRef, true);
|
|
72
|
+
else this.write(value.agentId, "native-session.json", { host: "dsh", sessionId: value.agentId }, dshSessionRef, true);
|
|
67
73
|
}
|
|
68
74
|
list() {
|
|
69
75
|
if (!existsSync(this.root)) return [];
|
|
@@ -79,7 +85,14 @@ export class AgentFiles {
|
|
|
79
85
|
return readdirSync(join(this.directory(id), "completions")).filter(n => n.endsWith(".json"))
|
|
80
86
|
.map(n => this.read(id, "completions/" + n, cycle)).sort((a, b) => (a.finishedAt ?? 0) - (b.finishedAt ?? 0));
|
|
81
87
|
}
|
|
88
|
+
reference(id: string): AgentSessionReference {
|
|
89
|
+
if (this.host === "pi") return { host: "pi", file: this.session(id) };
|
|
90
|
+
const ref = this.read(id, "native-session.json", dshSessionRef);
|
|
91
|
+
requireThat(ref.sessionId === id, "dsh 原生会话身份不匹配。");
|
|
92
|
+
return ref;
|
|
93
|
+
}
|
|
82
94
|
session(id: string): string | null {
|
|
95
|
+
requireThat(this.host === "pi", "该宿主不使用 Pi 会话文件引用。");
|
|
83
96
|
const ref = this.optional(id, "session-ref.json", sessionRef);
|
|
84
97
|
requireThat(ref, "缺少原生会话引用;不导入旧记录,请重新 start。");
|
|
85
98
|
return ref.file;
|
|
@@ -98,7 +111,7 @@ export class AgentFiles {
|
|
|
98
111
|
try { mkdirSync(path, { mode: 0o700 }); }
|
|
99
112
|
catch (error) {
|
|
100
113
|
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
101
|
-
throw Object.assign(new Error("子会话控制正在进行;稍后重试。崩溃遗留的 .control
|
|
114
|
+
throw Object.assign(new Error("子会话控制正在进行;稍后重试。崩溃遗留的 .control 需停止相关执行实例后手动移除。"), { code: "PBOT_CONTROL_BUSY" });
|
|
102
115
|
}
|
|
103
116
|
try { return work(); } finally { rmSync(path, { recursive: true }); }
|
|
104
117
|
}
|
|
@@ -1,24 +1,49 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readdirSync } from "node:fs";
|
|
2
|
-
import
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, rmdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import type { HistoryHost, HistoryRecord } from "../model/history.js";
|
|
3
4
|
import { object } from "../common/json.js";
|
|
4
5
|
import { requireThat } from "../common/validation.js";
|
|
5
6
|
import { readJsonFile, writeJsonFile } from "./atomic-json.js";
|
|
6
7
|
import { fileId } from "./file-access.js";
|
|
7
8
|
|
|
9
|
+
const strings = (value: unknown): value is string[] => Array.isArray(value) && value.every(item => typeof item === "string");
|
|
8
10
|
function record(value: unknown): HistoryRecord {
|
|
9
11
|
requireThat(object(value) && typeof value.id === "string" && typeof value.sessionId === "string" &&
|
|
10
12
|
typeof value.name === "string" && Array.isArray(value.nodes), "历史索引无效。");
|
|
11
|
-
|
|
13
|
+
for (const node of value.nodes) requireThat(object(node) && typeof node.id === "string" &&
|
|
14
|
+
["session", "exchange", "summary"].includes(String(node.kind)) && Number.isInteger(node.level) &&
|
|
15
|
+
strings(node.entryIds) && strings(node.parents) && strings(node.children) && typeof node.text === "string" &&
|
|
16
|
+
typeof node.from === "string" && typeof node.to === "string" && typeof node.ready === "boolean" && typeof node.completed === "boolean", "历史节点无效。");
|
|
17
|
+
const result = value as unknown as HistoryRecord, ids = new Set(result.nodes.map(node => node.id));
|
|
18
|
+
requireThat(ids.size === result.nodes.length && result.nodes.some(node => node.id === result.id && node.kind === "session") &&
|
|
19
|
+
result.nodes.every(node => [...node.parents, ...node.children].every(id => ids.has(id))), "历史节点关系无效。");
|
|
20
|
+
return result;
|
|
12
21
|
}
|
|
13
22
|
export class HistoryFiles {
|
|
14
|
-
constructor(readonly directory: string) {}
|
|
23
|
+
constructor(readonly directory: string, readonly host: HistoryHost = "pi") {}
|
|
15
24
|
list(workspace?: string) {
|
|
16
25
|
if (!existsSync(this.directory)) return [];
|
|
17
26
|
return readdirSync(this.directory).filter(name => name.endsWith(".json"))
|
|
18
|
-
.map(name => readJsonFile(this.directory, name,
|
|
27
|
+
.map(name => readJsonFile(this.directory, name, value => {
|
|
28
|
+
const result = record(value);
|
|
29
|
+
requireThat(this.host !== "dsh" || result.id.startsWith("dsh-"), "dsh 历史根 ID 缺少来源前缀。");
|
|
30
|
+
return result;
|
|
31
|
+
}))
|
|
19
32
|
.filter(value => workspace === undefined || ("workspace" in value && value.workspace === workspace));
|
|
20
33
|
}
|
|
34
|
+
/** The callback is synchronous: no SDK reads or model work can hold this source's write lock. */
|
|
35
|
+
control<T>(operation: () => T): T {
|
|
36
|
+
mkdirSync(this.directory, { recursive: true, mode: 0o700 });
|
|
37
|
+
const lock = join(this.directory, ".write");
|
|
38
|
+
try { mkdirSync(lock, { mode: 0o700 }); }
|
|
39
|
+
catch (error) {
|
|
40
|
+
if ((error as NodeJS.ErrnoException).code === "EEXIST") throw Object.assign(new Error("历史来源写锁被占用:" + this.host), { code: "PBOT_HISTORY_BUSY" });
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
try { return operation(); } finally { rmdirSync(lock); }
|
|
44
|
+
}
|
|
21
45
|
save(value: HistoryRecord) {
|
|
46
|
+
requireThat(this.host !== "dsh" || value.id.startsWith("dsh-"), "dsh 历史根 ID 缺少来源前缀。");
|
|
22
47
|
mkdirSync(this.directory, { recursive: true, mode: 0o700 });
|
|
23
48
|
writeJsonFile(this.directory, fileId(value.id) + ".json", value, record);
|
|
24
49
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Static } from "typebox";
|
|
2
|
+
import type { AgentService } from "../../services/agents/agent-service.js";
|
|
3
|
+
import type { AgentOptions, AgentOwner, AgentView } from "../../model/agent.js";
|
|
4
|
+
import { markdownResult } from "../result.js";
|
|
5
|
+
import type { agentParametersSchema, agentViewSchema } from "./contract.js";
|
|
6
|
+
|
|
7
|
+
export type AgentInput = Omit<Static<typeof agentParametersSchema>, "thinking"> & { thinking?: string };
|
|
8
|
+
type AgentOutput = Static<typeof agentViewSchema>;
|
|
9
|
+
function agentOutput(view: AgentView & Partial<AgentOutput>): AgentOutput {
|
|
10
|
+
const c = view.cycle;
|
|
11
|
+
return {
|
|
12
|
+
agentId: view.agentId, name: view.name, cwd: view.cwd, createdAt: view.createdAt,
|
|
13
|
+
owned: view.owned, state: view.state, activity: view.activity,
|
|
14
|
+
...(c ? { cycle: { cycleId: c.cycleId, startedAt: c.startedAt, state: c.state, requestIds: c.requestIds, source: c.source,
|
|
15
|
+
...(c.finishedAt === undefined ? {} : { finishedAt: c.finishedAt }), ...(c.error === undefined ? {} : { error: c.error }) } } : {}),
|
|
16
|
+
...(view.error === undefined ? {} : { error: view.error }),
|
|
17
|
+
...(view.requestId === undefined ? {} : { requestId: view.requestId }),
|
|
18
|
+
...(view.messageStatus === undefined ? {} : { messageStatus: view.messageStatus }),
|
|
19
|
+
...(view.cycles === undefined ? {} : { cycles: view.cycles }),
|
|
20
|
+
...(view.report === undefined ? {} : { report: view.report }),
|
|
21
|
+
...(view.totalLength === undefined ? {} : { totalLength: view.totalLength }),
|
|
22
|
+
...(view.nextOffset === undefined ? {} : { nextOffset: view.nextOffset }),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
export async function executeAgent(service: AgentService, input: AgentInput, owner: AgentOwner, defaults: AgentOptions = {}) {
|
|
28
|
+
let value: AgentView & Partial<AgentOutput>;
|
|
29
|
+
switch (input.action) {
|
|
30
|
+
case "start": {
|
|
31
|
+
const { action: _action, ...supplied } = input;
|
|
32
|
+
value = await service.start({ ...defaults, ...supplied } as AgentOptions, owner); break;
|
|
33
|
+
}
|
|
34
|
+
case "list": return markdownResult({ agents: service.list(owner).map(agentOutput) }, "子会话列表");
|
|
35
|
+
case "send": value = service.send(input.agentId!, input.message!, input.delivery ?? "steer", owner); break;
|
|
36
|
+
case "rename": value = service.rename(input.agentId!, input.name!, owner); break;
|
|
37
|
+
case "resume": value = await service.resume(input.agentId!, input.message!, owner); break;
|
|
38
|
+
case "stop": value = service.stop(input.agentId!, owner); break;
|
|
39
|
+
case "status": value = service.status(input.agentId!, owner); break;
|
|
40
|
+
case "read": {
|
|
41
|
+
const page = await service.read(input.agentId!, owner, input.cycleId, input.offset, input.limit);
|
|
42
|
+
const data = agentOutput(page), output = markdownResult(data, "子会话报告");
|
|
43
|
+
return { ...output, details: { ...data, ...(page.cycle && page.cycle.state !== "running" ? { agentReadKeys: [page.agentId + ":" + page.cycle.cycleId] } : {}) } };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return markdownResult(agentOutput(value), "子会话状态");
|
|
47
|
+
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
|
+
|
|
3
|
+
export const agentDescription = "独立专家会话,不继承父聊天历史。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 个存活子会话,空闲也计入。";
|
|
4
|
+
|
|
2
5
|
const id = Type.String({ minLength: 1, maxLength: 128 });
|
|
3
6
|
const message = Type.String({ minLength: 1, maxLength: 24000, description: "本次目标、交付要求、资料依据与约束。" });
|
|
4
7
|
const name = Type.String({ minLength: 1, maxLength: 64, pattern: "^[^\\r\\n\\x00-\\x1f]+$" });
|
|
5
|
-
const
|
|
8
|
+
export const agentStartOptions = {
|
|
6
9
|
name: Type.Optional(name), message: Type.Optional(message),
|
|
7
10
|
files: Type.Optional(Type.Array(Type.String({ minLength: 1 }), { maxItems: 32, description: "随初始消息读取的文件;相对路径基于原工作区根目录。" })),
|
|
8
11
|
prompts: Type.Optional(Type.Array(message, { maxItems: 32, description: "与 message 按顺序合并为同一初始消息,不是多个执行轮次。" })),
|
|
@@ -24,12 +27,12 @@ const page = {
|
|
|
24
27
|
const delivery = Type.Optional(Type.Union([Type.Literal("steer"), Type.Literal("followUp")], { description: "send 默认 steer 补充当前工作;followUp 排到当前工作之后。" }));
|
|
25
28
|
export const agentParametersSchema = Type.Object({
|
|
26
29
|
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
|
|
30
|
+
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
31
|
}),
|
|
29
|
-
agentId: Type.Optional(id), ...
|
|
32
|
+
agentId: Type.Optional(id), ...agentStartOptions, ...page, delivery,
|
|
30
33
|
}, { additionalProperties: false });
|
|
31
34
|
export const agentActionSchema = Type.Union([
|
|
32
|
-
Type.Object({ action: Type.Literal("start"), ...
|
|
35
|
+
Type.Object({ action: Type.Literal("start"), ...agentStartOptions }, { additionalProperties: false }),
|
|
33
36
|
Type.Object({ action: Type.Literal("send"), agentId: id, message, delivery }, { additionalProperties: false }),
|
|
34
37
|
Type.Object({ action: Type.Literal("rename"), agentId: id, name }, { additionalProperties: false }),
|
|
35
38
|
Type.Object({ action: Type.Literal("resume"), agentId: id, message }, { additionalProperties: false }),
|
|
@@ -45,7 +48,7 @@ const cycle = Type.Object({ cycleId: Type.String(), startedAt: Type.Number(), fi
|
|
|
45
48
|
}, { additionalProperties: false });
|
|
46
49
|
export const agentViewSchema = Type.Object({ agentId: Type.String(), name: Type.String(), cwd: Type.String(), createdAt: Type.Number(),
|
|
47
50
|
owned: Type.Boolean({ description: "是否属于当前父会话分支;false 时须回到所属分支操作。" }),
|
|
48
|
-
state: Type.Union([Type.Literal("starting"), Type.Literal("busy"), Type.Literal("idle"), Type.Literal("stopping"), Type.Literal("stopped"), Type.Literal("error")], { description: "
|
|
51
|
+
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
52
|
activity: Type.String(), cycle: Type.Optional(cycle), error: Type.Optional(Type.String()),
|
|
50
53
|
requestId: Type.Optional(Type.String()), messageStatus: Type.Optional(Type.Literal("queued", { description: "消息已入队,不代表模型已阅读或完成。" })),
|
|
51
54
|
cycles: Type.Optional(Type.Array(Type.Object({ cycleId: Type.String(), state: Type.String() }), { description: "仅 read 返回:可读取的周期索引。" })),
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { agentActionSchema, agentParametersSchema, agentStartOptions } from "./contract.js";
|
|
3
|
+
|
|
4
|
+
const options = Type.Omit(Type.Object(agentStartOptions), ["provider", "model", "thinking", "files", "tools", "noExtensions", "noPromptTemplates", "noContextFiles", "noSkills"]).properties;
|
|
5
|
+
const native = {
|
|
6
|
+
provider: Type.Optional(Type.String({ minLength: 1, maxLength: 128, description: "dsh 原生 provider route,默认继承父会话。" })),
|
|
7
|
+
model: Type.Optional(Type.String({ minLength: 1, maxLength: 128, description: "该 dsh provider 的模型 ID,默认继承父会话。" })),
|
|
8
|
+
thinking: Type.Optional(Type.String({ minLength: 1, maxLength: 128, description: "该 dsh 模型的原生 reasoning effort ID,默认继承父会话;不转换 Pi 思考等级。" })),
|
|
9
|
+
files: Type.Optional(Type.Array(Type.String({ minLength: 1 }), { maxItems: 32, description: "作为原生附件保存的文件;相对原工作区根目录。图片遵循宿主准入规则,其他文件保留原始字节供模型读取。" })),
|
|
10
|
+
tools: Type.Optional(Type.Array(Type.String({ minLength: 1 }), { minItems: 1, description: "原生能力工具名;不填写 run_code 传输名,不支持递归委派。省略时使用原生工具和 pbot_team_message。" })),
|
|
11
|
+
noSkills: Type.Optional(Type.Boolean({ description: "关闭该专家的模型技能目录和原生 skill 工具;不改变其他会话的技能配置。" })),
|
|
12
|
+
};
|
|
13
|
+
const omitted = ["noExtensions", "noPromptTemplates", "noContextFiles", "provider", "model", "thinking", "files", "tools", "noSkills"] as const;
|
|
14
|
+
export const dshAgentParametersSchema = Type.Object({ ...Type.Omit(agentParametersSchema, omitted).properties, ...native }, { additionalProperties: false });
|
|
15
|
+
export const dshAgentActionSchema = Type.Union(agentActionSchema.anyOf.map(row => row.properties.action.const === "start"
|
|
16
|
+
? Type.Object({ action: Type.Literal("start"), ...options, ...native }, { additionalProperties: false }) : row));
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Context } from "@deepseek-ai/cordis";
|
|
2
|
+
import type { Agent } from "@deepseek-ai/dsh-agent";
|
|
3
|
+
import { dshParameters, dshOutputSchema } from "../../integrations/dsh/schema.js";
|
|
4
|
+
import type { AgentService } from "../../services/agents/agent-service.js";
|
|
5
|
+
import type { AgentOwner } from "../../model/agent.js";
|
|
6
|
+
import { object, json } from "../../common/json.js";
|
|
7
|
+
import { requireThat, schema } from "../../common/validation.js";
|
|
8
|
+
import { markdownFields } from "../../common/markdown.js";
|
|
9
|
+
import { executeAgent, type AgentInput } from "./actions.js";
|
|
10
|
+
import { agentDescription, agentResultSchema } from "./contract.js";
|
|
11
|
+
import { dshAgentActionSchema, dshAgentParametersSchema } from "./dsh-contract.js";
|
|
12
|
+
|
|
13
|
+
export function registerDshAgentTool(ctx: Context, agent: Agent, service: AgentService, owner: AgentOwner, check: () => void) {
|
|
14
|
+
const validators = new Map(dshAgentActionSchema.anyOf.map(row => [row.properties.action.const, schema(row)]));
|
|
15
|
+
const validOutput = schema(agentResultSchema);
|
|
16
|
+
ctx.tools.register({ name: "pbot_agent", description: agentDescription,
|
|
17
|
+
parameters: dshParameters(dshAgentParametersSchema),
|
|
18
|
+
output: {
|
|
19
|
+
schema: dshOutputSchema(agentResultSchema),
|
|
20
|
+
render: (_args, value) => [{ type: "text", text: "# 专家\n\n" + markdownFields(value) }],
|
|
21
|
+
presentationMeta: (args, value) => {
|
|
22
|
+
if (!object(args) || args.action !== "read" || !object(value) || !object(value.cycle) || value.cycle.state === "running") return {};
|
|
23
|
+
return json({ pbotAgentReadKeys: [String(value.agentId) + ":" + String(value.cycle.cycleId)] });
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
async execute(args, exec) {
|
|
27
|
+
check(); exec.signal.throwIfAborted();
|
|
28
|
+
requireThat(exec.agent === agent && object(args) && typeof args.action === "string", "专家工具会话或参数无效。");
|
|
29
|
+
const valid = validators.get(args.action);
|
|
30
|
+
requireThat(valid && valid(args), "dsh 专家参数无效或包含不支持的宿主选项。");
|
|
31
|
+
const selection = agent.model.current();
|
|
32
|
+
const output = await executeAgent(service, args as AgentInput, owner, selection ? {
|
|
33
|
+
provider: selection.provider, model: selection.model,
|
|
34
|
+
...(selection.reasoningEffort && (!args.provider || args.provider === selection.provider) && (!args.model || args.model === selection.model)
|
|
35
|
+
? { thinking: selection.reasoningEffort } : {}),
|
|
36
|
+
} : {});
|
|
37
|
+
requireThat(validOutput(output.structuredContent), "专家输出不符合契约。");
|
|
38
|
+
return output.structuredContent;
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
}
|
package/src/tools/agents/tool.ts
CHANGED
|
@@ -1,61 +1,25 @@
|
|
|
1
|
-
import type { Static } from "typebox";
|
|
2
1
|
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
3
2
|
import type { AgentService } from "../../services/agents/agent-service.js";
|
|
4
3
|
import { agentOwner } from "../../integrations/pi/agent-context.js";
|
|
5
|
-
import type { AgentOptions, AgentView } from "../../model/agent.js";
|
|
6
4
|
import type { CallAccess } from "../access.js";
|
|
7
5
|
import { requireThat, schema } from "../../common/validation.js";
|
|
8
|
-
import {
|
|
9
|
-
import { agentActionSchema, agentParametersSchema, agentResultSchema
|
|
10
|
-
|
|
11
|
-
type AgentOutput = Static<typeof agentViewSchema>;
|
|
12
|
-
function agentOutput(view: AgentView & Partial<AgentOutput>): AgentOutput {
|
|
13
|
-
const c = view.cycle;
|
|
14
|
-
return {
|
|
15
|
-
agentId: view.agentId, name: view.name, cwd: view.cwd, createdAt: view.createdAt,
|
|
16
|
-
owned: view.owned, state: view.state, activity: view.activity,
|
|
17
|
-
...(c ? { cycle: { cycleId: c.cycleId, startedAt: c.startedAt, state: c.state, requestIds: c.requestIds, source: c.source,
|
|
18
|
-
...(c.finishedAt === undefined ? {} : { finishedAt: c.finishedAt }), ...(c.error === undefined ? {} : { error: c.error }) } } : {}),
|
|
19
|
-
...(view.error === undefined ? {} : { error: view.error }),
|
|
20
|
-
...(view.requestId === undefined ? {} : { requestId: view.requestId }),
|
|
21
|
-
...(view.messageStatus === undefined ? {} : { messageStatus: view.messageStatus }),
|
|
22
|
-
...(view.cycles === undefined ? {} : { cycles: view.cycles }),
|
|
23
|
-
...(view.report === undefined ? {} : { report: view.report }),
|
|
24
|
-
...(view.totalLength === undefined ? {} : { totalLength: view.totalLength }),
|
|
25
|
-
...(view.nextOffset === undefined ? {} : { nextOffset: view.nextOffset }),
|
|
26
|
-
};
|
|
27
|
-
}
|
|
6
|
+
import { executeAgent } from "./actions.js";
|
|
7
|
+
import { agentDescription, agentActionSchema, agentParametersSchema, agentResultSchema } from "./contract.js";
|
|
28
8
|
|
|
29
9
|
export function agentTool(service: AgentService, access: CallAccess) {
|
|
30
10
|
const validators = new Map(agentActionSchema.anyOf.map(action => [action.properties.action.const, schema(action)]));
|
|
31
11
|
return defineTool({ name: "pbot_agent", label: "子会话",
|
|
32
|
-
description:
|
|
12
|
+
description: agentDescription,
|
|
33
13
|
parameters: agentParametersSchema, outputSchema: agentResultSchema,
|
|
34
14
|
async execute(_id, args, signal, _update, ctx) {
|
|
35
15
|
access.check(); signal?.throwIfAborted();
|
|
36
16
|
const valid = validators.get(args.action);
|
|
37
17
|
requireThat(valid, "未知的子会话 action:" + args.action);
|
|
38
18
|
requireThat(valid(args), "子会话 " + args.action + " 参数无效:" + JSON.stringify(valid.errors));
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const { action: _action, ...supplied } = input;
|
|
44
|
-
value = service.start({ provider: ctx.model?.provider, model: ctx.model?.id, thinking: ctx.thinkingLevel, ...supplied } as AgentOptions, owner); break;
|
|
45
|
-
}
|
|
46
|
-
case "list": return markdownResult({ agents: service.list(owner).map(agentOutput) }, "子会话列表");
|
|
47
|
-
case "send": value = service.send(input.agentId!, input.message!, input.delivery ?? "steer", owner); break;
|
|
48
|
-
case "rename": value = service.rename(input.agentId!, input.name!, owner); break;
|
|
49
|
-
case "resume": value = service.resume(input.agentId!, input.message!, owner); break;
|
|
50
|
-
case "stop": value = service.stop(input.agentId!, owner); break;
|
|
51
|
-
case "status": value = service.status(input.agentId!, owner); break;
|
|
52
|
-
case "read": {
|
|
53
|
-
const page = service.read(input.agentId!, owner, input.cycleId, input.offset, input.limit);
|
|
54
|
-
const data = agentOutput(page), output = markdownResult(data, "子会话报告");
|
|
55
|
-
return { ...output, details: { ...data, ...(page.cycle && page.cycle.state !== "running" ? { agentReadKeys: [page.agentId + ":" + page.cycle.cycleId] } : {}) } };
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
return markdownResult(agentOutput(value), "子会话状态");
|
|
19
|
+
return executeAgent(service, args, agentOwner(ctx), {
|
|
20
|
+
...(ctx.model ? { provider: ctx.model.provider, model: ctx.model.id } : {}),
|
|
21
|
+
...(ctx.thinkingLevel ? { thinking: ctx.thinkingLevel } : {}),
|
|
22
|
+
});
|
|
59
23
|
},
|
|
60
24
|
});
|
|
61
25
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { HistoryLocation } from "../../model/history.js";
|
|
2
|
+
import type { HistoryService } from "../../services/history/history-service.js";
|
|
3
|
+
import type { HistoryInput } from "./contract.js";
|
|
4
|
+
|
|
5
|
+
export async function executeHistory(service: HistoryService, args: HistoryInput, location: HistoryLocation, signal?: AbortSignal) {
|
|
6
|
+
signal?.throwIfAborted(); await service.refresh(location, signal); signal?.throwIfAborted();
|
|
7
|
+
return args.action === "search" ? service.search(args) :
|
|
8
|
+
{ node: service.read(args.id!, args.offset, args.limit, args.childrenOffset, args.host), warnings: service.warnings };
|
|
9
|
+
}
|