-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathcontext.ts
More file actions
306 lines (279 loc) · 10 KB
/
Copy pathcontext.ts
File metadata and controls
306 lines (279 loc) · 10 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
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
/**
* OpenFunction — Context Provider System
*
* A context provider connects an external system (ExecuFunction, Obsidian,
* Notion, etc.) to the Open Functions agent runtime. Like memory and RAG,
* providers expose themselves as tools — the same tool definitions work
* across MCP, chat, workflows, and agents.
*
* The pattern follows the existing composition model:
* - memory: createConversationMemory() → .createMemoryTools() → registry
* - rag: createRAG() → .createTools() → registry
* - context: createProvider() → .createTools() → registry
*
* Providers also support system prompt injection: buildContext() returns
* a text block (active tasks, upcoming events, etc.) that agents can
* include in their system prompt for situational awareness.
*
* @example
* ```ts
* import { connectProvider, contextPrompt } from "./framework/index.js";
* import { createExecuFunctionProvider } from "@openfunctions/provider-execufunction";
*
* // Connect a provider — registers its tools into the global registry
* const exf = await connectProvider(
* createExecuFunctionProvider({ token: process.env.EXF_PAT }),
* registry,
* );
*
* // Build a context block for agent system prompts
* const context = await contextPrompt([exf]);
*
* const agent = defineAgent({
* name: "assistant",
* role: "Personal productivity assistant",
* goal: "Help the user manage tasks and schedule",
* toolTags: ["context"],
* });
* ```
*/
import type { ToolDefinition } from "./types.js";
import { ToolRegistry } from "./registry.js";
// ─── Types ──────────────────────────────────────────────────────────────────
/**
* Capability domains a context provider can support.
* Providers declare which domains they cover so the framework (and agents)
* know what's available without calling any tools.
*/
export type ContextCapability =
| "tasks"
| "projects"
| "calendar"
| "knowledge"
| "people"
| "organizations"
| "codebase"
| "code_memory"
| "vault"
| "datasets"
| "documents"
| "agent_work"
| "agents"
| "relationships"
| "approvals"
| "execution_grants"
| "ai"
| "capabilities";
/**
* Lightweight metadata about a context provider.
* Cheap to compute — no network calls, no auth needed.
* Used for discovery, setup UX, and agent prompt composition.
*/
export interface ContextProviderMetadata {
/** Unique provider ID (e.g. "execufunction", "obsidian", "notion") */
id: string;
/** Human-readable name */
name: string;
/** One-line description for agent system prompts and setup UX */
description: string;
/** Which capability domains this provider supports */
capabilities: ContextCapability[];
/** Auth requirements — helps setup UX and error messages */
auth?: {
/** How the user authenticates */
kind: "pat" | "oauth" | "api_key" | "local";
/** Environment variable to check for credentials */
envVar?: string;
/** URL where the user can create credentials */
setupUrl?: string;
/** Human-readable setup instructions */
instructions?: string;
};
}
/**
* A context provider definition.
*
* Implementors create one of these via a factory function
* (e.g. createExecuFunctionProvider()). The framework calls connect()
* to initialize the provider and get a ConnectedProvider back.
*
* This two-phase design (define → connect) mirrors the framework's
* existing patterns: defineTool() validates at definition time,
* registry.execute() runs at call time.
*/
export interface ContextProvider {
/** Cheap metadata — no network calls, no auth required */
metadata: ContextProviderMetadata;
/**
* Initialize the provider. May perform auth validation, health checks,
* or lazy resource setup. Returns a connected handle with tools.
*
* @param config - Optional provider-specific configuration
* @throws If auth fails or the provider cannot be reached
*/
connect(config?: Record<string, unknown>): Promise<ConnectedProvider>;
}
/**
* A connected, ready-to-use context provider.
*
* Returned by ContextProvider.connect(). Provides tools for the registry
* and optional context for agent system prompts.
*/
export interface ConnectedProvider {
/** The provider's metadata (same as the parent ContextProvider) */
readonly metadata: ContextProviderMetadata;
/**
* Generate tool definitions for this provider's capabilities.
* All returned tools are tagged with `"context"` and
* `"context:<providerId>"` for agent filtering.
*/
createTools(): ToolDefinition<any, any>[];
/**
* Build a text block summarizing the user's current context
* from this provider. Agents include this in their system prompt
* for situational awareness (active tasks, upcoming events, etc.).
*
* Returns undefined if no context is available or the provider
* doesn't support prompt injection.
*/
buildContext?(): Promise<string | undefined>;
/**
* Check if the provider is reachable and authenticated.
* Useful for setup UX and monitoring.
*/
healthCheck?(): Promise<{ ok: boolean; error?: string }>;
/**
* Clean up resources (close connections, cancel timers, etc.).
*/
disconnect?(): Promise<void>;
}
// ─── Provider Connection ────────────────────────────────────────────────────
/**
* Connect a context provider and register its tools into a registry.
*
* This is the primary way to wire a provider into the framework:
*
* ```ts
* const exf = await connectProvider(
* createExecuFunctionProvider({ token: "..." }),
* registry,
* );
* ```
*
* Tools are automatically tagged with `"context"` and
* `"context:<providerId>"` so agents can filter by provider or
* by the general context tag.
*
* @param provider - The provider to connect
* @param registry - The tool registry to register tools into
* @param config - Optional provider-specific configuration
* @returns The connected provider handle
*/
export async function connectProvider(
provider: ContextProvider,
registry: ToolRegistry,
config?: Record<string, unknown>,
): Promise<ConnectedProvider> {
const { id, name } = provider.metadata;
const connected = await provider.connect(config);
const before = new Set(registry.listNames());
try {
const tools = connected.createTools();
const taggedTools = tools.map((tool) => ({
...tool,
tags: dedupTags([
...(tool.tags ?? []),
"context",
`context:${id}`,
]),
}));
// User tools win on name collision — provider tools should not silently
// shadow them. Skip and warn instead of overwrite.
registry.registerAll(taggedTools, { overwrite: false });
console.log(
`🔗 ${name}: connected (${taggedTools.length} tool${taggedTools.length === 1 ? "" : "s"} registered)`,
);
return connected;
} catch (error) {
for (const registeredName of registry.listNames()) {
if (!before.has(registeredName)) registry.unregister(registeredName);
}
await connected.disconnect?.().catch(() => undefined);
throw error;
}
}
// ─── Context Prompt Builder ─────────────────────────────────────────────────
/**
* Build a system prompt section from one or more connected providers.
*
* Each provider that implements buildContext() contributes a section.
* The result is a single string suitable for inclusion in a system prompt
* via composePrompt({ context: ... }).
*
* ```ts
* const context = await contextPrompt([exf, obsidian]);
* const systemPrompt = composePrompt({
* role: "Personal assistant",
* context,
* toolGuide: autoToolGuide(registry),
* });
* ```
*
* @param providers - Connected providers to query for context
* @returns Combined context string, or empty string if no context available
*/
export async function contextPrompt(
providers: ConnectedProvider[],
): Promise<string> {
const sections: string[] = [];
for (const provider of providers) {
if (!provider.buildContext) continue;
try {
const block = await provider.buildContext();
if (block) {
sections.push(block);
}
} catch (error) {
const name = provider.metadata.name;
const message = error instanceof Error ? error.message : "unknown error";
console.warn(`⚠️ ${name}: failed to build context — ${message}`);
}
}
return sections.join("\n\n");
}
// ─── Provider Health ────────────────────────────────────────────────────────
/**
* Check the health of one or more connected providers.
*
* Returns a summary for each provider. Useful for setup UX
* and monitoring dashboards.
*
* ```ts
* const health = await checkProviderHealth([exf, obsidian]);
* // [{ id: "execufunction", name: "ExecuFunction", ok: true },
* // { id: "obsidian", name: "Obsidian", ok: false, error: "vault not found" }]
* ```
*/
export async function checkProviderHealth(
providers: ConnectedProvider[],
): Promise<Array<{ id: string; name: string; ok: boolean; error?: string }>> {
return Promise.all(
providers.map(async (provider) => {
const { id, name } = provider.metadata;
if (!provider.healthCheck) {
return { id, name, ok: true };
}
try {
const result = await provider.healthCheck();
return { id, name, ...result };
} catch (error) {
const message = error instanceof Error ? error.message : "unknown error";
return { id, name, ok: false, error: message };
}
}),
);
}
// ─── Utilities ──────────────────────────────────────────────────────────────
function dedupTags(tags: string[]): string[] {
return [...new Set(tags)];
}