@lumenflow/cli 3.17.7 → 3.18.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/dist/init-detection.js +5 -3
- package/dist/init-detection.js.map +1 -1
- package/dist/init-templates.js +4 -4
- package/dist/init-templates.js.map +1 -1
- package/dist/initiative-plan.js +1 -1
- package/dist/initiative-plan.js.map +1 -1
- package/dist/pre-commit-check.js +1 -1
- package/dist/pre-commit-check.js.map +1 -1
- package/dist/wu-edit-operations.js +4 -0
- package/dist/wu-edit-operations.js.map +1 -1
- package/dist/wu-edit-validators.js +4 -0
- package/dist/wu-edit-validators.js.map +1 -1
- package/dist/wu-edit.js +11 -0
- package/dist/wu-edit.js.map +1 -1
- package/dist/wu-spawn-strategy-resolver.js +13 -1
- package/dist/wu-spawn-strategy-resolver.js.map +1 -1
- package/package.json +8 -8
- package/packs/agent-runtime/.turbo/turbo-build.log +4 -0
- package/packs/agent-runtime/README.md +147 -0
- package/packs/agent-runtime/capability-factory.ts +104 -0
- package/packs/agent-runtime/config.schema.json +87 -0
- package/packs/agent-runtime/constants.ts +21 -0
- package/packs/agent-runtime/index.ts +11 -0
- package/packs/agent-runtime/manifest.ts +207 -0
- package/packs/agent-runtime/manifest.yaml +193 -0
- package/packs/agent-runtime/orchestration.ts +1787 -0
- package/packs/agent-runtime/pack-registration.ts +110 -0
- package/packs/agent-runtime/package.json +57 -0
- package/packs/agent-runtime/policy-factory.ts +165 -0
- package/packs/agent-runtime/tool-impl/agent-turn-tools.ts +793 -0
- package/packs/agent-runtime/tool-impl/index.ts +5 -0
- package/packs/agent-runtime/tool-impl/provider-adapters.ts +1245 -0
- package/packs/agent-runtime/tools/index.ts +4 -0
- package/packs/agent-runtime/tools/types.ts +47 -0
- package/packs/agent-runtime/tsconfig.json +20 -0
- package/packs/agent-runtime/types.ts +128 -0
- package/packs/agent-runtime/vitest.config.ts +11 -0
- package/packs/sidekick/.turbo/turbo-build.log +1 -1
- package/packs/sidekick/package.json +1 -1
- package/packs/software-delivery/.turbo/turbo-build.log +1 -1
- package/packs/software-delivery/package.json +1 -1
- package/templates/core/.lumenflow/rules/wu-workflow.md.template +1 -1
- package/templates/core/ai/onboarding/first-wu-mistakes.md.template +2 -2
- package/templates/core/ai/onboarding/quick-ref-commands.md.template +1 -1
- package/templates/core/ai/onboarding/starting-prompt.md.template +1 -1
- package/templates/vendors/claude/.claude/skills/frontend-design/SKILL.md.template +1 -1
|
@@ -0,0 +1,793 @@
|
|
|
1
|
+
// Copyright (c) 2026 Hellmai Ltd
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
TOOL_ERROR_CODES,
|
|
6
|
+
TOOL_OUTPUT_METADATA_KEYS,
|
|
7
|
+
type ExecutionContext,
|
|
8
|
+
type ToolOutput,
|
|
9
|
+
} from '@lumenflow/kernel';
|
|
10
|
+
import {
|
|
11
|
+
AGENT_RUNTIME_AGENT_TOOL_CALL_COUNT_METADATA_KEY,
|
|
12
|
+
AGENT_RUNTIME_AGENT_TURN_INDEX_METADATA_KEY,
|
|
13
|
+
AGENT_RUNTIME_API_KEY_ENV,
|
|
14
|
+
AGENT_RUNTIME_BASE_URL_ENV,
|
|
15
|
+
} from '../constants.js';
|
|
16
|
+
import type {
|
|
17
|
+
AgentRuntimeExecuteTurnInput,
|
|
18
|
+
AgentRuntimeIntentCatalogEntry,
|
|
19
|
+
AgentRuntimeLimitsConfig,
|
|
20
|
+
AgentRuntimeMessage,
|
|
21
|
+
AgentRuntimeModelProfileConfig,
|
|
22
|
+
AgentRuntimeProviderKind,
|
|
23
|
+
AgentRuntimeToolCatalogEntry,
|
|
24
|
+
} from '../types.js';
|
|
25
|
+
import { STATIC_PROVIDER_CAPABILITY_BASELINE, executeProviderTurn } from './provider-adapters.js';
|
|
26
|
+
|
|
27
|
+
const LIMIT_EXCEEDED_ERROR_CODE = 'LIMIT_EXCEEDED';
|
|
28
|
+
const MISSING_ENVIRONMENT_ERROR_CODE = 'MISSING_ENVIRONMENT';
|
|
29
|
+
const CONFIGURATION_ERROR_CODE = 'CONFIGURATION_ERROR';
|
|
30
|
+
const PROVIDER_CALL_COUNT_ONE = 1;
|
|
31
|
+
const PROVIDER_CALL_COUNT_ZERO = 0;
|
|
32
|
+
|
|
33
|
+
interface ValidatedTurnInput extends AgentRuntimeExecuteTurnInput {
|
|
34
|
+
messages: AgentRuntimeMessage[];
|
|
35
|
+
tool_catalog: AgentRuntimeToolCatalogEntry[];
|
|
36
|
+
intent_catalog: AgentRuntimeIntentCatalogEntry[];
|
|
37
|
+
limits?: AgentRuntimeLimitsConfig;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface ResolvedProviderEnvironment {
|
|
41
|
+
kind: AgentRuntimeProviderKind;
|
|
42
|
+
model: string;
|
|
43
|
+
apiKey: string;
|
|
44
|
+
baseUrl: string;
|
|
45
|
+
requiredEnv: string[];
|
|
46
|
+
networkAllowlist: string[];
|
|
47
|
+
allowedUrls: string[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function agentExecuteTurnTool(
|
|
51
|
+
input: unknown,
|
|
52
|
+
ctx: ExecutionContext,
|
|
53
|
+
): Promise<ToolOutput> {
|
|
54
|
+
const parsedInput = validateExecuteTurnInput(input);
|
|
55
|
+
if (!parsedInput.ok) {
|
|
56
|
+
return createFailureOutput(TOOL_ERROR_CODES.INVALID_INPUT, parsedInput.message);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const validatedInput = parsedInput.value;
|
|
60
|
+
const limitFailure = enforceExecutionLimits(validatedInput, ctx);
|
|
61
|
+
if (limitFailure) {
|
|
62
|
+
return limitFailure;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const environmentResult = resolveProviderEnvironment(validatedInput, ctx);
|
|
66
|
+
if (!environmentResult.ok) {
|
|
67
|
+
return environmentResult.output;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const providerResult = await executeProviderTurn({
|
|
71
|
+
kind: environmentResult.value.kind,
|
|
72
|
+
model: environmentResult.value.model,
|
|
73
|
+
url: validatedInput.url,
|
|
74
|
+
apiKey: environmentResult.value.apiKey,
|
|
75
|
+
stream: validatedInput.stream ?? false,
|
|
76
|
+
messages: validatedInput.messages,
|
|
77
|
+
toolCatalog: validatedInput.tool_catalog,
|
|
78
|
+
intentCatalog: validatedInput.intent_catalog,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const metadata = createToolMetadata({
|
|
82
|
+
provider_kind: environmentResult.value.kind,
|
|
83
|
+
network_allowlist: [...environmentResult.value.networkAllowlist],
|
|
84
|
+
allowed_urls: [...environmentResult.value.allowedUrls],
|
|
85
|
+
required_env: [...environmentResult.value.requiredEnv],
|
|
86
|
+
provider_call_count: PROVIDER_CALL_COUNT_ONE,
|
|
87
|
+
request_url: validatedInput.url,
|
|
88
|
+
configured_base_url: environmentResult.value.baseUrl,
|
|
89
|
+
response_mode: providerResult.metadata.response_mode ?? 'non_streaming',
|
|
90
|
+
...(providerResult.metadata.stream_snapshot_count !== undefined
|
|
91
|
+
? { stream_snapshot_count: providerResult.metadata.stream_snapshot_count }
|
|
92
|
+
: {}),
|
|
93
|
+
...(providerResult.metadata.response_status !== undefined
|
|
94
|
+
? { response_status: providerResult.metadata.response_status }
|
|
95
|
+
: {}),
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
if (!providerResult.ok) {
|
|
99
|
+
return {
|
|
100
|
+
success: false,
|
|
101
|
+
error: providerResult.error,
|
|
102
|
+
metadata,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
success: true,
|
|
108
|
+
data: providerResult.output,
|
|
109
|
+
metadata: {
|
|
110
|
+
...metadata,
|
|
111
|
+
...(providerResult.stream_snapshots && providerResult.stream_snapshots.length > 0
|
|
112
|
+
? {
|
|
113
|
+
[TOOL_OUTPUT_METADATA_KEYS.PROGRESS_SNAPSHOTS]: providerResult.stream_snapshots,
|
|
114
|
+
}
|
|
115
|
+
: {}),
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function validateExecuteTurnInput(
|
|
121
|
+
input: unknown,
|
|
122
|
+
): { ok: true; value: ValidatedTurnInput } | { ok: false; message: string } {
|
|
123
|
+
const record = asRecord(input);
|
|
124
|
+
if (!record) {
|
|
125
|
+
return { ok: false, message: 'Input must be an object.' };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (
|
|
129
|
+
hasUnexpectedKeys(record, [
|
|
130
|
+
'session_id',
|
|
131
|
+
'model_profile',
|
|
132
|
+
'url',
|
|
133
|
+
'stream',
|
|
134
|
+
'messages',
|
|
135
|
+
'tool_catalog',
|
|
136
|
+
'intent_catalog',
|
|
137
|
+
'limits',
|
|
138
|
+
])
|
|
139
|
+
) {
|
|
140
|
+
return { ok: false, message: 'Input contains unknown properties.' };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const sessionId = readNonEmptyString(record.session_id);
|
|
144
|
+
if (!sessionId) {
|
|
145
|
+
return { ok: false, message: 'session_id is required.' };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const modelProfile = readNonEmptyString(record.model_profile);
|
|
149
|
+
if (!modelProfile) {
|
|
150
|
+
return { ok: false, message: 'model_profile is required.' };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const url = readNonEmptyString(record.url);
|
|
154
|
+
if (!url) {
|
|
155
|
+
return { ok: false, message: 'url is required.' };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const stream = validateBoolean(record.stream, 'stream');
|
|
159
|
+
if (!stream.ok) {
|
|
160
|
+
return stream;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const messages = validateMessages(record.messages);
|
|
164
|
+
if (!messages.ok) {
|
|
165
|
+
return messages;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const toolCatalog = validateToolCatalog(record.tool_catalog);
|
|
169
|
+
if (!toolCatalog.ok) {
|
|
170
|
+
return toolCatalog;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const intentCatalog = validateIntentCatalog(record.intent_catalog);
|
|
174
|
+
if (!intentCatalog.ok) {
|
|
175
|
+
return intentCatalog;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const limits = validateLimits(record.limits);
|
|
179
|
+
if (!limits.ok) {
|
|
180
|
+
return limits;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
ok: true,
|
|
185
|
+
value: {
|
|
186
|
+
session_id: sessionId,
|
|
187
|
+
model_profile: modelProfile,
|
|
188
|
+
url,
|
|
189
|
+
...(stream.value !== undefined ? { stream: stream.value } : {}),
|
|
190
|
+
messages: messages.value,
|
|
191
|
+
tool_catalog: toolCatalog.value,
|
|
192
|
+
intent_catalog: intentCatalog.value,
|
|
193
|
+
...(limits.value ? { limits: limits.value } : {}),
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function validateMessages(
|
|
199
|
+
value: unknown,
|
|
200
|
+
): { ok: true; value: AgentRuntimeMessage[] } | { ok: false; message: string } {
|
|
201
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
202
|
+
return { ok: false, message: 'messages must be a non-empty array.' };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const messages: AgentRuntimeMessage[] = [];
|
|
206
|
+
for (const entry of value) {
|
|
207
|
+
const record = asRecord(entry);
|
|
208
|
+
if (!record) {
|
|
209
|
+
return { ok: false, message: 'messages entries must be objects.' };
|
|
210
|
+
}
|
|
211
|
+
if (hasUnexpectedKeys(record, ['role', 'content', 'tool_name', 'tool_call_id'])) {
|
|
212
|
+
return { ok: false, message: 'messages entries contain unknown properties.' };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const role = readNonEmptyString(record.role);
|
|
216
|
+
if (!isMessageRole(role)) {
|
|
217
|
+
return {
|
|
218
|
+
ok: false,
|
|
219
|
+
message: 'messages.role must be one of system, user, assistant, or tool.',
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const content = readString(record.content);
|
|
224
|
+
if (content === null) {
|
|
225
|
+
return { ok: false, message: 'messages.content must be a string.' };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const toolName = readOptionalNonEmptyString(record.tool_name);
|
|
229
|
+
if (record.tool_name !== undefined && toolName === null) {
|
|
230
|
+
return { ok: false, message: 'messages.tool_name must be a non-empty string when provided.' };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const toolCallId = readOptionalNonEmptyString(record.tool_call_id);
|
|
234
|
+
if (record.tool_call_id !== undefined && toolCallId === null) {
|
|
235
|
+
return {
|
|
236
|
+
ok: false,
|
|
237
|
+
message: 'messages.tool_call_id must be a non-empty string when provided.',
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
messages.push({
|
|
242
|
+
role,
|
|
243
|
+
content,
|
|
244
|
+
...(toolName ? { tool_name: toolName } : {}),
|
|
245
|
+
...(toolCallId ? { tool_call_id: toolCallId } : {}),
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return { ok: true, value: messages };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function validateToolCatalog(
|
|
253
|
+
value: unknown,
|
|
254
|
+
): { ok: true; value: AgentRuntimeToolCatalogEntry[] } | { ok: false; message: string } {
|
|
255
|
+
if (value === undefined) {
|
|
256
|
+
return { ok: true, value: [] };
|
|
257
|
+
}
|
|
258
|
+
if (!Array.isArray(value)) {
|
|
259
|
+
return { ok: false, message: 'tool_catalog must be an array when provided.' };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const toolCatalog: AgentRuntimeToolCatalogEntry[] = [];
|
|
263
|
+
for (const entry of value) {
|
|
264
|
+
const record = asRecord(entry);
|
|
265
|
+
if (!record) {
|
|
266
|
+
return { ok: false, message: 'tool_catalog entries must be objects.' };
|
|
267
|
+
}
|
|
268
|
+
if (hasUnexpectedKeys(record, ['name', 'description', 'input_schema'])) {
|
|
269
|
+
return { ok: false, message: 'tool_catalog entries contain unknown properties.' };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const name = readNonEmptyString(record.name);
|
|
273
|
+
const description = readNonEmptyString(record.description);
|
|
274
|
+
if (!name || !description) {
|
|
275
|
+
return {
|
|
276
|
+
ok: false,
|
|
277
|
+
message: 'tool_catalog entries require non-empty name and description.',
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const inputSchema = record.input_schema;
|
|
282
|
+
if (inputSchema !== undefined && !asRecord(inputSchema)) {
|
|
283
|
+
return { ok: false, message: 'tool_catalog.input_schema must be an object when provided.' };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
toolCatalog.push({
|
|
287
|
+
name,
|
|
288
|
+
description,
|
|
289
|
+
...(inputSchema !== undefined
|
|
290
|
+
? { input_schema: inputSchema as Record<string, unknown> }
|
|
291
|
+
: {}),
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return { ok: true, value: toolCatalog };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function validateIntentCatalog(
|
|
299
|
+
value: unknown,
|
|
300
|
+
): { ok: true; value: AgentRuntimeIntentCatalogEntry[] } | { ok: false; message: string } {
|
|
301
|
+
if (value === undefined) {
|
|
302
|
+
return { ok: true, value: [] };
|
|
303
|
+
}
|
|
304
|
+
if (!Array.isArray(value)) {
|
|
305
|
+
return { ok: false, message: 'intent_catalog must be an array when provided.' };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const intentCatalog: AgentRuntimeIntentCatalogEntry[] = [];
|
|
309
|
+
for (const entry of value) {
|
|
310
|
+
const record = asRecord(entry);
|
|
311
|
+
if (!record) {
|
|
312
|
+
return { ok: false, message: 'intent_catalog entries must be objects.' };
|
|
313
|
+
}
|
|
314
|
+
if (hasUnexpectedKeys(record, ['id', 'description'])) {
|
|
315
|
+
return { ok: false, message: 'intent_catalog entries contain unknown properties.' };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const id = readNonEmptyString(record.id);
|
|
319
|
+
const description = readNonEmptyString(record.description);
|
|
320
|
+
if (!id || !description) {
|
|
321
|
+
return {
|
|
322
|
+
ok: false,
|
|
323
|
+
message: 'intent_catalog entries require non-empty id and description.',
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
intentCatalog.push({ id, description });
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return { ok: true, value: intentCatalog };
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function validateLimits(
|
|
334
|
+
value: unknown,
|
|
335
|
+
): { ok: true; value?: AgentRuntimeLimitsConfig } | { ok: false; message: string } {
|
|
336
|
+
if (value === undefined) {
|
|
337
|
+
return { ok: true };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const record = asRecord(value);
|
|
341
|
+
if (!record) {
|
|
342
|
+
return { ok: false, message: 'limits must be an object when provided.' };
|
|
343
|
+
}
|
|
344
|
+
if (
|
|
345
|
+
hasUnexpectedKeys(record, [
|
|
346
|
+
'max_turns_per_session',
|
|
347
|
+
'max_tool_calls_per_session',
|
|
348
|
+
'max_input_bytes',
|
|
349
|
+
])
|
|
350
|
+
) {
|
|
351
|
+
return { ok: false, message: 'limits contains unknown properties.' };
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const maxTurnsPerSession = readOptionalPositiveInteger(record.max_turns_per_session);
|
|
355
|
+
if (record.max_turns_per_session !== undefined && maxTurnsPerSession === null) {
|
|
356
|
+
return { ok: false, message: 'limits.max_turns_per_session must be a positive integer.' };
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const maxToolCallsPerSession = readOptionalPositiveInteger(record.max_tool_calls_per_session);
|
|
360
|
+
if (record.max_tool_calls_per_session !== undefined && maxToolCallsPerSession === null) {
|
|
361
|
+
return {
|
|
362
|
+
ok: false,
|
|
363
|
+
message: 'limits.max_tool_calls_per_session must be a positive integer.',
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const maxInputBytes = readOptionalPositiveInteger(record.max_input_bytes);
|
|
368
|
+
if (record.max_input_bytes !== undefined && maxInputBytes === null) {
|
|
369
|
+
return { ok: false, message: 'limits.max_input_bytes must be a positive integer.' };
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return {
|
|
373
|
+
ok: true,
|
|
374
|
+
value: {
|
|
375
|
+
...(maxTurnsPerSession !== null ? { max_turns_per_session: maxTurnsPerSession } : {}),
|
|
376
|
+
...(maxToolCallsPerSession !== null
|
|
377
|
+
? { max_tool_calls_per_session: maxToolCallsPerSession }
|
|
378
|
+
: {}),
|
|
379
|
+
...(maxInputBytes !== null ? { max_input_bytes: maxInputBytes } : {}),
|
|
380
|
+
},
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function validateBoolean(
|
|
385
|
+
value: unknown,
|
|
386
|
+
field: string,
|
|
387
|
+
): { ok: true; value: boolean | undefined } | { ok: false; message: string } {
|
|
388
|
+
if (value === undefined) {
|
|
389
|
+
return { ok: true, value: undefined };
|
|
390
|
+
}
|
|
391
|
+
if (typeof value !== 'boolean') {
|
|
392
|
+
return { ok: false, message: `${field} must be a boolean when provided.` };
|
|
393
|
+
}
|
|
394
|
+
return { ok: true, value };
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function enforceExecutionLimits(
|
|
398
|
+
input: ValidatedTurnInput,
|
|
399
|
+
ctx: ExecutionContext,
|
|
400
|
+
): ToolOutput | null {
|
|
401
|
+
const maxInputBytes = input.limits?.max_input_bytes;
|
|
402
|
+
if (maxInputBytes !== undefined) {
|
|
403
|
+
const inputBytes = Buffer.byteLength(JSON.stringify(input), 'utf8');
|
|
404
|
+
if (inputBytes > maxInputBytes) {
|
|
405
|
+
return createFailureOutput(
|
|
406
|
+
LIMIT_EXCEEDED_ERROR_CODE,
|
|
407
|
+
`Serialized turn input exceeded max_input_bytes (${inputBytes} > ${maxInputBytes}).`,
|
|
408
|
+
{
|
|
409
|
+
provider_call_count: PROVIDER_CALL_COUNT_ZERO,
|
|
410
|
+
input_bytes: inputBytes,
|
|
411
|
+
max_input_bytes: maxInputBytes,
|
|
412
|
+
},
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const maxTurnsPerSession = input.limits?.max_turns_per_session;
|
|
418
|
+
if (maxTurnsPerSession !== undefined) {
|
|
419
|
+
const turnIndex = readAgentTurnIndex(ctx);
|
|
420
|
+
if (turnIndex !== null && turnIndex >= maxTurnsPerSession) {
|
|
421
|
+
return createFailureOutput(
|
|
422
|
+
LIMIT_EXCEEDED_ERROR_CODE,
|
|
423
|
+
`Execution metadata ${AGENT_RUNTIME_AGENT_TURN_INDEX_METADATA_KEY}=${turnIndex} reached max_turns_per_session=${maxTurnsPerSession}.`,
|
|
424
|
+
{
|
|
425
|
+
provider_call_count: PROVIDER_CALL_COUNT_ZERO,
|
|
426
|
+
[AGENT_RUNTIME_AGENT_TURN_INDEX_METADATA_KEY]: turnIndex,
|
|
427
|
+
max_turns_per_session: maxTurnsPerSession,
|
|
428
|
+
},
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const maxToolCallsPerSession = input.limits?.max_tool_calls_per_session;
|
|
434
|
+
if (maxToolCallsPerSession !== undefined) {
|
|
435
|
+
const toolCallCount = readAgentToolCallCount(ctx);
|
|
436
|
+
if (toolCallCount !== null && toolCallCount >= maxToolCallsPerSession) {
|
|
437
|
+
return createFailureOutput(
|
|
438
|
+
LIMIT_EXCEEDED_ERROR_CODE,
|
|
439
|
+
`Execution metadata ${AGENT_RUNTIME_AGENT_TOOL_CALL_COUNT_METADATA_KEY}=${toolCallCount} reached max_tool_calls_per_session=${maxToolCallsPerSession}.`,
|
|
440
|
+
{
|
|
441
|
+
provider_call_count: PROVIDER_CALL_COUNT_ZERO,
|
|
442
|
+
[AGENT_RUNTIME_AGENT_TOOL_CALL_COUNT_METADATA_KEY]: toolCallCount,
|
|
443
|
+
max_tool_calls_per_session: maxToolCallsPerSession,
|
|
444
|
+
},
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return null;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function resolveProviderEnvironment(
|
|
453
|
+
input: ValidatedTurnInput,
|
|
454
|
+
ctx: ExecutionContext,
|
|
455
|
+
): { ok: true; value: ResolvedProviderEnvironment } | { ok: false; output: ToolOutput } {
|
|
456
|
+
const packConfig = normalizePackConfig(Reflect.get(ctx, 'pack_config'));
|
|
457
|
+
const configuredProfile =
|
|
458
|
+
packConfig && isRecord(packConfig.models) ? packConfig.models[input.model_profile] : undefined;
|
|
459
|
+
|
|
460
|
+
if (configuredProfile !== undefined) {
|
|
461
|
+
return resolveConfiguredProviderEnvironment(input, input.model_profile, configuredProfile);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const apiKey = readNonEmptyString(process.env.AGENT_RUNTIME_API_KEY);
|
|
465
|
+
if (!apiKey) {
|
|
466
|
+
return {
|
|
467
|
+
ok: false,
|
|
468
|
+
output: createFailureOutput(
|
|
469
|
+
MISSING_ENVIRONMENT_ERROR_CODE,
|
|
470
|
+
`${AGENT_RUNTIME_API_KEY_ENV} must be set for agent-runtime provider calls.`,
|
|
471
|
+
),
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const baseUrl = readNonEmptyString(process.env.AGENT_RUNTIME_BASE_URL);
|
|
476
|
+
if (!baseUrl) {
|
|
477
|
+
return {
|
|
478
|
+
ok: false,
|
|
479
|
+
output: createFailureOutput(
|
|
480
|
+
MISSING_ENVIRONMENT_ERROR_CODE,
|
|
481
|
+
`${AGENT_RUNTIME_BASE_URL_ENV} must be set for agent-runtime provider calls.`,
|
|
482
|
+
),
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const normalizedInputUrl = normalizeUrlForComparison(input.url);
|
|
487
|
+
const normalizedBaseUrl = normalizeUrlForComparison(baseUrl);
|
|
488
|
+
if (!normalizedInputUrl || !normalizedBaseUrl) {
|
|
489
|
+
return {
|
|
490
|
+
ok: false,
|
|
491
|
+
output: createFailureOutput(
|
|
492
|
+
CONFIGURATION_ERROR_CODE,
|
|
493
|
+
'Agent-runtime provider URLs must be valid absolute URLs.',
|
|
494
|
+
),
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (normalizedInputUrl !== normalizedBaseUrl) {
|
|
499
|
+
return {
|
|
500
|
+
ok: false,
|
|
501
|
+
output: createFailureOutput(
|
|
502
|
+
TOOL_ERROR_CODES.INVALID_INPUT,
|
|
503
|
+
`input.url must match ${AGENT_RUNTIME_BASE_URL_ENV} for the selected provider profile.`,
|
|
504
|
+
{
|
|
505
|
+
configured_base_url: normalizedBaseUrl,
|
|
506
|
+
requested_url: normalizedInputUrl,
|
|
507
|
+
},
|
|
508
|
+
),
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
return {
|
|
513
|
+
ok: true,
|
|
514
|
+
value: {
|
|
515
|
+
kind: STATIC_PROVIDER_CAPABILITY_BASELINE.kind,
|
|
516
|
+
model: input.model_profile,
|
|
517
|
+
apiKey,
|
|
518
|
+
baseUrl: normalizedBaseUrl,
|
|
519
|
+
requiredEnv: [...STATIC_PROVIDER_CAPABILITY_BASELINE.required_env],
|
|
520
|
+
networkAllowlist: [...STATIC_PROVIDER_CAPABILITY_BASELINE.network_allowlist],
|
|
521
|
+
allowedUrls: [...STATIC_PROVIDER_CAPABILITY_BASELINE.allowed_urls],
|
|
522
|
+
},
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function createFailureOutput(
|
|
527
|
+
code: string,
|
|
528
|
+
message: string,
|
|
529
|
+
metadata?: Record<string, unknown>,
|
|
530
|
+
): ToolOutput {
|
|
531
|
+
return {
|
|
532
|
+
success: false,
|
|
533
|
+
error: {
|
|
534
|
+
code,
|
|
535
|
+
message,
|
|
536
|
+
},
|
|
537
|
+
metadata: createToolMetadata(metadata),
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function createToolMetadata(extra?: Record<string, unknown>): Record<string, unknown> {
|
|
542
|
+
return {
|
|
543
|
+
provider_kind: STATIC_PROVIDER_CAPABILITY_BASELINE.kind,
|
|
544
|
+
network_allowlist: [...STATIC_PROVIDER_CAPABILITY_BASELINE.network_allowlist],
|
|
545
|
+
allowed_urls: [...STATIC_PROVIDER_CAPABILITY_BASELINE.allowed_urls],
|
|
546
|
+
required_env: [...STATIC_PROVIDER_CAPABILITY_BASELINE.required_env],
|
|
547
|
+
...extra,
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function resolveConfiguredProviderEnvironment(
|
|
552
|
+
input: ValidatedTurnInput,
|
|
553
|
+
profileName: string,
|
|
554
|
+
profileValue: unknown,
|
|
555
|
+
): { ok: true; value: ResolvedProviderEnvironment } | { ok: false; output: ToolOutput } {
|
|
556
|
+
const profile = normalizeModelProfile(profileValue);
|
|
557
|
+
if (!profile) {
|
|
558
|
+
return {
|
|
559
|
+
ok: false,
|
|
560
|
+
output: createFailureOutput(
|
|
561
|
+
CONFIGURATION_ERROR_CODE,
|
|
562
|
+
`agent_runtime.models.${profileName} must define provider, model, and api_key_env.`,
|
|
563
|
+
),
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const apiKey = readNonEmptyString(process.env[profile.api_key_env]);
|
|
568
|
+
if (!apiKey) {
|
|
569
|
+
return {
|
|
570
|
+
ok: false,
|
|
571
|
+
output: createFailureOutput(
|
|
572
|
+
MISSING_ENVIRONMENT_ERROR_CODE,
|
|
573
|
+
`${profile.api_key_env} must be set for agent-runtime provider profile "${profileName}".`,
|
|
574
|
+
),
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const baseUrlResult = resolveProfileBaseUrl(profileName, profile);
|
|
579
|
+
if (!baseUrlResult.ok) {
|
|
580
|
+
return {
|
|
581
|
+
ok: false,
|
|
582
|
+
output: baseUrlResult.output,
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const normalizedInputUrl = normalizeUrlForComparison(input.url);
|
|
587
|
+
const normalizedBaseUrl = normalizeUrlForComparison(baseUrlResult.value);
|
|
588
|
+
if (!normalizedInputUrl || !normalizedBaseUrl) {
|
|
589
|
+
return {
|
|
590
|
+
ok: false,
|
|
591
|
+
output: createFailureOutput(
|
|
592
|
+
CONFIGURATION_ERROR_CODE,
|
|
593
|
+
'Agent-runtime provider URLs must be valid absolute URLs.',
|
|
594
|
+
),
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
if (normalizedInputUrl !== normalizedBaseUrl) {
|
|
599
|
+
return {
|
|
600
|
+
ok: false,
|
|
601
|
+
output: createFailureOutput(
|
|
602
|
+
TOOL_ERROR_CODES.INVALID_INPUT,
|
|
603
|
+
`input.url must match the resolved base URL for provider profile "${profileName}".`,
|
|
604
|
+
{
|
|
605
|
+
configured_base_url: normalizedBaseUrl,
|
|
606
|
+
requested_url: normalizedInputUrl,
|
|
607
|
+
},
|
|
608
|
+
),
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
return {
|
|
613
|
+
ok: true,
|
|
614
|
+
value: {
|
|
615
|
+
kind: profile.provider,
|
|
616
|
+
model: profile.model,
|
|
617
|
+
apiKey,
|
|
618
|
+
baseUrl: normalizedBaseUrl,
|
|
619
|
+
requiredEnv: [profile.api_key_env, ...(profile.base_url_env ? [profile.base_url_env] : [])],
|
|
620
|
+
networkAllowlist: [toNetworkAllowlistEntry(profileName, normalizedBaseUrl)],
|
|
621
|
+
allowedUrls: [normalizedBaseUrl],
|
|
622
|
+
},
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function normalizePackConfig(value: unknown): { models: Record<string, unknown> } | null {
|
|
627
|
+
if (!isRecord(value) || !isRecord(value.models)) {
|
|
628
|
+
return null;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
return {
|
|
632
|
+
models: value.models,
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function normalizeModelProfile(value: unknown): AgentRuntimeModelProfileConfig | null {
|
|
637
|
+
if (!isRecord(value)) {
|
|
638
|
+
return null;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
const provider = readNonEmptyString(value.provider);
|
|
642
|
+
const model = readNonEmptyString(value.model);
|
|
643
|
+
const apiKeyEnv = readNonEmptyString(value.api_key_env);
|
|
644
|
+
if (!provider || !isProviderKind(provider) || !model || !apiKeyEnv) {
|
|
645
|
+
return null;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const baseUrl = readOptionalNonEmptyString(value.base_url);
|
|
649
|
+
const baseUrlEnv = readOptionalNonEmptyString(value.base_url_env);
|
|
650
|
+
|
|
651
|
+
return {
|
|
652
|
+
provider,
|
|
653
|
+
model,
|
|
654
|
+
api_key_env: apiKeyEnv,
|
|
655
|
+
...(baseUrl ? { base_url: baseUrl } : {}),
|
|
656
|
+
...(baseUrlEnv ? { base_url_env: baseUrlEnv } : {}),
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function resolveProfileBaseUrl(
|
|
661
|
+
profileName: string,
|
|
662
|
+
profile: AgentRuntimeModelProfileConfig,
|
|
663
|
+
): { ok: true; value: string } | { ok: false; output: ToolOutput } {
|
|
664
|
+
if (profile.base_url) {
|
|
665
|
+
return {
|
|
666
|
+
ok: true,
|
|
667
|
+
value: profile.base_url,
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
if (!profile.base_url_env) {
|
|
672
|
+
return {
|
|
673
|
+
ok: false,
|
|
674
|
+
output: createFailureOutput(
|
|
675
|
+
CONFIGURATION_ERROR_CODE,
|
|
676
|
+
`agent_runtime.models.${profileName} must define base_url or base_url_env.`,
|
|
677
|
+
),
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
const environmentValue = readNonEmptyString(process.env[profile.base_url_env]);
|
|
682
|
+
if (!environmentValue) {
|
|
683
|
+
return {
|
|
684
|
+
ok: false,
|
|
685
|
+
output: createFailureOutput(
|
|
686
|
+
MISSING_ENVIRONMENT_ERROR_CODE,
|
|
687
|
+
`${profile.base_url_env} must be set for agent-runtime provider profile "${profileName}".`,
|
|
688
|
+
),
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
return {
|
|
693
|
+
ok: true,
|
|
694
|
+
value: environmentValue,
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function toNetworkAllowlistEntry(profileName: string, baseUrl: string): string {
|
|
699
|
+
try {
|
|
700
|
+
const parsed = new URL(baseUrl);
|
|
701
|
+
const port =
|
|
702
|
+
parsed.port ||
|
|
703
|
+
(parsed.protocol === 'https:' ? '443' : parsed.protocol === 'http:' ? '80' : '');
|
|
704
|
+
if (!port) {
|
|
705
|
+
throw new Error(`Unsupported protocol "${parsed.protocol}".`);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
return `${parsed.hostname}:${port}`;
|
|
709
|
+
} catch (error) {
|
|
710
|
+
const message = error instanceof Error ? error.message : 'unknown parsing error';
|
|
711
|
+
throw new Error(
|
|
712
|
+
`agent_runtime.models.${profileName} must resolve to a valid absolute base URL: ${message}`,
|
|
713
|
+
{
|
|
714
|
+
cause: error,
|
|
715
|
+
},
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function hasUnexpectedKeys(
|
|
721
|
+
record: Record<string, unknown>,
|
|
722
|
+
allowedKeys: readonly string[],
|
|
723
|
+
): boolean {
|
|
724
|
+
return Object.keys(record).some((key) => !allowedKeys.includes(key));
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
function isMessageRole(value: string | null): value is AgentRuntimeMessage['role'] {
|
|
728
|
+
return value === 'system' || value === 'user' || value === 'assistant' || value === 'tool';
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
732
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
733
|
+
? (value as Record<string, unknown>)
|
|
734
|
+
: null;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
738
|
+
return asRecord(value) !== null;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function readString(value: unknown): string | null {
|
|
742
|
+
return typeof value === 'string' ? value : null;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function readNonEmptyString(value: unknown): string | null {
|
|
746
|
+
if (typeof value !== 'string') {
|
|
747
|
+
return null;
|
|
748
|
+
}
|
|
749
|
+
const trimmed = value.trim();
|
|
750
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function readOptionalNonEmptyString(value: unknown): string | null {
|
|
754
|
+
return value === undefined ? null : readNonEmptyString(value);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function readOptionalPositiveInteger(value: unknown): number | null {
|
|
758
|
+
if (value === undefined) {
|
|
759
|
+
return null;
|
|
760
|
+
}
|
|
761
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value <= 0) {
|
|
762
|
+
return null;
|
|
763
|
+
}
|
|
764
|
+
return value;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function isProviderKind(value: string): value is AgentRuntimeProviderKind {
|
|
768
|
+
return value === 'openai_compatible' || value === 'messages_compatible';
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function readAgentTurnIndex(ctx: ExecutionContext): number | null {
|
|
772
|
+
if (!ctx.metadata) {
|
|
773
|
+
return null;
|
|
774
|
+
}
|
|
775
|
+
const value = ctx.metadata[AGENT_RUNTIME_AGENT_TURN_INDEX_METADATA_KEY];
|
|
776
|
+
return typeof value === 'number' && Number.isInteger(value) && value >= 0 ? value : null;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function readAgentToolCallCount(ctx: ExecutionContext): number | null {
|
|
780
|
+
if (!ctx.metadata) {
|
|
781
|
+
return null;
|
|
782
|
+
}
|
|
783
|
+
const value = ctx.metadata[AGENT_RUNTIME_AGENT_TOOL_CALL_COUNT_METADATA_KEY];
|
|
784
|
+
return typeof value === 'number' && Number.isInteger(value) && value >= 0 ? value : null;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function normalizeUrlForComparison(value: string): string | null {
|
|
788
|
+
try {
|
|
789
|
+
return new URL(value).toString();
|
|
790
|
+
} catch {
|
|
791
|
+
return null;
|
|
792
|
+
}
|
|
793
|
+
}
|