@jetcrab/pi-desk-subagent 0.0.0-stage → 1.0.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.
@@ -0,0 +1,100 @@
1
+ import { readFile, rename, writeFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { getAgentDir, ModelRuntime, withFileMutationQueue } from '@earendil-works/pi-coding-agent';
4
+ import { PluginMethodError } from '@jetcrab/pi-desk-sdk/session';
5
+ import { listGlobalSubagentSettings } from './agent-config.js';
6
+ const levels = new Set(['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max']);
7
+ function parseSettings(input) {
8
+ if (!input || typeof input !== 'object' || !('agents' in input) || !Array.isArray(input.agents)) {
9
+ throw new PluginMethodError(400, '子代理设置参数无效');
10
+ }
11
+ const names = new Set(listGlobalSubagentSettings().map((agent) => agent.name));
12
+ const seen = new Set();
13
+ return input.agents.map((item) => {
14
+ if (!item || typeof item !== 'object')
15
+ throw new PluginMethodError(400, '子代理设置参数无效');
16
+ const value = item;
17
+ if (typeof value.name !== 'string' ||
18
+ !names.has(value.name) ||
19
+ seen.has(value.name) ||
20
+ typeof value.model !== 'string' ||
21
+ (value.model !== '' && !/^[^/\s]+\/\S+$/.test(value.model)) ||
22
+ (value.thinking !== null && !levels.has(String(value.thinking))) ||
23
+ typeof value.disabled !== 'boolean') {
24
+ throw new PluginMethodError(400, '子代理设置参数无效');
25
+ }
26
+ seen.add(value.name);
27
+ return value;
28
+ });
29
+ }
30
+ async function availableModels() {
31
+ const runtime = await ModelRuntime.create({ allowModelNetwork: false });
32
+ const error = runtime.getError();
33
+ if (error)
34
+ throw new Error(error);
35
+ return (await runtime.getAvailable())
36
+ .filter((model) => model.input.includes('text'))
37
+ .map((model) => ({ provider: model.provider, modelId: model.id, name: model.name }))
38
+ .sort((a, b) => `${a.provider}/${a.modelId}`.localeCompare(`${b.provider}/${b.modelId}`));
39
+ }
40
+ export async function getGlobalSubagentSettings() {
41
+ return { agents: listGlobalSubagentSettings(), models: await availableModels() };
42
+ }
43
+ export async function saveGlobalSubagentSettings(input) {
44
+ const changes = parseSettings(input);
45
+ const models = changes.some((item) => item.model && !item.disabled)
46
+ ? new Set((await availableModels()).map((model) => `${model.provider}/${model.modelId}`))
47
+ : null;
48
+ for (const item of changes) {
49
+ if (item.model && !item.disabled && !models?.has(item.model)) {
50
+ throw new PluginMethodError(400, `模型当前不可用:${item.model}`);
51
+ }
52
+ }
53
+ const path = join(getAgentDir(), 'settings.json');
54
+ await withFileMutationQueue(path, async () => {
55
+ const settings = JSON.parse(await readFile(path, 'utf8'));
56
+ const agents = settings.agents;
57
+ if (agents !== undefined && (!agents || typeof agents !== 'object' || Array.isArray(agents))) {
58
+ throw new Error('全局 agents 配置不是对象');
59
+ }
60
+ const next = { ...agents };
61
+ for (const item of changes) {
62
+ const previous = next[item.name];
63
+ const original = previous && typeof previous === 'object' && !Array.isArray(previous)
64
+ ? previous
65
+ : {};
66
+ const entry = { ...original };
67
+ if (item.model)
68
+ entry.model = item.model;
69
+ else
70
+ delete entry.model;
71
+ if (item.thinking !== null && item.model)
72
+ entry.thinking = item.thinking;
73
+ else
74
+ delete entry.thinking;
75
+ if (item.disabled)
76
+ entry.disabled = true;
77
+ else
78
+ delete entry.disabled;
79
+ if (Object.keys(entry).length)
80
+ next[item.name] = entry;
81
+ else
82
+ delete next[item.name];
83
+ }
84
+ settings.agents = next;
85
+ const temporary = `${path}.${process.pid}.${crypto.randomUUID()}.tmp`;
86
+ try {
87
+ await writeFile(temporary, `${JSON.stringify(settings, null, 2)}\n`, 'utf8');
88
+ await rename(temporary, path);
89
+ }
90
+ catch (error) {
91
+ const { rm } = await import('node:fs/promises');
92
+ await rm(temporary, { force: true });
93
+ throw error;
94
+ }
95
+ });
96
+ console.info('[pi-desk-subagent] 全局子代理配置已保存', {
97
+ agents: changes.map((item) => item.name)
98
+ });
99
+ return { agents: listGlobalSubagentSettings() };
100
+ }
@@ -0,0 +1,2 @@
1
+ import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
2
+ export default function tempLocalSubagent(pi: ExtensionAPI): void;
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ import { registerSubagentExtension } from './subagent-runtime.js';
2
+ export default function tempLocalSubagent(pi) {
3
+ registerSubagentExtension(pi);
4
+ }
@@ -0,0 +1,7 @@
1
+ import { type PluginMessageDeclarationInput, type PluginMessageProjectionResult } from '@jetcrab/pi-desk-sdk/entry';
2
+ export declare function projectSubagentStartMessage(input: PluginMessageDeclarationInput): PluginMessageProjectionResult;
3
+ export declare function projectSubagentReturnMessage(input: PluginMessageDeclarationInput): PluginMessageProjectionResult;
4
+ export declare function projectSubagentListMessage(input: PluginMessageDeclarationInput): PluginMessageProjectionResult;
5
+ export declare function projectSubagentControlMessage(input: PluginMessageDeclarationInput): PluginMessageProjectionResult;
6
+ declare const _default: import("@jetcrab/pi-desk-sdk/entry").PiDeskPluginDefinition;
7
+ export default _default;
@@ -0,0 +1,164 @@
1
+ import { definePiDeskPlugin } from '@jetcrab/pi-desk-sdk/entry';
2
+ import { collectSubagentCapabilityOptions } from './agent-config.js';
3
+ import { getGlobalSubagentSettings, saveGlobalSubagentSettings } from './global-settings.js';
4
+ import { SUBAGENT_COMPLETION_CUSTOM_TYPE, parseSubagentCompletionContent } from './subagent-message.js';
5
+ function record(value) {
6
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
7
+ }
8
+ function text(value) {
9
+ return typeof value === 'string' && value.trim() ? value.trim() : null;
10
+ }
11
+ function resultDetails(value) {
12
+ return record(record(value)?.details ?? null);
13
+ }
14
+ function resultOutput(value) {
15
+ const result = record(value);
16
+ const output = text(result?.output);
17
+ if (output)
18
+ return output;
19
+ const content = result?.content;
20
+ if (!Array.isArray(content))
21
+ return text(content);
22
+ const parts = content.flatMap((part) => {
23
+ const item = record(part);
24
+ return item?.type === 'text' && typeof item.text === 'string' ? [item.text] : [];
25
+ });
26
+ const combined = parts.join('\n').trim();
27
+ return combined || null;
28
+ }
29
+ function terminalStatus(value) {
30
+ return value === 'completed' ||
31
+ value === 'failed' ||
32
+ value === 'stopped' ||
33
+ value === 'interrupted'
34
+ ? value
35
+ : null;
36
+ }
37
+ export function projectSubagentStartMessage(input) {
38
+ if (input.message.kind !== 'tool')
39
+ return input.defaultProjection;
40
+ const argumentsValue = input.message.arguments;
41
+ const details = resultDetails(input.message.result);
42
+ const action = input.message.toolName === 'agent_resume' ? 'resume' : 'start';
43
+ const agentType = text(argumentsValue.subagent_type) ?? text(details?.agentType);
44
+ const resumeId = text(argumentsValue.agent_id);
45
+ const title = text(argumentsValue.description) ?? text(details?.title) ?? '子代理任务';
46
+ const prompt = text(argumentsValue.prompt);
47
+ return {
48
+ viewKey: 'subagent/start',
49
+ summary: {
50
+ action,
51
+ taskId: text(details?.taskId) ?? resumeId,
52
+ agentType,
53
+ title,
54
+ error: input.message.isError ? resultOutput(input.message.result) : null
55
+ },
56
+ detail: prompt ? { markdown: prompt } : null
57
+ };
58
+ }
59
+ export function projectSubagentReturnMessage(input) {
60
+ if (input.message.kind !== 'custom')
61
+ return input.defaultProjection;
62
+ const details = record(input.message.details);
63
+ const content = parseSubagentCompletionContent(input.message.content);
64
+ return {
65
+ viewKey: 'subagent/return',
66
+ summary: {
67
+ taskId: text(details?.taskId),
68
+ agentType: content.agentType,
69
+ title: content.title ?? '子代理任务',
70
+ status: terminalStatus(details?.status) ?? 'failed'
71
+ },
72
+ detail: content.result ? { markdown: content.result } : null
73
+ };
74
+ }
75
+ export function projectSubagentListMessage(input) {
76
+ if (input.message.kind !== 'tool')
77
+ return input.defaultProjection;
78
+ const details = resultDetails(input.message.result);
79
+ const agents = Array.isArray(details?.agents) ? details.agents : null;
80
+ const output = resultOutput(input.message.result);
81
+ return {
82
+ viewKey: 'subagent/list',
83
+ summary: {
84
+ query: text(input.message.arguments.agent_id),
85
+ agents,
86
+ error: input.message.isError ? output : null
87
+ },
88
+ detail: !agents && !input.message.isError && output ? { output } : null
89
+ };
90
+ }
91
+ export function projectSubagentControlMessage(input) {
92
+ if (input.message.kind !== 'tool')
93
+ return input.defaultProjection;
94
+ const details = resultDetails(input.message.result);
95
+ const action = input.message.toolName === 'agent_stop' ? 'stop' : 'steer';
96
+ const taskId = text(details?.taskId) ?? text(input.message.arguments.agent_id);
97
+ const output = resultOutput(input.message.result);
98
+ const prompt = text(input.message.arguments.prompt);
99
+ const preview = prompt ? Array.from(prompt.replace(/\s+/gu, ' ')) : null;
100
+ return {
101
+ viewKey: 'subagent/control',
102
+ summary: {
103
+ action,
104
+ taskId,
105
+ agentType: text(details?.agentType),
106
+ title: text(details?.title) ?? '子代理任务',
107
+ ...(action === 'steer'
108
+ ? {
109
+ preview: preview
110
+ ? preview.length > 160
111
+ ? `${preview.slice(0, 159).join('')}…`
112
+ : preview.join('')
113
+ : null
114
+ }
115
+ : {}),
116
+ delivery: action === 'steer'
117
+ ? (text(details?.delivery) ?? (output?.startsWith('补充条件已排队') ? 'queued' : null))
118
+ : null,
119
+ error: input.message.isError ? output : null
120
+ },
121
+ detail: prompt ? { markdown: prompt } : null
122
+ };
123
+ }
124
+ export default definePiDeskPlugin({
125
+ name: 'subagent',
126
+ setup(plugin) {
127
+ plugin.registerMethod('settings-get', () => getGlobalSubagentSettings());
128
+ plugin.registerMethod('settings-save', (input) => saveGlobalSubagentSettings(input));
129
+ plugin.registerBrowserEntry('./dist/browser/entry.js');
130
+ plugin.declareCapabilities(async ({ signal }) => {
131
+ const workSessions = await plugin.host.workSessions.listWorkSessions();
132
+ return {
133
+ agents: {
134
+ label: '子代理',
135
+ options: collectSubagentCapabilityOptions(workSessions.map((session) => session.cwd), signal)
136
+ }
137
+ };
138
+ });
139
+ plugin.declareMessage('start', {
140
+ priority: 100,
141
+ match: (input) => input.message.kind === 'tool' &&
142
+ (input.message.toolName === 'agent' || input.message.toolName === 'agent_resume'),
143
+ project: projectSubagentStartMessage
144
+ });
145
+ plugin.declareMessage('list', {
146
+ priority: 100,
147
+ match: (input) => input.message.kind === 'tool' && input.message.toolName === 'agent_list',
148
+ project: projectSubagentListMessage
149
+ });
150
+ plugin.declareMessage('control', {
151
+ priority: 100,
152
+ match: (input) => input.message.kind === 'tool' &&
153
+ (input.message.toolName === 'agent_stop' || input.message.toolName === 'agent_steer'),
154
+ project: projectSubagentControlMessage
155
+ });
156
+ plugin.declareMessage('return', {
157
+ priority: 100,
158
+ match: (input) => input.message.kind === 'custom' &&
159
+ (input.message.customType === SUBAGENT_COMPLETION_CUSTOM_TYPE ||
160
+ input.message.customType === 'pi-super-subagent:completion:v1'),
161
+ project: projectSubagentReturnMessage
162
+ });
163
+ }
164
+ });
@@ -0,0 +1,23 @@
1
+ import type { ExtensionFactory } from '@earendil-works/pi-coding-agent';
2
+ import type { SubagentConfig } from './agent-config.js';
3
+ export declare const RESEARCH_WORKER_AGENT_TYPE = "research-worker";
4
+ export interface ResearchDelegateTask {
5
+ title: string;
6
+ prompt: string;
7
+ }
8
+ export type ResearchWorkerStatus = 'completed' | 'failed' | 'stopped' | 'interrupted';
9
+ export interface ResearchWorkerOutcome {
10
+ taskId: string | null;
11
+ title: string;
12
+ status: ResearchWorkerStatus;
13
+ result: string | null;
14
+ error: string | null;
15
+ sessionFile: string | null;
16
+ }
17
+ export interface ResearchDelegateProgress {
18
+ completed: number;
19
+ total: number;
20
+ }
21
+ export type ResearchWorkerDelegate = (tasks: readonly ResearchDelegateTask[], signal: AbortSignal | undefined, onProgress: (progress: ResearchDelegateProgress) => void) => Promise<ResearchWorkerOutcome[]>;
22
+ export declare function createResearchWorkerConfig(research: SubagentConfig): SubagentConfig;
23
+ export declare function createResearchCoordinatorExtension(delegate: ResearchWorkerDelegate): ExtensionFactory;
@@ -0,0 +1,101 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { Type } from 'typebox';
4
+ export const RESEARCH_WORKER_AGENT_TYPE = 'research-worker';
5
+ const RESEARCH_WORKER_PROMPT = readFileSync(fileURLToPath(new URL('../agents/internal/research-worker.md', import.meta.url)), 'utf8').trim();
6
+ const RESEARCH_WORKER_TOOLS = new Set(['external_research', 'read', 'grep', 'find', 'ls']);
7
+ const MAX_WORKER_RESULT_BYTES = 20_000;
8
+ const MAX_DELEGATE_OUTPUT_BYTES = 50 * 1024;
9
+ function requireText(value, field) {
10
+ const text = value.trim();
11
+ if (!text)
12
+ throw new Error(`${field} 不能为空`);
13
+ return text;
14
+ }
15
+ function truncateUtf8(value, maxBytes) {
16
+ const content = Buffer.from(value, 'utf8');
17
+ if (content.byteLength <= maxBytes)
18
+ return value;
19
+ let end = Math.max(0, maxBytes);
20
+ while (end > 0 && (content[end] & 0xc0) === 0x80)
21
+ end -= 1;
22
+ return content.subarray(0, end).toString('utf8');
23
+ }
24
+ function visibleWorkerResult(outcome, maxBytes) {
25
+ const body = outcome.result ?? outcome.error ?? '(没有结果)';
26
+ if (Buffer.byteLength(body, 'utf8') <= maxBytes)
27
+ return body;
28
+ const marker = `\n\n[结果已截断;完整 Conversation:${outcome.sessionFile ?? '未保存'}]`;
29
+ return `${truncateUtf8(body, Math.max(0, maxBytes - Buffer.byteLength(marker, 'utf8')))}${marker}`;
30
+ }
31
+ function formatOutcomes(outcomes) {
32
+ const completed = outcomes.filter((outcome) => outcome.status === 'completed').length;
33
+ const header = `Research Worker:${completed}/${outcomes.length} 完成\n\n`;
34
+ const perWorkerBytes = Math.min(MAX_WORKER_RESULT_BYTES, Math.max(256, Math.floor((MAX_DELEGATE_OUTPUT_BYTES - Buffer.byteLength(header)) / outcomes.length)));
35
+ const sections = outcomes.map((outcome) => `### [${outcome.title}] ${outcome.status}\n\n${visibleWorkerResult(outcome, perWorkerBytes)}`);
36
+ const content = `${header}${sections.join('\n\n---\n\n')}`;
37
+ if (Buffer.byteLength(content, 'utf8') <= MAX_DELEGATE_OUTPUT_BYTES)
38
+ return content;
39
+ const marker = '\n\n[聚合结果已截断;完整结果保留在各 Worker Conversation。]';
40
+ return `${truncateUtf8(content, MAX_DELEGATE_OUTPUT_BYTES - Buffer.byteLength(marker, 'utf8'))}${marker}`;
41
+ }
42
+ export function createResearchWorkerConfig(research) {
43
+ return {
44
+ name: RESEARCH_WORKER_AGENT_TYPE,
45
+ description: '执行 Root research 派发的单个独立外部研究轨道。',
46
+ tools: research.tools.filter((tool) => RESEARCH_WORKER_TOOLS.has(tool)),
47
+ model: research.model,
48
+ thinking: research.thinking,
49
+ systemPrompt: RESEARCH_WORKER_PROMPT
50
+ };
51
+ }
52
+ export function createResearchCoordinatorExtension(delegate) {
53
+ return (pi) => {
54
+ pi.registerTool({
55
+ name: 'research_delegate',
56
+ label: 'Research Delegate',
57
+ description: '将至少两个范围互不依赖的外部研究轨道并行交给叶子 Research Worker,并等待全部 Worker 进入终态。',
58
+ promptSnippet: '并行委派至少两个互不依赖的外部研究轨道,并等待全部结果。',
59
+ promptGuidelines: [
60
+ '研究对象未知的模糊需求先做表层探索,只获取候选清单和分片依据;发现至少两个独立对象或问题后再调用 research_delegate。',
61
+ '尽量拆分到一个 Worker 只负责一个明确对象或一个单一可交付问题;不要把可独立研究的多个对象打包给同一个 Worker,也不要机械切碎同一个不可分问题。',
62
+ 'research_delegate 返回前 Root research 停止重复研究相同范围,并等待全部 Worker 进入终态。'
63
+ ],
64
+ parameters: Type.Object({
65
+ tasks: Type.Array(Type.Object({
66
+ title: Type.String({ description: '任务中心展示的简短研究标题' }),
67
+ prompt: Type.String({ description: '完整、自包含的单轨道研究任务' })
68
+ }), { minItems: 2, description: '至少两个互不依赖的研究轨道' })
69
+ }),
70
+ async execute(_toolCallId, params, signal, onUpdate) {
71
+ const tasks = params.tasks.map((task, index) => ({
72
+ title: requireText(task.title, `tasks[${index}].title`),
73
+ prompt: requireText(task.prompt, `tasks[${index}].prompt`)
74
+ }));
75
+ const outcomes = await delegate(tasks, signal, (progress) => {
76
+ onUpdate?.({
77
+ content: [
78
+ {
79
+ type: 'text',
80
+ text: `Research Worker:${progress.completed}/${progress.total} 已进入终态`
81
+ }
82
+ ],
83
+ details: progress
84
+ });
85
+ });
86
+ return {
87
+ content: [{ type: 'text', text: formatOutcomes(outcomes) }],
88
+ details: {
89
+ version: 1,
90
+ workers: outcomes.map((outcome) => ({
91
+ taskId: outcome.taskId,
92
+ title: outcome.title,
93
+ status: outcome.status,
94
+ sessionFile: outcome.sessionFile
95
+ }))
96
+ }
97
+ };
98
+ }
99
+ });
100
+ };
101
+ }
@@ -0,0 +1,18 @@
1
+ import type { AgentSession, ExtensionAPI, ResourceLoader } from '@earendil-works/pi-coding-agent';
2
+ import { type SessionCapabilityRules } from '@jetcrab/pi-desk-sdk/capabilities';
3
+ /** Child sessions keep their role registry; a mode only filters its current view. */
4
+ export declare class SubagentCapabilities {
5
+ private readonly rules;
6
+ private session;
7
+ private requested;
8
+ private appliedToolsRules;
9
+ private setTools;
10
+ private originalSetTools;
11
+ constructor(rules: () => SessionCapabilityRules);
12
+ extension(pi: ExtensionAPI): void;
13
+ resources(base: ResourceLoader, appendPrompt: () => string[]): ResourceLoader;
14
+ attach(session: AgentSession): void;
15
+ refresh(): void;
16
+ private applyTools;
17
+ private dispose;
18
+ }
@@ -0,0 +1,83 @@
1
+ import { isCapabilityAllowed } from '@jetcrab/pi-desk-sdk/capabilities';
2
+ /** Child sessions keep their role registry; a mode only filters its current view. */
3
+ export class SubagentCapabilities {
4
+ rules;
5
+ session = null;
6
+ requested = new Set();
7
+ // Current rules cannot distinguish a previously masked tool from an explicit deactivation.
8
+ appliedToolsRules = {};
9
+ setTools = null;
10
+ originalSetTools = null;
11
+ constructor(rules) {
12
+ this.rules = rules;
13
+ }
14
+ extension(pi) {
15
+ pi.on('tool_call', (event) => {
16
+ if (!isCapabilityAllowed(this.rules().tools, event.toolName)) {
17
+ return { block: true, reason: `当前能力模式不允许工具:${event.toolName}` };
18
+ }
19
+ });
20
+ pi.on('session_shutdown', () => this.dispose());
21
+ }
22
+ resources(base, appendPrompt) {
23
+ return new Proxy(base, {
24
+ get: (target, property) => {
25
+ if (property === 'getSkills') {
26
+ return () => {
27
+ const result = target.getSkills();
28
+ return {
29
+ ...result,
30
+ skills: result.skills.filter((skill) => isCapabilityAllowed(this.rules().skills, skill.name))
31
+ };
32
+ };
33
+ }
34
+ if (property === 'getAppendSystemPrompt') {
35
+ return () => [...target.getAppendSystemPrompt(), ...appendPrompt()];
36
+ }
37
+ const value = Reflect.get(target, property, target);
38
+ return typeof value === 'function' ? value.bind(target) : value;
39
+ }
40
+ });
41
+ }
42
+ attach(session) {
43
+ this.session = session;
44
+ this.requested = new Set(session.getActiveToolNames());
45
+ this.originalSetTools = session.setActiveToolsByName;
46
+ this.setTools = session.setActiveToolsByName.bind(session);
47
+ session.setActiveToolsByName = (names) => {
48
+ this.applyTools(names);
49
+ };
50
+ this.refresh();
51
+ }
52
+ refresh() {
53
+ if (this.session)
54
+ this.applyTools(this.session.getActiveToolNames());
55
+ }
56
+ applyTools(names) {
57
+ if (!this.session || !this.setTools)
58
+ return;
59
+ const registered = new Set(this.session
60
+ .getAllTools()
61
+ .filter((tool) => tool.exposure !== 'hidden')
62
+ .map((tool) => tool.name));
63
+ const masked = [...this.requested].filter((name) => registered.has(name) && !isCapabilityAllowed(this.appliedToolsRules, name));
64
+ const selected = new Set([...names, ...masked]);
65
+ this.requested = new Set([...this.requested, ...names].filter((name) => registered.has(name) && selected.has(name)));
66
+ const rules = this.rules().tools;
67
+ this.appliedToolsRules = {
68
+ allow: rules.allow ? [...rules.allow] : undefined,
69
+ deny: rules.deny ? [...rules.deny] : undefined
70
+ };
71
+ this.setTools([...this.requested].filter((name) => isCapabilityAllowed(rules, name)));
72
+ }
73
+ dispose() {
74
+ if (this.session && this.originalSetTools) {
75
+ this.session.setActiveToolsByName = this.originalSetTools;
76
+ }
77
+ this.session = null;
78
+ this.setTools = null;
79
+ this.originalSetTools = null;
80
+ this.requested.clear();
81
+ this.appliedToolsRules = {};
82
+ }
83
+ }
@@ -0,0 +1,10 @@
1
+ export declare const SUBAGENT_COMPLETION_CUSTOM_TYPE = "pi-desk-subagent:completion:v1";
2
+ export declare const SUBAGENT_LAUNCH_STATE_CUSTOM_TYPE = "pi-desk-subagent:launch:v1";
3
+ export declare const SUBAGENT_SHUTDOWN_STATE_CUSTOM_TYPE = "pi-desk-subagent:state:v1";
4
+ export declare const SUBAGENT_RECORD_VERSION = 1;
5
+ export interface SubagentCompletionContent {
6
+ agentType: string | null;
7
+ title: string | null;
8
+ result: string;
9
+ }
10
+ export declare function parseSubagentCompletionContent(content: string): SubagentCompletionContent;
@@ -0,0 +1,24 @@
1
+ export const SUBAGENT_COMPLETION_CUSTOM_TYPE = 'pi-desk-subagent:completion:v1';
2
+ export const SUBAGENT_LAUNCH_STATE_CUSTOM_TYPE = 'pi-desk-subagent:launch:v1';
3
+ export const SUBAGENT_SHUTDOWN_STATE_CUSTOM_TYPE = 'pi-desk-subagent:state:v1';
4
+ export const SUBAGENT_RECORD_VERSION = 1;
5
+ export function parseSubagentCompletionContent(content) {
6
+ const resultMarker = '\n\n结果:\n';
7
+ const markerIndex = content.indexOf(resultMarker);
8
+ const header = markerIndex >= 0 ? content.slice(0, markerIndex) : content;
9
+ const result = markerIndex >= 0 ? content.slice(markerIndex + resultMarker.length).trim() : '';
10
+ const lines = header.split('\n');
11
+ const agentType = lines
12
+ .find((line) => line.startsWith('类型: '))
13
+ ?.slice('类型: '.length)
14
+ .trim();
15
+ const title = lines
16
+ .find((line) => line.startsWith('任务: '))
17
+ ?.slice('任务: '.length)
18
+ .trim();
19
+ return {
20
+ agentType: agentType || null,
21
+ title: title || null,
22
+ result
23
+ };
24
+ }
@@ -0,0 +1,93 @@
1
+ import type { AgentSession } from '@earendil-works/pi-coding-agent';
2
+ import { type ExtensionAPI, type ExtensionContext, type ExtensionFactory, type LoadExtensionsResult, ModelRuntime, SessionManager, type SessionEntry } from '@earendil-works/pi-coding-agent';
3
+ import { type AssistantMessage } from '@earendil-works/pi-ai';
4
+ import { type TaskHandle, type TaskStatus } from '@jetcrab/pi-desk-sdk';
5
+ import { type SubagentConfig, type SubagentName } from './agent-config.js';
6
+ import { type ResearchDelegateTask, type ResearchWorkerOutcome } from './research-runtime.js';
7
+ export declare const CHILD_BOUNDARY = "# \u5B50\u4EE3\u7406\u8FB9\u754C\n\n- \u4F60\u662F\u7531\u9876\u5C42\u4E3B\u4F1A\u8BDD\u6D3E\u53D1\u4EFB\u52A1\u7684 child subagent\uFF0C\u53EA\u6267\u884C\u5F53\u524D\u4EFB\u52A1\u3002\n- \u4E0D\u542F\u52A8\u3001\u5EFA\u8BAE\u6216\u7F16\u6392\u5176\u4ED6\u5B50\u4EE3\u7406\u3002\n- \u53EA\u5728\u4EFB\u52A1\u6388\u6743\u8303\u56F4\u5185\u64CD\u4F5C\uFF1B\u9047\u5230\u672A\u6388\u6743\u7684\u4EA7\u54C1\u3001\u67B6\u6784\u6216\u8303\u56F4\u51B3\u7B56\u65F6\u505C\u6B62\u5E76\u5728\u7ED3\u679C\u4E2D\u8BF4\u660E\u3002\n- \u6700\u7EC8\u56DE\u590D\u9075\u5FAA\u5F53\u524D\u89D2\u8272\u7684\u8F93\u51FA\u8981\u6C42\uFF1B\u5F00\u53D1\u4EFB\u52A1\u8BF4\u660E\u5B8C\u6210\u5185\u5BB9\u3001\u4FEE\u6539\u6587\u4EF6\u548C\u9A8C\u8BC1\u60C5\u51B5\uFF0C\u53EA\u8BFB\u4EFB\u52A1\u76F4\u63A5\u8FD4\u56DE\u5206\u6790\u6216\u5BA1\u67E5\u7ED3\u679C\u3002";
8
+ export declare const RESEARCH_COORDINATOR_BOUNDARY = "# \u5B50\u4EE3\u7406\u8FB9\u754C\n\n- \u4F60\u662F\u7531\u9876\u5C42\u4E3B\u4F1A\u8BDD\u6D3E\u53D1\u4EFB\u52A1\u7684 child subagent\uFF0C\u53EA\u6267\u884C\u5F53\u524D\u4EFB\u52A1\u3002\n- \u4EC5\u53EF\u901A\u8FC7 `research_delegate` \u542F\u52A8\u53F6\u5B50 Research Worker\uFF1B\u4E0D\u5F97\u4F7F\u7528\u6216\u6A21\u62DF\u901A\u7528 Agent \u59D4\u6D3E\u3002\n- \u53EA\u6709\u81F3\u5C11\u4E24\u4E2A\u8303\u56F4\u4E92\u4E0D\u4F9D\u8D56\u7684\u7814\u7A76\u8F68\u9053\u624D\u53EF\u59D4\u6D3E\uFF0CWorker \u6570\u91CF\u7531\u5B9E\u9645\u8303\u56F4\u51B3\u5B9A\u3002\n- \u8C03\u7528 `research_delegate` \u540E\u7B49\u5F85\u5168\u90E8 Worker \u7EC8\u6001\uFF0C\u4E0D\u5728\u7B49\u5F85\u671F\u95F4\u91CD\u590D\u7814\u7A76\u76F8\u540C\u8303\u56F4\u3002\n- \u53EA\u5728\u4EFB\u52A1\u6388\u6743\u8303\u56F4\u5185\u64CD\u4F5C\uFF1B\u9047\u5230\u672A\u6388\u6743\u7684\u4EA7\u54C1\u3001\u67B6\u6784\u6216\u8303\u56F4\u51B3\u7B56\u65F6\u505C\u6B62\u5E76\u5728\u7ED3\u679C\u4E2D\u8BF4\u660E\u3002\n- \u6700\u7EC8\u56DE\u590D\u9075\u5FAA\u5F53\u524D\u89D2\u8272\u7684\u8F93\u51FA\u8981\u6C42\uFF1B\u53EA\u8BFB\u4EFB\u52A1\u76F4\u63A5\u8FD4\u56DE\u7814\u7A76\u7ED3\u679C\u3002";
9
+ export interface SubagentRecord {
10
+ taskId: string;
11
+ agentType: SubagentName;
12
+ title: string;
13
+ status: TaskStatus;
14
+ sessionFile: string;
15
+ startedAt: number;
16
+ endedAt: number | null;
17
+ session: AgentSession | null;
18
+ taskHandle: TaskHandle | null;
19
+ taskInfoUnsubscribe: (() => void) | null;
20
+ pendingSteers: string[];
21
+ steerReady: boolean;
22
+ stopRequested: boolean;
23
+ ownerTaskId: string | null;
24
+ notifyParent: boolean;
25
+ persistLifecycle: boolean;
26
+ resolveRun: ((outcome: ResearchWorkerOutcome) => void) | null;
27
+ }
28
+ export type SubagentParentContext = {
29
+ cwd: string;
30
+ sessionId: string;
31
+ sessionDir: string;
32
+ sessionFile?: string;
33
+ model: ExtensionContext['model'];
34
+ thinkingLevel: ExtensionContext['thinkingLevel'];
35
+ projectTrusted: boolean;
36
+ };
37
+ export declare function excludeSubagentPackageExtensions(result: LoadExtensionsResult, packageRoot?: string): LoadExtensionsResult;
38
+ export declare function childAllowedTools(tools: readonly string[]): string[];
39
+ export declare function restoreSubagentRecords(branch: readonly SessionEntry[]): Map<string, SubagentRecord>;
40
+ export declare function normalizeRetryableAbortedAssistantMessage(message: AssistantMessage, stopRequested: boolean): AssistantMessage | undefined;
41
+ export declare function createRetryCompatibilityExtension(isStopRequested: (sessionId: string) => boolean): ExtensionFactory;
42
+ export declare class SubagentRuntime {
43
+ private readonly pi;
44
+ private readonly modelRuntime;
45
+ private readonly agentDir;
46
+ private readonly plugin;
47
+ private readonly records;
48
+ private readonly activeRuns;
49
+ private readonly pendingCreates;
50
+ private stopping;
51
+ private configs;
52
+ private capabilityRules;
53
+ private readonly childCapabilities;
54
+ private activeParent;
55
+ private sessionActive;
56
+ constructor(pi: ExtensionAPI, modelRuntime: ModelRuntime, agentDir?: string);
57
+ startSession(ctx: ExtensionContext): Promise<void>;
58
+ refreshConfigs(): void;
59
+ beforeTree(signal: AbortSignal): Promise<{
60
+ cancel: true;
61
+ } | undefined>;
62
+ shutdown(): Promise<void>;
63
+ private stopAndWait;
64
+ private registerTools;
65
+ private requireParent;
66
+ private requireText;
67
+ private resolveRecord;
68
+ private agentAllowed;
69
+ private resolveAgentConfig;
70
+ private createSessionManager;
71
+ private startRecord;
72
+ private startAgent;
73
+ private resumeAgent;
74
+ private steerAgent;
75
+ private stopAgent;
76
+ private requestStop;
77
+ private stopOwnedRecords;
78
+ private updateRootActivity;
79
+ private runDelegatedResearchWorker;
80
+ protected delegateResearchWorkers(ownerTaskId: string, tasks: readonly ResearchDelegateTask[], signal: AbortSignal | undefined, onProgress: (completed: number, total: number) => void): Promise<ResearchWorkerOutcome[]>;
81
+ private listAgents;
82
+ private createOwnedChildSession;
83
+ protected createChildSession(config: SubagentConfig, parent: SubagentParentContext, sessionManager: SessionManager): Promise<AgentSession>;
84
+ private resolveModel;
85
+ private launchRecord;
86
+ private flushPendingSteers;
87
+ private finishRun;
88
+ private finishRecord;
89
+ private notifyCompletion;
90
+ private makeLaunchDetails;
91
+ private makeTerminalDetails;
92
+ }
93
+ export declare function registerSubagentExtension(pi: ExtensionAPI): void;