@townco/agent 0.1.118 → 0.1.120
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/acp-server/http.js +2 -28
- package/dist/runner/e2b-sandbox-manager.d.ts +3 -0
- package/dist/runner/e2b-sandbox-manager.js +48 -19
- package/dist/runner/langchain/tools/artifacts.d.ts +68 -0
- package/dist/runner/langchain/tools/artifacts.js +474 -0
- package/dist/runner/langchain/tools/conversation_search.d.ts +22 -0
- package/dist/runner/langchain/tools/conversation_search.js +137 -0
- package/dist/runner/langchain/tools/e2b.d.ts +3 -3
- package/dist/runner/langchain/tools/e2b.js +37 -21
- package/dist/runner/langchain/tools/generate_image.d.ts +47 -0
- package/dist/runner/langchain/tools/generate_image.js +175 -0
- package/dist/runner/langchain/tools/port-utils.d.ts +8 -0
- package/dist/runner/langchain/tools/port-utils.js +35 -0
- package/dist/runner/langchain/tools/subagent-connections.d.ts +6 -0
- package/dist/runner/langchain/tools/subagent.js +114 -3
- package/dist/templates/index.d.ts +5 -1
- package/dist/templates/index.js +6 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -7
- package/templates/index.ts +12 -1
|
@@ -403,8 +403,8 @@ function makeE2BToolsInternal(getSandbox) {
|
|
|
403
403
|
shareSandboxFile.prettyName = "Share from Sandbox";
|
|
404
404
|
// biome-ignore lint/suspicious/noExplicitAny: Need to add custom properties to LangChain tool
|
|
405
405
|
shareSandboxFile.icon = "Share";
|
|
406
|
-
// Tool 6:
|
|
407
|
-
const
|
|
406
|
+
// Tool 6: Load Library Documents to Sandbox
|
|
407
|
+
const loadLibraryDocuments = tool(async ({ document_ids }) => {
|
|
408
408
|
const sandbox = await getSandbox();
|
|
409
409
|
try {
|
|
410
410
|
const libraryApiUrl = process.env.LIBRARY_API_URL;
|
|
@@ -412,14 +412,14 @@ function makeE2BToolsInternal(getSandbox) {
|
|
|
412
412
|
if (!libraryApiUrl || !libraryApiKey) {
|
|
413
413
|
throw new Error("LIBRARY_API_URL and LIBRARY_API_KEY environment variables are required");
|
|
414
414
|
}
|
|
415
|
-
const response = await fetch(`${libraryApiUrl}/sandbox/
|
|
415
|
+
const response = await fetch(`${libraryApiUrl}/sandbox/upload_documents_to_sandbox`, {
|
|
416
416
|
method: "POST",
|
|
417
417
|
headers: {
|
|
418
418
|
"Content-Type": "application/json",
|
|
419
419
|
Authorization: `Bearer ${libraryApiKey}`,
|
|
420
420
|
},
|
|
421
421
|
body: JSON.stringify({
|
|
422
|
-
|
|
422
|
+
document_ids,
|
|
423
423
|
sandbox_id: sandbox.sandboxId,
|
|
424
424
|
}),
|
|
425
425
|
});
|
|
@@ -428,34 +428,50 @@ function makeE2BToolsInternal(getSandbox) {
|
|
|
428
428
|
throw new Error(`Library API error: ${response.status} - ${text}`);
|
|
429
429
|
}
|
|
430
430
|
const result = await response.json();
|
|
431
|
-
|
|
431
|
+
// Format the response
|
|
432
|
+
const successfulUploads = result.results
|
|
433
|
+
.filter((r) => r.status === "success")
|
|
434
|
+
.map((r) => r.file_path);
|
|
435
|
+
const failedUploads = result.results
|
|
436
|
+
.filter((r) => r.status === "error")
|
|
437
|
+
.map((r) => `${r.document_id}: ${r.error}`);
|
|
438
|
+
let output = `Status: ${result.status}\n`;
|
|
439
|
+
output += `Uploaded ${result.successful_uploads}/${result.total_requested} documents\n`;
|
|
440
|
+
if (successfulUploads.length > 0) {
|
|
441
|
+
output += `\nSuccessful uploads:\n${successfulUploads.join("\n")}`;
|
|
442
|
+
}
|
|
443
|
+
if (failedUploads.length > 0) {
|
|
444
|
+
output += `\nFailed uploads:\n${failedUploads.join("\n")}`;
|
|
445
|
+
}
|
|
446
|
+
return output;
|
|
432
447
|
}
|
|
433
448
|
catch (error) {
|
|
434
|
-
logger.error("Error
|
|
449
|
+
logger.error("Error loading library documents to sandbox", {
|
|
435
450
|
error,
|
|
436
|
-
|
|
451
|
+
document_ids,
|
|
437
452
|
});
|
|
438
|
-
return `Error
|
|
453
|
+
return `Error loading documents: ${error instanceof Error ? error.message : String(error)}`;
|
|
439
454
|
}
|
|
440
455
|
}, {
|
|
441
|
-
name: "
|
|
442
|
-
description: "
|
|
443
|
-
"Use this to make library documents available for processing in the sandbox environment."
|
|
456
|
+
name: "Sandbox_LoadLibraryDocuments",
|
|
457
|
+
description: "Load multiple documents from the library to the cloud sandbox. " +
|
|
458
|
+
"Use this to make library documents available for processing in the sandbox environment. " +
|
|
459
|
+
"Supports batch uploads - some documents may succeed while others fail.",
|
|
444
460
|
schema: z.object({
|
|
445
|
-
|
|
446
|
-
.string()
|
|
447
|
-
.describe("The
|
|
461
|
+
document_ids: z
|
|
462
|
+
.array(z.string())
|
|
463
|
+
.describe("The IDs of the documents to load from the library"),
|
|
448
464
|
}),
|
|
449
465
|
});
|
|
450
466
|
// biome-ignore lint/suspicious/noExplicitAny: Need to add custom properties to LangChain tool
|
|
451
|
-
|
|
467
|
+
loadLibraryDocuments.prettyName = "Load Library Documents";
|
|
452
468
|
// biome-ignore lint/suspicious/noExplicitAny: Need to add custom properties to LangChain tool
|
|
453
|
-
|
|
469
|
+
loadLibraryDocuments.icon = "FileText";
|
|
454
470
|
// biome-ignore lint/suspicious/noExplicitAny: Need to add custom properties to LangChain tool
|
|
455
|
-
|
|
456
|
-
active: "
|
|
457
|
-
past: "
|
|
458
|
-
paramKey: "
|
|
471
|
+
loadLibraryDocuments.verbiage = {
|
|
472
|
+
active: "Loading {document_ids.length} documents",
|
|
473
|
+
past: "Loaded {document_ids.length} documents",
|
|
474
|
+
paramKey: "document_ids",
|
|
459
475
|
};
|
|
460
476
|
// Tool 7: Generate Image in Sandbox
|
|
461
477
|
const generateImage = tool(async ({ prompt }) => {
|
|
@@ -634,7 +650,7 @@ generateImage();
|
|
|
634
650
|
readSandboxFile,
|
|
635
651
|
writeSandboxFile,
|
|
636
652
|
shareSandboxFile,
|
|
637
|
-
|
|
653
|
+
loadLibraryDocuments,
|
|
638
654
|
generateImage,
|
|
639
655
|
];
|
|
640
656
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
interface GenerateImageResult {
|
|
3
|
+
success: boolean;
|
|
4
|
+
filePath?: string | undefined;
|
|
5
|
+
fileName?: string | undefined;
|
|
6
|
+
imageUrl?: string | undefined;
|
|
7
|
+
textResponse?: string | undefined;
|
|
8
|
+
mimeType?: string | undefined;
|
|
9
|
+
error?: string | undefined;
|
|
10
|
+
}
|
|
11
|
+
/** Create generate image tool using direct GEMINI_API_KEY/GOOGLE_API_KEY */
|
|
12
|
+
export declare function makeGenerateImageTool(): import("langchain").DynamicStructuredTool<z.ZodObject<{
|
|
13
|
+
prompt: z.ZodString;
|
|
14
|
+
aspectRatio: z.ZodDefault<z.ZodOptional<z.ZodEnum<{
|
|
15
|
+
"16:9": "16:9";
|
|
16
|
+
"1:1": "1:1";
|
|
17
|
+
"3:4": "3:4";
|
|
18
|
+
"4:3": "4:3";
|
|
19
|
+
"5:4": "5:4";
|
|
20
|
+
"9:16": "9:16";
|
|
21
|
+
}>>>;
|
|
22
|
+
}, z.core.$strip>, {
|
|
23
|
+
prompt: string;
|
|
24
|
+
aspectRatio: "16:9" | "1:1" | "3:4" | "4:3" | "5:4" | "9:16";
|
|
25
|
+
}, {
|
|
26
|
+
prompt: string;
|
|
27
|
+
aspectRatio?: "16:9" | "1:1" | "3:4" | "4:3" | "5:4" | "9:16" | undefined;
|
|
28
|
+
}, GenerateImageResult>;
|
|
29
|
+
/** Create generate image tool using Town proxy */
|
|
30
|
+
export declare function makeTownGenerateImageTool(): import("langchain").DynamicStructuredTool<z.ZodObject<{
|
|
31
|
+
prompt: z.ZodString;
|
|
32
|
+
aspectRatio: z.ZodDefault<z.ZodOptional<z.ZodEnum<{
|
|
33
|
+
"16:9": "16:9";
|
|
34
|
+
"1:1": "1:1";
|
|
35
|
+
"3:4": "3:4";
|
|
36
|
+
"4:3": "4:3";
|
|
37
|
+
"5:4": "5:4";
|
|
38
|
+
"9:16": "9:16";
|
|
39
|
+
}>>>;
|
|
40
|
+
}, z.core.$strip>, {
|
|
41
|
+
prompt: string;
|
|
42
|
+
aspectRatio: "16:9" | "1:1" | "3:4" | "4:3" | "5:4" | "9:16";
|
|
43
|
+
}, {
|
|
44
|
+
prompt: string;
|
|
45
|
+
aspectRatio?: "16:9" | "1:1" | "3:4" | "4:3" | "5:4" | "9:16" | undefined;
|
|
46
|
+
}, GenerateImageResult>;
|
|
47
|
+
export {};
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { GoogleGenAI } from "@google/genai";
|
|
4
|
+
import { getShedAuth } from "@townco/core/auth";
|
|
5
|
+
import { tool } from "langchain";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { getSessionContext, getToolOutputDir, hasSessionContext, } from "../../session-context";
|
|
8
|
+
let _directGenaiClient = null;
|
|
9
|
+
let _townGenaiClient = null;
|
|
10
|
+
/** Get Google GenAI client using direct GEMINI_API_KEY/GOOGLE_API_KEY environment variable */
|
|
11
|
+
function getDirectGenAIClient() {
|
|
12
|
+
if (_directGenaiClient) {
|
|
13
|
+
return _directGenaiClient;
|
|
14
|
+
}
|
|
15
|
+
const apiKey = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY;
|
|
16
|
+
if (!apiKey) {
|
|
17
|
+
throw new Error("GEMINI_API_KEY or GOOGLE_API_KEY environment variable is required to use the generate_image tool. " +
|
|
18
|
+
"Please set one of them to your Google AI API key.");
|
|
19
|
+
}
|
|
20
|
+
_directGenaiClient = new GoogleGenAI({ apiKey });
|
|
21
|
+
return _directGenaiClient;
|
|
22
|
+
}
|
|
23
|
+
/** Get Google GenAI client using Town proxy with authenticated credentials */
|
|
24
|
+
function getTownGenAIClient() {
|
|
25
|
+
if (_townGenaiClient) {
|
|
26
|
+
return _townGenaiClient;
|
|
27
|
+
}
|
|
28
|
+
const shedAuth = getShedAuth();
|
|
29
|
+
if (!shedAuth) {
|
|
30
|
+
throw new Error("Not logged in. Run 'town login' or set SHED_API_KEY to use the town_generate_image tool.");
|
|
31
|
+
}
|
|
32
|
+
// Configure the client to use shed as proxy
|
|
33
|
+
// The SDK will send requests to {shedUrl}/api/gemini/{apiVersion}/{path}
|
|
34
|
+
_townGenaiClient = new GoogleGenAI({
|
|
35
|
+
apiKey: shedAuth.accessToken,
|
|
36
|
+
httpOptions: {
|
|
37
|
+
baseUrl: `${shedAuth.shedUrl}/api/gemini/`,
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
return _townGenaiClient;
|
|
41
|
+
}
|
|
42
|
+
function makeGenerateImageToolInternal(getClient) {
|
|
43
|
+
const generateImage = tool(async ({ prompt, aspectRatio = "1:1" }) => {
|
|
44
|
+
try {
|
|
45
|
+
if (!hasSessionContext()) {
|
|
46
|
+
throw new Error("GenerateImage tool requires session context. Ensure the tool is called within a session.");
|
|
47
|
+
}
|
|
48
|
+
const { sessionId } = getSessionContext();
|
|
49
|
+
const toolOutputDir = getToolOutputDir("GenerateImage");
|
|
50
|
+
const client = getClient();
|
|
51
|
+
// Use Gemini 3 Pro Image for image generation
|
|
52
|
+
// Note: imageConfig is a valid API option but not yet in the TypeScript types
|
|
53
|
+
// biome-ignore lint/suspicious/noExplicitAny: imageConfig not yet typed in @google/genai
|
|
54
|
+
const config = {
|
|
55
|
+
responseModalities: ["TEXT", "IMAGE"],
|
|
56
|
+
imageConfig: {
|
|
57
|
+
aspectRatio: aspectRatio,
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
const response = await client.models.generateContent({
|
|
61
|
+
model: "gemini-3-pro-image-preview",
|
|
62
|
+
contents: [{ text: prompt }],
|
|
63
|
+
config,
|
|
64
|
+
});
|
|
65
|
+
if (!response.candidates || response.candidates.length === 0) {
|
|
66
|
+
return {
|
|
67
|
+
success: false,
|
|
68
|
+
error: "No response from the model. The request may have been filtered.",
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
const candidate = response.candidates[0];
|
|
72
|
+
if (!candidate) {
|
|
73
|
+
return {
|
|
74
|
+
success: false,
|
|
75
|
+
error: "No candidate in the response.",
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
const parts = candidate.content?.parts;
|
|
79
|
+
if (!parts || parts.length === 0) {
|
|
80
|
+
return {
|
|
81
|
+
success: false,
|
|
82
|
+
error: "No content parts in the response.",
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
let imageData;
|
|
86
|
+
let textResponse;
|
|
87
|
+
let mimeType;
|
|
88
|
+
for (const part of parts) {
|
|
89
|
+
if (part.text) {
|
|
90
|
+
textResponse = part.text;
|
|
91
|
+
}
|
|
92
|
+
else if (part.inlineData) {
|
|
93
|
+
imageData = part.inlineData.data;
|
|
94
|
+
mimeType = part.inlineData.mimeType || "image/png";
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (!imageData) {
|
|
98
|
+
return {
|
|
99
|
+
success: false,
|
|
100
|
+
error: "No image was generated in the response.",
|
|
101
|
+
...(textResponse ? { textResponse } : {}),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
// Save image to session-scoped tool output directory
|
|
105
|
+
await mkdir(toolOutputDir, { recursive: true });
|
|
106
|
+
// Generate unique filename
|
|
107
|
+
const timestamp = Date.now();
|
|
108
|
+
const extension = mimeType === "image/jpeg" ? "jpg" : "png";
|
|
109
|
+
const fileName = `image-${timestamp}.${extension}`;
|
|
110
|
+
const filePath = join(toolOutputDir, fileName);
|
|
111
|
+
// Save image to file
|
|
112
|
+
const buffer = Buffer.from(imageData, "base64");
|
|
113
|
+
await writeFile(filePath, buffer);
|
|
114
|
+
// Create URL for the static file server
|
|
115
|
+
// The agent HTTP server serves static files from the agent directory
|
|
116
|
+
// Use AGENT_BASE_URL if set (for production), otherwise construct from BIND_HOST/PORT
|
|
117
|
+
const port = process.env.PORT || "3100";
|
|
118
|
+
const hostname = process.env.BIND_HOST || "localhost";
|
|
119
|
+
const baseUrl = process.env.AGENT_BASE_URL || `http://${hostname}:${port}`;
|
|
120
|
+
const imageUrl = `${baseUrl}/static/.sessions/${sessionId}/artifacts/tool-GenerateImage/${fileName}`;
|
|
121
|
+
return {
|
|
122
|
+
success: true,
|
|
123
|
+
filePath,
|
|
124
|
+
fileName,
|
|
125
|
+
imageUrl,
|
|
126
|
+
...(mimeType ? { mimeType } : {}),
|
|
127
|
+
...(textResponse ? { textResponse } : {}),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
132
|
+
return {
|
|
133
|
+
success: false,
|
|
134
|
+
error: `Image generation failed: ${errorMessage}`,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
}, {
|
|
138
|
+
name: "GenerateImage",
|
|
139
|
+
description: "Generate an image based on a text prompt using Google's Gemini image generation model. " +
|
|
140
|
+
"Returns an imageUrl that can be displayed to the user. After calling this tool, " +
|
|
141
|
+
"include the imageUrl in your response as a markdown image like  " +
|
|
142
|
+
"so the user can see the generated image.\n" +
|
|
143
|
+
"- Creates images from detailed text descriptions\n" +
|
|
144
|
+
"- Supports various aspect ratios for different use cases\n" +
|
|
145
|
+
"- Be specific in prompts about style, composition, colors, and subjects\n" +
|
|
146
|
+
"\n" +
|
|
147
|
+
"Usage notes:\n" +
|
|
148
|
+
" - Provide detailed, specific prompts for best results\n" +
|
|
149
|
+
" - The generated image is saved to the session directory and served via URL\n" +
|
|
150
|
+
" - Always display the result using markdown: \n",
|
|
151
|
+
schema: z.object({
|
|
152
|
+
prompt: z
|
|
153
|
+
.string()
|
|
154
|
+
.describe("A detailed description of the image to generate. Be specific about style, composition, colors, and subjects."),
|
|
155
|
+
aspectRatio: z
|
|
156
|
+
.enum(["1:1", "3:4", "4:3", "9:16", "16:9", "5:4"])
|
|
157
|
+
.optional()
|
|
158
|
+
.default("1:1")
|
|
159
|
+
.describe("The aspect ratio of the generated image."),
|
|
160
|
+
}),
|
|
161
|
+
});
|
|
162
|
+
// biome-ignore lint/suspicious/noExplicitAny: Need to add custom properties to LangChain tool
|
|
163
|
+
generateImage.prettyName = "Generate Image";
|
|
164
|
+
// biome-ignore lint/suspicious/noExplicitAny: Need to add custom properties to LangChain tool
|
|
165
|
+
generateImage.icon = "Image";
|
|
166
|
+
return generateImage;
|
|
167
|
+
}
|
|
168
|
+
/** Create generate image tool using direct GEMINI_API_KEY/GOOGLE_API_KEY */
|
|
169
|
+
export function makeGenerateImageTool() {
|
|
170
|
+
return makeGenerateImageToolInternal(getDirectGenAIClient);
|
|
171
|
+
}
|
|
172
|
+
/** Create generate image tool using Town proxy */
|
|
173
|
+
export function makeTownGenerateImageTool() {
|
|
174
|
+
return makeGenerateImageToolInternal(getTownGenAIClient);
|
|
175
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check if a port is available
|
|
3
|
+
*/
|
|
4
|
+
export declare function isPortAvailable(port: number): Promise<boolean>;
|
|
5
|
+
/**
|
|
6
|
+
* Find the next available port starting from the given port
|
|
7
|
+
*/
|
|
8
|
+
export declare function findAvailablePort(startPort: number, maxAttempts?: number): Promise<number>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { createServer } from "node:net";
|
|
2
|
+
/**
|
|
3
|
+
* Check if a port is available
|
|
4
|
+
*/
|
|
5
|
+
export async function isPortAvailable(port) {
|
|
6
|
+
return new Promise((resolve) => {
|
|
7
|
+
const server = createServer();
|
|
8
|
+
server.once("error", (err) => {
|
|
9
|
+
if (err.code === "EADDRINUSE") {
|
|
10
|
+
resolve(false);
|
|
11
|
+
}
|
|
12
|
+
else {
|
|
13
|
+
resolve(false);
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
server.once("listening", () => {
|
|
17
|
+
server.close();
|
|
18
|
+
resolve(true);
|
|
19
|
+
});
|
|
20
|
+
server.listen(port);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Find the next available port starting from the given port
|
|
25
|
+
*/
|
|
26
|
+
export async function findAvailablePort(startPort, maxAttempts = 100) {
|
|
27
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
28
|
+
const port = startPort + i;
|
|
29
|
+
const available = await isPortAvailable(port);
|
|
30
|
+
if (available) {
|
|
31
|
+
return port;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
throw new Error(`Could not find an available port between ${startPort} and ${startPort + maxAttempts - 1}`);
|
|
35
|
+
}
|
|
@@ -28,6 +28,12 @@ export interface SubagentMessage {
|
|
|
28
28
|
content: string;
|
|
29
29
|
contentBlocks: SubagentContentBlock[];
|
|
30
30
|
toolCalls: SubagentToolCall[];
|
|
31
|
+
_meta?: {
|
|
32
|
+
semanticName?: string;
|
|
33
|
+
agentDefinitionName?: string;
|
|
34
|
+
currentActivity?: string;
|
|
35
|
+
statusGenerating?: boolean;
|
|
36
|
+
};
|
|
31
37
|
}
|
|
32
38
|
/**
|
|
33
39
|
* Maps query hash to toolCallId.
|
|
@@ -2,6 +2,7 @@ import * as crypto from "node:crypto";
|
|
|
2
2
|
import * as fs from "node:fs/promises";
|
|
3
3
|
import { mkdir } from "node:fs/promises";
|
|
4
4
|
import * as path from "node:path";
|
|
5
|
+
import { ChatAnthropic } from "@langchain/anthropic";
|
|
5
6
|
import { context, propagation, trace } from "@opentelemetry/api";
|
|
6
7
|
import { createLogger } from "@townco/core";
|
|
7
8
|
import { z } from "zod";
|
|
@@ -10,6 +11,58 @@ import { makeRunnerFromDefinition } from "../../index.js";
|
|
|
10
11
|
import { bindGeneratorToSessionContext, getAbortSignal, } from "../../session-context.js";
|
|
11
12
|
import { emitSubagentMessages, hashQuery, } from "./subagent-connections.js";
|
|
12
13
|
const logger = createLogger("subagent-tool", "debug");
|
|
14
|
+
/**
|
|
15
|
+
* Generate status message using Haiku (fast, cheap model)
|
|
16
|
+
*/
|
|
17
|
+
async function generateStatusMessage(recentContent, toolCalls) {
|
|
18
|
+
try {
|
|
19
|
+
const activeTool = toolCalls.find((tc) => tc.status === "in_progress");
|
|
20
|
+
const model = new ChatAnthropic({
|
|
21
|
+
modelName: "claude-3-haiku-20240307",
|
|
22
|
+
temperature: 0.3,
|
|
23
|
+
maxTokens: 30,
|
|
24
|
+
});
|
|
25
|
+
const prompt = `Summarize the current activity in 5-7 words for a progress indicator:
|
|
26
|
+
|
|
27
|
+
Recent output: ${recentContent.slice(-500)}
|
|
28
|
+
${activeTool ? `Active tool: ${activeTool.prettyName || activeTool.title}` : ""}
|
|
29
|
+
|
|
30
|
+
Requirements:
|
|
31
|
+
- Use present continuous tense (e.g., "Searching for...", "Analyzing...")
|
|
32
|
+
- Be specific but concise
|
|
33
|
+
- Focus on user-visible progress
|
|
34
|
+
- Return ONLY the status, no explanation
|
|
35
|
+
|
|
36
|
+
Status:`;
|
|
37
|
+
const response = await model.invoke(prompt);
|
|
38
|
+
const status = response.content.toString().trim().slice(0, 80);
|
|
39
|
+
return status;
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
logger.warn("Failed to generate status message", { error });
|
|
43
|
+
return extractHeuristicStatus(recentContent, toolCalls);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Heuristic status extraction (fallback when LLM fails)
|
|
48
|
+
*/
|
|
49
|
+
function extractHeuristicStatus(content, toolCalls) {
|
|
50
|
+
// Priority 1: Active tool
|
|
51
|
+
const activeTool = toolCalls.find((tc) => tc.status === "in_progress");
|
|
52
|
+
if (activeTool) {
|
|
53
|
+
return `${activeTool.prettyName || activeTool.title}...`;
|
|
54
|
+
}
|
|
55
|
+
// Priority 2: First complete sentence from recent content
|
|
56
|
+
if (content.length > 50) {
|
|
57
|
+
const lastAdded = content.slice(-200);
|
|
58
|
+
const firstSentence = lastAdded.match(/[^.!?]+[.!?]/)?.[0];
|
|
59
|
+
if (firstSentence && firstSentence.length > 10) {
|
|
60
|
+
return firstSentence.trim().slice(0, 80);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// Priority 3: Generic fallback
|
|
64
|
+
return "Processing...";
|
|
65
|
+
}
|
|
13
66
|
/**
|
|
14
67
|
* Helper to derive favicon URL from a domain
|
|
15
68
|
*/
|
|
@@ -311,23 +364,26 @@ assistant: "I'm going to use the Task tool to launch the greeting-responder agen
|
|
|
311
364
|
.enum(agentNames)
|
|
312
365
|
.describe("The name of the subagent to use"),
|
|
313
366
|
query: z.string().describe("The query or task to send to the subagent"),
|
|
367
|
+
taskName: z
|
|
368
|
+
.string()
|
|
369
|
+
.describe("A concise 3-5 word name describing this specific task (e.g., 'Searching for React patterns', 'Analyzing API responses'). IMPORTANT: Be specific and action-oriented!"),
|
|
314
370
|
}),
|
|
315
371
|
// Expose subagent configs for metadata extraction by the adapter
|
|
316
372
|
subagentConfigs,
|
|
317
373
|
fn: async (input) => {
|
|
318
|
-
const { agentName, query } = input;
|
|
374
|
+
const { agentName, query, taskName } = input;
|
|
319
375
|
const agent = agentMap.get(agentName);
|
|
320
376
|
if (!agent) {
|
|
321
377
|
throw new Error(`Unknown agent: ${agentName}`);
|
|
322
378
|
}
|
|
323
|
-
return await querySubagent(agentName, agent.agentPath, agent.agentDir, query);
|
|
379
|
+
return await querySubagent(agentName, agent.agentPath, agent.agentDir, query, taskName);
|
|
324
380
|
},
|
|
325
381
|
};
|
|
326
382
|
}
|
|
327
383
|
/**
|
|
328
384
|
* Internal function that runs a subagent in-process and queries it.
|
|
329
385
|
*/
|
|
330
|
-
async function querySubagent(agentName, agentPath, agentWorkingDirectory, query) {
|
|
386
|
+
async function querySubagent(agentName, agentPath, agentWorkingDirectory, query, taskName) {
|
|
331
387
|
// Get the abort signal from context (set by parent agent's cancellation)
|
|
332
388
|
const parentAbortSignal = getAbortSignal();
|
|
333
389
|
// Check if already cancelled before starting
|
|
@@ -358,6 +414,11 @@ async function querySubagent(agentName, agentPath, agentWorkingDirectory, query)
|
|
|
358
414
|
const otelCarrier = {};
|
|
359
415
|
if (activeSpan) {
|
|
360
416
|
propagation.inject(trace.setSpan(activeCtx, activeSpan), otelCarrier);
|
|
417
|
+
// Set span attributes for observability
|
|
418
|
+
activeSpan.setAttributes({
|
|
419
|
+
"subagent.semantic_name": taskName,
|
|
420
|
+
"subagent.agent_definition": agentName,
|
|
421
|
+
});
|
|
361
422
|
}
|
|
362
423
|
// Create invoke request
|
|
363
424
|
const invokeRequest = {
|
|
@@ -386,10 +447,37 @@ async function querySubagent(agentName, agentPath, agentWorkingDirectory, query)
|
|
|
386
447
|
content: "",
|
|
387
448
|
contentBlocks: [],
|
|
388
449
|
toolCalls: [],
|
|
450
|
+
_meta: {
|
|
451
|
+
semanticName: taskName,
|
|
452
|
+
agentDefinitionName: agentName,
|
|
453
|
+
statusGenerating: true,
|
|
454
|
+
},
|
|
389
455
|
};
|
|
390
456
|
const toolCallMap = new Map();
|
|
391
457
|
const toolNameMap = new Map(); // Map toolCallId -> toolName
|
|
392
458
|
const queryHash = hashQuery(query);
|
|
459
|
+
// Emit initial message with semantic name immediately
|
|
460
|
+
emitSubagentMessages(queryHash, [currentMessage]);
|
|
461
|
+
// Track status updates for periodic generation
|
|
462
|
+
let lastStatusUpdate = Date.now();
|
|
463
|
+
let statusUpdateInProgress = false;
|
|
464
|
+
const STATUS_UPDATE_INTERVAL = 5000; // 5 seconds
|
|
465
|
+
// Fire async initial status generation (don't await)
|
|
466
|
+
generateStatusMessage(query, [])
|
|
467
|
+
.then((status) => {
|
|
468
|
+
if (currentMessage._meta) {
|
|
469
|
+
currentMessage._meta.currentActivity = status;
|
|
470
|
+
currentMessage._meta.statusGenerating = false;
|
|
471
|
+
}
|
|
472
|
+
// Emit update with status
|
|
473
|
+
emitSubagentMessages(queryHash, [currentMessage]);
|
|
474
|
+
})
|
|
475
|
+
.catch((error) => {
|
|
476
|
+
logger.warn("Initial status generation failed", { error });
|
|
477
|
+
if (currentMessage._meta) {
|
|
478
|
+
currentMessage._meta.statusGenerating = false;
|
|
479
|
+
}
|
|
480
|
+
});
|
|
393
481
|
logger.info("[DEBUG] Starting subagent generator loop", {
|
|
394
482
|
agentName,
|
|
395
483
|
queryHash,
|
|
@@ -507,6 +595,29 @@ async function querySubagent(agentName, agentPath, agentWorkingDirectory, query)
|
|
|
507
595
|
}
|
|
508
596
|
shouldEmit = true; // Emit when sources are added
|
|
509
597
|
}
|
|
598
|
+
// Periodic status update check
|
|
599
|
+
const now = Date.now();
|
|
600
|
+
const shouldUpdateStatus = !statusUpdateInProgress &&
|
|
601
|
+
now - lastStatusUpdate > STATUS_UPDATE_INTERVAL &&
|
|
602
|
+
currentMessage.content.length > 100; // Only if there's meaningful content
|
|
603
|
+
if (shouldUpdateStatus) {
|
|
604
|
+
statusUpdateInProgress = true;
|
|
605
|
+
lastStatusUpdate = now;
|
|
606
|
+
// Fire async status update (don't await)
|
|
607
|
+
generateStatusMessage(currentMessage.content, currentMessage.toolCalls)
|
|
608
|
+
.then((status) => {
|
|
609
|
+
if (currentMessage._meta) {
|
|
610
|
+
currentMessage._meta.currentActivity = status;
|
|
611
|
+
}
|
|
612
|
+
// Emit update
|
|
613
|
+
emitSubagentMessages(queryHash, [currentMessage]);
|
|
614
|
+
statusUpdateInProgress = false;
|
|
615
|
+
})
|
|
616
|
+
.catch((error) => {
|
|
617
|
+
logger.warn("Status update failed", { error });
|
|
618
|
+
statusUpdateInProgress = false;
|
|
619
|
+
});
|
|
620
|
+
}
|
|
510
621
|
// Emit incremental update to parent (for live streaming)
|
|
511
622
|
if (shouldEmit) {
|
|
512
623
|
logger.debug("[SUBAGENT-ACCUMULATION] Emitting incremental update", {
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { z } from "zod";
|
|
2
|
+
import type { AgentDefinition, McpConfigSchema } from "../definition";
|
|
3
|
+
type McpConfig = z.infer<typeof McpConfigSchema>;
|
|
2
4
|
export interface TemplateVars {
|
|
3
5
|
name: string;
|
|
4
6
|
model: string;
|
|
@@ -18,6 +20,7 @@ export interface TemplateVars {
|
|
|
18
20
|
fn: any;
|
|
19
21
|
schema: unknown;
|
|
20
22
|
}>;
|
|
23
|
+
mcps?: McpConfig[];
|
|
21
24
|
systemPrompt: string | null;
|
|
22
25
|
hasWebSearch: boolean;
|
|
23
26
|
hasGenerateImage: boolean;
|
|
@@ -44,3 +47,4 @@ export declare function generateGitignore(): string;
|
|
|
44
47
|
export declare function generateTsConfig(): string;
|
|
45
48
|
export declare function generateReadme(vars: TemplateVars): string;
|
|
46
49
|
export declare function generateEnvExample(vars: TemplateVars): string | null;
|
|
50
|
+
export {};
|
package/dist/templates/index.js
CHANGED
|
@@ -20,6 +20,9 @@ export function getTemplateVars(name, definition) {
|
|
|
20
20
|
if (definition.suggestedPrompts) {
|
|
21
21
|
result.suggestedPrompts = definition.suggestedPrompts;
|
|
22
22
|
}
|
|
23
|
+
if (definition.mcps && definition.mcps.length > 0) {
|
|
24
|
+
result.mcps = definition.mcps;
|
|
25
|
+
}
|
|
23
26
|
return result;
|
|
24
27
|
}
|
|
25
28
|
export function generatePackageJson(vars) {
|
|
@@ -71,6 +74,9 @@ export async function generateIndexTs(vars) {
|
|
|
71
74
|
}
|
|
72
75
|
agentDef.systemPrompt = vars.systemPrompt;
|
|
73
76
|
agentDef.tools = vars.tools;
|
|
77
|
+
if (vars.mcps && vars.mcps.length > 0) {
|
|
78
|
+
agentDef.mcps = vars.mcps;
|
|
79
|
+
}
|
|
74
80
|
if (vars.hooks) {
|
|
75
81
|
agentDef.hooks = vars.hooks;
|
|
76
82
|
}
|