@cybernetyx1/atlasflow-runtime 0.1.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/LICENSE +18 -0
- package/README.md +56 -0
- package/dist/adapter/index.d.ts +1 -0
- package/dist/adapter/index.js +0 -0
- package/dist/channel-Dv3Hv1ee.d.ts +634 -0
- package/dist/chunk-4DU4GJ2X.js +347 -0
- package/dist/chunk-HO6QHSUS.js +85 -0
- package/dist/chunk-M4JW76IL.js +1161 -0
- package/dist/chunk-RF6W3TKJ.js +2762 -0
- package/dist/chunk-S7RZJMCF.js +47 -0
- package/dist/cloudflare/index.d.ts +109 -0
- package/dist/cloudflare/index.js +212 -0
- package/dist/command-kxrqWIH7.d.ts +204 -0
- package/dist/index-UFTgKRK4.d.ts +589 -0
- package/dist/index.d.ts +739 -0
- package/dist/index.js +965 -0
- package/dist/node/index.d.ts +65 -0
- package/dist/node/index.js +251 -0
- package/dist/providers.d.ts +40 -0
- package/dist/providers.js +26 -0
- package/dist/routing/index.d.ts +256 -0
- package/dist/routing/index.js +2184 -0
- package/package.json +68 -0
- package/schemas/persona-manifest.v1.schema.json +258 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,965 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BUILTIN_TOOL_NAMES,
|
|
3
|
+
EngineError,
|
|
4
|
+
EventBus,
|
|
5
|
+
Harness,
|
|
6
|
+
PiAgentEngine,
|
|
7
|
+
RuleViolation,
|
|
8
|
+
Session,
|
|
9
|
+
ToolApprovalRequiredError,
|
|
10
|
+
WorkflowDefSchema,
|
|
11
|
+
addUsage,
|
|
12
|
+
advanceWorkflow,
|
|
13
|
+
applyProfile,
|
|
14
|
+
approveGate,
|
|
15
|
+
attachToolApprovalPartialTranscript,
|
|
16
|
+
builtinTools,
|
|
17
|
+
createAgent,
|
|
18
|
+
cronMatches,
|
|
19
|
+
defaultEngine,
|
|
20
|
+
defineAgent,
|
|
21
|
+
defineAgentProfile,
|
|
22
|
+
defineChannel,
|
|
23
|
+
defineTool,
|
|
24
|
+
discoverSessionContext,
|
|
25
|
+
dispatchWorkflow,
|
|
26
|
+
emptyUsage,
|
|
27
|
+
envApiKeyFor,
|
|
28
|
+
genId,
|
|
29
|
+
getDefaultPersistence,
|
|
30
|
+
invokeAgent,
|
|
31
|
+
isChannelDefinition,
|
|
32
|
+
isCreatedAgent,
|
|
33
|
+
isRawTool,
|
|
34
|
+
isToolApprovalRequiredError,
|
|
35
|
+
isWorkspaceSkill,
|
|
36
|
+
loadWorkspaceSkill,
|
|
37
|
+
makeCall,
|
|
38
|
+
makeRuleGuard,
|
|
39
|
+
observe,
|
|
40
|
+
parseSkillMarkdown,
|
|
41
|
+
parseWorkflowDef,
|
|
42
|
+
rawTool,
|
|
43
|
+
recoverRuns,
|
|
44
|
+
recoverWorkflows,
|
|
45
|
+
resolveAnyTool,
|
|
46
|
+
resolveTool,
|
|
47
|
+
startCronScheduler,
|
|
48
|
+
startWorkflow,
|
|
49
|
+
verifyGithubWebhookSignature,
|
|
50
|
+
verifySlackRequestSignature,
|
|
51
|
+
waitingGate
|
|
52
|
+
} from "./chunk-RF6W3TKJ.js";
|
|
53
|
+
import {
|
|
54
|
+
BaseSandboxEnv,
|
|
55
|
+
bash,
|
|
56
|
+
bashFactoryToSessionEnv,
|
|
57
|
+
createCwdSessionEnv,
|
|
58
|
+
defineSandbox,
|
|
59
|
+
isSandboxFactory,
|
|
60
|
+
makeFs,
|
|
61
|
+
resolveSandboxNetworkPolicy,
|
|
62
|
+
sandboxCascade,
|
|
63
|
+
sandboxHeaderFromEnv,
|
|
64
|
+
virtualSandbox,
|
|
65
|
+
writeFileCreatingParents
|
|
66
|
+
} from "./chunk-4DU4GJ2X.js";
|
|
67
|
+
import {
|
|
68
|
+
DurableStreamSubscriptionStoreBase,
|
|
69
|
+
InMemoryRunStore,
|
|
70
|
+
InMemorySessionStore,
|
|
71
|
+
InMemoryStreamStore,
|
|
72
|
+
InMemorySubscriptionStore,
|
|
73
|
+
inMemoryAdapter,
|
|
74
|
+
isJsonStreamContentType,
|
|
75
|
+
isUnsafeNetworkHost,
|
|
76
|
+
kvAdapter,
|
|
77
|
+
normalizeStreamContentType,
|
|
78
|
+
streamMessageBytes
|
|
79
|
+
} from "./chunk-M4JW76IL.js";
|
|
80
|
+
import {
|
|
81
|
+
createScopedSessionEnv,
|
|
82
|
+
defineCommand,
|
|
83
|
+
mergeCommands,
|
|
84
|
+
normalizeCommandExecutor
|
|
85
|
+
} from "./chunk-S7RZJMCF.js";
|
|
86
|
+
import {
|
|
87
|
+
ProviderRegistrationError,
|
|
88
|
+
getApiProvider,
|
|
89
|
+
getApiProviders,
|
|
90
|
+
getRegisteredApiKey,
|
|
91
|
+
getRegisteredStoreResponses,
|
|
92
|
+
hasRegisteredProvider,
|
|
93
|
+
registerApiProvider,
|
|
94
|
+
registerProvider,
|
|
95
|
+
resetProvidersForTests,
|
|
96
|
+
resolveRegisteredModel,
|
|
97
|
+
unregisterApiProviders
|
|
98
|
+
} from "./chunk-HO6QHSUS.js";
|
|
99
|
+
|
|
100
|
+
// src/persona.ts
|
|
101
|
+
import * as v from "valibot";
|
|
102
|
+
var ProvenanceSchema = v.optional(v.picklist(["earned", "taught", "ingested", "researched", "inherited"]), "taught");
|
|
103
|
+
var IdentitySchema = v.object({
|
|
104
|
+
role: v.optional(v.string()),
|
|
105
|
+
disposition: v.optional(v.string()),
|
|
106
|
+
voice: v.optional(v.string()),
|
|
107
|
+
taste: v.optional(v.string()),
|
|
108
|
+
instructions: v.optional(v.string())
|
|
109
|
+
});
|
|
110
|
+
var SkillSchema = v.object({
|
|
111
|
+
name: v.pipe(v.string(), v.minLength(1)),
|
|
112
|
+
description: v.optional(v.string(), ""),
|
|
113
|
+
/** Alias accepted on import for agentskills.io round-tripping. */
|
|
114
|
+
when_to_use: v.optional(v.string()),
|
|
115
|
+
body: v.string(),
|
|
116
|
+
allowedTools: v.optional(v.array(v.string())),
|
|
117
|
+
provenance: ProvenanceSchema
|
|
118
|
+
});
|
|
119
|
+
var ContextPackSchema = v.object({
|
|
120
|
+
name: v.pipe(v.string(), v.minLength(1)),
|
|
121
|
+
description: v.optional(v.string()),
|
|
122
|
+
version: v.optional(v.string()),
|
|
123
|
+
mount: v.optional(v.picklist(["context", "sandbox"])),
|
|
124
|
+
items: v.array(v.object({ name: v.pipe(v.string(), v.minLength(1)), content: v.string() })),
|
|
125
|
+
provenance: ProvenanceSchema
|
|
126
|
+
});
|
|
127
|
+
var StandardSchema = v.object({
|
|
128
|
+
name: v.pipe(v.string(), v.minLength(1)),
|
|
129
|
+
content: v.string(),
|
|
130
|
+
provenance: ProvenanceSchema
|
|
131
|
+
});
|
|
132
|
+
var RuleSchema = v.object({
|
|
133
|
+
name: v.pipe(v.string(), v.minLength(1)),
|
|
134
|
+
enforce: v.picklist(["before_prompt", "before_tool", "after_tool"]),
|
|
135
|
+
tools: v.optional(v.array(v.string())),
|
|
136
|
+
pattern: v.optional(v.string()),
|
|
137
|
+
action: v.optional(v.picklist(["block", "warn"]), "block"),
|
|
138
|
+
message: v.string(),
|
|
139
|
+
provenance: ProvenanceSchema
|
|
140
|
+
});
|
|
141
|
+
var SubPersonaSchema = v.object({
|
|
142
|
+
name: v.pipe(v.string(), v.minLength(1)),
|
|
143
|
+
model: v.optional(v.string()),
|
|
144
|
+
instructions: v.string(),
|
|
145
|
+
provenance: ProvenanceSchema
|
|
146
|
+
});
|
|
147
|
+
var TriggersSchema = v.object({
|
|
148
|
+
webhook: v.optional(v.boolean()),
|
|
149
|
+
cron: v.optional(v.string())
|
|
150
|
+
});
|
|
151
|
+
var SlotSchema = v.object({
|
|
152
|
+
name: v.pipe(v.string(), v.minLength(1)),
|
|
153
|
+
type: v.picklist(["credential", "mcp", "tool", "sandbox", "memory"]),
|
|
154
|
+
/** For credential slots: the env var the workspace must bind. */
|
|
155
|
+
env: v.optional(v.string()),
|
|
156
|
+
required: v.optional(v.boolean(), false),
|
|
157
|
+
description: v.optional(v.string())
|
|
158
|
+
});
|
|
159
|
+
var DefaultsSchema = v.object({
|
|
160
|
+
thinkingLevel: v.optional(v.picklist(["off", "minimal", "low", "medium", "high", "xhigh"])),
|
|
161
|
+
headless: v.optional(v.boolean()),
|
|
162
|
+
builtinTools: v.optional(v.union([v.boolean(), v.array(v.string())])),
|
|
163
|
+
compaction: v.optional(v.union([v.literal(false), v.object({ threshold: v.optional(v.number()), thresholdTokens: v.optional(v.number()), keepRecent: v.optional(v.number()) })])),
|
|
164
|
+
durability: v.optional(v.object({ retry: v.optional(v.number()), timeoutMs: v.optional(v.number()) }))
|
|
165
|
+
});
|
|
166
|
+
var PersonaManifestSchema = v.object({
|
|
167
|
+
/** Optional editor schema reference. Ignored by the runtime. */
|
|
168
|
+
$schema: v.optional(v.string()),
|
|
169
|
+
/** Manifest version. v1 is frozen; breaking changes bump it. */
|
|
170
|
+
atlasflow: v.literal(1),
|
|
171
|
+
name: v.pipe(v.string(), v.minLength(1)),
|
|
172
|
+
description: v.optional(v.string()),
|
|
173
|
+
identity: v.optional(IdentitySchema),
|
|
174
|
+
model: v.pipe(v.string(), v.minLength(1)),
|
|
175
|
+
loadout: v.optional(
|
|
176
|
+
v.object({
|
|
177
|
+
skills: v.optional(v.array(SkillSchema)),
|
|
178
|
+
contextPacks: v.optional(v.array(ContextPackSchema)),
|
|
179
|
+
standards: v.optional(v.array(StandardSchema)),
|
|
180
|
+
rules: v.optional(v.array(RuleSchema)),
|
|
181
|
+
workflows: v.optional(v.array(WorkflowDefSchema)),
|
|
182
|
+
personas: v.optional(v.array(SubPersonaSchema)),
|
|
183
|
+
triggers: v.optional(TriggersSchema)
|
|
184
|
+
})
|
|
185
|
+
),
|
|
186
|
+
slots: v.optional(v.array(SlotSchema)),
|
|
187
|
+
defaults: v.optional(DefaultsSchema),
|
|
188
|
+
/** Opaque Brain metadata (lineage, fork base, provenance summary). */
|
|
189
|
+
provenance: v.optional(v.record(v.string(), v.unknown()))
|
|
190
|
+
});
|
|
191
|
+
function parsePersonaManifest(value) {
|
|
192
|
+
const r = v.safeParse(PersonaManifestSchema, value);
|
|
193
|
+
if (!r.success) {
|
|
194
|
+
const issues = r.issues.map((i) => `${i.path?.map((p) => String(p.key)).join(".") || "(root)"}: ${i.message}`).join("; ");
|
|
195
|
+
throw new Error(`Invalid persona manifest: ${issues}`);
|
|
196
|
+
}
|
|
197
|
+
return r.output;
|
|
198
|
+
}
|
|
199
|
+
function composeIdentity(m) {
|
|
200
|
+
const id = m.identity ?? {};
|
|
201
|
+
const sections = [];
|
|
202
|
+
if (id.role) sections.push(`You are ${id.role}.`);
|
|
203
|
+
if (id.disposition) sections.push(`Disposition: ${id.disposition}`);
|
|
204
|
+
if (id.voice) sections.push(`Voice: ${id.voice}`);
|
|
205
|
+
if (id.taste) sections.push(`Taste: ${id.taste}`);
|
|
206
|
+
if (id.instructions) sections.push(id.instructions);
|
|
207
|
+
for (const s of m.loadout?.standards ?? []) {
|
|
208
|
+
sections.push(`## Standard: ${s.name}
|
|
209
|
+
${s.content}`);
|
|
210
|
+
}
|
|
211
|
+
return sections.length ? sections.join("\n\n") : void 0;
|
|
212
|
+
}
|
|
213
|
+
function validateSlots(m, env, bindings, resolvedTools) {
|
|
214
|
+
const unbound = [];
|
|
215
|
+
for (const slot of m.slots ?? []) {
|
|
216
|
+
if (!slot.required) continue;
|
|
217
|
+
if (slot.type === "credential") {
|
|
218
|
+
const name = slot.env ?? slot.name;
|
|
219
|
+
if (typeof env[name] !== "string" || !env[name]) unbound.push(`${slot.name} (credential: set ${name})`);
|
|
220
|
+
} else if (slot.type === "sandbox") {
|
|
221
|
+
if (!bindings?.sandbox) unbound.push(`${slot.name} (sandbox: pass bindings.sandbox)`);
|
|
222
|
+
} else if (slot.type === "mcp" || slot.type === "tool" || slot.type === "memory") {
|
|
223
|
+
const hasSlotTools = Boolean(resolvedTools?.toolsBySlot[slot.name]?.length);
|
|
224
|
+
const hasLegacyTools = slot.type !== "memory" && !bindings?.toolSlots && Boolean(resolvedTools?.tools.length);
|
|
225
|
+
if (!hasSlotTools && !hasLegacyTools) {
|
|
226
|
+
if (slot.type === "memory") {
|
|
227
|
+
unbound.push(`${slot.name} (memory: pass a read-only connector in bindings.toolSlots.${slot.name})`);
|
|
228
|
+
} else {
|
|
229
|
+
const hint = slot.type === "mcp" ? "mcp/tool" : "tool";
|
|
230
|
+
unbound.push(`${slot.name} (${hint}: pass bindings.toolSlots.${slot.name}${slot.type === "mcp" ? " or bindings.tools" : ""})`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (unbound.length) {
|
|
236
|
+
throw new Error(`Persona "${m.name}" has unbound required slots: ${unbound.join("; ")}. Credentials are bound by the workspace and never travel with a persona.`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
async function resolveOneToolsBinding(source, ctx) {
|
|
240
|
+
if (!source) return { tools: [] };
|
|
241
|
+
const resolved = typeof source === "function" ? await source(ctx) : source;
|
|
242
|
+
if (Array.isArray(resolved)) return { tools: resolved };
|
|
243
|
+
if (isAnyTool(resolved)) return { tools: [resolved] };
|
|
244
|
+
if (isToolBindingResult(resolved)) return resolved;
|
|
245
|
+
const slot = ctx.slot?.name ? ` for slot "${ctx.slot.name}"` : "";
|
|
246
|
+
throw new Error(`Invalid persona tools binding${slot}: expected a tool, an array of tools, or { tools, cleanup? }.`);
|
|
247
|
+
}
|
|
248
|
+
function isAnyTool(value) {
|
|
249
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
250
|
+
const tool = value;
|
|
251
|
+
return typeof tool.name === "string" && typeof tool.description === "string" && "parameters" in tool && typeof tool.execute === "function";
|
|
252
|
+
}
|
|
253
|
+
function isToolBindingResult(value) {
|
|
254
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
255
|
+
const result = value;
|
|
256
|
+
if (!Array.isArray(result.tools)) return false;
|
|
257
|
+
if (!result.tools.every(isAnyTool)) return false;
|
|
258
|
+
return (result.cleanup === void 0 || typeof result.cleanup === "function") && (result.close === void 0 || typeof result.close === "function");
|
|
259
|
+
}
|
|
260
|
+
async function resolveToolsBinding(m, ctx, bindings) {
|
|
261
|
+
const tools = [];
|
|
262
|
+
const toolsBySlot = {};
|
|
263
|
+
const cleanup = [];
|
|
264
|
+
const collect = (slotName, resolved) => {
|
|
265
|
+
tools.push(...resolved.tools);
|
|
266
|
+
if (slotName) toolsBySlot[slotName] = resolved.tools;
|
|
267
|
+
const dispose = resolved.cleanup ?? resolved.close;
|
|
268
|
+
if (dispose) cleanup.push(dispose);
|
|
269
|
+
};
|
|
270
|
+
collect(void 0, await resolveOneToolsBinding(bindings?.tools, { ...ctx, manifest: m }));
|
|
271
|
+
for (const [slotName, source] of Object.entries(bindings?.toolSlots ?? {})) {
|
|
272
|
+
const slot = (m.slots ?? []).find((s) => s.name === slotName);
|
|
273
|
+
collect(slotName, await resolveOneToolsBinding(source, { ...ctx, manifest: m, slot }));
|
|
274
|
+
}
|
|
275
|
+
return {
|
|
276
|
+
tools,
|
|
277
|
+
toolsBySlot,
|
|
278
|
+
cleanup: cleanup.length === 0 ? void 0 : async () => {
|
|
279
|
+
for (const dispose of cleanup.toReversed()) await dispose();
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
function personaAgent(manifest, bindings) {
|
|
284
|
+
const m = parsePersonaManifest(manifest);
|
|
285
|
+
return createAgent(async (ctx) => {
|
|
286
|
+
const env = ctx.env;
|
|
287
|
+
const toolBinding = await resolveToolsBinding(m, { id: ctx.id, env }, bindings);
|
|
288
|
+
validateSlots(m, env, bindings, toolBinding);
|
|
289
|
+
const sandbox = typeof bindings?.sandbox === "function" ? bindings.sandbox({ id: ctx.id, env }) : bindings?.sandbox;
|
|
290
|
+
const config = {
|
|
291
|
+
name: m.name,
|
|
292
|
+
description: m.description,
|
|
293
|
+
model: m.model,
|
|
294
|
+
instructions: composeIdentity(m),
|
|
295
|
+
tools: toolBinding.tools,
|
|
296
|
+
skillLoading: bindings?.skillLoading,
|
|
297
|
+
skills: (m.loadout?.skills ?? []).map((s) => ({
|
|
298
|
+
name: s.name,
|
|
299
|
+
description: s.when_to_use ?? s.description,
|
|
300
|
+
body: s.body,
|
|
301
|
+
allowedTools: s.allowedTools
|
|
302
|
+
})),
|
|
303
|
+
contextPacks: m.loadout?.contextPacks,
|
|
304
|
+
contextLoading: bindings?.contextLoading,
|
|
305
|
+
rules: m.loadout?.rules,
|
|
306
|
+
personas: m.loadout?.personas?.map((p) => ({ name: p.name, model: p.model, instructions: p.instructions })),
|
|
307
|
+
sandbox,
|
|
308
|
+
thinkingLevel: m.defaults?.thinkingLevel,
|
|
309
|
+
headless: m.defaults?.headless,
|
|
310
|
+
builtinTools: m.defaults?.builtinTools,
|
|
311
|
+
compaction: m.defaults?.compaction,
|
|
312
|
+
durability: m.defaults?.durability,
|
|
313
|
+
cleanup: toolBinding.cleanup
|
|
314
|
+
};
|
|
315
|
+
return config;
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
function personaWorkflows(manifest) {
|
|
319
|
+
const m = parsePersonaManifest(manifest);
|
|
320
|
+
return Object.fromEntries((m.loadout?.workflows ?? []).map((w) => [w.name, w]));
|
|
321
|
+
}
|
|
322
|
+
function personaWorkflowMounts(manifest) {
|
|
323
|
+
const m = parsePersonaManifest(manifest);
|
|
324
|
+
const skills = (m.loadout?.skills ?? []).map((s) => ({ name: s.name, description: s.when_to_use ?? s.description, body: s.body }));
|
|
325
|
+
const profiles = (m.loadout?.personas ?? []).map((p) => ({ name: p.name, model: p.model, instructions: p.instructions }));
|
|
326
|
+
return Object.fromEntries(
|
|
327
|
+
// defaultPersona = the owner: a persona's workflow steps run AS the persona.
|
|
328
|
+
(m.loadout?.workflows ?? []).map((w) => [w.name, { def: w, skills, defaultModel: m.model, defaultPersona: m.name, profiles }])
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
function personaTriggers(manifest) {
|
|
332
|
+
const m = parsePersonaManifest(manifest);
|
|
333
|
+
return m.loadout?.triggers ?? { webhook: true };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// src/memory.ts
|
|
337
|
+
import * as v2 from "valibot";
|
|
338
|
+
var MemorySearchParametersSchema = v2.object({
|
|
339
|
+
query: v2.pipe(v2.string(), v2.minLength(1)),
|
|
340
|
+
limit: v2.optional(v2.number()),
|
|
341
|
+
scope: v2.optional(v2.string())
|
|
342
|
+
});
|
|
343
|
+
function defineMemoryConnector(options) {
|
|
344
|
+
return defineTool({
|
|
345
|
+
name: options.name ?? "memory_search",
|
|
346
|
+
description: options.description ?? "Search workspace-bound read-only memory for relevant facts.",
|
|
347
|
+
parameters: MemorySearchParametersSchema,
|
|
348
|
+
execute: async (args, signal) => {
|
|
349
|
+
const results = await options.search(args, signal);
|
|
350
|
+
return JSON.stringify({ results });
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// src/connection.ts
|
|
356
|
+
var CallParameters = {
|
|
357
|
+
type: "object",
|
|
358
|
+
properties: {
|
|
359
|
+
operation: { type: "string", description: "Operation name returned by the search tool." },
|
|
360
|
+
arguments: { type: "object", additionalProperties: true, description: "Arguments for the selected operation." }
|
|
361
|
+
},
|
|
362
|
+
required: ["operation"],
|
|
363
|
+
additionalProperties: false
|
|
364
|
+
};
|
|
365
|
+
var SearchParameters = {
|
|
366
|
+
type: "object",
|
|
367
|
+
properties: {
|
|
368
|
+
query: { type: "string", description: "Search text for operation names and descriptions." },
|
|
369
|
+
limit: { type: "number", description: "Maximum operations to return." }
|
|
370
|
+
},
|
|
371
|
+
required: ["query"],
|
|
372
|
+
additionalProperties: false
|
|
373
|
+
};
|
|
374
|
+
function defineHttpConnection(options) {
|
|
375
|
+
const operations = normalizeOperations(options.operations);
|
|
376
|
+
const expose = options.expose ?? "lazy";
|
|
377
|
+
return async (ctx) => {
|
|
378
|
+
const baseUrl = resolveBaseUrl(options, ctx);
|
|
379
|
+
const sharedHeaders = await resolveHeaders(options.headers, ctx);
|
|
380
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
381
|
+
const tools = [];
|
|
382
|
+
if (expose === "lazy" || expose === "both") {
|
|
383
|
+
tools.push(
|
|
384
|
+
connectionSearchTool(options.name, operations),
|
|
385
|
+
connectionCallTool(options.name, operations, baseUrl, sharedHeaders, fetchImpl, ctx, options.allowInsecureHttp === true, options.allowPrivateNetwork === true)
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
if (expose === "direct" || expose === "both") {
|
|
389
|
+
for (const operation of operations) {
|
|
390
|
+
tools.push(operationTool(options.name, operation, baseUrl, sharedHeaders, fetchImpl, ctx, options.allowInsecureHttp === true, options.allowPrivateNetwork === true));
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return { tools };
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
function connectionSearchTool(name, operations) {
|
|
397
|
+
return rawTool({
|
|
398
|
+
name: `${toolPart(name)}__search`,
|
|
399
|
+
description: `Search operations available on the "${name}" connection.`,
|
|
400
|
+
parameters: SearchParameters,
|
|
401
|
+
execute: async (args) => {
|
|
402
|
+
const query = String(args.query ?? "").trim().toLowerCase();
|
|
403
|
+
const limit = finitePositiveNumber(args.limit) ?? 10;
|
|
404
|
+
const scored = operations.map((operation) => ({ operation, score: scoreOperation(operation, query) })).filter((item) => item.score > 0 || !query).sort((a, b) => b.score - a.score || a.operation.name.localeCompare(b.operation.name)).slice(0, limit).map(({ operation }) => describeOperation(operation));
|
|
405
|
+
return JSON.stringify({ operations: scored });
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
function connectionCallTool(name, operations, baseUrl, sharedHeaders, fetchImpl, ctx, allowInsecureHttp, allowPrivateNetwork) {
|
|
410
|
+
return rawTool({
|
|
411
|
+
name: `${toolPart(name)}__call`,
|
|
412
|
+
description: `Call one operation on the "${name}" connection. Use ${toolPart(name)}__search first when unsure which operation to call.`,
|
|
413
|
+
parameters: CallParameters,
|
|
414
|
+
execute: async (args, signal) => {
|
|
415
|
+
const operationName = String(args.operation ?? "");
|
|
416
|
+
const operation = operations.find((item) => item.name === operationName);
|
|
417
|
+
if (!operation) throw new Error(`Unknown connection operation "${operationName}". Available: ${operations.map((item) => item.name).join(", ")}`);
|
|
418
|
+
return executeOperation(operation, objectArg(args.arguments), { baseUrl, sharedHeaders, fetchImpl, ctx, signal, allowInsecureHttp, allowPrivateNetwork });
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
function operationTool(name, operation, baseUrl, sharedHeaders, fetchImpl, ctx, allowInsecureHttp, allowPrivateNetwork) {
|
|
423
|
+
return rawTool({
|
|
424
|
+
name: `${toolPart(name)}__${toolPart(operation.name)}`,
|
|
425
|
+
description: operation.description,
|
|
426
|
+
parameters: operation.parameters ?? { type: "object", properties: {}, additionalProperties: true },
|
|
427
|
+
execute: async (args, signal) => executeOperation(operation, objectArg(args), { baseUrl, sharedHeaders, fetchImpl, ctx, signal, allowInsecureHttp, allowPrivateNetwork })
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
async function executeOperation(operation, args, options) {
|
|
431
|
+
const request = await buildRequest(operation, args, options);
|
|
432
|
+
const response = await options.fetchImpl(request, { signal: options.signal, redirect: "manual" });
|
|
433
|
+
const text = await response.text();
|
|
434
|
+
if (response.status >= 300 && response.status < 400) {
|
|
435
|
+
throw new Error(`Connection operation "${operation.name}" was redirected (${response.status}); redirects are not followed for credential safety.`);
|
|
436
|
+
}
|
|
437
|
+
if (!response.ok) throw new Error(`Connection operation "${operation.name}" failed with HTTP ${response.status}: ${text.slice(0, 1e3)}`);
|
|
438
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
439
|
+
if (contentType.includes("application/json")) {
|
|
440
|
+
try {
|
|
441
|
+
return JSON.stringify(JSON.parse(text));
|
|
442
|
+
} catch {
|
|
443
|
+
return text;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return text;
|
|
447
|
+
}
|
|
448
|
+
async function buildRequest(operation, args, options) {
|
|
449
|
+
const method = operation.method ?? "GET";
|
|
450
|
+
const consumed = /* @__PURE__ */ new Set();
|
|
451
|
+
let path = operation.path;
|
|
452
|
+
for (const key of operation.pathParams ?? templateParams(path)) {
|
|
453
|
+
const value = args[key];
|
|
454
|
+
if (value === void 0) throw new Error(`Connection operation "${operation.name}" missing path argument "${key}".`);
|
|
455
|
+
consumed.add(key);
|
|
456
|
+
path = path.replaceAll(`{${key}}`, encodeURIComponent(String(value)));
|
|
457
|
+
}
|
|
458
|
+
const url = new URL(path.replace(/^\/+/, ""), normalizedBaseUrl(options.baseUrl, { allowInsecureHttp: options.allowInsecureHttp, allowPrivateNetwork: options.allowPrivateNetwork }));
|
|
459
|
+
assertSafeConnectionUrl(url, options);
|
|
460
|
+
const queryKeys = operation.queryParams ?? (method === "GET" || method === "HEAD" ? Object.keys(args).filter((key) => !consumed.has(key)) : []);
|
|
461
|
+
for (const key of queryKeys) {
|
|
462
|
+
consumed.add(key);
|
|
463
|
+
appendQuery(url, key, args[key]);
|
|
464
|
+
}
|
|
465
|
+
let body;
|
|
466
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
467
|
+
const bodyValue = operation.body !== void 0 ? await resolveBody(operation.body, args, options.ctx) : operation.bodyParam ? args[operation.bodyParam] : remainingArgs(args, consumed);
|
|
468
|
+
if (operation.bodyParam) consumed.add(operation.bodyParam);
|
|
469
|
+
if (bodyValue !== void 0) body = JSON.stringify(bodyValue);
|
|
470
|
+
}
|
|
471
|
+
const operationHeaders = await resolveHeaders(operation.headers, options.ctx);
|
|
472
|
+
const headers = new Headers({ ...options.sharedHeaders, ...operationHeaders });
|
|
473
|
+
if (body !== void 0 && !headers.has("content-type")) headers.set("content-type", "application/json");
|
|
474
|
+
return new Request(url, { method, headers, body });
|
|
475
|
+
}
|
|
476
|
+
function normalizeOperations(operations) {
|
|
477
|
+
if (!operations.length) throw new Error("defineHttpConnection requires at least one operation.");
|
|
478
|
+
const names = /* @__PURE__ */ new Set();
|
|
479
|
+
return operations.map((operation) => {
|
|
480
|
+
const name = operation.name.trim();
|
|
481
|
+
if (!name) throw new Error("Connection operation names must be non-empty.");
|
|
482
|
+
if (names.has(name)) throw new Error(`Duplicate connection operation "${name}".`);
|
|
483
|
+
names.add(name);
|
|
484
|
+
return { ...operation, name };
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
function resolveBaseUrl(options, ctx) {
|
|
488
|
+
const fromEnv = options.baseUrlEnv ? stringValue(ctx.env[options.baseUrlEnv]) ?? nodeEnv(options.baseUrlEnv) : void 0;
|
|
489
|
+
const baseUrl = fromEnv ?? options.baseUrl;
|
|
490
|
+
if (!baseUrl) throw new Error(`Connection "${options.name}" requires ${options.baseUrlEnv ? `env ${options.baseUrlEnv}` : "a baseUrl"}.`);
|
|
491
|
+
return normalizedBaseUrl(baseUrl, { allowInsecureHttp: options.allowInsecureHttp === true, allowPrivateNetwork: options.allowPrivateNetwork === true });
|
|
492
|
+
}
|
|
493
|
+
async function resolveHeaders(input, ctx) {
|
|
494
|
+
if (!input) return {};
|
|
495
|
+
return typeof input === "function" ? input(ctx) : input;
|
|
496
|
+
}
|
|
497
|
+
async function resolveBody(input, args, ctx) {
|
|
498
|
+
return typeof input === "function" ? input(args, ctx) : input;
|
|
499
|
+
}
|
|
500
|
+
function normalizedBaseUrl(value, options) {
|
|
501
|
+
const url = new URL(value.endsWith("/") ? value : `${value}/`);
|
|
502
|
+
assertSafeConnectionUrl(url, options);
|
|
503
|
+
return url.toString();
|
|
504
|
+
}
|
|
505
|
+
function assertSafeConnectionUrl(url, options) {
|
|
506
|
+
if (url.protocol !== "https:" && !(options.allowInsecureHttp && url.protocol === "http:")) {
|
|
507
|
+
throw new Error("Connection URL must use https: unless allowInsecureHttp is set.");
|
|
508
|
+
}
|
|
509
|
+
if (!options.allowPrivateNetwork && isUnsafeNetworkHost(url.hostname)) {
|
|
510
|
+
throw new Error("Connection URL must not target localhost, private, loopback, link-local, or unspecified hosts unless allowPrivateNetwork is set.");
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
function appendQuery(url, key, value) {
|
|
514
|
+
if (value === void 0 || value === null) return;
|
|
515
|
+
if (Array.isArray(value)) {
|
|
516
|
+
for (const item of value) appendQuery(url, key, item);
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
url.searchParams.append(key, typeof value === "object" ? JSON.stringify(value) : String(value));
|
|
520
|
+
}
|
|
521
|
+
function remainingArgs(args, consumed) {
|
|
522
|
+
const entries = Object.entries(args).filter(([key, value]) => !consumed.has(key) && value !== void 0);
|
|
523
|
+
return entries.length ? Object.fromEntries(entries) : void 0;
|
|
524
|
+
}
|
|
525
|
+
function templateParams(path) {
|
|
526
|
+
return [...path.matchAll(/\{([^}]+)\}/g)].map((match) => match[1]).filter(Boolean);
|
|
527
|
+
}
|
|
528
|
+
function describeOperation(operation) {
|
|
529
|
+
return {
|
|
530
|
+
name: operation.name,
|
|
531
|
+
description: operation.description,
|
|
532
|
+
method: operation.method ?? "GET",
|
|
533
|
+
path: operation.path,
|
|
534
|
+
parameters: operation.parameters ?? { type: "object", properties: {}, additionalProperties: true }
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
function scoreOperation(operation, query) {
|
|
538
|
+
if (!query) return 1;
|
|
539
|
+
const haystack = `${operation.name} ${operation.description} ${operation.path}`.toLowerCase();
|
|
540
|
+
if (operation.name.toLowerCase() === query) return 100;
|
|
541
|
+
if (operation.name.toLowerCase().includes(query)) return 50;
|
|
542
|
+
return query.split(/\s+/).filter((term) => term && haystack.includes(term)).length;
|
|
543
|
+
}
|
|
544
|
+
function finitePositiveNumber(value) {
|
|
545
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : void 0;
|
|
546
|
+
}
|
|
547
|
+
function objectArg(value) {
|
|
548
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
549
|
+
}
|
|
550
|
+
function stringValue(value) {
|
|
551
|
+
return typeof value === "string" && value ? value : void 0;
|
|
552
|
+
}
|
|
553
|
+
function nodeEnv(key) {
|
|
554
|
+
return typeof process !== "undefined" ? process.env[key] : void 0;
|
|
555
|
+
}
|
|
556
|
+
function toolPart(value) {
|
|
557
|
+
return value.replace(/[^A-Za-z0-9_-]/g, "_").replace(/^_+|_+$/g, "") || "connection";
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// src/mcp.ts
|
|
561
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
562
|
+
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
563
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
564
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
565
|
+
import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js";
|
|
566
|
+
import { AjvJsonSchemaValidator } from "@modelcontextprotocol/sdk/validation/ajv";
|
|
567
|
+
async function connectMcpServer(options) {
|
|
568
|
+
const client = new Client({ name: options.name ?? "atlasflow", version: "0.0.0" });
|
|
569
|
+
const serverName = options.prefix ?? options.name ?? "server";
|
|
570
|
+
const transport = await makeTransport(options);
|
|
571
|
+
return connectMcpServerWithClient(serverName, client, transport, {
|
|
572
|
+
timeout: options.timeoutMs,
|
|
573
|
+
resetTimeoutOnProgress: options.resetTimeoutOnProgress
|
|
574
|
+
}, options.namespace !== false, describeMcpServer(options));
|
|
575
|
+
}
|
|
576
|
+
async function connectMcpServerWithClient(name, client, transport, requestOptions = {}, namespace = true, description) {
|
|
577
|
+
try {
|
|
578
|
+
await client.connect(transport);
|
|
579
|
+
let page = await client.listTools(void 0, requestOptions);
|
|
580
|
+
const tools = [...page.tools];
|
|
581
|
+
const seenCursors = /* @__PURE__ */ new Set();
|
|
582
|
+
while (page.nextCursor !== void 0) {
|
|
583
|
+
if (seenCursors.has(page.nextCursor)) {
|
|
584
|
+
throw new Error(`MCP server "${name}" repeated tools/list cursor ${JSON.stringify(page.nextCursor)} during tool discovery.`);
|
|
585
|
+
}
|
|
586
|
+
seenCursors.add(page.nextCursor);
|
|
587
|
+
page = await client.listTools({ cursor: page.nextCursor }, requestOptions);
|
|
588
|
+
tools.push(...page.tools);
|
|
589
|
+
}
|
|
590
|
+
return { name, tools: createMcpTools(name, client, tools, requestOptions, namespace), close: () => client.close() };
|
|
591
|
+
} catch (err) {
|
|
592
|
+
await client.close().catch(() => void 0);
|
|
593
|
+
if (!description) throw err;
|
|
594
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
595
|
+
throw new Error(`Could not connect to MCP server "${name}" (${description}): ${reason}`);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
async function makeTransport(options) {
|
|
599
|
+
const kind = options.transport ?? (options.url ? "http" : "stdio");
|
|
600
|
+
if (kind === "stdio") {
|
|
601
|
+
if (!options.command) throw new Error("connectMcpServer: `command` is required for the stdio transport");
|
|
602
|
+
return new StdioClientTransport({ command: options.command, args: options.args ?? [], env: options.env, cwd: options.cwd });
|
|
603
|
+
}
|
|
604
|
+
if (!options.url) throw new Error(`connectMcpServer: \`url\` is required for the ${kind} transport`);
|
|
605
|
+
const url = new URL(options.url);
|
|
606
|
+
const init = { requestInit: mergeRequestInit(options.requestInit, options.headers), fetch: options.fetch };
|
|
607
|
+
if (kind === "sse") return new SSEClientTransport(url, init);
|
|
608
|
+
return new StreamableHTTPClientTransport(url, init);
|
|
609
|
+
}
|
|
610
|
+
function describeMcpServer(options) {
|
|
611
|
+
const kind = options.transport ?? (options.url ? "http" : "stdio");
|
|
612
|
+
if (kind === "stdio") return `stdio ${[options.command, ...options.args ?? []].filter(Boolean).join(" ")}`;
|
|
613
|
+
return `${kind} ${options.url}`;
|
|
614
|
+
}
|
|
615
|
+
function createMcpTools(serverName, client, tools, requestOptions, namespace) {
|
|
616
|
+
const names = /* @__PURE__ */ new Set();
|
|
617
|
+
const validator = new AjvJsonSchemaValidator();
|
|
618
|
+
const callable = tools.filter((tool) => {
|
|
619
|
+
const execution = tool;
|
|
620
|
+
if (execution.execution?.taskSupport !== "required") return true;
|
|
621
|
+
console.warn(`Skipping MCP tool "${tool.name}" from server "${serverName}": it requires task-based execution, which is not supported.`);
|
|
622
|
+
return false;
|
|
623
|
+
});
|
|
624
|
+
return callable.map((tool) => {
|
|
625
|
+
const toolName = namespace ? createToolName(serverName, tool.name) : tool.name;
|
|
626
|
+
if (names.has(toolName)) throw new Error(`MCP tools from server "${serverName}" produced duplicate tool name "${toolName}".`);
|
|
627
|
+
names.add(toolName);
|
|
628
|
+
const outputValidator = tool.outputSchema ? validator.getValidator(tool.outputSchema) : void 0;
|
|
629
|
+
return rawTool({
|
|
630
|
+
name: toolName,
|
|
631
|
+
description: createToolDescription(serverName, tool),
|
|
632
|
+
parameters: normalizeInputSchema(tool.inputSchema),
|
|
633
|
+
execute: async (args, signal) => {
|
|
634
|
+
if (signal?.aborted) throw signal.reason ?? new Error("Operation aborted");
|
|
635
|
+
const result = await client.callTool({ name: tool.name, arguments: args }, void 0, { ...requestOptions, signal });
|
|
636
|
+
validateMcpResult(tool.name, result, outputValidator);
|
|
637
|
+
const text = formatMcpResult(result);
|
|
638
|
+
if (result.isError) throw new Error(text);
|
|
639
|
+
return text;
|
|
640
|
+
}
|
|
641
|
+
});
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
function validateMcpResult(toolName, result, validator) {
|
|
645
|
+
if (!validator) return;
|
|
646
|
+
if (result.structuredContent === void 0 && !result.isError) {
|
|
647
|
+
throw new McpError(ErrorCode.InvalidRequest, `Tool ${toolName} has an output schema but did not return structured content`);
|
|
648
|
+
}
|
|
649
|
+
if (result.structuredContent === void 0) return;
|
|
650
|
+
const validation = validator(result.structuredContent);
|
|
651
|
+
if (!validation.valid) {
|
|
652
|
+
throw new McpError(ErrorCode.InvalidParams, `Structured content does not match the tool's output schema: ${validation.errorMessage}`);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
function mergeRequestInit(requestInit, headers) {
|
|
656
|
+
if (!headers) return requestInit ?? {};
|
|
657
|
+
const mergedHeaders = new Headers(requestInit?.headers);
|
|
658
|
+
for (const [key, value] of Object.entries(headers)) mergedHeaders.set(key, value);
|
|
659
|
+
return { ...requestInit, headers: mergedHeaders };
|
|
660
|
+
}
|
|
661
|
+
function createToolName(serverName, toolName) {
|
|
662
|
+
return `mcp__${sanitizeToolNamePart(serverName)}__${sanitizeToolNamePart(toolName)}`;
|
|
663
|
+
}
|
|
664
|
+
function sanitizeToolNamePart(value) {
|
|
665
|
+
const sanitized = value.replace(/[^A-Za-z0-9_-]/g, "_").replace(/^_+|_+$/g, "");
|
|
666
|
+
return sanitized || "unnamed";
|
|
667
|
+
}
|
|
668
|
+
function createToolDescription(serverName, tool) {
|
|
669
|
+
const originalName = tool.name;
|
|
670
|
+
const title = tool.title ?? tool.annotations?.title;
|
|
671
|
+
const parts = [`MCP tool "${originalName}" from server "${serverName}".`];
|
|
672
|
+
if (title && title !== originalName) parts.push(`Title: ${title}.`);
|
|
673
|
+
if (tool.description) parts.push(tool.description);
|
|
674
|
+
return parts.join(" ");
|
|
675
|
+
}
|
|
676
|
+
function normalizeInputSchema(schema) {
|
|
677
|
+
return {
|
|
678
|
+
...schema,
|
|
679
|
+
type: schema.type ?? "object",
|
|
680
|
+
properties: schema.properties ?? {},
|
|
681
|
+
required: schema.required
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
function formatMcpResult(result) {
|
|
685
|
+
const parts = [];
|
|
686
|
+
if (result.structuredContent !== void 0) parts.push(`Structured content:
|
|
687
|
+
${JSON.stringify(result.structuredContent, null, 2)}`);
|
|
688
|
+
for (const item of result.content ?? []) {
|
|
689
|
+
if (item.type === "text") {
|
|
690
|
+
parts.push(item.text);
|
|
691
|
+
} else if (item.type === "image") {
|
|
692
|
+
parts.push(`[Image: ${item.mimeType}, ${item.data.length} base64 chars]`);
|
|
693
|
+
} else if (item.type === "audio") {
|
|
694
|
+
parts.push(`[Audio: ${item.mimeType}, ${item.data.length} base64 chars]`);
|
|
695
|
+
} else if (item.type === "resource") {
|
|
696
|
+
const resource = item.resource;
|
|
697
|
+
parts.push("text" in resource ? `[Resource: ${resource.uri}]
|
|
698
|
+
${resource.text}` : `[Resource: ${resource.uri}, ${resource.blob.length} base64 chars]`);
|
|
699
|
+
} else if (item.type === "resource_link") {
|
|
700
|
+
const description = item.description ? ` - ${item.description}` : "";
|
|
701
|
+
parts.push(`[Resource link: ${item.name} (${item.uri})${description}]`);
|
|
702
|
+
} else {
|
|
703
|
+
parts.push(JSON.stringify(item));
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
return parts.filter(Boolean).join("\n\n") || "(MCP tool returned no content)";
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// src/testing.ts
|
|
710
|
+
function defineEval(def) {
|
|
711
|
+
return def;
|
|
712
|
+
}
|
|
713
|
+
function fakeEngine(responder) {
|
|
714
|
+
return {
|
|
715
|
+
async run(input, hooks) {
|
|
716
|
+
const appended = [];
|
|
717
|
+
const messages = [...input.messages];
|
|
718
|
+
let usage = emptyUsage();
|
|
719
|
+
const pending = pendingToolCalls(messages);
|
|
720
|
+
const startTurn = pending.length ? messages.filter((message) => message.role === "assistant").length : 0;
|
|
721
|
+
await resumePendingFakeToolCalls(pending, messages, input.tools, input.signal, hooks, appended);
|
|
722
|
+
for (let turn = startTurn; turn < 50; turn++) {
|
|
723
|
+
const t = responder({ ...input, messages }, turn);
|
|
724
|
+
hooks.bus.emit({ type: "turn_start", turnId: `t${turn}`, model: input.model, runId: hooks.runId, session: hooks.session });
|
|
725
|
+
const content = [];
|
|
726
|
+
if (t.text) content.push({ type: "text", text: t.text });
|
|
727
|
+
for (const tc of t.toolCalls ?? []) {
|
|
728
|
+
content.push({ type: "tool_call", id: tc.id ?? `call_${turn}`, name: tc.name, arguments: tc.arguments });
|
|
729
|
+
}
|
|
730
|
+
const hasTools = (t.toolCalls?.length ?? 0) > 0;
|
|
731
|
+
const assistant = { role: "assistant", content, usage: emptyUsage(), stopReason: hasTools ? "tool_use" : "stop" };
|
|
732
|
+
appended.push(assistant);
|
|
733
|
+
messages.push(assistant);
|
|
734
|
+
hooks.bus.emit({ type: "turn_end", turnId: `t${turn}`, usage: emptyUsage(), stopReason: hasTools ? "tool_use" : "stop", runId: hooks.runId, session: hooks.session });
|
|
735
|
+
if (!hasTools) return { appended, text: t.text ?? "", usage };
|
|
736
|
+
for (const tc of t.toolCalls ?? []) {
|
|
737
|
+
const callId = tc.id ?? `call_${turn}`;
|
|
738
|
+
hooks.bus.emit({ type: "tool_call", tool: tc.name, callId, arguments: tc.arguments, runId: hooks.runId, session: hooks.session });
|
|
739
|
+
const tool = input.tools.find((x) => x.name === tc.name);
|
|
740
|
+
let text;
|
|
741
|
+
let ok = true;
|
|
742
|
+
try {
|
|
743
|
+
text = tool ? await tool.execute(tc.arguments, input.signal) : `Error: unknown tool "${tc.name}"`;
|
|
744
|
+
ok = Boolean(tool);
|
|
745
|
+
} catch (err) {
|
|
746
|
+
if (isToolApprovalRequiredError(err)) {
|
|
747
|
+
attachToolApprovalPartialTranscript(err, { appended: [...appended], usage });
|
|
748
|
+
throw err;
|
|
749
|
+
}
|
|
750
|
+
text = `Error: ${err instanceof Error ? err.message : String(err)}`;
|
|
751
|
+
ok = false;
|
|
752
|
+
}
|
|
753
|
+
const result = { role: "tool_result", toolCallId: callId, toolName: tc.name, content: [{ type: "text", text }], isError: !ok };
|
|
754
|
+
appended.push(result);
|
|
755
|
+
messages.push(result);
|
|
756
|
+
hooks.bus.emit({ type: "tool_result", tool: tc.name, callId, ok, runId: hooks.runId, session: hooks.session });
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
return { appended, text: "", usage };
|
|
760
|
+
}
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
async function resumePendingFakeToolCalls(pending, messages, tools, signal, hooks, appended) {
|
|
764
|
+
for (const tc of pending) {
|
|
765
|
+
const tool = tools.find((x) => x.name === tc.name);
|
|
766
|
+
hooks.bus.emit({ type: "tool_call", tool: tc.name, callId: tc.id, arguments: tc.arguments, runId: hooks.runId, session: hooks.session });
|
|
767
|
+
let text;
|
|
768
|
+
let ok = true;
|
|
769
|
+
try {
|
|
770
|
+
text = tool ? await tool.execute(tc.arguments, signal) : `Error: unknown tool "${tc.name}"`;
|
|
771
|
+
ok = Boolean(tool);
|
|
772
|
+
} catch (err) {
|
|
773
|
+
if (isToolApprovalRequiredError(err)) {
|
|
774
|
+
attachToolApprovalPartialTranscript(err, { appended: [...appended], usage: emptyUsage() });
|
|
775
|
+
throw err;
|
|
776
|
+
}
|
|
777
|
+
text = `Error: ${err instanceof Error ? err.message : String(err)}`;
|
|
778
|
+
ok = false;
|
|
779
|
+
}
|
|
780
|
+
const result = { role: "tool_result", toolCallId: tc.id, toolName: tc.name, content: [{ type: "text", text }], isError: !ok };
|
|
781
|
+
appended.push(result);
|
|
782
|
+
messages.push(result);
|
|
783
|
+
hooks.bus.emit({ type: "tool_result", tool: tc.name, callId: tc.id, ok, runId: hooks.runId, session: hooks.session });
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
function pendingToolCalls(messages) {
|
|
787
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
788
|
+
const message = messages[index];
|
|
789
|
+
if (message.role === "tool_result") continue;
|
|
790
|
+
if (message.role !== "assistant") return [];
|
|
791
|
+
const calls = message.content.filter((part) => part.type === "tool_call").map((part) => ({ id: part.id, name: part.name, arguments: part.arguments }));
|
|
792
|
+
if (!calls.length) return [];
|
|
793
|
+
const resultIds = new Set(
|
|
794
|
+
messages.slice(index + 1).filter((candidate) => candidate.role === "tool_result").map((candidate) => candidate.toolCallId)
|
|
795
|
+
);
|
|
796
|
+
return calls.filter((call) => !resultIds.has(call.id));
|
|
797
|
+
}
|
|
798
|
+
return [];
|
|
799
|
+
}
|
|
800
|
+
async function assertSandboxConformance(factory, options = {}) {
|
|
801
|
+
const env = await factory.createSessionEnv({ id: options.id ?? "conformance" });
|
|
802
|
+
try {
|
|
803
|
+
await assertFileApi(env);
|
|
804
|
+
if (options.shell !== false) await assertShellApi(env, options);
|
|
805
|
+
} finally {
|
|
806
|
+
await env.dispose?.().catch(() => {
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
async function assertFileApi(env) {
|
|
811
|
+
assert(typeof env.cwd === "string" && env.cwd.length > 0, "env.cwd must be a non-empty string");
|
|
812
|
+
assert(typeof env.resolvePath("nested/file.txt") === "string", "resolvePath must return a string");
|
|
813
|
+
await env.mkdir("nested/deeper", { recursive: true });
|
|
814
|
+
assert(await env.exists("nested"), "exists() should return true for created directories");
|
|
815
|
+
assert(!await env.exists("missing.txt"), "exists() should return false for missing paths");
|
|
816
|
+
await env.writeFile("nested/deeper/hello.txt", "hello atlas");
|
|
817
|
+
assertEquals(await env.readFile("nested/deeper/hello.txt"), "hello atlas", "readFile should return text written by writeFile");
|
|
818
|
+
const bytes = new Uint8Array([0, 1, 2, 65, 255]);
|
|
819
|
+
await env.writeFile("nested/blob.bin", bytes);
|
|
820
|
+
assertBytes(await env.readFileBuffer("nested/blob.bin"), bytes, "readFileBuffer should preserve byte content");
|
|
821
|
+
const fileStat = await env.stat("nested/deeper/hello.txt");
|
|
822
|
+
assert(fileStat.isFile, "stat(file).isFile should be true");
|
|
823
|
+
assert(!fileStat.isDirectory, "stat(file).isDirectory should be false");
|
|
824
|
+
assert(fileStat.size >= "hello atlas".length, "stat(file).size should be populated");
|
|
825
|
+
const dirStat = await env.stat("nested");
|
|
826
|
+
assert(dirStat.isDirectory, "stat(directory).isDirectory should be true");
|
|
827
|
+
const entries = await env.readdir("nested");
|
|
828
|
+
assert(entries.includes("deeper"), "readdir should include child directories");
|
|
829
|
+
assert(entries.includes("blob.bin"), "readdir should include child files");
|
|
830
|
+
await env.rm("nested/blob.bin");
|
|
831
|
+
assert(!await env.exists("nested/blob.bin"), "rm(file) should remove files");
|
|
832
|
+
await env.rm("nested", { recursive: true, force: true });
|
|
833
|
+
assert(!await env.exists("nested"), "rm(recursive, force) should remove directories");
|
|
834
|
+
}
|
|
835
|
+
async function assertShellApi(env, options) {
|
|
836
|
+
await env.mkdir("shell", { recursive: true });
|
|
837
|
+
await env.writeFile("shell/input.txt", "shell-canary");
|
|
838
|
+
const envRes = await env.exec(`printf '%s' "$ATLAS_CONF_CANARY"`, { env: { ATLAS_CONF_CANARY: "env-ok" } });
|
|
839
|
+
assertEquals(envRes.stdout, "env-ok", "exec should pass environment variables to the command");
|
|
840
|
+
assertEquals(envRes.exitCode, 0, "exec should return exitCode 0 for successful commands");
|
|
841
|
+
const cwdRes = await env.exec("cat input.txt", { cwd: env.resolvePath("shell") });
|
|
842
|
+
assertEquals(cwdRes.stdout, "shell-canary", "exec should honor cwd");
|
|
843
|
+
assertEquals(cwdRes.exitCode, 0, "exec with cwd should succeed");
|
|
844
|
+
const failRes = await env.exec("printf 'bad' >&2; exit 7");
|
|
845
|
+
assert(failRes.stderr.includes("bad"), "exec should capture stderr");
|
|
846
|
+
assertEquals(failRes.exitCode, 7, "exec should preserve non-zero exit codes");
|
|
847
|
+
if (options.timeout !== false) {
|
|
848
|
+
const timed = await env.exec("sleep 5", { timeoutMs: 100 });
|
|
849
|
+
assert(timed.exitCode !== 0, "exec timeout should not report success");
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
function assert(condition, message) {
|
|
853
|
+
if (!condition) throw new Error(`Sandbox conformance failed: ${message}`);
|
|
854
|
+
}
|
|
855
|
+
function assertEquals(actual, expected, message) {
|
|
856
|
+
if (actual !== expected) throw new Error(`Sandbox conformance failed: ${message}; expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
|
|
857
|
+
}
|
|
858
|
+
function assertBytes(actual, expected, message) {
|
|
859
|
+
assert(actual.length === expected.length, `${message}; byte length differs`);
|
|
860
|
+
for (let i = 0; i < expected.length; i++) {
|
|
861
|
+
assert(actual[i] === expected[i], `${message}; byte ${i} differs`);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
export {
|
|
865
|
+
BUILTIN_TOOL_NAMES,
|
|
866
|
+
BaseSandboxEnv,
|
|
867
|
+
DurableStreamSubscriptionStoreBase,
|
|
868
|
+
EngineError,
|
|
869
|
+
EventBus,
|
|
870
|
+
Harness,
|
|
871
|
+
InMemoryRunStore,
|
|
872
|
+
InMemorySessionStore,
|
|
873
|
+
InMemoryStreamStore,
|
|
874
|
+
InMemorySubscriptionStore,
|
|
875
|
+
MemorySearchParametersSchema,
|
|
876
|
+
PersonaManifestSchema,
|
|
877
|
+
PiAgentEngine,
|
|
878
|
+
ProviderRegistrationError,
|
|
879
|
+
RuleViolation,
|
|
880
|
+
Session,
|
|
881
|
+
ToolApprovalRequiredError,
|
|
882
|
+
WorkflowDefSchema,
|
|
883
|
+
addUsage,
|
|
884
|
+
advanceWorkflow,
|
|
885
|
+
applyProfile,
|
|
886
|
+
approveGate,
|
|
887
|
+
assertSandboxConformance,
|
|
888
|
+
bash,
|
|
889
|
+
bashFactoryToSessionEnv,
|
|
890
|
+
builtinTools,
|
|
891
|
+
connectMcpServer,
|
|
892
|
+
createAgent,
|
|
893
|
+
createCwdSessionEnv,
|
|
894
|
+
createScopedSessionEnv,
|
|
895
|
+
cronMatches,
|
|
896
|
+
defaultEngine,
|
|
897
|
+
defineAgent,
|
|
898
|
+
defineAgentProfile,
|
|
899
|
+
defineChannel,
|
|
900
|
+
defineCommand,
|
|
901
|
+
defineEval,
|
|
902
|
+
defineHttpConnection,
|
|
903
|
+
defineMemoryConnector,
|
|
904
|
+
defineSandbox,
|
|
905
|
+
defineTool,
|
|
906
|
+
discoverSessionContext,
|
|
907
|
+
dispatchWorkflow,
|
|
908
|
+
emptyUsage,
|
|
909
|
+
envApiKeyFor,
|
|
910
|
+
fakeEngine,
|
|
911
|
+
genId,
|
|
912
|
+
getApiProvider,
|
|
913
|
+
getApiProviders,
|
|
914
|
+
getDefaultPersistence,
|
|
915
|
+
getRegisteredApiKey,
|
|
916
|
+
getRegisteredStoreResponses,
|
|
917
|
+
hasRegisteredProvider,
|
|
918
|
+
inMemoryAdapter,
|
|
919
|
+
invokeAgent,
|
|
920
|
+
isChannelDefinition,
|
|
921
|
+
isCreatedAgent,
|
|
922
|
+
isJsonStreamContentType,
|
|
923
|
+
isRawTool,
|
|
924
|
+
isSandboxFactory,
|
|
925
|
+
isToolApprovalRequiredError,
|
|
926
|
+
isWorkspaceSkill,
|
|
927
|
+
kvAdapter,
|
|
928
|
+
loadWorkspaceSkill,
|
|
929
|
+
makeCall,
|
|
930
|
+
makeFs,
|
|
931
|
+
makeRuleGuard,
|
|
932
|
+
mergeCommands,
|
|
933
|
+
normalizeCommandExecutor,
|
|
934
|
+
normalizeStreamContentType,
|
|
935
|
+
observe,
|
|
936
|
+
parsePersonaManifest,
|
|
937
|
+
parseSkillMarkdown,
|
|
938
|
+
parseWorkflowDef,
|
|
939
|
+
personaAgent,
|
|
940
|
+
personaTriggers,
|
|
941
|
+
personaWorkflowMounts,
|
|
942
|
+
personaWorkflows,
|
|
943
|
+
rawTool,
|
|
944
|
+
recoverRuns,
|
|
945
|
+
recoverWorkflows,
|
|
946
|
+
registerApiProvider,
|
|
947
|
+
registerProvider,
|
|
948
|
+
resetProvidersForTests,
|
|
949
|
+
resolveAnyTool,
|
|
950
|
+
resolveRegisteredModel,
|
|
951
|
+
resolveSandboxNetworkPolicy,
|
|
952
|
+
resolveTool,
|
|
953
|
+
sandboxCascade,
|
|
954
|
+
sandboxHeaderFromEnv,
|
|
955
|
+
startCronScheduler,
|
|
956
|
+
startWorkflow,
|
|
957
|
+
streamMessageBytes,
|
|
958
|
+
unregisterApiProviders,
|
|
959
|
+
validateSlots,
|
|
960
|
+
verifyGithubWebhookSignature,
|
|
961
|
+
verifySlackRequestSignature,
|
|
962
|
+
virtualSandbox,
|
|
963
|
+
waitingGate,
|
|
964
|
+
writeFileCreatingParents
|
|
965
|
+
};
|