-
Notifications
You must be signed in to change notification settings - Fork 156
Expand file tree
/
Copy pathdescription.ts
More file actions
157 lines (146 loc) · 9.78 KB
/
Copy pathdescription.ts
File metadata and controls
157 lines (146 loc) · 9.78 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
import { Effect } from "effect";
import type { Connection, Integration, Executor } from "@executor-js/sdk/core";
/**
* Builds a tool description dynamically.
*
* Structure:
* 1. Workflow (top — critical, least likely to be truncated)
* 2. Available connection prefixes (bottom)
*
* v2: callable API tools are scoped by saved connections. A tool's sandbox
* address is `tools.<integration>.<owner>.<connection>.<tool>`, so the useful
* inventory is the connection prefix rather than only the integration slug.
*/
export const buildExecuteDescription = (executor: Executor): Effect.Effect<string> =>
Effect.gen(function* () {
const connections: readonly Connection[] = yield* executor.connections.list().pipe(
// oxlint-disable-next-line executor/no-effect-escape-hatch -- boundary: ExecutionEngine.getDescription currently exposes no error channel; engine typed-error widening is covered separately
Effect.orDie,
Effect.withSpan("executor.connections.list"),
);
const integrations: readonly Integration[] = yield* executor.integrations.list().pipe(
// oxlint-disable-next-line executor/no-effect-escape-hatch -- boundary: same getDescription error-channel constraint as connections.list above
Effect.orDie,
Effect.withSpan("executor.integrations.list"),
);
const description = yield* Effect.sync(() =>
formatDescription(connections.map((connection) => connectionEntry(connection, integrations))),
).pipe(
Effect.withSpan("schema.compile.description", {
attributes: { "executor.connection_count": connections.length },
}),
);
yield* Effect.annotateCurrentSpan({
"executor.connection_count": connections.length,
"schema.kind": "execute",
// Connection inventory so a failing session build (which runs this during
// init) names the callable prefixes it resolved without listing tools.
"executor.connection_addresses": connections
.map((connection) => connectionPath(connection))
.slice(0, 50)
.join(","),
"executor.connection_integrations": [
...new Set(connections.map((connection) => String(connection.integration))),
].join(","),
"executor.connection_owners": [
...new Set(connections.map((connection) => connection.owner)),
].join(","),
});
return description;
}).pipe(Effect.withSpan("schema.describe.execute"));
const connectionPath = (connection: Connection): string => {
const address = String(connection.address);
return address.startsWith("tools.") ? address.slice("tools.".length) : address;
};
/** One inventory line: the callable prefix plus the best available context.
* Connection description wins (the user wrote it about THIS credential);
* otherwise the integration description, unless it is just the slug again. */
interface ConnectionInventoryEntry {
readonly prefix: string;
readonly description?: string;
}
const inventoryNote = (
text: string | null | undefined,
identityEchoes: readonly string[],
): string | undefined => {
const firstLine = (text ?? "").split("\n", 1)[0]!.trim();
if (firstLine.length === 0) return undefined;
// A description that just restates the slug or display name carries no
// information beyond identity — drop it from the inventory line.
if (identityEchoes.some((echo) => firstLine.toLowerCase() === echo.toLowerCase())) {
return undefined;
}
return firstLine.length > 140 ? `${firstLine.slice(0, 139)}…` : firstLine;
};
const connectionEntry = (
connection: Connection,
integrations: readonly Integration[],
): ConnectionInventoryEntry => {
const slug = String(connection.integration);
const integration = integrations.find((candidate) => String(candidate.slug) === slug);
const identityEchoes = [slug, ...(integration ? [integration.name] : [])];
return {
prefix: connectionPath(connection),
description:
inventoryNote(connection.description, identityEchoes) ??
inventoryNote(integration?.description, identityEchoes),
};
};
const formatDescription = (connectionEntries: readonly ConnectionInventoryEntry[]): string => {
const lines: string[] = [
"Execute TypeScript in a sandboxed runtime with access to configured API tools.",
"",
"## Workflow",
"",
'1. `const { items: matches } = await tools.search({ query: "<intent + key nouns>", limit: 12 });`',
'2. `const path = matches[0]?.path; if (!path) return "No matching tools found.";`',
"3. `const details = await tools.describe.tool({ path });`",
"4. Use `details.inputTypeScript` / `details.outputTypeScript` and `details.typeScriptDefinitions` for compact shapes.",
"5. Use `tools.executor.coreTools.connections.list({})` when you need live saved-connection inventory.",
"6. Call the tool: `const result = await tools.<path>(input);`",
"",
"## Rules",
"",
"- `tools.search()` returns paginated, ranked matches: `{ items, total, hasMore, nextOffset }`. Best-first. Use short intent phrases like `github issues`, `repo details`, or `create calendar event`.",
'- When you already know the namespace, narrow with `tools.search({ namespace: "github", query: "issues" })`.',
"- `tools.executor.coreTools.connections.list({})` returns saved connections with `{ address, integration, owner, name, ... }`. The `address` field includes the leading `tools.` root.",
"- Tool calls return a value union: `{ ok: true, data }` for success or `{ ok: false, error: { code, message, status?, details?, retryable? } }` for expected tool/domain failures. Branch on `result.ok`.",
"- `data` is the upstream payload itself. HTTP-backed tools (OpenAPI) also set `http: { status, headers }` beside `data` — read `result.http?.headers` for pagination (Link) or rate-limit headers.",
"- Use `emit(value)` to append user-visible output and return `undefined`. Plain values become MCP text content. MCP content blocks are forwarded as-is. `ToolFile` values are rendered by MIME. Emitted output goes to the user, not back to you; the result envelope reports an `emitted` count so you can confirm it landed, but to read a value yourself, `return` it.",
"- Prefer explicit output helpers when the content kind is known: `text(value)`, `image(value, detail?)`, `audio(block)`, `file(toolFile)`, `resource(block)`, and `notify(value)`. `image` accepts a base64 data URI string, `{ image_url, detail }`, or a raw MCP image block; `detail` may be `auto`, `low`, `high`, or `original`.",
'- File-returning tools may return `ToolFile` values: `{ _tag: "ToolFile", name?, mimeType, encoding: "base64", data, byteLength }`. Emit any attachment with `emit(result.data)`.',
'- To emit MCP-native content directly, pass an MCP content block to `emit(...)`, such as `{ type: "image", data, mimeType }`, `{ type: "audio", data, mimeType }`, `{ type: "text", text }`, `{ type: "resource", resource }`, or `{ type: "resource_link", uri, name, ... }`.',
"- `emit(ToolFile)` is MIME-based: `image/*` becomes MCP image content, `audio/*` becomes MCP audio content, text-like files become decoded text, and other binary files become embedded MCP resources.",
"- `return` is only for ordinary structured data. Returning a `ToolFile`, a `ToolResult`, an MCP content block, or a bare base64 string does not emit content to the MCP client.",
"- Some providers, including Gmail, return attachment bytes without a public URL. To send that attachment to another API from code, decode `ToolFile.data` from base64 and pass the bytes to that API's upload/file input.",
"- If `tools.search()` returns `hasMore: true` and you didn't find what you need, fetch the next page: `tools.search({ query, offset: nextOffset, limit })`.",
"- Always use the full address when calling tools: `tools.<integration>.<owner>.<connection>.<tool>(args)`. The `path` returned by `tools.search()` / `tools.describe.tool()` is already the exact path under `tools` — call `tools[path]` rather than guessing segments.",
"- The `tools` object is a lazy proxy. `Object.keys(tools)` returns only bounded built-in discovery keys (`search`, `describe`, `executor`), not the full API catalog. Use `tools.search()` or `tools.executor.coreTools.connections.list({})` for configured tools.",
'- Pass an object to system tools, e.g. `tools.search({ query: "..." })`, `tools.executor.coreTools.connections.list({})`, and `tools.describe.tool({ path })`.',
'- `tools.describe.tool()` returns compact TypeScript shapes. Use `inputTypeScript`, `outputTypeScript`, and `typeScriptDefinitions`. If the path doesn\'t resolve, the result carries `error: { code: "tool_not_found", suggestions }` — use a suggestion instead of retrying the same path.',
"- For tools that return large collections (e.g. `getStates`, `getAll`), filter results in code rather than calling per-item tools.",
"- Do not use `fetch` — all API calls go through `tools.*`.",
"- If execution pauses for interaction, resume it with the returned `resumePayload`.",
"- TypeScript type syntax (`: T`, `as T`, generics, interfaces, type aliases) is stripped before execution — feel free to write idiomatic TypeScript using the shapes from `tools.describe.tool()`. Decorators and `enum` are not supported.",
];
if (connectionEntries.length > 0) {
lines.push("");
lines.push("## Available connection prefixes");
lines.push("");
lines.push("These are paths under `tools.`; append the final tool segment.");
const sorted = [...connectionEntries]
.sort((a, b) => a.prefix.localeCompare(b.prefix))
.slice(0, 50);
for (const entry of sorted) {
lines.push(
entry.description
? `- \`${entry.prefix}\` — ${entry.description}`
: `- \`${entry.prefix}\``,
);
}
if (connectionEntries.length > sorted.length) {
lines.push(`- ... ${connectionEntries.length - sorted.length} more`);
}
}
return lines.join("\n");
};