@ct-agents/worker 0.1.7 → 0.1.9
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 +6 -5
- package/src/index.ts +5 -1
- package/src/resources/index.ts +1 -0
- package/src/resources/text-generation/index.ts +371 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ct-agents/worker",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"src"
|
|
@@ -12,10 +12,11 @@
|
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"zod": "4.4.3",
|
|
15
|
-
"@ct-agents/
|
|
16
|
-
"@ct-agents/
|
|
17
|
-
"@ct-agents/
|
|
18
|
-
"@ct-agents/
|
|
15
|
+
"@ct-agents/harness": "0.1.9",
|
|
16
|
+
"@ct-agents/memory": "0.1.9",
|
|
17
|
+
"@ct-agents/prompts": "0.1.9",
|
|
18
|
+
"@ct-agents/protocol": "0.1.9",
|
|
19
|
+
"@ct-agents/tools": "0.1.9"
|
|
19
20
|
},
|
|
20
21
|
"optionalDependencies": {
|
|
21
22
|
"dockerode": "4.0.9"
|
package/src/index.ts
CHANGED
|
@@ -951,7 +951,7 @@ function buildSyntheticSession(item: ToolWorkItem, harnessId: string, harnessVer
|
|
|
951
951
|
environmentId: item.environmentId as EnvironmentId,
|
|
952
952
|
environmentUpdatedAt: now,
|
|
953
953
|
title: item.sessionId,
|
|
954
|
-
metadata: {},
|
|
954
|
+
metadata: item.sessionMetadata ?? {},
|
|
955
955
|
createdAt: now,
|
|
956
956
|
updatedAt: now,
|
|
957
957
|
};
|
|
@@ -1022,6 +1022,10 @@ function parseToolWorkItem(value: unknown): ToolWorkItem {
|
|
|
1022
1022
|
item.resume = parseToolResumeWorkPayload(record.resume);
|
|
1023
1023
|
}
|
|
1024
1024
|
|
|
1025
|
+
if (record.sessionMetadata !== undefined) {
|
|
1026
|
+
item.sessionMetadata = expectRecord(record.sessionMetadata, 'sessionMetadata 必须是对象');
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1025
1029
|
if (isRecord(record.resourceBindings)) {
|
|
1026
1030
|
item.resourceBindings = record.resourceBindings;
|
|
1027
1031
|
}
|
package/src/resources/index.ts
CHANGED
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
HarnessModelRunInput,
|
|
3
|
+
ResourceDefinition,
|
|
4
|
+
TextGenerationErrorCode,
|
|
5
|
+
TextGenerationInput,
|
|
6
|
+
TextGenerationProvider,
|
|
7
|
+
TextGenerationResource,
|
|
8
|
+
TextGenerationResult,
|
|
9
|
+
} from '@ct-agents/protocol';
|
|
10
|
+
import { runAnthropic } from '@ct-agents/harness/llm/anthropic-provider.js';
|
|
11
|
+
import type {
|
|
12
|
+
NormalizedModelOutput,
|
|
13
|
+
ResolvedProviderConfig,
|
|
14
|
+
} from '@ct-agents/harness/llm/model-port.js';
|
|
15
|
+
import { runOpenAI } from '@ct-agents/harness/llm/openai-provider.js';
|
|
16
|
+
import { z } from 'zod';
|
|
17
|
+
import type { EnvironmentWorkerResourceImplementation } from '../../index.js';
|
|
18
|
+
|
|
19
|
+
const DEFAULT_MAX_OUTPUT_TOKENS = 4_096;
|
|
20
|
+
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
21
|
+
const MAX_PROMPT_BYTES = 512 * 1_024;
|
|
22
|
+
const MAX_SYSTEM_PROMPT_BYTES = 32 * 1_024;
|
|
23
|
+
const MAX_RESPONSE_BYTES = 1_024 * 1_024;
|
|
24
|
+
|
|
25
|
+
export type TextGenerationOptions = {
|
|
26
|
+
provider: TextGenerationProvider;
|
|
27
|
+
apiKey: string;
|
|
28
|
+
defaultModel: string;
|
|
29
|
+
baseURL?: string;
|
|
30
|
+
maxOutputTokens: number;
|
|
31
|
+
timeoutMs: number;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const providerBaseUrlSchema = z.string().trim().max(2_048).url().refine((value) => {
|
|
35
|
+
const url = new URL(value);
|
|
36
|
+
return (url.protocol === 'https:' || url.protocol === 'http:')
|
|
37
|
+
&& !url.username
|
|
38
|
+
&& !url.password;
|
|
39
|
+
}, 'baseURL 必须是无用户凭证的 HTTP(S) URL');
|
|
40
|
+
|
|
41
|
+
export const textGenerationOptionsSchema = z.object({
|
|
42
|
+
provider: z.enum(['openai', 'anthropic']),
|
|
43
|
+
apiKey: z.string().trim().min(1).meta({ secret: true }),
|
|
44
|
+
defaultModel: z.string().trim().min(1).max(256),
|
|
45
|
+
baseURL: providerBaseUrlSchema.optional(),
|
|
46
|
+
maxOutputTokens: z.number().int().min(128).max(16_384).default(DEFAULT_MAX_OUTPUT_TOKENS),
|
|
47
|
+
timeoutMs: z.number().int().min(1_000).max(120_000).default(DEFAULT_TIMEOUT_MS),
|
|
48
|
+
}).strict();
|
|
49
|
+
|
|
50
|
+
const textGenerationInputSchema = z.object({
|
|
51
|
+
prompt: z.string().min(1),
|
|
52
|
+
systemPrompt: z.string().optional(),
|
|
53
|
+
model: z.string().trim().min(1).max(256).optional(),
|
|
54
|
+
maxOutputTokens: z.number().int().min(128).max(16_384).optional(),
|
|
55
|
+
}).strict();
|
|
56
|
+
|
|
57
|
+
export type TextGenerationProviderRunInput = {
|
|
58
|
+
provider: TextGenerationProvider;
|
|
59
|
+
config: ResolvedProviderConfig;
|
|
60
|
+
input: HarnessModelRunInput;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export type TextGenerationResourceDependencies = {
|
|
64
|
+
runProvider?: (input: TextGenerationProviderRunInput) => Promise<NormalizedModelOutput>;
|
|
65
|
+
now?: () => number;
|
|
66
|
+
defaultAbortSignal?: AbortSignal;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
function byteLength(value: string): number {
|
|
70
|
+
return new TextEncoder().encode(value).byteLength;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function durationSince(startedAt: number, now: () => number): number {
|
|
74
|
+
return Math.max(0, Math.round(now() - startedAt));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function failure(input: {
|
|
78
|
+
provider: TextGenerationProvider;
|
|
79
|
+
model: string;
|
|
80
|
+
startedAt: number;
|
|
81
|
+
now: () => number;
|
|
82
|
+
code: TextGenerationErrorCode;
|
|
83
|
+
message: string;
|
|
84
|
+
retryable: boolean;
|
|
85
|
+
}): TextGenerationResult {
|
|
86
|
+
return {
|
|
87
|
+
success: false,
|
|
88
|
+
provider: input.provider,
|
|
89
|
+
model: input.model,
|
|
90
|
+
durationMs: durationSince(input.startedAt, input.now),
|
|
91
|
+
error: {
|
|
92
|
+
code: input.code,
|
|
93
|
+
message: input.message,
|
|
94
|
+
retryable: input.retryable,
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function readHttpStatus(error: unknown): number | undefined {
|
|
100
|
+
const visited = new Set<unknown>();
|
|
101
|
+
let current: unknown = error;
|
|
102
|
+
while (current && typeof current === 'object' && !visited.has(current)) {
|
|
103
|
+
visited.add(current);
|
|
104
|
+
const status = 'status' in current ? (current as { status?: unknown }).status : undefined;
|
|
105
|
+
if (typeof status === 'number' && Number.isFinite(status)) {
|
|
106
|
+
return status;
|
|
107
|
+
}
|
|
108
|
+
current = 'cause' in current ? (current as { cause?: unknown }).cause : undefined;
|
|
109
|
+
}
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function classifyProviderError(error: unknown): {
|
|
114
|
+
code: TextGenerationErrorCode;
|
|
115
|
+
message: string;
|
|
116
|
+
retryable: boolean;
|
|
117
|
+
} {
|
|
118
|
+
const status = readHttpStatus(error);
|
|
119
|
+
if (status === 400 || status === 404 || status === 422) {
|
|
120
|
+
return {
|
|
121
|
+
code: 'TEXT_GENERATION_INVALID_REQUEST',
|
|
122
|
+
message: '模型服务拒绝了生成请求,请检查 Environment 模型配置',
|
|
123
|
+
retryable: false,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
if (status === 401 || status === 403) {
|
|
127
|
+
return {
|
|
128
|
+
code: 'TEXT_GENERATION_AUTH_FAILED',
|
|
129
|
+
message: '模型服务认证失败,请检查 Environment 密钥',
|
|
130
|
+
retryable: false,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
if (status === 429) {
|
|
134
|
+
return {
|
|
135
|
+
code: 'TEXT_GENERATION_RATE_LIMITED',
|
|
136
|
+
message: '模型服务请求过于频繁,请稍后重试',
|
|
137
|
+
retryable: true,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
if (status !== undefined && status >= 500) {
|
|
141
|
+
return {
|
|
142
|
+
code: 'TEXT_GENERATION_PROVIDER_UNAVAILABLE',
|
|
143
|
+
message: '模型服务暂时不可用,请稍后重试',
|
|
144
|
+
retryable: true,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
code: 'TEXT_GENERATION_PROVIDER_ERROR',
|
|
149
|
+
message: '模型服务返回了非预期错误',
|
|
150
|
+
retryable: status === undefined,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function abortError(reason?: unknown): Error {
|
|
155
|
+
if (reason instanceof Error && reason.name === 'AbortError') {
|
|
156
|
+
return reason;
|
|
157
|
+
}
|
|
158
|
+
return new DOMException('aborted', 'AbortError');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function defaultRunProvider(
|
|
162
|
+
input: TextGenerationProviderRunInput,
|
|
163
|
+
): Promise<NormalizedModelOutput> {
|
|
164
|
+
return input.provider === 'openai'
|
|
165
|
+
? runOpenAI(input.input, input.config)
|
|
166
|
+
: runAnthropic(input.input, input.config);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function buildModelInput(input: TextGenerationInput, abortSignal: AbortSignal): HarnessModelRunInput {
|
|
170
|
+
const systemPrompt = input.systemPrompt?.trim() ?? '';
|
|
171
|
+
return {
|
|
172
|
+
context: {
|
|
173
|
+
messages: [
|
|
174
|
+
...(systemPrompt
|
|
175
|
+
? [{
|
|
176
|
+
role: 'system' as const,
|
|
177
|
+
content: [{ type: 'text' as const, text: systemPrompt }],
|
|
178
|
+
}]
|
|
179
|
+
: []),
|
|
180
|
+
{
|
|
181
|
+
role: 'user',
|
|
182
|
+
content: [{ type: 'text', text: input.prompt }],
|
|
183
|
+
},
|
|
184
|
+
],
|
|
185
|
+
},
|
|
186
|
+
systemPrompt,
|
|
187
|
+
tools: [],
|
|
188
|
+
abortSignal,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* 创建不进入 Harness Agent loop 的单轮文本生成 Resource。
|
|
194
|
+
*
|
|
195
|
+
* Environment 冻结 provider、凭证、baseURL 和最大预算;调用方只能在该边界内选择模型和更小预算。
|
|
196
|
+
*/
|
|
197
|
+
export function createTextGenerationResource(
|
|
198
|
+
rawOptions: TextGenerationOptions,
|
|
199
|
+
dependencies: TextGenerationResourceDependencies = {},
|
|
200
|
+
): TextGenerationResource {
|
|
201
|
+
const options = textGenerationOptionsSchema.parse(rawOptions);
|
|
202
|
+
const runProvider = dependencies.runProvider ?? defaultRunProvider;
|
|
203
|
+
const now = dependencies.now ?? Date.now;
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
async generate(rawInput, context) {
|
|
207
|
+
const startedAt = now();
|
|
208
|
+
const candidateModel = typeof rawInput?.model === 'string' && rawInput.model.trim()
|
|
209
|
+
? rawInput.model.trim()
|
|
210
|
+
: options.defaultModel;
|
|
211
|
+
const parsedInput = textGenerationInputSchema.safeParse(rawInput);
|
|
212
|
+
if (!parsedInput.success) {
|
|
213
|
+
return failure({
|
|
214
|
+
provider: options.provider,
|
|
215
|
+
model: candidateModel,
|
|
216
|
+
startedAt,
|
|
217
|
+
now,
|
|
218
|
+
code: 'TEXT_GENERATION_INVALID_REQUEST',
|
|
219
|
+
message: '文本生成请求不合法',
|
|
220
|
+
retryable: false,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
const input = parsedInput.data;
|
|
224
|
+
const model = input.model ?? options.defaultModel;
|
|
225
|
+
if (
|
|
226
|
+
byteLength(input.prompt) > MAX_PROMPT_BYTES
|
|
227
|
+
|| byteLength(input.systemPrompt ?? '') > MAX_SYSTEM_PROMPT_BYTES
|
|
228
|
+
) {
|
|
229
|
+
return failure({
|
|
230
|
+
provider: options.provider,
|
|
231
|
+
model,
|
|
232
|
+
startedAt,
|
|
233
|
+
now,
|
|
234
|
+
code: 'TEXT_GENERATION_INVALID_REQUEST',
|
|
235
|
+
message: '文本生成输入超过 Resource 安全上限',
|
|
236
|
+
retryable: false,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
const maxOutputTokens = input.maxOutputTokens ?? options.maxOutputTokens;
|
|
240
|
+
if (maxOutputTokens > options.maxOutputTokens) {
|
|
241
|
+
return failure({
|
|
242
|
+
provider: options.provider,
|
|
243
|
+
model,
|
|
244
|
+
startedAt,
|
|
245
|
+
now,
|
|
246
|
+
code: 'TEXT_GENERATION_INVALID_REQUEST',
|
|
247
|
+
message: '调用级输出预算不能超过 Environment 上限',
|
|
248
|
+
retryable: false,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const upstreamSignal = context?.abortSignal ?? dependencies.defaultAbortSignal;
|
|
253
|
+
if (upstreamSignal?.aborted) {
|
|
254
|
+
throw abortError(upstreamSignal.reason);
|
|
255
|
+
}
|
|
256
|
+
const controller = new AbortController();
|
|
257
|
+
let timedOut = false;
|
|
258
|
+
const onUpstreamAbort = () => controller.abort(upstreamSignal?.reason);
|
|
259
|
+
upstreamSignal?.addEventListener('abort', onUpstreamAbort, { once: true });
|
|
260
|
+
const timer = setTimeout(() => {
|
|
261
|
+
timedOut = true;
|
|
262
|
+
controller.abort();
|
|
263
|
+
}, options.timeoutMs);
|
|
264
|
+
|
|
265
|
+
try {
|
|
266
|
+
const output = await runProvider({
|
|
267
|
+
provider: options.provider,
|
|
268
|
+
config: {
|
|
269
|
+
model,
|
|
270
|
+
apiKey: options.apiKey,
|
|
271
|
+
...(options.baseURL ? { baseURL: options.baseURL } : {}),
|
|
272
|
+
maxOutputTokens,
|
|
273
|
+
},
|
|
274
|
+
input: buildModelInput(input, controller.signal),
|
|
275
|
+
});
|
|
276
|
+
const text = output.text.trim();
|
|
277
|
+
if (!text) {
|
|
278
|
+
return failure({
|
|
279
|
+
provider: options.provider,
|
|
280
|
+
model,
|
|
281
|
+
startedAt,
|
|
282
|
+
now,
|
|
283
|
+
code: 'TEXT_GENERATION_EMPTY_RESPONSE',
|
|
284
|
+
message: '模型服务返回了空文本',
|
|
285
|
+
retryable: false,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
if (output.toolCalls.length > 0 || byteLength(text) > MAX_RESPONSE_BYTES) {
|
|
289
|
+
return failure({
|
|
290
|
+
provider: options.provider,
|
|
291
|
+
model,
|
|
292
|
+
startedAt,
|
|
293
|
+
now,
|
|
294
|
+
code: 'TEXT_GENERATION_PROVIDER_ERROR',
|
|
295
|
+
message: '模型服务返回了不符合单轮文本契约的结果',
|
|
296
|
+
retryable: false,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
return {
|
|
300
|
+
success: true,
|
|
301
|
+
provider: options.provider,
|
|
302
|
+
model,
|
|
303
|
+
text,
|
|
304
|
+
durationMs: durationSince(startedAt, now),
|
|
305
|
+
};
|
|
306
|
+
} catch (error) {
|
|
307
|
+
if (upstreamSignal?.aborted) {
|
|
308
|
+
throw abortError(upstreamSignal.reason ?? error);
|
|
309
|
+
}
|
|
310
|
+
if (timedOut) {
|
|
311
|
+
return failure({
|
|
312
|
+
provider: options.provider,
|
|
313
|
+
model,
|
|
314
|
+
startedAt,
|
|
315
|
+
now,
|
|
316
|
+
code: 'TEXT_GENERATION_TIMEOUT',
|
|
317
|
+
message: '文本生成超时,请稍后重试',
|
|
318
|
+
retryable: true,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
const classified = classifyProviderError(error);
|
|
322
|
+
return failure({
|
|
323
|
+
provider: options.provider,
|
|
324
|
+
model,
|
|
325
|
+
startedAt,
|
|
326
|
+
now,
|
|
327
|
+
...classified,
|
|
328
|
+
});
|
|
329
|
+
} finally {
|
|
330
|
+
clearTimeout(timer);
|
|
331
|
+
upstreamSignal?.removeEventListener('abort', onUpstreamAbort);
|
|
332
|
+
}
|
|
333
|
+
},
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export function createTextGenerationResourceImplementation(
|
|
338
|
+
dependencies: TextGenerationResourceDependencies = {},
|
|
339
|
+
): EnvironmentWorkerResourceImplementation<TextGenerationResource, TextGenerationOptions> {
|
|
340
|
+
return {
|
|
341
|
+
title: '模型文本生成',
|
|
342
|
+
description: '使用 Environment 配置的模型服务执行单轮、无 Tool 的文本生成。',
|
|
343
|
+
options: textGenerationOptionsSchema,
|
|
344
|
+
factory: (options, context) => createTextGenerationResource(options, {
|
|
345
|
+
...dependencies,
|
|
346
|
+
defaultAbortSignal: context.abortSignal ?? dependencies.defaultAbortSignal,
|
|
347
|
+
}),
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export function createTextGenerationResourceDefinition(
|
|
352
|
+
dependencies: TextGenerationResourceDependencies = {},
|
|
353
|
+
): ResourceDefinition<TextGenerationResource, TextGenerationOptions> {
|
|
354
|
+
return {
|
|
355
|
+
id: 'textGeneration',
|
|
356
|
+
title: '模型文本生成',
|
|
357
|
+
description: '供应商无关的单轮文本生成 Resource。',
|
|
358
|
+
implementations: [{
|
|
359
|
+
id: 'provider-api',
|
|
360
|
+
title: '模型 Provider API',
|
|
361
|
+
description: '通过 OpenAI 或 Anthropic 兼容 API 生成文本。',
|
|
362
|
+
supportedExecutionModes: ['hosted', 'self_hosted'],
|
|
363
|
+
optionsSchema: textGenerationOptionsSchema,
|
|
364
|
+
optionsJsonSchema: z.toJSONSchema(textGenerationOptionsSchema),
|
|
365
|
+
factory: (options) => createTextGenerationResource(
|
|
366
|
+
textGenerationOptionsSchema.parse(options),
|
|
367
|
+
dependencies,
|
|
368
|
+
),
|
|
369
|
+
}],
|
|
370
|
+
};
|
|
371
|
+
}
|