@ct-agents/prompts 0.1.2 → 0.1.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ct-agents/prompts",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src"
@@ -11,8 +11,8 @@
11
11
  },
12
12
  "dependencies": {
13
13
  "zod": "4.4.3",
14
- "@ct-agents/protocol": "0.1.2",
15
- "@ct-agents/store": "0.1.2"
14
+ "@ct-agents/protocol": "0.1.3",
15
+ "@ct-agents/store": "0.1.3"
16
16
  },
17
17
  "publishConfig": {
18
18
  "access": "public"
package/src/load-skill.ts CHANGED
@@ -1,11 +1,8 @@
1
1
  import { z } from 'zod';
2
2
  import type {
3
3
  HarnessConfig,
4
- HarnessConfigStore,
5
4
  RuntimeSkillView,
6
5
  SkillResource,
7
- SkillStore,
8
- SkillView,
9
6
  ToolDescriptor,
10
7
  ToolDescriptorContext,
11
8
  ToolHandler,
@@ -92,62 +89,6 @@ export function resolveHarnessSkillNames(harness: HarnessConfig) {
92
89
  return skillNames;
93
90
  }
94
91
 
95
- async function readHarnessConfig(input: {
96
- harnessStore: HarnessConfigStore;
97
- appId?: string;
98
- harnessId: string;
99
- harnessVersion: number;
100
- }) {
101
- return input.harnessStore.get({
102
- appId: input.appId,
103
- id: input.harnessId,
104
- version: input.harnessVersion,
105
- });
106
- }
107
-
108
- /** 读取 harness 关联且当前启用的 Skill 目录(仅元数据,不含 instructions)。 */
109
- export async function readAvailableSkills(input: {
110
- skillStore: SkillStore;
111
- skillNames: string[];
112
- }) {
113
- const enabledSkills = await input.skillStore.list();
114
- const enabledByName = new Map(enabledSkills.map((skill) => [skill.name, skill]));
115
- return input.skillNames
116
- .map((skillName) => enabledByName.get(skillName))
117
- .filter((skill): skill is SkillView => Boolean(skill))
118
- .map(toCatalogItem);
119
- }
120
-
121
- /**
122
- * 构造 harness 关联 Skill 的 markdown 目录,供 {@link createLoadSkillToolHandler} 注入 description。
123
- *
124
- * 只列关联且启用的 Skill;读取失败或无可用 Skill 时返回空串,让调用方回退静态 description。
125
- */
126
- export async function buildHarnessSkillCatalogMarkdown(input: {
127
- harnessStore: HarnessConfigStore;
128
- skillStore: SkillStore;
129
- appId?: string;
130
- harnessId: string;
131
- harnessVersion: number;
132
- }): Promise<string> {
133
- const harnessConfig = await readHarnessConfig(input);
134
- if (!harnessConfig) {
135
- return '';
136
- }
137
-
138
- const items = await readAvailableSkills({
139
- skillStore: input.skillStore,
140
- skillNames: resolveHarnessSkillNames(harnessConfig),
141
- });
142
- if (items.length === 0) {
143
- return '';
144
- }
145
-
146
- return items
147
- .map((item) => `- ${item.name}:${item.description}`)
148
- .join('\n');
149
- }
150
-
151
92
  function warning(message: string): LoadSkillToolResult {
152
93
  return { status: 'warning', message };
153
94
  }
@@ -1,6 +1,7 @@
1
1
  import type { SqlPool } from '@ct-agents/store';
2
2
  import type {
3
3
  CreateSkillInput,
4
+ SkillKey,
4
5
  SkillListQuery,
5
6
  SkillStore,
6
7
  SkillView,
@@ -18,6 +19,13 @@ type SkillRow = {
18
19
  updated_at?: string | Date | null;
19
20
  };
20
21
 
22
+ function isUniqueViolation(error: unknown) {
23
+ return error !== null
24
+ && typeof error === 'object'
25
+ && 'code' in error
26
+ && error.code === '23505';
27
+ }
28
+
21
29
  function toIsoString(value: string | Date | null | undefined) {
22
30
  if (!value) return undefined;
23
31
  return value instanceof Date ? value.toISOString() : value;
@@ -46,15 +54,19 @@ export class PostgresSkillStore implements SkillStore {
46
54
  // 指定 appId 时按 app 精确过滤,includeGlobal 时额外纳入 global(app_id is null)。
47
55
  const conditions = ['enabled = true'];
48
56
  const params: unknown[] = [];
49
- if (query?.appId) {
57
+ if (query?.appId === null) {
58
+ conditions.push('app_id is null');
59
+ } else if (query?.appId) {
50
60
  params.push(query.appId);
51
61
  conditions.push(query.includeGlobal ? '(app_id = $1 or app_id is null)' : 'app_id = $1');
52
62
  }
63
+ const effective = query?.resolution === 'effective' && Boolean(query.appId);
53
64
  const result = await client.query<SkillRow>(`
54
- select app_id, version, name, description, instructions, enabled, created_at, updated_at
65
+ select ${effective ? 'distinct on (name)' : ''}
66
+ app_id, version, name, description, instructions, enabled, created_at, updated_at
55
67
  from agent.agent_skills
56
68
  where ${conditions.join(' and ')}
57
- order by name asc
69
+ order by name asc${effective ? ', (app_id = $1) desc' : ', app_id asc nulls first'}
58
70
  `, params);
59
71
  return result.rows.map(mapSkillRow);
60
72
  } finally {
@@ -62,15 +74,15 @@ export class PostgresSkillStore implements SkillStore {
62
74
  }
63
75
  }
64
76
 
65
- async get(name: string): Promise<SkillView | null> {
77
+ async get(key: SkillKey): Promise<SkillView | null> {
66
78
  const client = await this.pool.connect();
67
79
  try {
68
80
  const result = await client.query<SkillRow>(`
69
81
  select app_id, version, name, description, instructions, enabled, created_at, updated_at
70
82
  from agent.agent_skills
71
- where name = $1
83
+ where app_id is not distinct from $1 and name = $2
72
84
  limit 1
73
- `, [name]);
85
+ `, [key.appId, key.name]);
74
86
  return result.rows[0] ? mapSkillRow(result.rows[0]) : null;
75
87
  } finally {
76
88
  client.release();
@@ -98,31 +110,37 @@ export class PostgresSkillStore implements SkillStore {
98
110
  input.enabled ?? true,
99
111
  ]);
100
112
  return mapSkillRow(result.rows[0]);
113
+ } catch (error) {
114
+ if (isUniqueViolation(error)) {
115
+ throw new Error(`SKILL_ALREADY_EXISTS: ${input.name.trim()}`);
116
+ }
117
+ throw error;
101
118
  } finally {
102
119
  client.release();
103
120
  }
104
121
  }
105
122
 
106
123
  async update(input: UpdateSkillInput): Promise<SkillView> {
107
- const current = await this.get(input.name);
124
+ const current = await this.get({ appId: input.appId, name: input.name });
108
125
  if (!current) {
109
126
  throw new Error(`Skill not found: ${input.name}`);
110
127
  }
111
128
 
112
129
  const client = await this.pool.connect();
113
130
  try {
114
- // name 为主键不可改,仅作定位;description/instructions/enabled 可更新。
131
+ // app_id + name 是稳定管理身份,仅更新内容与启用状态。
115
132
  const result = await client.query<SkillRow>(`
116
133
  update agent.agent_skills
117
134
  set
118
- description = $2,
119
- instructions = $3,
120
- enabled = $4,
135
+ description = $3,
136
+ instructions = $4,
137
+ enabled = $5,
121
138
  version = version + 1,
122
139
  updated_at = now()
123
- where name = $1 and version = $5
140
+ where app_id is not distinct from $1 and name = $2 and version = $6
124
141
  returning app_id, version, name, description, instructions, enabled, created_at, updated_at
125
142
  `, [
143
+ input.appId,
126
144
  input.name,
127
145
  input.description === undefined ? current.description : input.description.trim(),
128
146
  input.instructions === undefined ? current.instructions : input.instructions.trim(),
@@ -140,7 +158,7 @@ export class PostgresSkillStore implements SkillStore {
140
158
  }
141
159
  }
142
160
 
143
- async delete(name: string): Promise<void> {
161
+ async delete(key: SkillKey): Promise<void> {
144
162
  const client = await this.pool.connect();
145
163
  try {
146
164
  await client.query(`
@@ -149,8 +167,8 @@ export class PostgresSkillStore implements SkillStore {
149
167
  enabled = false,
150
168
  version = version + 1,
151
169
  updated_at = now()
152
- where name = $1
153
- `, [name]);
170
+ where app_id is not distinct from $1 and name = $2
171
+ `, [key.appId, key.name]);
154
172
  } finally {
155
173
  client.release();
156
174
  }
package/src/testing.ts CHANGED
@@ -4,6 +4,8 @@ import type {
4
4
  CreateSkillInput,
5
5
  PromptFragmentStore,
6
6
  PromptFragmentView,
7
+ SkillKey,
8
+ SkillListQuery,
7
9
  SkillStore,
8
10
  SkillView,
9
11
  UpdateSkillInput,
@@ -101,22 +103,38 @@ export class InMemoryPromptFragmentStore implements PromptFragmentStore {
101
103
  export class InMemorySkillStore implements SkillStore {
102
104
  private readonly skills = new Map<string, SkillView>();
103
105
 
104
- async list(query?: { appId?: string; includeGlobal?: boolean }) {
105
- return Array.from(this.skills.values())
106
+ private key(appId: string | null | undefined, name: string) {
107
+ return JSON.stringify([appId ?? null, name]);
108
+ }
109
+
110
+ async list(query?: SkillListQuery) {
111
+ const filtered = Array.from(this.skills.values())
106
112
  .filter((skill) => skill.enabled)
107
113
  .filter((skill) => {
114
+ if (query?.appId === null) {
115
+ return skill.appId === null;
116
+ }
108
117
  if (!query?.appId) {
109
118
  return true;
110
119
  }
111
120
  return query.includeGlobal
112
121
  ? skill.appId === query.appId || skill.appId === null
113
122
  : skill.appId === query.appId;
114
- })
115
- .map((skill) => structuredClone(skill));
123
+ });
124
+ const resolved = query?.resolution === 'effective' && query.appId !== undefined
125
+ ? Array.from(filtered.reduce((byName, skill) => {
126
+ const current = byName.get(skill.name);
127
+ if (!current || (current.appId === null && skill.appId !== null)) {
128
+ byName.set(skill.name, skill);
129
+ }
130
+ return byName;
131
+ }, new Map<string, SkillView>()).values())
132
+ : filtered;
133
+ return resolved.map((skill) => structuredClone(skill));
116
134
  }
117
135
 
118
- async get(name: string) {
119
- const skill = this.skills.get(name);
136
+ async get(input: SkillKey) {
137
+ const skill = this.skills.get(this.key(input.appId, input.name));
120
138
  return skill ? structuredClone(skill) : null;
121
139
  }
122
140
 
@@ -132,12 +150,17 @@ export class InMemorySkillStore implements SkillStore {
132
150
  createdAt: now,
133
151
  updatedAt: now,
134
152
  };
135
- this.skills.set(skill.name, skill);
153
+ const key = this.key(skill.appId, skill.name);
154
+ if (this.skills.has(key)) {
155
+ throw new Error(`SKILL_ALREADY_EXISTS: ${skill.name}`);
156
+ }
157
+ this.skills.set(key, skill);
136
158
  return structuredClone(skill);
137
159
  }
138
160
 
139
161
  async update(input: UpdateSkillInput) {
140
- const current = this.skills.get(input.name);
162
+ const key = this.key(input.appId, input.name);
163
+ const current = this.skills.get(key);
141
164
  if (!current) {
142
165
  throw new Error(`Skill not found: ${input.name}`);
143
166
  }
@@ -153,16 +176,17 @@ export class InMemorySkillStore implements SkillStore {
153
176
  enabled: input.enabled ?? current.enabled,
154
177
  updatedAt: new Date().toISOString(),
155
178
  };
156
- this.skills.set(input.name, next);
179
+ this.skills.set(key, next);
157
180
  return structuredClone(next);
158
181
  }
159
182
 
160
- async delete(name: string) {
161
- const current = this.skills.get(name);
183
+ async delete(input: SkillKey) {
184
+ const key = this.key(input.appId, input.name);
185
+ const current = this.skills.get(key);
162
186
  if (!current) {
163
187
  return;
164
188
  }
165
- this.skills.set(name, {
189
+ this.skills.set(key, {
166
190
  ...current,
167
191
  version: current.version + 1,
168
192
  enabled: false,