@townco/agent 0.1.53 → 0.1.55
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/adapter.d.ts +16 -0
- package/dist/acp-server/adapter.js +231 -17
- package/dist/acp-server/cli.d.ts +1 -3
- package/dist/acp-server/http.js +51 -7
- package/dist/acp-server/session-storage.d.ts +16 -1
- package/dist/acp-server/session-storage.js +23 -0
- package/dist/bin.js +0 -0
- package/dist/definition/index.d.ts +2 -2
- package/dist/definition/index.js +1 -0
- package/dist/index.js +1 -1
- package/dist/logger.d.ts +26 -0
- package/dist/logger.js +43 -0
- package/dist/runner/agent-runner.d.ts +7 -2
- package/dist/runner/hooks/executor.js +1 -1
- package/dist/runner/hooks/loader.js +1 -1
- package/dist/runner/hooks/predefined/compaction-tool.js +1 -1
- package/dist/runner/hooks/predefined/tool-response-compactor.js +1 -1
- package/dist/runner/index.d.ts +1 -3
- package/dist/runner/langchain/index.js +179 -39
- package/dist/runner/langchain/model-factory.js +1 -1
- package/dist/runner/langchain/tools/generate_image.d.ts +28 -0
- package/dist/runner/langchain/tools/generate_image.js +135 -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.d.ts +6 -1
- package/dist/runner/langchain/tools/subagent.js +242 -129
- package/dist/runner/tools.d.ts +19 -2
- package/dist/runner/tools.js +9 -0
- package/dist/storage/index.js +1 -1
- package/dist/telemetry/index.js +7 -1
- package/dist/templates/index.d.ts +3 -0
- package/dist/templates/index.js +27 -5
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/index.ts +1 -1
- package/package.json +11 -6
- package/templates/index.ts +37 -6
- package/dist/definition/mcp.d.ts +0 -0
- package/dist/definition/mcp.js +0 -0
- package/dist/definition/tools/todo.d.ts +0 -49
- package/dist/definition/tools/todo.js +0 -80
- package/dist/definition/tools/web_search.d.ts +0 -4
- package/dist/definition/tools/web_search.js +0 -26
- package/dist/dev-agent/index.d.ts +0 -2
- package/dist/dev-agent/index.js +0 -18
- package/dist/example.d.ts +0 -2
- package/dist/example.js +0 -19
- package/dist/scaffold/link-local.d.ts +0 -1
- package/dist/scaffold/link-local.js +0 -54
- package/dist/utils/__tests__/tool-overhead-calculator.test.d.ts +0 -1
- package/dist/utils/__tests__/tool-overhead-calculator.test.js +0 -153
- package/dist/utils/logger.d.ts +0 -39
- package/dist/utils/logger.js +0 -175
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ChatAnthropic } from "@langchain/anthropic";
|
|
2
2
|
import { HumanMessage, SystemMessage } from "@langchain/core/messages";
|
|
3
|
-
import { createLogger } from "
|
|
3
|
+
import { createLogger } from "../../../logger.js";
|
|
4
4
|
import { createContextEntry, createFullMessageEntry, } from "../types";
|
|
5
5
|
const logger = createLogger("compaction-tool");
|
|
6
6
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ChatAnthropic } from "@langchain/anthropic";
|
|
2
2
|
import { HumanMessage, SystemMessage } from "@langchain/core/messages";
|
|
3
|
-
import { createLogger } from "
|
|
3
|
+
import { createLogger } from "../../../logger.js";
|
|
4
4
|
import { countToolResultTokens } from "../../../utils/token-counter.js";
|
|
5
5
|
const logger = createLogger("tool-response-compactor");
|
|
6
6
|
// Haiku 4.5 for compaction (fast and cost-effective)
|
package/dist/runner/index.d.ts
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import type { AgentDefinition } from "../definition";
|
|
2
2
|
import { type AgentRunner } from "./agent-runner";
|
|
3
3
|
export type { AgentRunner };
|
|
4
|
-
export declare const makeRunnerFromDefinition: (
|
|
5
|
-
definition: AgentDefinition,
|
|
6
|
-
) => AgentRunner;
|
|
4
|
+
export declare const makeRunnerFromDefinition: (definition: AgentDefinition) => AgentRunner;
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { MultiServerMCPClient } from "@langchain/mcp-adapters";
|
|
2
2
|
import { context, propagation, trace } from "@opentelemetry/api";
|
|
3
|
-
import { createLogger } from "@townco/core";
|
|
4
3
|
import { AIMessageChunk, createAgent, ToolMessage, tool, } from "langchain";
|
|
5
4
|
import { z } from "zod";
|
|
6
5
|
import { SUBAGENT_MODE_KEY } from "../../acp-server/adapter";
|
|
6
|
+
import { createLogger } from "../../logger.js";
|
|
7
7
|
import { telemetry } from "../../telemetry/index.js";
|
|
8
8
|
import { loadCustomToolModule, } from "../tool-loader.js";
|
|
9
9
|
import { createModelFromString, detectProvider } from "./model-factory.js";
|
|
10
10
|
import { makeOtelCallbacks } from "./otel-callbacks.js";
|
|
11
11
|
import { makeFilesystemTools } from "./tools/filesystem";
|
|
12
|
-
import {
|
|
12
|
+
import { makeGenerateImageTool } from "./tools/generate_image";
|
|
13
|
+
import { SUBAGENT_TOOL_NAME } from "./tools/subagent";
|
|
13
14
|
import { TODO_WRITE_TOOL_NAME, todoWrite } from "./tools/todo";
|
|
14
15
|
import { makeWebSearchTools } from "./tools/web_search";
|
|
15
16
|
const _logger = createLogger("agent-runner");
|
|
@@ -27,6 +28,7 @@ export const TOOL_REGISTRY = {
|
|
|
27
28
|
get_weather: getWeather,
|
|
28
29
|
web_search: () => makeWebSearchTools(),
|
|
29
30
|
filesystem: () => makeFilesystemTools(process.cwd()),
|
|
31
|
+
generate_image: () => makeGenerateImageTool(),
|
|
30
32
|
};
|
|
31
33
|
// ============================================================================
|
|
32
34
|
// Custom tool loading
|
|
@@ -74,6 +76,8 @@ export class LangchainAgent {
|
|
|
74
76
|
totalTokens: 0,
|
|
75
77
|
};
|
|
76
78
|
const countedMessageIds = new Set();
|
|
79
|
+
// Track tool calls for which we've emitted preliminary notifications (from early tool_use blocks)
|
|
80
|
+
const preliminaryToolCallIds = new Set();
|
|
77
81
|
// Start telemetry span for entire invocation
|
|
78
82
|
const invocationSpan = telemetry.startSpan("agent.invoke", {
|
|
79
83
|
"agent.model": this.definition.model,
|
|
@@ -286,7 +290,7 @@ export class LangchainAgent {
|
|
|
286
290
|
// Filter tools if running in subagent mode
|
|
287
291
|
const isSubagent = req.sessionMeta?.[SUBAGENT_MODE_KEY] === true;
|
|
288
292
|
const filteredTools = isSubagent
|
|
289
|
-
? wrappedTools.filter((t) => t.name !== TODO_WRITE_TOOL_NAME && t.name !==
|
|
293
|
+
? wrappedTools.filter((t) => t.name !== TODO_WRITE_TOOL_NAME && t.name !== SUBAGENT_TOOL_NAME)
|
|
290
294
|
: wrappedTools;
|
|
291
295
|
// Wrap tools with tracing so each tool executes within its own span context.
|
|
292
296
|
// This ensures subagent spans are children of the Task tool span.
|
|
@@ -314,35 +318,93 @@ export class LangchainAgent {
|
|
|
314
318
|
const provider = detectProvider(this.definition.model);
|
|
315
319
|
// Build messages from context history if available, otherwise use just the prompt
|
|
316
320
|
let messages;
|
|
321
|
+
// Helper to convert content blocks to LangChain format
|
|
322
|
+
// LangChain expects image_url type with data URL, not Claude's native image+source format
|
|
323
|
+
const convertContentBlocks = (blocks) => {
|
|
324
|
+
// Check if we have any image blocks
|
|
325
|
+
const hasImages = blocks.some((block) => block.type === "image");
|
|
326
|
+
if (!hasImages) {
|
|
327
|
+
// Simple text-only message
|
|
328
|
+
return blocks
|
|
329
|
+
.filter((block) => block.type === "text")
|
|
330
|
+
.map((block) => block.text)
|
|
331
|
+
.join("");
|
|
332
|
+
}
|
|
333
|
+
// Multi-modal message with images - return as content block array
|
|
334
|
+
// LangChain uses image_url type with data URL format
|
|
335
|
+
return blocks
|
|
336
|
+
.map((block) => {
|
|
337
|
+
if (block.type === "text") {
|
|
338
|
+
return {
|
|
339
|
+
type: "text",
|
|
340
|
+
text: block.text,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
else if (block.type === "image") {
|
|
344
|
+
// Extract base64 data and media type from various formats
|
|
345
|
+
let base64Data;
|
|
346
|
+
let mediaType = "image/png";
|
|
347
|
+
// Check if it has the source format (Claude API format)
|
|
348
|
+
if ("source" in block && block.source) {
|
|
349
|
+
base64Data = block.source.data;
|
|
350
|
+
mediaType = block.source.media_type || "image/png";
|
|
351
|
+
}
|
|
352
|
+
// ACP format: { type: "image", data: "...", mimeType: "..." }
|
|
353
|
+
else if ("data" in block && block.data) {
|
|
354
|
+
base64Data = block.data;
|
|
355
|
+
if (block.mimeType) {
|
|
356
|
+
const mt = block.mimeType.toLowerCase();
|
|
357
|
+
if (mt === "image/jpeg" || mt === "image/jpg") {
|
|
358
|
+
mediaType = "image/jpeg";
|
|
359
|
+
}
|
|
360
|
+
else if (mt === "image/png") {
|
|
361
|
+
mediaType = "image/png";
|
|
362
|
+
}
|
|
363
|
+
else if (mt === "image/gif") {
|
|
364
|
+
mediaType = "image/gif";
|
|
365
|
+
}
|
|
366
|
+
else if (mt === "image/webp") {
|
|
367
|
+
mediaType = "image/webp";
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
if (base64Data) {
|
|
372
|
+
// LangChain format: image_url with data URL
|
|
373
|
+
return {
|
|
374
|
+
type: "image_url",
|
|
375
|
+
image_url: {
|
|
376
|
+
url: `data:${mediaType};base64,${base64Data}`,
|
|
377
|
+
},
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return null;
|
|
382
|
+
})
|
|
383
|
+
.filter(Boolean);
|
|
384
|
+
};
|
|
317
385
|
if (req.contextMessages && req.contextMessages.length > 0) {
|
|
318
386
|
// Use context messages (already resolved from context entries)
|
|
319
387
|
// Convert to LangChain format
|
|
320
388
|
messages = req.contextMessages.map((msg) => ({
|
|
321
389
|
type: msg.role === "user" ? "human" : "ai",
|
|
322
|
-
|
|
323
|
-
content: msg.content
|
|
324
|
-
.filter((block) => block.type === "text")
|
|
325
|
-
.map((block) => block.text)
|
|
326
|
-
.join(""),
|
|
390
|
+
content: convertContentBlocks(msg.content),
|
|
327
391
|
}));
|
|
328
392
|
// Add the current prompt as the final human message
|
|
329
|
-
const
|
|
330
|
-
.filter((promptMsg) => promptMsg.type === "text")
|
|
331
|
-
.map((promptMsg) => promptMsg.text)
|
|
332
|
-
.join("\n");
|
|
393
|
+
const promptContent = convertContentBlocks(req.prompt);
|
|
333
394
|
messages.push({
|
|
334
395
|
type: "human",
|
|
335
|
-
content:
|
|
396
|
+
content: promptContent,
|
|
336
397
|
});
|
|
337
398
|
}
|
|
338
399
|
else {
|
|
339
400
|
// Fallback: No context history, use just the prompt
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
401
|
+
const promptContent = convertContentBlocks(req.prompt);
|
|
402
|
+
messages = [
|
|
403
|
+
{
|
|
404
|
+
type: "human",
|
|
405
|
+
content: promptContent,
|
|
406
|
+
},
|
|
407
|
+
];
|
|
346
408
|
}
|
|
347
409
|
// Create OTEL callbacks for instrumentation
|
|
348
410
|
const otelCallbacks = makeOtelCallbacks({
|
|
@@ -391,7 +453,12 @@ export class LangchainAgent {
|
|
|
391
453
|
turnTokenUsage.totalTokens += tokenUsage.totalTokens ?? 0;
|
|
392
454
|
countedMessageIds.add(msg.id);
|
|
393
455
|
}
|
|
394
|
-
|
|
456
|
+
// Generate a batch ID if there are multiple tool calls (parallel execution)
|
|
457
|
+
const toolCalls = msg.tool_calls ?? [];
|
|
458
|
+
const batchId = toolCalls.length > 1
|
|
459
|
+
? `batch-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
|
460
|
+
: undefined;
|
|
461
|
+
for (const toolCall of toolCalls) {
|
|
395
462
|
if (toolCall.id == null) {
|
|
396
463
|
throw new Error(`Tool call is missing id: ${JSON.stringify(toolCall)}`);
|
|
397
464
|
}
|
|
@@ -427,22 +494,61 @@ export class LangchainAgent {
|
|
|
427
494
|
// continue;
|
|
428
495
|
//}
|
|
429
496
|
const matchingTool = finalTools.find((t) => t.name === toolCall.name);
|
|
430
|
-
|
|
497
|
+
let prettyName = matchingTool?.prettyName;
|
|
431
498
|
const icon = matchingTool?.icon;
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
499
|
+
// For the Task tool, use the displayName (or agentName as fallback) as the prettyName
|
|
500
|
+
if (toolCall.name === SUBAGENT_TOOL_NAME &&
|
|
501
|
+
toolCall.args &&
|
|
502
|
+
typeof toolCall.args === "object" &&
|
|
503
|
+
"agentName" in toolCall.args &&
|
|
504
|
+
typeof toolCall.args.agentName === "string") {
|
|
505
|
+
const agentName = toolCall.args.agentName;
|
|
506
|
+
// Look up displayName from subagentConfigs in the original tool definition
|
|
507
|
+
// (not from matchingTool, which is a LangChain tool without subagentConfigs)
|
|
508
|
+
const taskTool = this.definition.tools?.find((t) => typeof t === "object" &&
|
|
509
|
+
t.type === "direct" &&
|
|
510
|
+
t.name === SUBAGENT_TOOL_NAME);
|
|
511
|
+
const subagentConfigs = taskTool?.subagentConfigs;
|
|
512
|
+
const subagentConfig = subagentConfigs?.find((config) => config.agentName === agentName);
|
|
513
|
+
prettyName = subagentConfig?.displayName ?? agentName;
|
|
514
|
+
}
|
|
515
|
+
// Check if we already emitted a preliminary notification from early tool_use block
|
|
516
|
+
const alreadyEmittedPreliminary = preliminaryToolCallIds.has(toolCall.id);
|
|
517
|
+
if (alreadyEmittedPreliminary) {
|
|
518
|
+
// Update the existing preliminary notification with full details
|
|
519
|
+
yield {
|
|
520
|
+
sessionUpdate: "tool_call_update",
|
|
521
|
+
toolCallId: toolCall.id,
|
|
522
|
+
title: toolCall.name,
|
|
523
|
+
rawInput: toolCall.args,
|
|
524
|
+
...(tokenUsage ? { tokenUsage } : {}),
|
|
525
|
+
_meta: {
|
|
526
|
+
messageId: req.messageId,
|
|
527
|
+
...(prettyName ? { prettyName } : {}),
|
|
528
|
+
...(icon ? { icon } : {}),
|
|
529
|
+
...(batchId ? { batchId } : {}),
|
|
530
|
+
},
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
else {
|
|
534
|
+
// Emit full tool_call notification (fallback for non-streaming scenarios)
|
|
535
|
+
yield {
|
|
536
|
+
sessionUpdate: "tool_call",
|
|
537
|
+
toolCallId: toolCall.id,
|
|
538
|
+
title: toolCall.name,
|
|
539
|
+
kind: "other",
|
|
540
|
+
status: "pending",
|
|
541
|
+
rawInput: toolCall.args,
|
|
542
|
+
...(tokenUsage ? { tokenUsage } : {}),
|
|
543
|
+
_meta: {
|
|
544
|
+
messageId: req.messageId,
|
|
545
|
+
...(prettyName ? { prettyName } : {}),
|
|
546
|
+
...(icon ? { icon } : {}),
|
|
547
|
+
...(batchId ? { batchId } : {}),
|
|
548
|
+
},
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
// Always emit in_progress status update
|
|
446
552
|
yield {
|
|
447
553
|
sessionUpdate: "tool_call_update",
|
|
448
554
|
toolCallId: toolCall.id,
|
|
@@ -556,10 +662,26 @@ export class LangchainAgent {
|
|
|
556
662
|
yield msgToYield;
|
|
557
663
|
}
|
|
558
664
|
else if (part.type === "tool_use") {
|
|
559
|
-
//
|
|
665
|
+
// Emit early notification for tool use as soon as we detect it
|
|
666
|
+
// The tool_use block contains { type, id, name, input }
|
|
667
|
+
const toolUseBlock = part;
|
|
668
|
+
if (toolUseBlock.id &&
|
|
669
|
+
toolUseBlock.name &&
|
|
670
|
+
!preliminaryToolCallIds.has(toolUseBlock.id)) {
|
|
671
|
+
preliminaryToolCallIds.add(toolUseBlock.id);
|
|
672
|
+
yield {
|
|
673
|
+
sessionUpdate: "tool_call",
|
|
674
|
+
toolCallId: toolUseBlock.id,
|
|
675
|
+
title: toolUseBlock.name,
|
|
676
|
+
kind: "other",
|
|
677
|
+
status: "pending",
|
|
678
|
+
rawInput: {}, // Args not available yet
|
|
679
|
+
_meta: { messageId: req.messageId },
|
|
680
|
+
};
|
|
681
|
+
}
|
|
560
682
|
}
|
|
561
683
|
else if (part.type === "input_json_delta") {
|
|
562
|
-
//
|
|
684
|
+
// Input JSON delta chunks - we don't process these as tool_call is already emitted
|
|
563
685
|
}
|
|
564
686
|
else {
|
|
565
687
|
throw new Error(`Unhandled AIMessageChunk content block type: ${part.type}\n${JSON.stringify(part)}`);
|
|
@@ -576,14 +698,22 @@ export class LangchainAgent {
|
|
|
576
698
|
// Skip tool_call_update for todo_write tools
|
|
577
699
|
continue;
|
|
578
700
|
}
|
|
579
|
-
|
|
701
|
+
// Check if the tool execution failed
|
|
702
|
+
// LangChain may set status: "error" OR the content may start with "Error:"
|
|
703
|
+
const contentLooksLikeError = typeof aiMessage.content === "string" &&
|
|
704
|
+
aiMessage.content.trim().startsWith("Error:");
|
|
705
|
+
const isError = aiMessage.status === "error" || contentLooksLikeError;
|
|
706
|
+
const status = isError ? "failed" : "completed";
|
|
707
|
+
telemetry.log(isError ? "error" : "info", `Tool call ${status}`, {
|
|
580
708
|
toolCallId: aiMessage.tool_call_id,
|
|
709
|
+
...(isError ? { error: aiMessage.content } : {}),
|
|
581
710
|
});
|
|
582
711
|
// Send status update (metadata only, no content)
|
|
583
712
|
yield {
|
|
584
713
|
sessionUpdate: "tool_call_update",
|
|
585
714
|
toolCallId: aiMessage.tool_call_id,
|
|
586
|
-
status
|
|
715
|
+
status,
|
|
716
|
+
...(isError ? { error: aiMessage.content } : {}),
|
|
587
717
|
_meta: { messageId: req.messageId },
|
|
588
718
|
};
|
|
589
719
|
// Send tool output separately (via direct SSE, bypassing PostgreSQL NOTIFY)
|
|
@@ -645,6 +775,16 @@ const modelRequestSchema = z.object({
|
|
|
645
775
|
});
|
|
646
776
|
const makeMcpToolsClient = (mcpConfigs) => {
|
|
647
777
|
const mcpServers = mcpConfigs?.map((config) => {
|
|
778
|
+
if (typeof config === "string") {
|
|
779
|
+
// Default to localhost:3000/mcp_proxy if not specified
|
|
780
|
+
const proxyUrl = process.env.MCP_PROXY_URL || "http://localhost:3000/mcp_proxy";
|
|
781
|
+
return [
|
|
782
|
+
config,
|
|
783
|
+
{
|
|
784
|
+
url: `${proxyUrl}?server=${config}`,
|
|
785
|
+
},
|
|
786
|
+
];
|
|
787
|
+
}
|
|
648
788
|
if (config.transport === "http") {
|
|
649
789
|
return [
|
|
650
790
|
config.name,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ChatAnthropic } from "@langchain/anthropic";
|
|
2
2
|
import { ChatGoogleGenerativeAI } from "@langchain/google-genai";
|
|
3
3
|
import { ChatVertexAI } from "@langchain/google-vertexai";
|
|
4
|
-
import { createLogger } from "
|
|
4
|
+
import { createLogger } from "../../logger.js";
|
|
5
5
|
const logger = createLogger("model-factory");
|
|
6
6
|
/**
|
|
7
7
|
* Detects the provider from a model string and returns the appropriate
|
|
@@ -0,0 +1,28 @@
|
|
|
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
|
+
export declare function makeGenerateImageTool(): import("langchain").DynamicStructuredTool<z.ZodObject<{
|
|
12
|
+
prompt: z.ZodString;
|
|
13
|
+
aspectRatio: z.ZodDefault<z.ZodOptional<z.ZodEnum<{
|
|
14
|
+
"1:1": "1:1";
|
|
15
|
+
"3:4": "3:4";
|
|
16
|
+
"4:3": "4:3";
|
|
17
|
+
"9:16": "9:16";
|
|
18
|
+
"16:9": "16:9";
|
|
19
|
+
"5:4": "5:4";
|
|
20
|
+
}>>>;
|
|
21
|
+
}, z.core.$strip>, {
|
|
22
|
+
prompt: string;
|
|
23
|
+
aspectRatio: "1:1" | "3:4" | "4:3" | "9:16" | "16:9" | "5:4";
|
|
24
|
+
}, {
|
|
25
|
+
prompt: string;
|
|
26
|
+
aspectRatio?: "1:1" | "3:4" | "4:3" | "9:16" | "16:9" | "5:4" | undefined;
|
|
27
|
+
}, GenerateImageResult>;
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { GoogleGenAI } from "@google/genai";
|
|
4
|
+
import { tool } from "langchain";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
let _genaiClient = null;
|
|
7
|
+
function getGenAIClient() {
|
|
8
|
+
if (_genaiClient) {
|
|
9
|
+
return _genaiClient;
|
|
10
|
+
}
|
|
11
|
+
const apiKey = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY;
|
|
12
|
+
if (!apiKey) {
|
|
13
|
+
throw new Error("GEMINI_API_KEY or GOOGLE_API_KEY environment variable is required to use the generate_image tool. " +
|
|
14
|
+
"Please set one of them to your Google AI API key.");
|
|
15
|
+
}
|
|
16
|
+
_genaiClient = new GoogleGenAI({ apiKey });
|
|
17
|
+
return _genaiClient;
|
|
18
|
+
}
|
|
19
|
+
export function makeGenerateImageTool() {
|
|
20
|
+
const generateImage = tool(async ({ prompt, aspectRatio = "1:1" }) => {
|
|
21
|
+
try {
|
|
22
|
+
const client = getGenAIClient();
|
|
23
|
+
// Use Gemini 3 Pro Image for image generation
|
|
24
|
+
// Note: imageConfig is a valid API option but not yet in the TypeScript types
|
|
25
|
+
// biome-ignore lint/suspicious/noExplicitAny: imageConfig not yet typed in @google/genai
|
|
26
|
+
const config = {
|
|
27
|
+
responseModalities: ["TEXT", "IMAGE"],
|
|
28
|
+
imageConfig: {
|
|
29
|
+
aspectRatio: aspectRatio,
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
const response = await client.models.generateContent({
|
|
33
|
+
model: "gemini-3-pro-image-preview",
|
|
34
|
+
contents: [{ text: prompt }],
|
|
35
|
+
config,
|
|
36
|
+
});
|
|
37
|
+
if (!response.candidates || response.candidates.length === 0) {
|
|
38
|
+
return {
|
|
39
|
+
success: false,
|
|
40
|
+
error: "No response from the model. The request may have been filtered.",
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
const candidate = response.candidates[0];
|
|
44
|
+
if (!candidate) {
|
|
45
|
+
return {
|
|
46
|
+
success: false,
|
|
47
|
+
error: "No candidate in the response.",
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
const parts = candidate.content?.parts;
|
|
51
|
+
if (!parts || parts.length === 0) {
|
|
52
|
+
return {
|
|
53
|
+
success: false,
|
|
54
|
+
error: "No content parts in the response.",
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
let imageData;
|
|
58
|
+
let textResponse;
|
|
59
|
+
let mimeType;
|
|
60
|
+
for (const part of parts) {
|
|
61
|
+
if (part.text) {
|
|
62
|
+
textResponse = part.text;
|
|
63
|
+
}
|
|
64
|
+
else if (part.inlineData) {
|
|
65
|
+
imageData = part.inlineData.data;
|
|
66
|
+
mimeType = part.inlineData.mimeType || "image/png";
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (!imageData) {
|
|
70
|
+
return {
|
|
71
|
+
success: false,
|
|
72
|
+
error: "No image was generated in the response.",
|
|
73
|
+
...(textResponse ? { textResponse } : {}),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
// Save image to disk in generated-images directory (relative to cwd)
|
|
77
|
+
const outputDir = join(process.cwd(), "generated-images");
|
|
78
|
+
await mkdir(outputDir, { recursive: true });
|
|
79
|
+
// Generate unique filename
|
|
80
|
+
const timestamp = Date.now();
|
|
81
|
+
const extension = mimeType === "image/jpeg" ? "jpg" : "png";
|
|
82
|
+
const fileName = `image-${timestamp}.${extension}`;
|
|
83
|
+
const filePath = join(outputDir, fileName);
|
|
84
|
+
// Save image to file
|
|
85
|
+
const buffer = Buffer.from(imageData, "base64");
|
|
86
|
+
await writeFile(filePath, buffer);
|
|
87
|
+
// Create URL for the static file server
|
|
88
|
+
// The agent HTTP server serves static files from the agent directory
|
|
89
|
+
const port = process.env.PORT || "3100";
|
|
90
|
+
const imageUrl = `http://localhost:${port}/static/generated-images/${fileName}`;
|
|
91
|
+
return {
|
|
92
|
+
success: true,
|
|
93
|
+
filePath,
|
|
94
|
+
fileName,
|
|
95
|
+
imageUrl,
|
|
96
|
+
...(mimeType ? { mimeType } : {}),
|
|
97
|
+
...(textResponse ? { textResponse } : {}),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
102
|
+
return {
|
|
103
|
+
success: false,
|
|
104
|
+
error: `Image generation failed: ${errorMessage}`,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}, {
|
|
108
|
+
name: "GenerateImage",
|
|
109
|
+
description: "Generate an image based on a text prompt using Google's Gemini image generation model. " +
|
|
110
|
+
"Returns an imageUrl that can be displayed to the user. After calling this tool, " +
|
|
111
|
+
"include the imageUrl in your response as a markdown image like  " +
|
|
112
|
+
"so the user can see the generated image.\n" +
|
|
113
|
+
"- Creates images from detailed text descriptions\n" +
|
|
114
|
+
"- Supports various aspect ratios for different use cases\n" +
|
|
115
|
+
"- Be specific in prompts about style, composition, colors, and subjects\n" +
|
|
116
|
+
"\n" +
|
|
117
|
+
"Usage notes:\n" +
|
|
118
|
+
" - Provide detailed, specific prompts for best results\n" +
|
|
119
|
+
" - The generated image is saved and served via URL\n" +
|
|
120
|
+
" - Always display the result using markdown: \n",
|
|
121
|
+
schema: z.object({
|
|
122
|
+
prompt: z
|
|
123
|
+
.string()
|
|
124
|
+
.describe("A detailed description of the image to generate. Be specific about style, composition, colors, and subjects."),
|
|
125
|
+
aspectRatio: z
|
|
126
|
+
.enum(["1:1", "3:4", "4:3", "9:16", "16:9", "5:4"])
|
|
127
|
+
.optional()
|
|
128
|
+
.default("1:1")
|
|
129
|
+
.describe("The aspect ratio of the generated image."),
|
|
130
|
+
}),
|
|
131
|
+
});
|
|
132
|
+
generateImage.prettyName = "Generate Image";
|
|
133
|
+
generateImage.icon = "Image";
|
|
134
|
+
return generateImage;
|
|
135
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -2,19 +2,24 @@ import type { DirectTool } from "../../tools.js";
|
|
|
2
2
|
/**
|
|
3
3
|
* Name of the Task tool created by makeSubagentsTool
|
|
4
4
|
*/
|
|
5
|
-
export declare const
|
|
5
|
+
export declare const SUBAGENT_TOOL_NAME = "subagent";
|
|
6
6
|
/**
|
|
7
7
|
* Configuration for a single subagent - supports two variants:
|
|
8
8
|
* 1. Agent name with optional working directory
|
|
9
9
|
* 2. Direct path to agent's index.ts file
|
|
10
|
+
*
|
|
11
|
+
* The optional displayName field provides a human-readable name for the UI.
|
|
12
|
+
* If not provided, agentName will be used for display.
|
|
10
13
|
*/
|
|
11
14
|
type SubagentConfig = {
|
|
12
15
|
agentName: string;
|
|
13
16
|
description: string;
|
|
17
|
+
displayName?: string;
|
|
14
18
|
cwd?: string;
|
|
15
19
|
} | {
|
|
16
20
|
agentName: string;
|
|
17
21
|
description: string;
|
|
22
|
+
displayName?: string;
|
|
18
23
|
path: string;
|
|
19
24
|
};
|
|
20
25
|
/**
|