@falai/agent 1.1.3 → 1.2.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/README.md +9 -0
- package/dist/cjs/core/Agent.d.ts +17 -1
- package/dist/cjs/core/Agent.d.ts.map +1 -1
- package/dist/cjs/core/Agent.js +47 -0
- package/dist/cjs/core/Agent.js.map +1 -1
- package/dist/cjs/core/BatchPromptBuilder.d.ts +3 -0
- package/dist/cjs/core/BatchPromptBuilder.d.ts.map +1 -1
- package/dist/cjs/core/BatchPromptBuilder.js +4 -1
- package/dist/cjs/core/BatchPromptBuilder.js.map +1 -1
- package/dist/cjs/core/CompactionEngine.d.ts +65 -0
- package/dist/cjs/core/CompactionEngine.d.ts.map +1 -0
- package/dist/cjs/core/CompactionEngine.js +251 -0
- package/dist/cjs/core/CompactionEngine.js.map +1 -0
- package/dist/cjs/core/PromptComposer.d.ts +8 -1
- package/dist/cjs/core/PromptComposer.d.ts.map +1 -1
- package/dist/cjs/core/PromptComposer.js +238 -126
- package/dist/cjs/core/PromptComposer.js.map +1 -1
- package/dist/cjs/core/PromptSectionCache.d.ts +57 -0
- package/dist/cjs/core/PromptSectionCache.d.ts.map +1 -0
- package/dist/cjs/core/PromptSectionCache.js +108 -0
- package/dist/cjs/core/PromptSectionCache.js.map +1 -0
- package/dist/cjs/core/ResponseEngine.d.ts +3 -0
- package/dist/cjs/core/ResponseEngine.d.ts.map +1 -1
- package/dist/cjs/core/ResponseEngine.js +10 -6
- package/dist/cjs/core/ResponseEngine.js.map +1 -1
- package/dist/cjs/core/ResponseModal.d.ts.map +1 -1
- package/dist/cjs/core/ResponseModal.js +75 -16
- package/dist/cjs/core/ResponseModal.js.map +1 -1
- package/dist/cjs/core/RoutingEngine.d.ts +10 -0
- package/dist/cjs/core/RoutingEngine.d.ts.map +1 -1
- package/dist/cjs/core/RoutingEngine.js +3 -2
- package/dist/cjs/core/RoutingEngine.js.map +1 -1
- package/dist/cjs/core/SessionManager.d.ts.map +1 -1
- package/dist/cjs/core/SessionManager.js +20 -0
- package/dist/cjs/core/SessionManager.js.map +1 -1
- package/dist/cjs/core/StreamingToolExecutor.d.ts +142 -0
- package/dist/cjs/core/StreamingToolExecutor.d.ts.map +1 -0
- package/dist/cjs/core/StreamingToolExecutor.js +455 -0
- package/dist/cjs/core/StreamingToolExecutor.js.map +1 -0
- package/dist/cjs/core/ToolManager.d.ts +18 -1
- package/dist/cjs/core/ToolManager.d.ts.map +1 -1
- package/dist/cjs/core/ToolManager.js +91 -0
- package/dist/cjs/core/ToolManager.js.map +1 -1
- package/dist/cjs/index.d.ts +5 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +8 -2
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/providers/AnthropicProvider.d.ts.map +1 -1
- package/dist/cjs/providers/AnthropicProvider.js +8 -7
- package/dist/cjs/providers/AnthropicProvider.js.map +1 -1
- package/dist/cjs/providers/GeminiProvider.d.ts +25 -0
- package/dist/cjs/providers/GeminiProvider.d.ts.map +1 -1
- package/dist/cjs/providers/GeminiProvider.js +79 -51
- package/dist/cjs/providers/GeminiProvider.js.map +1 -1
- package/dist/cjs/providers/OpenAIProvider.d.ts.map +1 -1
- package/dist/cjs/providers/OpenAIProvider.js +14 -6
- package/dist/cjs/providers/OpenAIProvider.js.map +1 -1
- package/dist/cjs/providers/OpenRouterProvider.d.ts.map +1 -1
- package/dist/cjs/providers/OpenRouterProvider.js +7 -6
- package/dist/cjs/providers/OpenRouterProvider.js.map +1 -1
- package/dist/cjs/types/agent.d.ts +44 -0
- package/dist/cjs/types/agent.d.ts.map +1 -1
- package/dist/cjs/types/agent.js.map +1 -1
- package/dist/cjs/types/compaction.d.ts +50 -0
- package/dist/cjs/types/compaction.d.ts.map +1 -0
- package/dist/cjs/types/compaction.js +6 -0
- package/dist/cjs/types/compaction.js.map +1 -0
- package/dist/cjs/types/index.d.ts +4 -2
- package/dist/cjs/types/index.d.ts.map +1 -1
- package/dist/cjs/types/index.js.map +1 -1
- package/dist/cjs/types/tool.d.ts +84 -0
- package/dist/cjs/types/tool.d.ts.map +1 -1
- package/dist/core/Agent.d.ts +17 -1
- package/dist/core/Agent.d.ts.map +1 -1
- package/dist/core/Agent.js +47 -0
- package/dist/core/Agent.js.map +1 -1
- package/dist/core/BatchPromptBuilder.d.ts +3 -0
- package/dist/core/BatchPromptBuilder.d.ts.map +1 -1
- package/dist/core/BatchPromptBuilder.js +4 -1
- package/dist/core/BatchPromptBuilder.js.map +1 -1
- package/dist/core/CompactionEngine.d.ts +65 -0
- package/dist/core/CompactionEngine.d.ts.map +1 -0
- package/dist/core/CompactionEngine.js +244 -0
- package/dist/core/CompactionEngine.js.map +1 -0
- package/dist/core/PromptComposer.d.ts +8 -1
- package/dist/core/PromptComposer.d.ts.map +1 -1
- package/dist/core/PromptComposer.js +238 -126
- package/dist/core/PromptComposer.js.map +1 -1
- package/dist/core/PromptSectionCache.d.ts +57 -0
- package/dist/core/PromptSectionCache.d.ts.map +1 -0
- package/dist/core/PromptSectionCache.js +104 -0
- package/dist/core/PromptSectionCache.js.map +1 -0
- package/dist/core/ResponseEngine.d.ts +3 -0
- package/dist/core/ResponseEngine.d.ts.map +1 -1
- package/dist/core/ResponseEngine.js +10 -6
- package/dist/core/ResponseEngine.js.map +1 -1
- package/dist/core/ResponseModal.d.ts.map +1 -1
- package/dist/core/ResponseModal.js +75 -16
- package/dist/core/ResponseModal.js.map +1 -1
- package/dist/core/RoutingEngine.d.ts +10 -0
- package/dist/core/RoutingEngine.d.ts.map +1 -1
- package/dist/core/RoutingEngine.js +3 -2
- package/dist/core/RoutingEngine.js.map +1 -1
- package/dist/core/SessionManager.d.ts.map +1 -1
- package/dist/core/SessionManager.js +17 -0
- package/dist/core/SessionManager.js.map +1 -1
- package/dist/core/StreamingToolExecutor.d.ts +142 -0
- package/dist/core/StreamingToolExecutor.d.ts.map +1 -0
- package/dist/core/StreamingToolExecutor.js +448 -0
- package/dist/core/StreamingToolExecutor.js.map +1 -0
- package/dist/core/ToolManager.d.ts +18 -1
- package/dist/core/ToolManager.d.ts.map +1 -1
- package/dist/core/ToolManager.js +91 -0
- package/dist/core/ToolManager.js.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/providers/AnthropicProvider.d.ts.map +1 -1
- package/dist/providers/AnthropicProvider.js +8 -7
- package/dist/providers/AnthropicProvider.js.map +1 -1
- package/dist/providers/GeminiProvider.d.ts +25 -0
- package/dist/providers/GeminiProvider.d.ts.map +1 -1
- package/dist/providers/GeminiProvider.js +79 -51
- package/dist/providers/GeminiProvider.js.map +1 -1
- package/dist/providers/OpenAIProvider.d.ts.map +1 -1
- package/dist/providers/OpenAIProvider.js +14 -6
- package/dist/providers/OpenAIProvider.js.map +1 -1
- package/dist/providers/OpenRouterProvider.d.ts.map +1 -1
- package/dist/providers/OpenRouterProvider.js +7 -6
- package/dist/providers/OpenRouterProvider.js.map +1 -1
- package/dist/types/agent.d.ts +44 -0
- package/dist/types/agent.d.ts.map +1 -1
- package/dist/types/agent.js.map +1 -1
- package/dist/types/compaction.d.ts +50 -0
- package/dist/types/compaction.d.ts.map +1 -0
- package/dist/types/compaction.js +5 -0
- package/dist/types/compaction.js.map +1 -0
- package/dist/types/index.d.ts +4 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/types/tool.d.ts +84 -0
- package/dist/types/tool.d.ts.map +1 -1
- package/docs/api/overview.md +140 -0
- package/docs/core/tools/enhanced-tool.md +186 -0
- package/docs/core/tools/streaming-execution.md +161 -0
- package/docs/guides/context-compaction.md +96 -0
- package/docs/guides/prompt-optimization.md +164 -0
- package/examples/advanced-patterns/context-compaction.ts +223 -0
- package/examples/advanced-patterns/streaming-responses.ts +85 -7
- package/examples/tools/enhanced-tool-metadata.ts +268 -0
- package/examples/tools/streaming-tool-execution.ts +283 -0
- package/package.json +1 -1
- package/src/core/Agent.ts +58 -2
- package/src/core/BatchPromptBuilder.ts +4 -1
- package/src/core/CompactionEngine.ts +318 -0
- package/src/core/PromptComposer.ts +259 -156
- package/src/core/PromptSectionCache.ts +136 -0
- package/src/core/ResponseEngine.ts +9 -6
- package/src/core/ResponseModal.ts +77 -16
- package/src/core/RoutingEngine.ts +13 -2
- package/src/core/SessionManager.ts +19 -0
- package/src/core/StreamingToolExecutor.ts +572 -0
- package/src/core/ToolManager.ts +151 -41
- package/src/index.ts +14 -0
- package/src/providers/AnthropicProvider.ts +11 -12
- package/src/providers/GeminiProvider.ts +83 -52
- package/src/providers/OpenAIProvider.ts +21 -13
- package/src/providers/OpenRouterProvider.ts +13 -13
- package/src/types/agent.ts +45 -0
- package/src/types/compaction.ts +52 -0
- package/src/types/index.ts +35 -14
- package/src/types/tool.ts +108 -0
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
AnthropicProvider,
|
|
15
15
|
OpenAIProvider,
|
|
16
16
|
GeminiProvider,
|
|
17
|
+
type EnhancedTool,
|
|
17
18
|
} from "../../src/index";
|
|
18
19
|
|
|
19
20
|
// Custom context type
|
|
@@ -141,8 +142,8 @@ async function legacyStreamingWithAnthropic() {
|
|
|
141
142
|
|
|
142
143
|
// Legacy respondStream API - requires manual session management
|
|
143
144
|
let fullMessage = "";
|
|
144
|
-
for await (const chunk of agent.respondStream({
|
|
145
|
-
history: agent.session.getHistory()
|
|
145
|
+
for await (const chunk of agent.respondStream({
|
|
146
|
+
history: agent.session.getHistory()
|
|
146
147
|
})) {
|
|
147
148
|
if (chunk.delta) {
|
|
148
149
|
process.stdout.write(chunk.delta);
|
|
@@ -157,7 +158,7 @@ async function legacyStreamingWithAnthropic() {
|
|
|
157
158
|
);
|
|
158
159
|
console.log(` - Data:`, agent.session.getData() || "None");
|
|
159
160
|
console.log(` - Tool Calls: ${chunk.toolCalls?.length || 0}`);
|
|
160
|
-
|
|
161
|
+
|
|
161
162
|
// Manual session history management required
|
|
162
163
|
await agent.session.addMessage("assistant", fullMessage);
|
|
163
164
|
console.log(` - Session Messages: ${agent.session.getHistory().length}`);
|
|
@@ -220,7 +221,7 @@ async function modernStreamingWithOpenAI() {
|
|
|
220
221
|
` - Route: ${chunk.session?.currentRoute?.title || "None"}`
|
|
221
222
|
);
|
|
222
223
|
console.log(` - Data:`, agent.session.getData() || "None");
|
|
223
|
-
|
|
224
|
+
|
|
224
225
|
// Session automatically updated - no manual work needed!
|
|
225
226
|
console.log(` - Session Messages: ${agent.session.getHistory().length}`);
|
|
226
227
|
}
|
|
@@ -267,10 +268,10 @@ async function modernStreamingComparison() {
|
|
|
267
268
|
|
|
268
269
|
// Manual session management
|
|
269
270
|
await agent.session.addMessage("user", userMessage);
|
|
270
|
-
|
|
271
|
+
|
|
271
272
|
let oldWayMessage = "";
|
|
272
|
-
for await (const chunk of agent.respondStream({
|
|
273
|
-
history: agent.session.getHistory()
|
|
273
|
+
for await (const chunk of agent.respondStream({
|
|
274
|
+
history: agent.session.getHistory()
|
|
274
275
|
})) {
|
|
275
276
|
if (chunk.delta) {
|
|
276
277
|
process.stdout.write(chunk.delta);
|
|
@@ -528,6 +529,81 @@ async function logFeedback(data: { rating: number; comments: string }) {
|
|
|
528
529
|
console.log("✨ Feedback logged successfully!");
|
|
529
530
|
}
|
|
530
531
|
|
|
532
|
+
async function streamingWithToolExecution() {
|
|
533
|
+
console.log("\n🤖 Example 7: Streaming with Tool Execution\n");
|
|
534
|
+
|
|
535
|
+
const provider = new AnthropicProvider({
|
|
536
|
+
apiKey: process.env.ANTHROPIC_API_KEY || "",
|
|
537
|
+
model: "claude-sonnet-4-5",
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
// Define EnhancedTools with concurrency metadata
|
|
541
|
+
const readFileTool: EnhancedTool = {
|
|
542
|
+
id: "read_file",
|
|
543
|
+
name: "Read File",
|
|
544
|
+
description: "Read a file from disk",
|
|
545
|
+
parameters: {
|
|
546
|
+
type: "object",
|
|
547
|
+
properties: { path: { type: "string" } },
|
|
548
|
+
required: ["path"],
|
|
549
|
+
},
|
|
550
|
+
handler: async (_ctx, args) => {
|
|
551
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
552
|
+
return { data: `Contents of ${args?.path}`, success: true };
|
|
553
|
+
},
|
|
554
|
+
isConcurrencySafe: () => true,
|
|
555
|
+
isReadOnly: () => true,
|
|
556
|
+
interruptBehavior: () => "cancel",
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
const writeFileTool: EnhancedTool = {
|
|
560
|
+
id: "write_file",
|
|
561
|
+
name: "Write File",
|
|
562
|
+
description: "Write content to a file",
|
|
563
|
+
parameters: {
|
|
564
|
+
type: "object",
|
|
565
|
+
properties: { path: { type: "string" }, content: { type: "string" } },
|
|
566
|
+
required: ["path", "content"],
|
|
567
|
+
},
|
|
568
|
+
handler: async (_ctx, args) => {
|
|
569
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
570
|
+
return { data: `Wrote to ${args?.path}`, success: true };
|
|
571
|
+
},
|
|
572
|
+
isConcurrencySafe: () => false,
|
|
573
|
+
isDestructive: () => true,
|
|
574
|
+
interruptBehavior: () => "block",
|
|
575
|
+
maxResultSizeChars: 1_000,
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
const agent = new Agent({
|
|
579
|
+
name: "ToolStreamingAssistant",
|
|
580
|
+
description: "Demonstrates streaming with concurrent tool execution",
|
|
581
|
+
provider,
|
|
582
|
+
tools: [readFileTool, writeFileTool],
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
try {
|
|
586
|
+
console.log("📤 Streaming with tool execution...\n");
|
|
587
|
+
console.log("When the LLM calls multiple read-only tools, they execute in parallel.");
|
|
588
|
+
console.log("Write tools wait for exclusive access.\n");
|
|
589
|
+
console.log("Response: ");
|
|
590
|
+
|
|
591
|
+
for await (const chunk of agent.stream("Read index.ts and utils.ts, then write output.ts")) {
|
|
592
|
+
if (chunk.delta) {
|
|
593
|
+
process.stdout.write(chunk.delta);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
if (chunk.done) {
|
|
597
|
+
console.log("\n\n✅ Stream complete!");
|
|
598
|
+
console.log(` Tool Calls: ${chunk.toolCalls?.length || 0}`);
|
|
599
|
+
console.log(` Session Messages: ${agent.session.getHistory().length}`);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
} catch (error) {
|
|
603
|
+
console.error("❌ Error:", error);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
531
607
|
async function main() {
|
|
532
608
|
console.log("🚀 Starting Streaming Examples\n");
|
|
533
609
|
console.log("=".repeat(60));
|
|
@@ -539,6 +615,7 @@ async function main() {
|
|
|
539
615
|
{ name: "API Comparison (Gemini)", fn: modernStreamingComparison },
|
|
540
616
|
{ name: "Modern Streaming with Routes", fn: modernStreamingWithRoutes },
|
|
541
617
|
{ name: "Modern Streaming with Abort", fn: modernStreamingWithAbortSignal },
|
|
618
|
+
{ name: "Streaming with Tool Execution", fn: streamingWithToolExecution },
|
|
542
619
|
];
|
|
543
620
|
|
|
544
621
|
console.log("\nAvailable Examples:");
|
|
@@ -553,6 +630,7 @@ async function main() {
|
|
|
553
630
|
console.log(" - Streaming provides real-time responses for better UX");
|
|
554
631
|
console.log(" - Use AbortSignal to cancel long-running streams");
|
|
555
632
|
console.log(" - Access chunk.route and chunk.step for flow information");
|
|
633
|
+
console.log(" - NEW: EnhancedTool metadata enables parallel read-only tool execution");
|
|
556
634
|
|
|
557
635
|
console.log("\n" + "=".repeat(60));
|
|
558
636
|
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enhanced Tool Metadata Example
|
|
3
|
+
*
|
|
4
|
+
* Demonstrates the EnhancedTool interface with rich metadata for concurrency
|
|
5
|
+
* control, input validation, permission gating, and result size budgeting.
|
|
6
|
+
*
|
|
7
|
+
* Key concepts:
|
|
8
|
+
* - `isConcurrencySafe` — classify tools for parallel vs serial execution
|
|
9
|
+
* - `validateInput` — reject bad inputs before the handler runs
|
|
10
|
+
* - `checkPermissions` — gate execution behind authorization checks
|
|
11
|
+
* - `maxResultSizeChars` — cap result size to prevent context overflow
|
|
12
|
+
* - `interruptBehavior` — control abort signal handling ('cancel' vs 'block')
|
|
13
|
+
* - Backward compatibility: plain `Tool` objects work without any metadata
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
Agent,
|
|
18
|
+
GeminiProvider,
|
|
19
|
+
type EnhancedTool,
|
|
20
|
+
type Tool,
|
|
21
|
+
type ToolContext,
|
|
22
|
+
} from "../../src/index";
|
|
23
|
+
|
|
24
|
+
// --- Context type for permission checks ---
|
|
25
|
+
|
|
26
|
+
interface AppContext {
|
|
27
|
+
userRole: "admin" | "editor" | "viewer";
|
|
28
|
+
userId: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// --- 1. Read-only tool with concurrency metadata ---
|
|
32
|
+
|
|
33
|
+
const fetchUserTool: EnhancedTool<AppContext> = {
|
|
34
|
+
id: "fetch_user",
|
|
35
|
+
name: "Fetch User",
|
|
36
|
+
description: "Fetch user profile by ID",
|
|
37
|
+
parameters: {
|
|
38
|
+
type: "object",
|
|
39
|
+
properties: { userId: { type: "string" } },
|
|
40
|
+
required: ["userId"],
|
|
41
|
+
},
|
|
42
|
+
handler: async (_ctx, args) => {
|
|
43
|
+
const userId = args?.userId as string;
|
|
44
|
+
return { data: `User ${userId}: Alice (admin)`, success: true };
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
// Concurrency metadata — safe to run alongside other reads
|
|
48
|
+
isConcurrencySafe: () => true,
|
|
49
|
+
isReadOnly: () => true,
|
|
50
|
+
isDestructive: () => false,
|
|
51
|
+
interruptBehavior: () => "cancel",
|
|
52
|
+
|
|
53
|
+
// Cap result size to 10k chars
|
|
54
|
+
maxResultSizeChars: 10_000,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// --- 2. Write tool with validation and permissions ---
|
|
58
|
+
|
|
59
|
+
const deleteResourceTool: EnhancedTool<AppContext> = {
|
|
60
|
+
id: "delete_resource",
|
|
61
|
+
name: "Delete Resource",
|
|
62
|
+
description: "Permanently delete a resource",
|
|
63
|
+
parameters: {
|
|
64
|
+
type: "object",
|
|
65
|
+
properties: { resourceId: { type: "string" } },
|
|
66
|
+
required: ["resourceId"],
|
|
67
|
+
},
|
|
68
|
+
handler: async (_ctx, args) => {
|
|
69
|
+
const id = args?.resourceId as string;
|
|
70
|
+
return { data: `Resource ${id} deleted`, success: true };
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
// Not concurrency-safe — must run exclusively
|
|
74
|
+
isConcurrencySafe: () => false,
|
|
75
|
+
isReadOnly: () => false,
|
|
76
|
+
isDestructive: () => true,
|
|
77
|
+
interruptBehavior: () => "block", // don't abort mid-delete
|
|
78
|
+
|
|
79
|
+
maxResultSizeChars: 500,
|
|
80
|
+
|
|
81
|
+
// Input validation — runs before handler
|
|
82
|
+
validateInput: (input) => {
|
|
83
|
+
const id = input.resourceId;
|
|
84
|
+
if (!id || typeof id !== "string" || id.trim().length === 0) {
|
|
85
|
+
return { valid: false, error: "resourceId must be a non-empty string" };
|
|
86
|
+
}
|
|
87
|
+
// Suggest correction for common prefix mistake
|
|
88
|
+
if (typeof id === "string" && !id.startsWith("res_")) {
|
|
89
|
+
return {
|
|
90
|
+
valid: false,
|
|
91
|
+
error: "resourceId must start with 'res_'",
|
|
92
|
+
correctedInput: { resourceId: `res_${id}` },
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
return { valid: true };
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
// Permission check — runs before handler
|
|
99
|
+
checkPermissions: (_input, ctx) => {
|
|
100
|
+
if (ctx.context.userRole !== "admin") {
|
|
101
|
+
return {
|
|
102
|
+
allowed: false,
|
|
103
|
+
reason: "Only admins can delete resources",
|
|
104
|
+
canOverride: false,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
return { allowed: true };
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// --- 3. Tool with input-dependent concurrency ---
|
|
112
|
+
|
|
113
|
+
const queryDatabaseTool: EnhancedTool<AppContext> = {
|
|
114
|
+
id: "query_database",
|
|
115
|
+
name: "Query Database",
|
|
116
|
+
description: "Run a database query (SELECT is concurrent-safe, mutations are not)",
|
|
117
|
+
parameters: {
|
|
118
|
+
type: "object",
|
|
119
|
+
properties: {
|
|
120
|
+
sql: { type: "string" },
|
|
121
|
+
readonly: { type: "boolean" },
|
|
122
|
+
},
|
|
123
|
+
required: ["sql"],
|
|
124
|
+
},
|
|
125
|
+
handler: async (_ctx, args) => {
|
|
126
|
+
const sql = args?.sql as string;
|
|
127
|
+
return { data: `Query result for: ${sql}`, success: true };
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
// Concurrency depends on the query type
|
|
131
|
+
isConcurrencySafe: (input) => input?.readonly === true,
|
|
132
|
+
isReadOnly: (input) => input?.readonly === true,
|
|
133
|
+
isDestructive: (input) => {
|
|
134
|
+
const sql = (input?.sql as string)?.toUpperCase() ?? "";
|
|
135
|
+
return sql.includes("DROP") || sql.includes("DELETE") || sql.includes("TRUNCATE");
|
|
136
|
+
},
|
|
137
|
+
interruptBehavior: () => "block",
|
|
138
|
+
|
|
139
|
+
// Validate SQL isn't empty
|
|
140
|
+
validateInput: (input) => {
|
|
141
|
+
if (!input.sql || typeof input.sql !== "string") {
|
|
142
|
+
return { valid: false, error: "sql must be a non-empty string" };
|
|
143
|
+
}
|
|
144
|
+
return { valid: true };
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
// Large query results get truncated
|
|
148
|
+
maxResultSizeChars: 50_000,
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
// --- 4. Plain Tool (backward compatible, no metadata) ---
|
|
152
|
+
|
|
153
|
+
const echoTool: Tool<AppContext> = {
|
|
154
|
+
id: "echo",
|
|
155
|
+
name: "Echo",
|
|
156
|
+
description: "Echo back the input (plain Tool, no EnhancedTool metadata)",
|
|
157
|
+
parameters: {
|
|
158
|
+
type: "object",
|
|
159
|
+
properties: { message: { type: "string" } },
|
|
160
|
+
required: ["message"],
|
|
161
|
+
},
|
|
162
|
+
handler: async (_ctx, args) => {
|
|
163
|
+
return { data: args?.message as string, success: true };
|
|
164
|
+
},
|
|
165
|
+
// No isConcurrencySafe, validateInput, checkPermissions, etc.
|
|
166
|
+
// Defaults: isConcurrencySafe → false, interruptBehavior → 'block'
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
// --- Demo functions ---
|
|
170
|
+
|
|
171
|
+
function demonstrateMetadata() {
|
|
172
|
+
console.log("=== EnhancedTool Metadata ===\n");
|
|
173
|
+
|
|
174
|
+
const tools = [fetchUserTool, deleteResourceTool, queryDatabaseTool, echoTool];
|
|
175
|
+
|
|
176
|
+
for (const tool of tools) {
|
|
177
|
+
const enhanced = tool as EnhancedTool;
|
|
178
|
+
console.log(`${tool.id}:`);
|
|
179
|
+
console.log(` concurrencySafe: ${enhanced.isConcurrencySafe?.() ?? "default (false)"}`);
|
|
180
|
+
console.log(` readOnly: ${enhanced.isReadOnly?.() ?? "default (false)"}`);
|
|
181
|
+
console.log(` destructive: ${enhanced.isDestructive?.() ?? "default (false)"}`);
|
|
182
|
+
console.log(` interruptBehavior: ${enhanced.interruptBehavior?.() ?? "default (block)"}`);
|
|
183
|
+
console.log(` maxResultSizeChars: ${enhanced.maxResultSizeChars ?? "none"}`);
|
|
184
|
+
console.log(` hasValidateInput: ${!!enhanced.validateInput}`);
|
|
185
|
+
console.log(` hasCheckPermissions: ${!!enhanced.checkPermissions}`);
|
|
186
|
+
console.log();
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function demonstrateInputDependentConcurrency() {
|
|
191
|
+
console.log("=== Input-Dependent Concurrency ===\n");
|
|
192
|
+
|
|
193
|
+
const selectInput = { sql: "SELECT * FROM users", readonly: true };
|
|
194
|
+
const insertInput = { sql: "INSERT INTO users VALUES (...)", readonly: false };
|
|
195
|
+
const dropInput = { sql: "DROP TABLE users", readonly: false };
|
|
196
|
+
|
|
197
|
+
console.log(`SELECT query: concurrencySafe=${queryDatabaseTool.isConcurrencySafe!(selectInput)}`);
|
|
198
|
+
console.log(`INSERT query: concurrencySafe=${queryDatabaseTool.isConcurrencySafe!(insertInput)}`);
|
|
199
|
+
console.log(`DROP query: destructive=${queryDatabaseTool.isDestructive!(dropInput)}`);
|
|
200
|
+
console.log();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function demonstrateValidation() {
|
|
204
|
+
console.log("=== Input Validation ===\n");
|
|
205
|
+
|
|
206
|
+
const cases = [
|
|
207
|
+
{ label: "valid ID", input: { resourceId: "res_123" } },
|
|
208
|
+
{ label: "missing prefix", input: { resourceId: "123" } },
|
|
209
|
+
{ label: "empty string", input: { resourceId: "" } },
|
|
210
|
+
];
|
|
211
|
+
|
|
212
|
+
for (const { label, input } of cases) {
|
|
213
|
+
const result = deleteResourceTool.validateInput!(input, {} as any);
|
|
214
|
+
const resolved = result instanceof Promise ? await result : result;
|
|
215
|
+
console.log(` ${label}: valid=${resolved.valid}${resolved.error ? `, error="${resolved.error}"` : ""}${resolved.correctedInput ? `, corrected=${JSON.stringify(resolved.correctedInput)}` : ""}`);
|
|
216
|
+
}
|
|
217
|
+
console.log();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function demonstratePermissions() {
|
|
221
|
+
console.log("=== Permission Gating ===\n");
|
|
222
|
+
|
|
223
|
+
const roles: Array<"admin" | "editor" | "viewer"> = ["admin", "editor", "viewer"];
|
|
224
|
+
|
|
225
|
+
for (const role of roles) {
|
|
226
|
+
const mockCtx = { context: { userRole: role, userId: "u1" } } as ToolContext<AppContext>;
|
|
227
|
+
const result = deleteResourceTool.checkPermissions!({ resourceId: "res_1" }, mockCtx);
|
|
228
|
+
const resolved = result instanceof Promise ? await result : result;
|
|
229
|
+
console.log(` role=${role}: allowed=${resolved.allowed}${resolved.reason ? `, reason="${resolved.reason}"` : ""}`);
|
|
230
|
+
}
|
|
231
|
+
console.log();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function demonstrateAgentRegistration() {
|
|
235
|
+
console.log("=== Agent Registration ===\n");
|
|
236
|
+
|
|
237
|
+
const agent = new Agent<AppContext>({
|
|
238
|
+
name: "MetadataDemo",
|
|
239
|
+
description: "Demonstrates EnhancedTool metadata",
|
|
240
|
+
provider: new GeminiProvider({
|
|
241
|
+
apiKey: process.env.GEMINI_API_KEY || "demo-key",
|
|
242
|
+
model: "models/gemini-2.5-flash",
|
|
243
|
+
}),
|
|
244
|
+
context: { userRole: "admin", userId: "u1" },
|
|
245
|
+
// Mix of EnhancedTool and plain Tool — both accepted seamlessly
|
|
246
|
+
tools: [fetchUserTool, deleteResourceTool, queryDatabaseTool, echoTool],
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
console.log("Registered tools:");
|
|
250
|
+
for (const t of agent.getTools()) {
|
|
251
|
+
console.log(` - ${t.id}`);
|
|
252
|
+
}
|
|
253
|
+
console.log("\nPlain Tool objects work alongside EnhancedTool without changes.");
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function main() {
|
|
257
|
+
demonstrateMetadata();
|
|
258
|
+
demonstrateInputDependentConcurrency();
|
|
259
|
+
await demonstrateValidation();
|
|
260
|
+
await demonstratePermissions();
|
|
261
|
+
demonstrateAgentRegistration();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
265
|
+
main().catch(console.error);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export { main };
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Streaming Tool Execution Example
|
|
3
|
+
*
|
|
4
|
+
* Demonstrates the StreamingToolExecutor with mixed read-only and write tools,
|
|
5
|
+
* showing how read-only tools execute in parallel while write tools run serially.
|
|
6
|
+
*
|
|
7
|
+
* Key concepts:
|
|
8
|
+
* - EnhancedTool with `isConcurrencySafe` metadata
|
|
9
|
+
* - Parallel execution of concurrent-safe (read-only) tools
|
|
10
|
+
* - Serial execution of non-concurrent-safe (write) tools
|
|
11
|
+
* - Result ordering preserved regardless of completion order
|
|
12
|
+
* - Progress reporting from long-running tools
|
|
13
|
+
* - Abort signal support
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
Agent,
|
|
18
|
+
GeminiProvider,
|
|
19
|
+
StreamingToolExecutor,
|
|
20
|
+
type EnhancedTool,
|
|
21
|
+
type ToolCallRequest,
|
|
22
|
+
} from "../../src/index";
|
|
23
|
+
|
|
24
|
+
// --- Read-only tools (concurrency-safe, run in parallel) ---
|
|
25
|
+
|
|
26
|
+
const readFileTool: EnhancedTool = {
|
|
27
|
+
id: "read_file",
|
|
28
|
+
name: "Read File",
|
|
29
|
+
description: "Read a file from disk",
|
|
30
|
+
parameters: {
|
|
31
|
+
type: "object",
|
|
32
|
+
properties: { path: { type: "string" } },
|
|
33
|
+
required: ["path"],
|
|
34
|
+
},
|
|
35
|
+
handler: async (_ctx, args) => {
|
|
36
|
+
const path = args?.path as string;
|
|
37
|
+
// Simulate file read latency
|
|
38
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
39
|
+
return { data: `Contents of ${path}: [mock file data]`, success: true };
|
|
40
|
+
},
|
|
41
|
+
isConcurrencySafe: () => true,
|
|
42
|
+
isReadOnly: () => true,
|
|
43
|
+
isDestructive: () => false,
|
|
44
|
+
interruptBehavior: () => "cancel",
|
|
45
|
+
maxResultSizeChars: 50_000,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const listDirectoryTool: EnhancedTool = {
|
|
49
|
+
id: "list_directory",
|
|
50
|
+
name: "List Directory",
|
|
51
|
+
description: "List files in a directory",
|
|
52
|
+
parameters: {
|
|
53
|
+
type: "object",
|
|
54
|
+
properties: { path: { type: "string" } },
|
|
55
|
+
required: ["path"],
|
|
56
|
+
},
|
|
57
|
+
handler: async (_ctx, args) => {
|
|
58
|
+
const path = args?.path as string;
|
|
59
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
60
|
+
return {
|
|
61
|
+
data: `Files in ${path}: index.ts, utils.ts, types.ts`,
|
|
62
|
+
success: true,
|
|
63
|
+
};
|
|
64
|
+
},
|
|
65
|
+
isConcurrencySafe: () => true,
|
|
66
|
+
isReadOnly: () => true,
|
|
67
|
+
isDestructive: () => false,
|
|
68
|
+
interruptBehavior: () => "cancel",
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const searchCodeTool: EnhancedTool = {
|
|
72
|
+
id: "search_code",
|
|
73
|
+
name: "Search Code",
|
|
74
|
+
description: "Search for patterns in the codebase",
|
|
75
|
+
parameters: {
|
|
76
|
+
type: "object",
|
|
77
|
+
properties: { query: { type: "string" } },
|
|
78
|
+
required: ["query"],
|
|
79
|
+
},
|
|
80
|
+
handler: async (_ctx, args) => {
|
|
81
|
+
const query = args?.query as string;
|
|
82
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
83
|
+
return {
|
|
84
|
+
data: `Found 3 matches for "${query}" in src/`,
|
|
85
|
+
success: true,
|
|
86
|
+
};
|
|
87
|
+
},
|
|
88
|
+
isConcurrencySafe: () => true,
|
|
89
|
+
isReadOnly: () => true,
|
|
90
|
+
isDestructive: () => false,
|
|
91
|
+
interruptBehavior: () => "cancel",
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// --- Write tools (NOT concurrency-safe, run serially) ---
|
|
95
|
+
|
|
96
|
+
const writeFileTool: EnhancedTool = {
|
|
97
|
+
id: "write_file",
|
|
98
|
+
name: "Write File",
|
|
99
|
+
description: "Write content to a file",
|
|
100
|
+
parameters: {
|
|
101
|
+
type: "object",
|
|
102
|
+
properties: {
|
|
103
|
+
path: { type: "string" },
|
|
104
|
+
content: { type: "string" },
|
|
105
|
+
},
|
|
106
|
+
required: ["path", "content"],
|
|
107
|
+
},
|
|
108
|
+
handler: async (_ctx, args) => {
|
|
109
|
+
const path = args?.path as string;
|
|
110
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
111
|
+
return { data: `Wrote to ${path}`, success: true };
|
|
112
|
+
},
|
|
113
|
+
isConcurrencySafe: () => false,
|
|
114
|
+
isReadOnly: () => false,
|
|
115
|
+
isDestructive: (input) => true,
|
|
116
|
+
interruptBehavior: () => "block",
|
|
117
|
+
maxResultSizeChars: 1_000,
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const deleteFileTool: EnhancedTool = {
|
|
121
|
+
id: "delete_file",
|
|
122
|
+
name: "Delete File",
|
|
123
|
+
description: "Delete a file from disk",
|
|
124
|
+
parameters: {
|
|
125
|
+
type: "object",
|
|
126
|
+
properties: { path: { type: "string" } },
|
|
127
|
+
required: ["path"],
|
|
128
|
+
},
|
|
129
|
+
handler: async (_ctx, args) => {
|
|
130
|
+
const path = args?.path as string;
|
|
131
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
132
|
+
return { data: `Deleted ${path}`, success: true };
|
|
133
|
+
},
|
|
134
|
+
isConcurrencySafe: () => false,
|
|
135
|
+
isReadOnly: () => false,
|
|
136
|
+
isDestructive: () => true,
|
|
137
|
+
interruptBehavior: () => "block",
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// Helper: create a minimal ToolContext for standalone executor usage
|
|
141
|
+
function createMockToolContext() {
|
|
142
|
+
return {
|
|
143
|
+
context: {},
|
|
144
|
+
data: {},
|
|
145
|
+
history: [],
|
|
146
|
+
updateContext: async () => { },
|
|
147
|
+
updateData: async () => { },
|
|
148
|
+
getField: () => undefined,
|
|
149
|
+
setField: async () => { },
|
|
150
|
+
hasField: () => false,
|
|
151
|
+
} as any;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// --- Direct StreamingToolExecutor usage ---
|
|
155
|
+
|
|
156
|
+
async function demonstrateDirectExecutor() {
|
|
157
|
+
console.log("=== Direct StreamingToolExecutor Demo ===\n");
|
|
158
|
+
|
|
159
|
+
const toolMap = new Map<string, EnhancedTool>([
|
|
160
|
+
["read_file", readFileTool],
|
|
161
|
+
["list_directory", listDirectoryTool],
|
|
162
|
+
["search_code", searchCodeTool],
|
|
163
|
+
["write_file", writeFileTool],
|
|
164
|
+
]);
|
|
165
|
+
|
|
166
|
+
// Simulate a sequence of tool calls from an LLM response:
|
|
167
|
+
// 3 reads (parallel) followed by 1 write (serial)
|
|
168
|
+
const toolCalls: ToolCallRequest[] = [
|
|
169
|
+
{ id: "call_1", toolName: "read_file", arguments: { path: "src/index.ts" } },
|
|
170
|
+
{ id: "call_2", toolName: "list_directory", arguments: { path: "src/" } },
|
|
171
|
+
{ id: "call_3", toolName: "search_code", arguments: { query: "import" } },
|
|
172
|
+
{ id: "call_4", toolName: "write_file", arguments: { path: "out.ts", content: "// generated" } },
|
|
173
|
+
];
|
|
174
|
+
|
|
175
|
+
const executor = new StreamingToolExecutor<unknown, unknown>(
|
|
176
|
+
createMockToolContext(),
|
|
177
|
+
{ maxParallel: 5 },
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
console.log("Queueing tools as they arrive from the LLM stream...\n");
|
|
181
|
+
|
|
182
|
+
for (const call of toolCalls) {
|
|
183
|
+
const tool = toolMap.get(call.toolName)!;
|
|
184
|
+
const safe = tool.isConcurrencySafe?.() ? "parallel" : "serial";
|
|
185
|
+
console.log(` + Queued: ${call.toolName} (${safe})`);
|
|
186
|
+
executor.addTool(call, tool);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
console.log("\nResults (yielded in original request order):\n");
|
|
190
|
+
|
|
191
|
+
for await (const update of executor.getRemainingResults()) {
|
|
192
|
+
if (update.progress) {
|
|
193
|
+
console.log(` [progress] ${update.toolCallId}: ${update.progress}`);
|
|
194
|
+
}
|
|
195
|
+
if (update.result) {
|
|
196
|
+
console.log(` [result] ${update.toolCallId}: ${update.result.data}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
console.log("\nAll tools complete.");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// --- Agent-level integration ---
|
|
204
|
+
|
|
205
|
+
async function demonstrateAgentIntegration() {
|
|
206
|
+
console.log("\n=== Agent Integration Demo ===\n");
|
|
207
|
+
|
|
208
|
+
const agent = new Agent({
|
|
209
|
+
name: "CodeAssistant",
|
|
210
|
+
description: "An assistant with streaming tool execution",
|
|
211
|
+
provider: new GeminiProvider({
|
|
212
|
+
apiKey: process.env.GEMINI_API_KEY || "demo-key",
|
|
213
|
+
model: "models/gemini-2.5-flash",
|
|
214
|
+
}),
|
|
215
|
+
tools: [readFileTool, listDirectoryTool, searchCodeTool, writeFileTool, deleteFileTool],
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
console.log("Tools registered:");
|
|
219
|
+
for (const t of agent.getTools()) {
|
|
220
|
+
const enhanced = t as EnhancedTool;
|
|
221
|
+
const safe = enhanced.isConcurrencySafe?.() ?? false;
|
|
222
|
+
console.log(` - ${t.id} (concurrencySafe: ${safe})`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
console.log("\nStreaming tool execution happens automatically during respondStream().");
|
|
226
|
+
console.log("Read-only tools run in parallel; write tools wait for exclusive access.\n");
|
|
227
|
+
|
|
228
|
+
// In a real scenario you'd call agent.stream() or agent.respondStream()
|
|
229
|
+
// and the StreamingToolExecutor handles concurrency internally.
|
|
230
|
+
console.log("Example streaming usage:");
|
|
231
|
+
console.log(' for await (const chunk of agent.stream("Read index.ts and list src/")) {');
|
|
232
|
+
console.log(" process.stdout.write(chunk.delta);");
|
|
233
|
+
console.log(" }");
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// --- Abort signal demo ---
|
|
237
|
+
|
|
238
|
+
async function demonstrateAbortSignal() {
|
|
239
|
+
console.log("\n=== Abort Signal Demo ===\n");
|
|
240
|
+
|
|
241
|
+
const controller = new AbortController();
|
|
242
|
+
const executor = new StreamingToolExecutor<unknown, unknown>(
|
|
243
|
+
createMockToolContext(),
|
|
244
|
+
{ maxParallel: 5, signal: controller.signal },
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
// Queue a slow read and a write
|
|
248
|
+
executor.addTool(
|
|
249
|
+
{ id: "slow_read", toolName: "search_code", arguments: { query: "TODO" } },
|
|
250
|
+
searchCodeTool
|
|
251
|
+
);
|
|
252
|
+
executor.addTool(
|
|
253
|
+
{ id: "slow_write", toolName: "write_file", arguments: { path: "tmp.ts", content: "x" } },
|
|
254
|
+
writeFileTool
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
// Abort after 100ms — 'cancel' tools abort immediately, 'block' tools finish
|
|
258
|
+
setTimeout(() => {
|
|
259
|
+
console.log(" Aborting...");
|
|
260
|
+
controller.abort();
|
|
261
|
+
}, 100);
|
|
262
|
+
|
|
263
|
+
for await (const update of executor.getRemainingResults()) {
|
|
264
|
+
if (update.result) {
|
|
265
|
+
const status = update.result.success ? "ok" : "aborted";
|
|
266
|
+
console.log(` ${update.toolCallId}: ${status} — ${update.result.data ?? update.result.error}`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
console.log(" Done (abort handled gracefully).");
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async function main() {
|
|
274
|
+
await demonstrateDirectExecutor();
|
|
275
|
+
await demonstrateAgentIntegration();
|
|
276
|
+
await demonstrateAbortSignal();
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
280
|
+
main().catch(console.error);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export { main };
|