@onco-foundry/agent-template 0.6.1 → 0.7.0

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.
@@ -1,4 +1,4 @@
1
- import { defineAgent } from 'agent-lattice';
1
+ import { defineAgent, skill } from 'agent-lattice';
2
2
  import { createTypedAgentTool } from '@onco-foundry/agent-runtime';
3
3
  import { createTracer } from '@onco-foundry/trace-port';
4
4
  import { clampThinkingBudgetTokens } from './agent_template.js';
@@ -12,6 +12,9 @@ export const createAgentSpecExecutor = (declaration) => ({
12
12
  },
13
13
  task: declaration.task,
14
14
  async createSpec(input) {
15
+ if (input.skills?.some(item => Object.keys(item.files).some(path => path !== 'SKILL.md'))) {
16
+ throw new Error('通用执行器不挂载 Skill 资源文件;请使用提供只读工作区的执行器');
17
+ }
15
18
  const memberTools = await Promise.all(input.members.map(async (member) => createTypedAgentTool(await member.agent.createCapability({
16
19
  tracer: input.tracer,
17
20
  modelClient: input.modelClient,
@@ -30,6 +33,7 @@ export const createAgentSpecExecutor = (declaration) => ({
30
33
  ...(input.model.apiKey === undefined ? {} : { apiKey: input.model.apiKey }),
31
34
  modelClient: input.modelClient,
32
35
  systemPrompt: input.systemPrompt,
36
+ ...(input.skills === undefined ? {} : { skills: input.skills.map(item => skill({ name: item.name, description: item.description, instructions: item.instructions })) }),
33
37
  outputSchema: declaration.output,
34
38
  tools: [...input.tools, ...memberTools],
35
39
  ...(maxTokens === undefined ? {} : { maxTokens }),
@@ -4,9 +4,13 @@ import type { Capability } from '@onco-foundry/capability-registry';
4
4
  import type { CapabilityDelegation } from '@onco-foundry/agent-runtime';
5
5
  import type { Tracer } from '@onco-foundry/trace-port';
6
6
  import type { AgentTemplateCard, TemplateToolDefinition } from './agent_template.ts';
7
+ import type { ResolvedSkillPackage, SkillPackageReference, SkillPackageSummary } from './skill_packages.ts';
7
8
  export declare const agentResourceVersionSchema: z.ZodString;
8
9
  export declare const agentVersionSchema: z.ZodObject<{
9
- schemaVersion: z.ZodLiteral<"agent-config-v1">;
10
+ schemaVersion: z.ZodEnum<{
11
+ "agent-config-v1": "agent-config-v1";
12
+ "agent-config-v2": "agent-config-v2";
13
+ }>;
10
14
  executor: z.ZodObject<{
11
15
  id: z.ZodString;
12
16
  version: z.ZodString;
@@ -44,6 +48,10 @@ export declare const agentVersionSchema: z.ZodObject<{
44
48
  id: z.ZodString;
45
49
  version: z.ZodString;
46
50
  }, z.core.$strict>>;
51
+ skills: z.ZodOptional<z.ZodArray<z.ZodObject<{
52
+ id: z.ZodString;
53
+ version: z.ZodString;
54
+ }, z.core.$strict>>>;
47
55
  limits: z.ZodOptional<z.ZodObject<{
48
56
  maxTurns: z.ZodOptional<z.ZodNumber>;
49
57
  maxTokens: z.ZodOptional<z.ZodNumber>;
@@ -74,6 +82,7 @@ export type ResolvedAgentVersion = {
74
82
  }>;
75
83
  tools: AgentVersionToolReference[];
76
84
  members: AgentVersionMemberReference[];
85
+ skills?: SkillPackageSummary[];
77
86
  limits?: AgentVersion['limits'];
78
87
  };
79
88
  export type ResolvedAgentSnapshot = {
@@ -102,6 +111,7 @@ export type ResolvedAgent = {
102
111
  version: string;
103
112
  };
104
113
  readSnapshot(): ResolvedAgentSnapshot;
114
+ readSkills(): ResolvedSkillPackage[];
105
115
  };
106
116
  export type ResolvedAgentMember = {
107
117
  id: string;
@@ -116,6 +126,7 @@ export type AgentExecutorSpecInput<C> = {
116
126
  tools: TemplateToolDefinition[];
117
127
  members: ResolvedAgentMember[];
118
128
  context: C;
129
+ skills?: ResolvedSkillPackage[];
119
130
  tracer?: Tracer;
120
131
  modelClient?: ModelClient;
121
132
  submitOutputEndTurn?: boolean;
@@ -137,11 +148,12 @@ export type AgentVersionPorts<C = unknown> = {
137
148
  readPrompt(reference: AgentVersionPromptReference): Promise<{
138
149
  content: string;
139
150
  }>;
140
- readCommonPrompt(reference: AgentVersionCommonPromptReference): Promise<{
151
+ readCommonPrompt(reference: AgentVersionCommonPromptReference, agentId: string): Promise<{
141
152
  content: string;
142
153
  }>;
143
154
  createTool(reference: AgentVersionToolReference, context: C): TemplateToolDefinition | Promise<TemplateToolDefinition>;
144
155
  resolveExecutor(reference: AgentVersionExecutorReference): AgentVersionExecutor<C>;
156
+ readSkill?(reference: SkillPackageReference): Promise<ResolvedSkillPackage>;
145
157
  };
146
158
  export type ResolveAgentVersionInput<C = unknown> = {
147
159
  agentId: string;
@@ -27,7 +27,7 @@ const memberReferenceSchema = z.object({
27
27
  version: agentResourceVersionSchema,
28
28
  }).strict();
29
29
  export const agentVersionSchema = z.object({
30
- schemaVersion: z.literal('agent-config-v1'),
30
+ schemaVersion: z.enum(['agent-config-v1', 'agent-config-v2']),
31
31
  executor: executorReferenceSchema,
32
32
  model: z.object({
33
33
  configVersion: z.string().min(1),
@@ -41,9 +41,15 @@ export const agentVersionSchema = z.object({
41
41
  commonPrompts: z.array(commonPromptReferenceSchema),
42
42
  tools: z.array(localToolReferenceSchema),
43
43
  members: z.array(memberReferenceSchema),
44
+ skills: z.array(z.object({ id: z.string().regex(/^[a-z][a-z0-9-]{0,79}$/), version: agentResourceVersionSchema }).strict()).max(32).optional(),
44
45
  limits: z.object({
45
46
  maxTurns: z.number().int().positive().optional(),
46
47
  maxTokens: z.number().int().min(2).optional(),
47
48
  thinkingBudgetTokens: z.number().int().positive().optional(),
48
49
  }).strict().optional(),
49
- }).strict();
50
+ }).strict().superRefine((value, ctx) => {
51
+ if (value.schemaVersion === 'agent-config-v1' && value.skills !== undefined)
52
+ ctx.addIssue({ code: 'custom', path: ['skills'], message: 'Skill 引用需要 agent-config-v2' });
53
+ if (value.schemaVersion === 'agent-config-v2' && value.skills === undefined)
54
+ ctx.addIssue({ code: 'custom', path: ['skills'], message: 'agent-config-v2 必须声明 skills' });
55
+ });
@@ -41,6 +41,7 @@ export const createAgentVersionResolver = (ports) => {
41
41
  commonPrompts: record.commonPrompts.map(({ content: _content, ...part }) => ({ ...part })),
42
42
  tools: record.config.tools.map(tool => ({ ...tool })),
43
43
  members: record.config.members.map(member => ({ ...member })),
44
+ ...(record.skills === undefined ? {} : { skills: record.skills.map(({ id, version, name, description, checksum }) => ({ id, version, name, description, checksum })) }),
44
45
  ...(record.config.limits === undefined ? {} : { limits: { ...record.config.limits } }),
45
46
  };
46
47
  for (const member of record.config.members)
@@ -85,6 +86,23 @@ export const createAgentVersionResolver = (ports) => {
85
86
  try {
86
87
  const executor = ports.resolveExecutor(loaded.config.executor);
87
88
  executor.validate?.({ agentId, config: loaded.config });
89
+ const skills = loaded.config.skills === undefined ? undefined : [];
90
+ const skillNames = new Set();
91
+ const skillIds = new Set();
92
+ for (const reference of loaded.config.skills ?? []) {
93
+ if (!ports.readSkill)
94
+ throw new AppError('当前宿主未提供 Skill 读取端口');
95
+ if (skillIds.has(reference.id))
96
+ throw new AppError(`重复的 Skill 引用:${reference.id}`);
97
+ const skill = await ports.readSkill(reference);
98
+ if (skill.id !== reference.id || skill.version !== reference.version || !/^[a-f0-9]{64}$/.test(skill.checksum))
99
+ throw new AppError(`Skill 解析结果与精确引用不符:${reference.id}`);
100
+ if (skillNames.has(skill.name))
101
+ throw new AppError(`Skill 名称冲突:${skill.name}`);
102
+ skillIds.add(reference.id);
103
+ skillNames.add(skill.name);
104
+ skills.push(structuredClone(skill));
105
+ }
88
106
  const promptVersion = agentId === input.agentId
89
107
  ? input.promptVersion
90
108
  : input.memberPromptVersions?.[agentId];
@@ -97,7 +115,7 @@ export const createAgentVersionResolver = (ports) => {
97
115
  ports.resolveModel(loaded.config.model),
98
116
  ]);
99
117
  const commonPrompts = await Promise.all(loaded.config.commonPrompts.map(async (reference) => {
100
- const { content } = await ports.readCommonPrompt(reference);
118
+ const { content } = await ports.readCommonPrompt(reference, agentId);
101
119
  return { ...reference, content, checksum: checksum(content) };
102
120
  }));
103
121
  const ordered = [...commonPrompts].sort((a, b) => a.placement === b.placement
@@ -121,6 +139,7 @@ export const createAgentVersionResolver = (ports) => {
121
139
  commonPrompts,
122
140
  systemPrompt,
123
141
  systemPromptChecksum: checksum(systemPrompt),
142
+ ...(skills === undefined ? {} : { skills }),
124
143
  };
125
144
  records.set(agentId, record);
126
145
  const createSpec = async (options = {}) => {
@@ -135,6 +154,7 @@ export const createAgentVersionResolver = (ports) => {
135
154
  config: record.config,
136
155
  model: record.model,
137
156
  systemPrompt: record.systemPrompt,
157
+ ...(record.skills === undefined ? {} : { skills: structuredClone(record.skills) }),
138
158
  tools,
139
159
  members: await Promise.all(record.config.members.map(async (member) => ({
140
160
  id: member.id,
@@ -172,6 +192,7 @@ export const createAgentVersionResolver = (ports) => {
172
192
  },
173
193
  readSystemPrompt: () => ({ content: record.systemPrompt, version: record.prompt.version }),
174
194
  readSnapshot: () => structuredClone(snapshotFor(agentId)),
195
+ readSkills: () => structuredClone(record.skills ?? []),
175
196
  };
176
197
  record.agent = agent;
177
198
  for (const member of record.config.members)
@@ -1,4 +1,5 @@
1
1
  import type { TemplateToolDefinition } from './agent_template.ts';
2
+ import { type SkillPackageFiles, type SkillPackageReference } from './skill_packages.ts';
2
3
  import type { AgentVersion, AgentVersionCommonPromptReference, AgentVersionExecutor, AgentVersionPorts, AgentVersionPromptReference } from './agent_version.ts';
3
4
  export declare const createFakeAgentVersionPorts: <C>(options: {
4
5
  executors: Record<string, AgentVersionExecutor<C>>;
@@ -9,4 +10,5 @@ export declare const createFakeAgentVersionPorts: <C>(options: {
9
10
  activateAgentVersion(agentId: string, version: string): Promise<void>;
10
11
  setPrompt(reference: AgentVersionPromptReference, content: string): void;
11
12
  setCommonPrompt(reference: AgentVersionCommonPromptReference, content: string): void;
13
+ writeSkill(reference: SkillPackageReference, files: SkillPackageFiles): Promise<void>;
12
14
  };
@@ -1,5 +1,6 @@
1
1
  import { AppError } from '@onco-foundry/errors';
2
2
  import { activateVersion, createFakeVersionedResourceStore, createVersionedResource, } from '@onco-foundry/resource-versioning';
3
+ import { createSkillPackages } from './skill_packages.js';
3
4
  const promptKey = (reference) => `${reference.resource}@${reference.version}`;
4
5
  const commonPromptKey = (reference) => `${reference.source}:${reference.id}@${reference.version}`;
5
6
  const executorKey = (reference) => `${reference.id}@${reference.version}`;
@@ -8,6 +9,12 @@ export const createFakeAgentVersionPorts = (options) => {
8
9
  const stores = new Map();
9
10
  const prompts = new Map();
10
11
  const commonPrompts = new Map();
12
+ const skillStores = new Map();
13
+ const skills = createSkillPackages({ storeFor: id => {
14
+ if (!skillStores.has(id))
15
+ skillStores.set(id, createFakeVersionedResourceStore());
16
+ return skillStores.get(id);
17
+ } });
11
18
  const ports = {
12
19
  storeFor(agentId) {
13
20
  let store = stores.get(agentId);
@@ -17,6 +24,8 @@ export const createFakeAgentVersionPorts = (options) => {
17
24
  }
18
25
  return store;
19
26
  },
27
+ readSkill: reference => skills.read(reference),
28
+ async writeSkill(reference, files) { await skills.publish({ ...reference, files }); },
20
29
  resolveModel: async () => options.modelApiKey === undefined ? {} : { apiKey: options.modelApiKey },
21
30
  readPrompt: async (reference) => {
22
31
  const content = prompts.get(promptKey(reference));
package/dist/index.d.ts CHANGED
@@ -4,3 +4,4 @@ export { createAgentSpecExecutor, type AgentSpecExecutorDeclaration } from './ag
4
4
  export { agentResourceVersionSchema, agentVersionSchema, type AgentVersion, type AgentVersionCommonPromptReference, type AgentVersionExecutor, type AgentVersionExecutorReference, type AgentVersionMemberReference, type AgentVersionModel, type AgentVersionPorts, type AgentVersionPromptReference, type AgentVersionToolReference, type CreateResolvedAgentSpecOptions, type ResolveAgentVersionInput, type ResolvedAgent, type ResolvedAgentMember, type ResolvedAgentModel, type ResolvedAgentSnapshot, type ResolvedAgentVersion, } from './agent_version.ts';
5
5
  export { createAgentVersionResolver } from './agent_version_resolver.ts';
6
6
  export { createFakeAgentVersionPorts } from './fake_agent_version_ports.ts';
7
+ export { createSkillPackages, type SkillPackageFiles, type SkillPackageReference, type SkillPackageSummary, type ResolvedSkillPackage } from './skill_packages.ts';
package/dist/index.js CHANGED
@@ -3,3 +3,4 @@ export { createAgentSpecExecutor } from './agent_spec_executor.js';
3
3
  export { agentResourceVersionSchema, agentVersionSchema, } from './agent_version.js';
4
4
  export { createAgentVersionResolver } from './agent_version_resolver.js';
5
5
  export { createFakeAgentVersionPorts } from './fake_agent_version_ports.js';
6
+ export { createSkillPackages } from './skill_packages.js';
@@ -0,0 +1,33 @@
1
+ import { z } from 'zod';
2
+ import { type VersionedResourceStore } from '@onco-foundry/resource-versioning';
3
+ declare const filesSchema: z.ZodRecord<z.ZodString, z.ZodObject<{
4
+ encoding: z.ZodEnum<{
5
+ base64: "base64";
6
+ utf8: "utf8";
7
+ }>;
8
+ content: z.ZodString;
9
+ }, z.core.$strict>>;
10
+ export type SkillPackageFiles = z.infer<typeof filesSchema>;
11
+ export type SkillPackageReference = {
12
+ id: string;
13
+ version: string;
14
+ };
15
+ export type SkillPackageSummary = SkillPackageReference & {
16
+ name: string;
17
+ description: string;
18
+ checksum: string;
19
+ };
20
+ export type ResolvedSkillPackage = SkillPackageSummary & {
21
+ instructions: string;
22
+ files: SkillPackageFiles;
23
+ };
24
+ /** 文件是数据,不接收文件系统路径或链接;实际只读挂载由宿主运行器负责。 */
25
+ export declare const createSkillPackages: (deps: {
26
+ storeFor(id: string): VersionedResourceStore;
27
+ }) => {
28
+ publish(input: SkillPackageReference & {
29
+ files: SkillPackageFiles;
30
+ }): Promise<ResolvedSkillPackage>;
31
+ read: (reference: SkillPackageReference) => Promise<ResolvedSkillPackage>;
32
+ };
33
+ export {};
@@ -0,0 +1,74 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { parse } from 'yaml';
3
+ import { z } from 'zod';
4
+ import { createVersionedResource, loadActiveVersionedResource } from '@onco-foundry/resource-versioning';
5
+ import { AppError } from '@onco-foundry/errors';
6
+ const idSchema = z.string().regex(/^[a-z][a-z0-9-]{0,79}$/);
7
+ const versionSchema = z.string().regex(/^v[1-9]\d{0,8}$/);
8
+ const fileSchema = z.object({ encoding: z.enum(['utf8', 'base64']), content: z.string().max(24 * 1024 * 1024) }).strict();
9
+ const filesSchema = z.record(z.string(), fileSchema);
10
+ const hash = (value) => createHash('sha256').update(value).digest('hex');
11
+ /** 文件是数据,不接收文件系统路径或链接;实际只读挂载由宿主运行器负责。 */
12
+ export const createSkillPackages = (deps) => {
13
+ const inspect = (input) => {
14
+ const files = filesSchema.parse(input);
15
+ const paths = Object.keys(files).sort();
16
+ if (paths.length === 0 || paths.length > 128)
17
+ throw new AppError('技能包文件数必须为 1 到 128');
18
+ let size = 0;
19
+ for (const path of paths) {
20
+ const parts = path.split('/');
21
+ if (path.length > 240 || parts.some(part => !part || part === '.' || part === '..' || /[\\:\x00-\x1f\x7f]/.test(part))) {
22
+ throw new AppError(`技能包文件路径非法:${path}`);
23
+ }
24
+ if (parts.at(-1) === 'SKILL.md' && path !== 'SKILL.md')
25
+ throw new AppError('技能包只能在根目录包含一个 SKILL.md');
26
+ for (let count = 1; count < parts.length; count++) {
27
+ if (Object.hasOwn(files, parts.slice(0, count).join('/')))
28
+ throw new AppError(`技能包文件与目录冲突:${path}`);
29
+ }
30
+ const file = files[path];
31
+ const bytes = Buffer.from(file.content, file.encoding === 'utf8' ? 'utf8' : 'base64');
32
+ if (file.encoding === 'base64' && bytes.toString('base64') !== file.content)
33
+ throw new AppError(`技能包 base64 非法:${path}`);
34
+ size += bytes.byteLength;
35
+ if (size > 16 * 1024 * 1024)
36
+ throw new AppError('技能包解码后不能超过 16 MiB');
37
+ }
38
+ const skill = files['SKILL.md'];
39
+ if (!skill || skill.encoding !== 'utf8' || Buffer.byteLength(skill.content) > 100_000)
40
+ throw new AppError('技能包必须包含 UTF-8 SKILL.md,且不能超过 100000 字节');
41
+ const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]+)$/.exec(skill.content);
42
+ if (!match)
43
+ throw new AppError('SKILL.md 必须包含 YAML frontmatter 和正文');
44
+ const metadata = z.object({ name: idSchema, description: z.string().trim().min(1).max(2048) }).passthrough().parse(parse(match[1], { maxAliasCount: 0, uniqueKeys: true }));
45
+ const instructions = match[2].trim();
46
+ if (!instructions)
47
+ throw new AppError('SKILL.md 正文不能为空');
48
+ return { name: metadata.name, description: metadata.description, instructions, files: Object.fromEntries(paths.map(path => [path, files[path]])) };
49
+ };
50
+ const read = async (reference) => {
51
+ idSchema.parse(reference.id);
52
+ versionSchema.parse(reference.version);
53
+ const loaded = await loadActiveVersionedResource({ store: deps.storeFor(reference.id), versionOverride: reference.version });
54
+ const content = loaded.files.get('skill.json');
55
+ if (content === undefined || Buffer.byteLength(content) > 24 * 1024 * 1024)
56
+ throw new AppError('技能包缺少 skill.json 或内容过大');
57
+ const bundle = z.object({ schemaVersion: z.literal('skill-package-v1'), files: filesSchema }).strict().parse(JSON.parse(content));
58
+ return { ...reference, ...inspect(bundle.files), checksum: hash(content) };
59
+ };
60
+ return {
61
+ async publish(input) {
62
+ idSchema.parse(input.id);
63
+ versionSchema.parse(input.version);
64
+ const inspected = inspect(input.files);
65
+ const content = JSON.stringify({ schemaVersion: 'skill-package-v1', files: inspected.files });
66
+ if (Buffer.byteLength(content) > 24 * 1024 * 1024)
67
+ throw new AppError('技能包编码后不能超过 24 MiB');
68
+ await createVersionedResource({ store: deps.storeFor(input.id), version: input.version,
69
+ files: { 'skill.json': content } });
70
+ return await read({ id: input.id, version: input.version });
71
+ },
72
+ read,
73
+ };
74
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onco-foundry/agent-template",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist"
@@ -13,12 +13,13 @@
13
13
  },
14
14
  "dependencies": {
15
15
  "agent-lattice": "0.26.0",
16
+ "yaml": "^2.9.1",
16
17
  "zod": "^4.4.3",
17
18
  "@onco-foundry/agent-runtime": "0.3.0",
19
+ "@onco-foundry/errors": "0.1.1",
18
20
  "@onco-foundry/capability-registry": "0.1.1",
19
21
  "@onco-foundry/resource-versioning": "0.3.0",
20
- "@onco-foundry/trace-port": "0.3.3",
21
- "@onco-foundry/errors": "0.1.1"
22
+ "@onco-foundry/trace-port": "0.3.3"
22
23
  },
23
24
  "publishConfig": {
24
25
  "access": "public"