@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 +3 -3
- package/src/index.ts +5 -259
- package/src/load-skill.ts +0 -21
- package/src/testing.ts +11 -48
- package/src/postgres-skill-store.ts +0 -176
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ct-agents/prompts",
|
|
3
|
-
"version": "0.
|
|
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.
|
|
15
|
-
"@ct-agents/store": "0.
|
|
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(
|
|
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: {
|
|
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(
|
|
519
|
-
const persisted = await this.dependencies.store.list(
|
|
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(
|
|
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
|
-
|
|
107
|
-
return
|
|
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
|
-
.
|
|
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(
|
|
137
|
-
const skill = this.skills.get(
|
|
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 =
|
|
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 =
|
|
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(
|
|
184
|
-
const
|
|
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(
|
|
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
|
-
}
|