@ct-agents/prompts 0.1.8 → 0.2.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ct-agents/prompts",
3
- "version": "0.1.8",
3
+ "version": "0.2.0",
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.8",
15
- "@ct-agents/store": "0.1.8"
14
+ "@ct-agents/protocol": "0.2.0",
15
+ "@ct-agents/store": "0.2.0"
16
16
  },
17
17
  "publishConfig": {
18
18
  "access": "public"
package/src/index.ts CHANGED
@@ -1,5 +1,3 @@
1
- import { randomUUID } from 'node:crypto';
2
- import type { SqlPool } from '@ct-agents/store';
3
1
  import type {
4
2
  AssemblePromptInput,
5
3
  CreatePromptFragmentInput,
@@ -8,255 +6,9 @@ import type {
8
6
  PromptFragmentView,
9
7
  PromptGenerator,
10
8
  PromptRegistry,
11
- ResourceSlots,
12
9
  UpdatePromptFragmentInput,
13
10
  } from '@ct-agents/protocol';
14
11
 
15
- type PromptFragmentRow = {
16
- app_id?: string | null;
17
- id: string;
18
- version: number;
19
- name: string;
20
- description?: string | null;
21
- content: string;
22
- enabled: boolean;
23
- created_at?: string | Date | null;
24
- updated_at?: string | Date | null;
25
- };
26
-
27
- function assertFragmentContent(name: string, content: string) {
28
- if (!name.trim()) {
29
- throw new Error('name 为必填');
30
- }
31
- if (!content.trim()) {
32
- throw new Error('content 为必填');
33
- }
34
- }
35
-
36
- function toIsoString(value: string | Date | null | undefined) {
37
- if (!value) {
38
- return undefined;
39
- }
40
-
41
- return value instanceof Date ? value.toISOString() : value;
42
- }
43
-
44
- function mapPromptFragmentRow(row: PromptFragmentRow): PromptFragmentView {
45
- return {
46
- appId: row.app_id ?? null,
47
- id: row.id,
48
- version: row.version,
49
- name: row.name,
50
- ...(row.description?.trim() ? { description: row.description.trim() } : {}),
51
- content: row.content,
52
- enabled: row.enabled,
53
- source: {
54
- kind: 'persisted',
55
- },
56
- createdAt: toIsoString(row.created_at),
57
- updatedAt: toIsoString(row.updated_at),
58
- };
59
- }
60
-
61
- export type PostgresPromptFragmentStoreOptions = {
62
- createId?: () => string;
63
- };
64
-
65
- export class PostgresPromptFragmentStore implements PromptFragmentStore {
66
- private readonly createId: () => string;
67
-
68
- constructor(
69
- private readonly pool: SqlPool,
70
- options: PostgresPromptFragmentStoreOptions = {},
71
- ) {
72
- this.createId = options.createId ?? (() => `persisted:${randomUUID()}`);
73
- }
74
-
75
- async list(query?: { appId?: string; includeGlobal?: boolean }) {
76
- const client = await this.pool.connect();
77
-
78
- try {
79
- const conditions = ['enabled = true'];
80
- const params: unknown[] = [];
81
- if (query?.appId) {
82
- params.push(query.appId);
83
- conditions.push(query.includeGlobal ? '(app_id = $1 or app_id is null)' : 'app_id = $1');
84
- }
85
- const result = await client.query<PromptFragmentRow>(
86
- `
87
- select
88
- app_id,
89
- id,
90
- version,
91
- name,
92
- description,
93
- content,
94
- enabled,
95
- created_at,
96
- updated_at
97
- from agent.agent_prompt_fragments
98
- where ${conditions.join(' and ')}
99
- order by updated_at desc, id desc
100
- `,
101
- params,
102
- );
103
-
104
- return result.rows.map(mapPromptFragmentRow);
105
- } finally {
106
- client.release();
107
- }
108
- }
109
-
110
- async get(id: string) {
111
- const client = await this.pool.connect();
112
-
113
- try {
114
- const result = await client.query<PromptFragmentRow>(
115
- `
116
- select
117
- app_id,
118
- id,
119
- version,
120
- name,
121
- description,
122
- content,
123
- enabled,
124
- created_at,
125
- updated_at
126
- from agent.agent_prompt_fragments
127
- where id = $1
128
- limit 1
129
- `,
130
- [id],
131
- );
132
-
133
- return result.rows[0] ? mapPromptFragmentRow(result.rows[0]) : null;
134
- } finally {
135
- client.release();
136
- }
137
- }
138
-
139
- async create(input: CreatePromptFragmentInput) {
140
- assertFragmentContent(input.name, input.content);
141
- const client = await this.pool.connect();
142
-
143
- try {
144
- const result = await client.query<PromptFragmentRow>(
145
- `
146
- insert into agent.agent_prompt_fragments (
147
- app_id,
148
- id,
149
- name,
150
- description,
151
- content,
152
- enabled
153
- )
154
- values ($1, $2, $3, $4, $5, $6)
155
- returning
156
- app_id,
157
- id,
158
- version,
159
- name,
160
- description,
161
- content,
162
- enabled,
163
- created_at,
164
- updated_at
165
- `,
166
- [
167
- input.appId ?? null,
168
- this.createId(),
169
- input.name.trim(),
170
- input.description?.trim() || null,
171
- input.content.trim(),
172
- input.enabled ?? true,
173
- ],
174
- );
175
-
176
- return mapPromptFragmentRow(result.rows[0]);
177
- } finally {
178
- client.release();
179
- }
180
- }
181
-
182
- async update(input: UpdatePromptFragmentInput) {
183
- const current = await this.get(input.id);
184
- if (!current) {
185
- throw new Error(`Prompt fragment not found: ${input.id}`);
186
- }
187
-
188
- const name = input.name === undefined ? current.name : input.name.trim();
189
- const description = input.description === undefined
190
- ? current.description ?? null
191
- : (input.description.trim() || null);
192
- const content = input.content === undefined ? current.content : input.content.trim();
193
- assertFragmentContent(name, content);
194
- const client = await this.pool.connect();
195
-
196
- try {
197
- const result = await client.query<PromptFragmentRow>(
198
- `
199
- update agent.agent_prompt_fragments
200
- set
201
- name = $2,
202
- description = $3,
203
- content = $4,
204
- enabled = $5,
205
- version = version + 1,
206
- updated_at = now()
207
- where id = $1 and version = $6
208
- returning
209
- app_id,
210
- id,
211
- version,
212
- name,
213
- description,
214
- content,
215
- enabled,
216
- created_at,
217
- updated_at
218
- `,
219
- [
220
- input.id,
221
- name,
222
- description,
223
- content,
224
- input.enabled ?? current.enabled,
225
- input.baseVersion,
226
- ],
227
- );
228
-
229
- if (!result.rows[0]) {
230
- throw new Error(`VERSION_CONFLICT: Prompt fragment has changed: ${input.id}`);
231
- }
232
-
233
- return mapPromptFragmentRow(result.rows[0]);
234
- } finally {
235
- client.release();
236
- }
237
- }
238
-
239
- async delete(id: string) {
240
- const client = await this.pool.connect();
241
-
242
- try {
243
- await client.query(
244
- `
245
- update agent.agent_prompt_fragments
246
- set
247
- enabled = false,
248
- version = version + 1,
249
- updated_at = now()
250
- where id = $1
251
- `,
252
- [id],
253
- );
254
- } finally {
255
- client.release();
256
- }
257
- }
258
- }
259
-
260
12
  export type DefaultPromptRegistryDependencies = {
261
13
  store: PromptFragmentStore;
262
14
  generators?: GeneratorRegistry;
@@ -451,9 +203,7 @@ export class DefaultPromptRegistry implements PromptRegistry {
451
203
  }
452
204
 
453
205
  async assemblePrompt(input: AssemblePromptInput) {
454
- const persisted = await this.dependencies.store.list(input.appId
455
- ? { appId: input.appId, includeGlobal: true }
456
- : undefined);
206
+ const persisted = await this.dependencies.store.list();
457
207
  const byId = new Map<string, PromptFragmentView>(
458
208
  persisted.map((fragment) => [`fragment:${fragment.id}`, fragment]),
459
209
  );
@@ -501,13 +251,10 @@ export class DefaultPromptRegistry implements PromptRegistry {
501
251
  .join('\n--------------\n');
502
252
  }
503
253
 
504
- async hasPromptReference(input: { appId?: string; id: string }) {
254
+ async hasPromptReference(input: { id: string }) {
505
255
  if (input.id.startsWith('fragment:')) {
506
256
  const fragment = await this.dependencies.store.get(input.id.slice('fragment:'.length));
507
- return Boolean(
508
- fragment?.enabled
509
- && (!input.appId || fragment.appId === input.appId || fragment.appId === null),
510
- );
257
+ return Boolean(fragment?.enabled);
511
258
  }
512
259
  if (input.id.startsWith('generator:')) {
513
260
  return Boolean(this.generators.get(input.id.slice('generator:'.length)));
@@ -515,8 +262,8 @@ export class DefaultPromptRegistry implements PromptRegistry {
515
262
  return false;
516
263
  }
517
264
 
518
- async listPromptFragments(query?: Parameters<PromptFragmentStore['list']>[0]) {
519
- const persisted = await this.dependencies.store.list(query);
265
+ async listPromptFragments() {
266
+ const persisted = await this.dependencies.store.list();
520
267
  return [...persisted];
521
268
  }
522
269
 
@@ -542,5 +289,4 @@ export function createDefaultPromptRegistry(dependencies: DefaultPromptRegistryD
542
289
  return new DefaultPromptRegistry(dependencies);
543
290
  }
544
291
 
545
- export { PostgresSkillStore } from './postgres-skill-store.js';
546
292
  export * from './load-skill.js';
package/src/load-skill.ts CHANGED
@@ -111,26 +111,6 @@ const loadSkillDescriptor: ToolDescriptor = {
111
111
  },
112
112
  },
113
113
  },
114
- resultSchema: {
115
- type: 'object',
116
- additionalProperties: false,
117
- required: ['status'],
118
- properties: {
119
- status: { enum: ['ok', 'warning'] },
120
- message: { type: 'string' },
121
- skill: {
122
- type: 'object',
123
- additionalProperties: false,
124
- required: ['name', 'description', 'instructions', 'version'],
125
- properties: {
126
- name: { type: 'string' },
127
- description: { type: 'string' },
128
- instructions: { type: 'string' },
129
- version: { type: 'integer' },
130
- },
131
- },
132
- },
133
- },
134
114
  annotations: {
135
115
  readOnly: true,
136
116
  idempotent: true,
@@ -182,7 +162,6 @@ export function createLoadSkillToolHandler(_deps: LoadSkillToolDeps = {}): ToolH
182
162
  }
183
163
  },
184
164
  inputSchema: loadSkillToolInputSchema,
185
- resultSchema: loadSkillToolResultSchema,
186
165
  async execute(input, context) {
187
166
  const skills = context.resources.skills;
188
167
  if (!skills) {
package/src/testing.ts CHANGED
@@ -4,8 +4,6 @@ import type {
4
4
  CreateSkillInput,
5
5
  PromptFragmentStore,
6
6
  PromptFragmentView,
7
- SkillKey,
8
- SkillListQuery,
9
7
  SkillStore,
10
8
  SkillView,
11
9
  UpdateSkillInput,
@@ -19,17 +17,9 @@ import {
19
17
  export class InMemoryPromptFragmentStore implements PromptFragmentStore {
20
18
  private readonly fragments = new Map<string, PromptFragmentView>();
21
19
 
22
- async list(query?: { appId?: string; includeGlobal?: boolean }) {
20
+ async list() {
23
21
  return Array.from(this.fragments.values())
24
22
  .filter((fragment) => fragment.enabled)
25
- .filter((fragment) => {
26
- if (!query?.appId) {
27
- return true;
28
- }
29
- return query.includeGlobal
30
- ? fragment.appId === query.appId || fragment.appId === null
31
- : fragment.appId === query.appId;
32
- })
33
23
  .map((fragment) => structuredClone(fragment))
34
24
  .sort((left, right) => (right.updatedAt ?? '').localeCompare(left.updatedAt ?? ''));
35
25
  }
@@ -42,7 +32,6 @@ export class InMemoryPromptFragmentStore implements PromptFragmentStore {
42
32
  async create(input: CreatePromptFragmentInput) {
43
33
  const now = new Date().toISOString();
44
34
  const fragment: PromptFragmentView = {
45
- appId: input.appId ?? null,
46
35
  id: `persisted:${randomUUID()}`,
47
36
  version: 1,
48
37
  name: input.name.trim(),
@@ -103,45 +92,20 @@ export class InMemoryPromptFragmentStore implements PromptFragmentStore {
103
92
  export class InMemorySkillStore implements SkillStore {
104
93
  private readonly skills = new Map<string, SkillView>();
105
94
 
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())
95
+ async list() {
96
+ return Array.from(this.skills.values())
112
97
  .filter((skill) => skill.enabled)
113
- .filter((skill) => {
114
- if (query?.appId === null) {
115
- return skill.appId === null;
116
- }
117
- if (!query?.appId) {
118
- return true;
119
- }
120
- return query.includeGlobal
121
- ? skill.appId === query.appId || skill.appId === null
122
- : skill.appId === query.appId;
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));
98
+ .map((skill) => structuredClone(skill));
134
99
  }
135
100
 
136
- async get(input: SkillKey) {
137
- const skill = this.skills.get(this.key(input.appId, input.name));
101
+ async get(name: string) {
102
+ const skill = this.skills.get(name);
138
103
  return skill ? structuredClone(skill) : null;
139
104
  }
140
105
 
141
106
  async create(input: CreateSkillInput) {
142
107
  const now = new Date().toISOString();
143
108
  const skill: SkillView = {
144
- appId: input.appId ?? null,
145
109
  name: input.name.trim(),
146
110
  version: 1,
147
111
  description: input.description.trim(),
@@ -150,7 +114,7 @@ export class InMemorySkillStore implements SkillStore {
150
114
  createdAt: now,
151
115
  updatedAt: now,
152
116
  };
153
- const key = this.key(skill.appId, skill.name);
117
+ const key = skill.name;
154
118
  if (this.skills.has(key)) {
155
119
  throw new Error(`SKILL_ALREADY_EXISTS: ${skill.name}`);
156
120
  }
@@ -159,7 +123,7 @@ export class InMemorySkillStore implements SkillStore {
159
123
  }
160
124
 
161
125
  async update(input: UpdateSkillInput) {
162
- const key = this.key(input.appId, input.name);
126
+ const key = input.name;
163
127
  const current = this.skills.get(key);
164
128
  if (!current) {
165
129
  throw new Error(`Skill not found: ${input.name}`);
@@ -180,13 +144,12 @@ export class InMemorySkillStore implements SkillStore {
180
144
  return structuredClone(next);
181
145
  }
182
146
 
183
- async delete(input: SkillKey) {
184
- const key = this.key(input.appId, input.name);
185
- const current = this.skills.get(key);
147
+ async delete(name: string) {
148
+ const current = this.skills.get(name);
186
149
  if (!current) {
187
150
  return;
188
151
  }
189
- this.skills.set(key, {
152
+ this.skills.set(name, {
190
153
  ...current,
191
154
  version: current.version + 1,
192
155
  enabled: false,
@@ -1,176 +0,0 @@
1
- import type { SqlPool } from '@ct-agents/store';
2
- import type {
3
- CreateSkillInput,
4
- SkillKey,
5
- SkillListQuery,
6
- SkillStore,
7
- SkillView,
8
- UpdateSkillInput,
9
- } from '@ct-agents/protocol';
10
-
11
- type SkillRow = {
12
- app_id?: string | null;
13
- version: number;
14
- name: string;
15
- description: string;
16
- instructions: string;
17
- enabled: boolean;
18
- created_at?: string | Date | null;
19
- updated_at?: string | Date | null;
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
-
29
- function toIsoString(value: string | Date | null | undefined) {
30
- if (!value) return undefined;
31
- return value instanceof Date ? value.toISOString() : value;
32
- }
33
-
34
- function mapSkillRow(row: SkillRow): SkillView {
35
- return {
36
- appId: row.app_id ?? null,
37
- version: row.version,
38
- name: row.name,
39
- description: row.description,
40
- instructions: row.instructions,
41
- enabled: row.enabled,
42
- createdAt: toIsoString(row.created_at),
43
- updatedAt: toIsoString(row.updated_at),
44
- };
45
- }
46
-
47
- export class PostgresSkillStore implements SkillStore {
48
- constructor(private readonly pool: SqlPool) {}
49
-
50
- async list(query?: SkillListQuery): Promise<SkillView[]> {
51
- const client = await this.pool.connect();
52
- try {
53
- // 与 PostgresPromptFragmentStore 保持一致的 app 归属过滤:
54
- // 指定 appId 时按 app 精确过滤,includeGlobal 时额外纳入 global(app_id is null)。
55
- const conditions = ['enabled = true'];
56
- const params: unknown[] = [];
57
- if (query?.appId === null) {
58
- conditions.push('app_id is null');
59
- } else if (query?.appId) {
60
- params.push(query.appId);
61
- conditions.push(query.includeGlobal ? '(app_id = $1 or app_id is null)' : 'app_id = $1');
62
- }
63
- const effective = query?.resolution === 'effective' && Boolean(query.appId);
64
- const result = await client.query<SkillRow>(`
65
- select ${effective ? 'distinct on (name)' : ''}
66
- app_id, version, name, description, instructions, enabled, created_at, updated_at
67
- from agent.agent_skills
68
- where ${conditions.join(' and ')}
69
- order by name asc${effective ? ', (app_id = $1) desc' : ', app_id asc nulls first'}
70
- `, params);
71
- return result.rows.map(mapSkillRow);
72
- } finally {
73
- client.release();
74
- }
75
- }
76
-
77
- async get(key: SkillKey): Promise<SkillView | null> {
78
- const client = await this.pool.connect();
79
- try {
80
- const result = await client.query<SkillRow>(`
81
- select app_id, version, name, description, instructions, enabled, created_at, updated_at
82
- from agent.agent_skills
83
- where app_id is not distinct from $1 and name = $2
84
- limit 1
85
- `, [key.appId, key.name]);
86
- return result.rows[0] ? mapSkillRow(result.rows[0]) : null;
87
- } finally {
88
- client.release();
89
- }
90
- }
91
-
92
- async create(input: CreateSkillInput): Promise<SkillView> {
93
- const client = await this.pool.connect();
94
- try {
95
- const result = await client.query<SkillRow>(`
96
- insert into agent.agent_skills (
97
- app_id,
98
- name,
99
- description,
100
- instructions,
101
- enabled
102
- )
103
- values ($1, $2, $3, $4, $5)
104
- returning app_id, version, name, description, instructions, enabled, created_at, updated_at
105
- `, [
106
- input.appId ?? null,
107
- input.name.trim(),
108
- input.description.trim(),
109
- input.instructions.trim(),
110
- input.enabled ?? true,
111
- ]);
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;
118
- } finally {
119
- client.release();
120
- }
121
- }
122
-
123
- async update(input: UpdateSkillInput): Promise<SkillView> {
124
- const current = await this.get({ appId: input.appId, name: input.name });
125
- if (!current) {
126
- throw new Error(`Skill not found: ${input.name}`);
127
- }
128
-
129
- const client = await this.pool.connect();
130
- try {
131
- // app_id + name 是稳定管理身份,仅更新内容与启用状态。
132
- const result = await client.query<SkillRow>(`
133
- update agent.agent_skills
134
- set
135
- description = $3,
136
- instructions = $4,
137
- enabled = $5,
138
- version = version + 1,
139
- updated_at = now()
140
- where app_id is not distinct from $1 and name = $2 and version = $6
141
- returning app_id, version, name, description, instructions, enabled, created_at, updated_at
142
- `, [
143
- input.appId,
144
- input.name,
145
- input.description === undefined ? current.description : input.description.trim(),
146
- input.instructions === undefined ? current.instructions : input.instructions.trim(),
147
- input.enabled ?? current.enabled,
148
- input.baseVersion,
149
- ]);
150
-
151
- if (!result.rows[0]) {
152
- throw new Error(`VERSION_CONFLICT: Skill has changed: ${input.name}`);
153
- }
154
-
155
- return mapSkillRow(result.rows[0]);
156
- } finally {
157
- client.release();
158
- }
159
- }
160
-
161
- async delete(key: SkillKey): Promise<void> {
162
- const client = await this.pool.connect();
163
- try {
164
- await client.query(`
165
- update agent.agent_skills
166
- set
167
- enabled = false,
168
- version = version + 1,
169
- updated_at = now()
170
- where app_id is not distinct from $1 and name = $2
171
- `, [key.appId, key.name]);
172
- } finally {
173
- client.release();
174
- }
175
- }
176
- }