@townco/agent 0.1.54 → 0.1.56
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 +1 -0
- package/dist/acp-server/adapter.js +9 -1
- package/dist/acp-server/http.js +75 -6
- package/dist/acp-server/session-storage.d.ts +14 -0
- package/dist/acp-server/session-storage.js +47 -0
- package/dist/definition/index.d.ts +9 -0
- package/dist/definition/index.js +9 -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 +5 -1
- package/dist/runner/agent-runner.js +2 -1
- 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/langchain/index.js +19 -7
- package/dist/runner/langchain/model-factory.d.ts +2 -0
- package/dist/runner/langchain/model-factory.js +20 -1
- package/dist/runner/langchain/tools/browser.d.ts +100 -0
- package/dist/runner/langchain/tools/browser.js +412 -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.js +230 -127
- package/dist/runner/tools.d.ts +2 -2
- package/dist/runner/tools.js +1 -0
- package/dist/scaffold/index.js +7 -1
- package/dist/scaffold/templates/dot-claude/CLAUDE-append.md +2 -0
- package/dist/storage/index.js +1 -1
- package/dist/telemetry/index.d.ts +5 -0
- package/dist/telemetry/index.js +10 -0
- package/dist/templates/index.d.ts +2 -0
- package/dist/templates/index.js +30 -8
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/index.ts +1 -1
- package/package.json +11 -6
- package/templates/index.ts +40 -8
|
@@ -24,6 +24,7 @@ export declare class AgentAcpAdapter implements acp.Agent {
|
|
|
24
24
|
private agentVersion;
|
|
25
25
|
private agentDescription;
|
|
26
26
|
private agentSuggestedPrompts;
|
|
27
|
+
private agentInitialMessage;
|
|
27
28
|
private currentToolOverheadTokens;
|
|
28
29
|
private currentMcpOverheadTokens;
|
|
29
30
|
constructor(agent: AgentRunner, connection: acp.AgentSideConnection, agentDir?: string, agentName?: string);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as acp from "@agentclientprotocol/sdk";
|
|
2
2
|
import { context, trace } from "@opentelemetry/api";
|
|
3
|
-
import { createLogger } from "
|
|
3
|
+
import { createLogger } from "../logger.js";
|
|
4
4
|
import { HookExecutor, loadHookCallback } from "../runner/hooks";
|
|
5
5
|
import { telemetry } from "../telemetry/index.js";
|
|
6
6
|
import { calculateContextSize, } from "../utils/context-size-calculator.js";
|
|
@@ -105,6 +105,7 @@ export class AgentAcpAdapter {
|
|
|
105
105
|
agentVersion;
|
|
106
106
|
agentDescription;
|
|
107
107
|
agentSuggestedPrompts;
|
|
108
|
+
agentInitialMessage;
|
|
108
109
|
currentToolOverheadTokens = 0; // Track tool overhead for current turn
|
|
109
110
|
currentMcpOverheadTokens = 0; // Track MCP overhead for current turn
|
|
110
111
|
constructor(agent, connection, agentDir, agentName) {
|
|
@@ -117,6 +118,7 @@ export class AgentAcpAdapter {
|
|
|
117
118
|
this.agentVersion = agent.definition.version;
|
|
118
119
|
this.agentDescription = agent.definition.description;
|
|
119
120
|
this.agentSuggestedPrompts = agent.definition.suggestedPrompts;
|
|
121
|
+
this.agentInitialMessage = agent.definition.initialMessage;
|
|
120
122
|
this.noSession = process.env.TOWN_NO_SESSION === "true";
|
|
121
123
|
this.storage =
|
|
122
124
|
agentDir && agentName && !this.noSession
|
|
@@ -129,6 +131,7 @@ export class AgentAcpAdapter {
|
|
|
129
131
|
agentVersion: this.agentVersion,
|
|
130
132
|
agentDescription: this.agentDescription,
|
|
131
133
|
suggestedPrompts: this.agentSuggestedPrompts,
|
|
134
|
+
initialMessage: this.agentInitialMessage,
|
|
132
135
|
noSession: this.noSession,
|
|
133
136
|
hasStorage: this.storage !== null,
|
|
134
137
|
sessionStoragePath: this.storage ? `${agentDir}/.sessions` : null,
|
|
@@ -259,6 +262,9 @@ export class AgentAcpAdapter {
|
|
|
259
262
|
...(this.agentSuggestedPrompts
|
|
260
263
|
? { suggestedPrompts: this.agentSuggestedPrompts }
|
|
261
264
|
: {}),
|
|
265
|
+
...(this.agentInitialMessage
|
|
266
|
+
? { initialMessage: this.agentInitialMessage }
|
|
267
|
+
: {}),
|
|
262
268
|
...(toolsMetadata.length > 0 ? { tools: toolsMetadata } : {}),
|
|
263
269
|
...(mcpsMetadata.length > 0 ? { mcps: mcpsMetadata } : {}),
|
|
264
270
|
...(subagentsMetadata.length > 0 ? { subagents: subagentsMetadata } : {}),
|
|
@@ -273,6 +279,8 @@ export class AgentAcpAdapter {
|
|
|
273
279
|
context: [],
|
|
274
280
|
requestParams: params,
|
|
275
281
|
});
|
|
282
|
+
// Note: Initial message is sent by the HTTP transport when SSE connection is established
|
|
283
|
+
// This ensures the message is delivered after the client is ready to receive it
|
|
276
284
|
return {
|
|
277
285
|
sessionId,
|
|
278
286
|
};
|
package/dist/acp-server/http.js
CHANGED
|
@@ -3,13 +3,15 @@ import { join, resolve } from "node:path";
|
|
|
3
3
|
import { gzipSync } from "node:zlib";
|
|
4
4
|
import * as acp from "@agentclientprotocol/sdk";
|
|
5
5
|
import { PGlite } from "@electric-sql/pglite";
|
|
6
|
-
import { configureLogsDir
|
|
6
|
+
import { configureLogsDir } from "@townco/core";
|
|
7
7
|
import { Hono } from "hono";
|
|
8
8
|
import { cors } from "hono/cors";
|
|
9
9
|
import { streamSSE } from "hono/streaming";
|
|
10
|
+
import { createLogger, isSubagent } from "../logger.js";
|
|
10
11
|
import { makeRunnerFromDefinition } from "../runner";
|
|
11
12
|
import { AgentAcpAdapter } from "./adapter";
|
|
12
|
-
|
|
13
|
+
import { SessionStorage } from "./session-storage";
|
|
14
|
+
const logger = createLogger("http");
|
|
13
15
|
/**
|
|
14
16
|
* Compress a payload using gzip if it's too large for PostgreSQL NOTIFY
|
|
15
17
|
* Returns an object with the payload and metadata about compression
|
|
@@ -49,11 +51,16 @@ function safeChannelName(prefix, id) {
|
|
|
49
51
|
return `${prefix}_${hash}`;
|
|
50
52
|
}
|
|
51
53
|
export function makeHttpTransport(agent, agentDir, agentName) {
|
|
52
|
-
// Configure logger to write to .logs/ directory
|
|
53
|
-
if (
|
|
54
|
-
|
|
54
|
+
// Configure logger to write to .logs/ directory
|
|
55
|
+
// Use TOWN_LOGS_DIR env var if set (for subagents), otherwise use agentDir
|
|
56
|
+
const logsDir = process.env.TOWN_LOGS_DIR ||
|
|
57
|
+
(agentDir ? join(agentDir, ".logs") : undefined);
|
|
58
|
+
if (logsDir) {
|
|
55
59
|
configureLogsDir(logsDir);
|
|
56
|
-
logger.info("Configured logs directory", {
|
|
60
|
+
logger.info("Configured logs directory", {
|
|
61
|
+
logsDir,
|
|
62
|
+
isSubagent: isSubagent(),
|
|
63
|
+
});
|
|
57
64
|
}
|
|
58
65
|
const inbound = new TransformStream();
|
|
59
66
|
const outbound = new TransformStream();
|
|
@@ -63,6 +70,10 @@ export function makeHttpTransport(agent, agentDir, agentName) {
|
|
|
63
70
|
const app = new Hono();
|
|
64
71
|
// Track active SSE streams by sessionId for direct output delivery
|
|
65
72
|
const sseStreams = new Map();
|
|
73
|
+
// Track sessions that have already received initial message
|
|
74
|
+
const initialMessageSentSessions = new Set();
|
|
75
|
+
// Get initial message config from agent definition
|
|
76
|
+
const initialMessageConfig = agentRunner.definition.initialMessage;
|
|
66
77
|
const decoder = new TextDecoder();
|
|
67
78
|
const encoder = new TextEncoder();
|
|
68
79
|
(async () => {
|
|
@@ -264,6 +275,28 @@ export function makeHttpTransport(agent, agentDir, agentName) {
|
|
|
264
275
|
allowMethods: ["GET", "POST", "OPTIONS"],
|
|
265
276
|
}));
|
|
266
277
|
app.get("/health", (c) => c.json({ ok: true }));
|
|
278
|
+
// List available sessions
|
|
279
|
+
app.get("/sessions", async (c) => {
|
|
280
|
+
if (!agentDir || !agentName) {
|
|
281
|
+
return c.json({ sessions: [], error: "Session storage not configured" });
|
|
282
|
+
}
|
|
283
|
+
const noSession = process.env.TOWN_NO_SESSION === "true";
|
|
284
|
+
if (noSession) {
|
|
285
|
+
return c.json({ sessions: [], error: "Sessions disabled" });
|
|
286
|
+
}
|
|
287
|
+
try {
|
|
288
|
+
const storage = new SessionStorage(agentDir, agentName);
|
|
289
|
+
const sessions = await storage.listSessionsWithMetadata();
|
|
290
|
+
return c.json({ sessions });
|
|
291
|
+
}
|
|
292
|
+
catch (error) {
|
|
293
|
+
logger.error("Failed to list sessions", { error });
|
|
294
|
+
return c.json({
|
|
295
|
+
sessions: [],
|
|
296
|
+
error: error instanceof Error ? error.message : String(error),
|
|
297
|
+
}, 500);
|
|
298
|
+
}
|
|
299
|
+
});
|
|
267
300
|
// Serve static files from agent directory (for generated images, etc.)
|
|
268
301
|
if (agentDir) {
|
|
269
302
|
app.get("/static/*", async (c) => {
|
|
@@ -313,6 +346,42 @@ export function makeHttpTransport(agent, agentDir, agentName) {
|
|
|
313
346
|
// Register this stream for direct tool output delivery
|
|
314
347
|
sseStreams.set(sessionId, stream);
|
|
315
348
|
await stream.writeSSE({ event: "ping", data: "{}" });
|
|
349
|
+
// Send initial message if configured and not already sent for this session
|
|
350
|
+
if (initialMessageConfig?.enabled &&
|
|
351
|
+
initialMessageConfig.content &&
|
|
352
|
+
!initialMessageSentSessions.has(sessionId)) {
|
|
353
|
+
initialMessageSentSessions.add(sessionId);
|
|
354
|
+
// Process template variables in the content
|
|
355
|
+
let content = initialMessageConfig.content;
|
|
356
|
+
content = content.replace(/\{\{\.AgentName\}\}/g, agentName ?? "Agent");
|
|
357
|
+
const displayName = agentRunner.definition.displayName;
|
|
358
|
+
content = content.replace(/\{\{\.DisplayName\}\}/g, displayName ?? agentName ?? "Agent");
|
|
359
|
+
const initialMessage = {
|
|
360
|
+
jsonrpc: "2.0",
|
|
361
|
+
method: "session/update",
|
|
362
|
+
params: {
|
|
363
|
+
sessionId,
|
|
364
|
+
update: {
|
|
365
|
+
sessionUpdate: "agent_message_chunk",
|
|
366
|
+
content: {
|
|
367
|
+
type: "text",
|
|
368
|
+
text: content,
|
|
369
|
+
},
|
|
370
|
+
_meta: {
|
|
371
|
+
isInitialMessage: true,
|
|
372
|
+
},
|
|
373
|
+
},
|
|
374
|
+
},
|
|
375
|
+
};
|
|
376
|
+
await stream.writeSSE({
|
|
377
|
+
event: "message",
|
|
378
|
+
data: JSON.stringify(initialMessage),
|
|
379
|
+
});
|
|
380
|
+
logger.info("Sent initial message via SSE", {
|
|
381
|
+
sessionId,
|
|
382
|
+
contentPreview: content.slice(0, 100),
|
|
383
|
+
});
|
|
384
|
+
}
|
|
316
385
|
const hb = setInterval(() => {
|
|
317
386
|
// Heartbeat to keep proxies from terminating idle connections
|
|
318
387
|
void stream.writeSSE({ event: "ping", data: "{}" });
|
|
@@ -150,4 +150,18 @@ export declare class SessionStorage {
|
|
|
150
150
|
* List all session IDs
|
|
151
151
|
*/
|
|
152
152
|
listSessions(): Promise<string[]>;
|
|
153
|
+
/**
|
|
154
|
+
* Session summary for listing
|
|
155
|
+
*/
|
|
156
|
+
/**
|
|
157
|
+
* List all sessions with metadata
|
|
158
|
+
* Returns sessions sorted by updatedAt (most recent first)
|
|
159
|
+
*/
|
|
160
|
+
listSessionsWithMetadata(): Promise<Array<{
|
|
161
|
+
sessionId: string;
|
|
162
|
+
createdAt: string;
|
|
163
|
+
updatedAt: string;
|
|
164
|
+
messageCount: number;
|
|
165
|
+
firstUserMessage?: string;
|
|
166
|
+
}>>;
|
|
153
167
|
}
|
|
@@ -236,4 +236,51 @@ export class SessionStorage {
|
|
|
236
236
|
throw new Error(`Failed to list sessions: ${error instanceof Error ? error.message : String(error)}`);
|
|
237
237
|
}
|
|
238
238
|
}
|
|
239
|
+
/**
|
|
240
|
+
* Session summary for listing
|
|
241
|
+
*/
|
|
242
|
+
/**
|
|
243
|
+
* List all sessions with metadata
|
|
244
|
+
* Returns sessions sorted by updatedAt (most recent first)
|
|
245
|
+
*/
|
|
246
|
+
async listSessionsWithMetadata() {
|
|
247
|
+
if (!existsSync(this.sessionsDir)) {
|
|
248
|
+
return [];
|
|
249
|
+
}
|
|
250
|
+
try {
|
|
251
|
+
const files = readdirSync(this.sessionsDir);
|
|
252
|
+
const sessionFiles = files.filter((file) => file.endsWith(".json") && !file.endsWith(".tmp"));
|
|
253
|
+
const sessions = [];
|
|
254
|
+
for (const file of sessionFiles) {
|
|
255
|
+
const sessionId = file.replace(".json", "");
|
|
256
|
+
try {
|
|
257
|
+
const session = this.loadSessionSync(sessionId);
|
|
258
|
+
if (session) {
|
|
259
|
+
// Find the first user message for preview
|
|
260
|
+
const firstUserMsg = session.messages.find((m) => m.role === "user");
|
|
261
|
+
const firstUserText = firstUserMsg?.content.find((c) => c.type === "text");
|
|
262
|
+
const entry = {
|
|
263
|
+
sessionId: session.sessionId,
|
|
264
|
+
createdAt: session.metadata.createdAt,
|
|
265
|
+
updatedAt: session.metadata.updatedAt,
|
|
266
|
+
messageCount: session.messages.length,
|
|
267
|
+
};
|
|
268
|
+
if (firstUserText && "text" in firstUserText) {
|
|
269
|
+
entry.firstUserMessage = firstUserText.text.slice(0, 100);
|
|
270
|
+
}
|
|
271
|
+
sessions.push(entry);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
// Skip invalid sessions
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
// Sort by updatedAt, most recent first
|
|
279
|
+
sessions.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
|
|
280
|
+
return sessions;
|
|
281
|
+
}
|
|
282
|
+
catch (error) {
|
|
283
|
+
throw new Error(`Failed to list sessions: ${error instanceof Error ? error.message : String(error)}`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
239
286
|
}
|
|
@@ -26,6 +26,11 @@ export declare const HookConfigSchema: z.ZodObject<{
|
|
|
26
26
|
}, z.core.$strip>]>>;
|
|
27
27
|
callback: z.ZodString;
|
|
28
28
|
}, z.core.$strip>;
|
|
29
|
+
/** Initial message configuration schema. */
|
|
30
|
+
export declare const InitialMessageSchema: z.ZodObject<{
|
|
31
|
+
enabled: z.ZodBoolean;
|
|
32
|
+
content: z.ZodString;
|
|
33
|
+
}, z.core.$strip>;
|
|
29
34
|
/** Agent definition schema. */
|
|
30
35
|
export declare const AgentDefinitionSchema: z.ZodObject<{
|
|
31
36
|
displayName: z.ZodOptional<z.ZodString>;
|
|
@@ -74,4 +79,8 @@ export declare const AgentDefinitionSchema: z.ZodObject<{
|
|
|
74
79
|
}, z.core.$strip>]>>;
|
|
75
80
|
callback: z.ZodString;
|
|
76
81
|
}, z.core.$strip>>>;
|
|
82
|
+
initialMessage: z.ZodOptional<z.ZodObject<{
|
|
83
|
+
enabled: z.ZodBoolean;
|
|
84
|
+
content: z.ZodString;
|
|
85
|
+
}, z.core.$strip>>;
|
|
77
86
|
}, z.core.$strip>;
|
package/dist/definition/index.js
CHANGED
|
@@ -71,6 +71,13 @@ export const HookConfigSchema = z.object({
|
|
|
71
71
|
.optional(),
|
|
72
72
|
callback: z.string(),
|
|
73
73
|
});
|
|
74
|
+
/** Initial message configuration schema. */
|
|
75
|
+
export const InitialMessageSchema = z.object({
|
|
76
|
+
/** Whether the agent should send an initial message when a session starts. */
|
|
77
|
+
enabled: z.boolean(),
|
|
78
|
+
/** The content of the initial message to send. Supports template variables like {{.AgentName}}. */
|
|
79
|
+
content: z.string(),
|
|
80
|
+
});
|
|
74
81
|
/** Agent definition schema. */
|
|
75
82
|
export const AgentDefinitionSchema = z.object({
|
|
76
83
|
/** Human-readable display name for the agent (shown in UI). */
|
|
@@ -84,4 +91,6 @@ export const AgentDefinitionSchema = z.object({
|
|
|
84
91
|
mcps: z.array(McpConfigSchema).optional(),
|
|
85
92
|
harnessImplementation: z.literal("langchain").optional(),
|
|
86
93
|
hooks: z.array(HookConfigSchema).optional(),
|
|
94
|
+
/** Configuration for an initial message the agent sends when a session starts. */
|
|
95
|
+
initialMessage: InitialMessageSchema.optional(),
|
|
87
96
|
});
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { basename } from "node:path";
|
|
2
|
-
import { createLogger } from "@townco/core";
|
|
3
2
|
import { makeHttpTransport, makeStdioTransport } from "./acp-server";
|
|
3
|
+
import { createLogger } from "./logger.js";
|
|
4
4
|
import { initializeOpenTelemetryFromEnv } from "./telemetry/setup.js";
|
|
5
5
|
import { makeSubagentsTool } from "./utils";
|
|
6
6
|
// Re-export telemetry configuration for library users
|
package/dist/logger.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logger utilities for the agent package.
|
|
3
|
+
* Provides subagent-aware service name generation.
|
|
4
|
+
*/
|
|
5
|
+
import { createLogger as coreCreateLogger } from "@townco/core";
|
|
6
|
+
/**
|
|
7
|
+
* Check if running as a subagent (detected via TOWN_LOGS_DIR env var)
|
|
8
|
+
*/
|
|
9
|
+
export declare function isSubagent(): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Create a logger with subagent-aware service name.
|
|
12
|
+
* When running as a subagent, the service name is prefixed with "subagent:{port}:{name}:".
|
|
13
|
+
*
|
|
14
|
+
* @param service - The service name (e.g., "adapter", "hook-executor")
|
|
15
|
+
* @returns A logger instance with the appropriate service name
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* // In main agent: creates logger with service "adapter"
|
|
19
|
+
* // In subagent on port 4001 named "researcher": creates logger with service "subagent:4001:researcher:adapter"
|
|
20
|
+
* const logger = createLogger("adapter");
|
|
21
|
+
*/
|
|
22
|
+
export declare function createLogger(service: string): import("@townco/core").Logger;
|
|
23
|
+
/**
|
|
24
|
+
* Re-export the core createLogger for cases where subagent prefix is not wanted
|
|
25
|
+
*/
|
|
26
|
+
export { coreCreateLogger as createCoreLogger };
|
package/dist/logger.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logger utilities for the agent package.
|
|
3
|
+
* Provides subagent-aware service name generation.
|
|
4
|
+
*/
|
|
5
|
+
import { createLogger as coreCreateLogger } from "@townco/core";
|
|
6
|
+
/**
|
|
7
|
+
* Check if running as a subagent (detected via TOWN_LOGS_DIR env var)
|
|
8
|
+
*/
|
|
9
|
+
export function isSubagent() {
|
|
10
|
+
return !!process.env.TOWN_LOGS_DIR;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Get the subagent prefix if running as a subagent.
|
|
14
|
+
* Returns format: "subagent:{port}:{name}:" or empty string if not a subagent.
|
|
15
|
+
*/
|
|
16
|
+
function getSubagentPrefix() {
|
|
17
|
+
if (!isSubagent()) {
|
|
18
|
+
return "";
|
|
19
|
+
}
|
|
20
|
+
const port = process.env.PORT || "unknown";
|
|
21
|
+
const name = process.env.TOWN_SUBAGENT_NAME || "agent";
|
|
22
|
+
return `subagent:${port}:${name}:`;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Create a logger with subagent-aware service name.
|
|
26
|
+
* When running as a subagent, the service name is prefixed with "subagent:{port}:{name}:".
|
|
27
|
+
*
|
|
28
|
+
* @param service - The service name (e.g., "adapter", "hook-executor")
|
|
29
|
+
* @returns A logger instance with the appropriate service name
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* // In main agent: creates logger with service "adapter"
|
|
33
|
+
* // In subagent on port 4001 named "researcher": creates logger with service "subagent:4001:researcher:adapter"
|
|
34
|
+
* const logger = createLogger("adapter");
|
|
35
|
+
*/
|
|
36
|
+
export function createLogger(service) {
|
|
37
|
+
const prefix = getSubagentPrefix();
|
|
38
|
+
return coreCreateLogger(`${prefix}${service}`);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Re-export the core createLogger for cases where subagent prefix is not wanted
|
|
42
|
+
*/
|
|
43
|
+
export { coreCreateLogger as createCoreLogger };
|
|
@@ -8,7 +8,7 @@ export declare const zAgentRunnerParams: z.ZodObject<{
|
|
|
8
8
|
suggestedPrompts: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
9
9
|
systemPrompt: z.ZodNullable<z.ZodString>;
|
|
10
10
|
model: z.ZodString;
|
|
11
|
-
tools: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodUnion<readonly [z.ZodLiteral<"todo_write">, z.ZodLiteral<"get_weather">, z.ZodLiteral<"web_search">, z.ZodLiteral<"filesystem">, z.ZodLiteral<"generate_image">]>, z.ZodObject<{
|
|
11
|
+
tools: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodUnion<readonly [z.ZodLiteral<"todo_write">, z.ZodLiteral<"get_weather">, z.ZodLiteral<"web_search">, z.ZodLiteral<"filesystem">, z.ZodLiteral<"generate_image">, z.ZodLiteral<"browser">]>, z.ZodObject<{
|
|
12
12
|
type: z.ZodLiteral<"custom">;
|
|
13
13
|
modulePath: z.ZodString;
|
|
14
14
|
}, z.core.$strip>, z.ZodObject<{
|
|
@@ -52,6 +52,10 @@ export declare const zAgentRunnerParams: z.ZodObject<{
|
|
|
52
52
|
}, z.core.$strip>]>>;
|
|
53
53
|
callback: z.ZodString;
|
|
54
54
|
}, z.core.$strip>>>;
|
|
55
|
+
initialMessage: z.ZodOptional<z.ZodObject<{
|
|
56
|
+
enabled: z.ZodBoolean;
|
|
57
|
+
content: z.ZodString;
|
|
58
|
+
}, z.core.$strip>>;
|
|
55
59
|
}, z.core.$strip>;
|
|
56
60
|
export type CreateAgentRunnerParams = z.infer<typeof zAgentRunnerParams>;
|
|
57
61
|
export interface SessionMessage {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { HookConfigSchema, McpConfigSchema } from "../definition";
|
|
2
|
+
import { HookConfigSchema, InitialMessageSchema, McpConfigSchema, } from "../definition";
|
|
3
3
|
import { zToolType } from "./tools";
|
|
4
4
|
export const zAgentRunnerParams = z.object({
|
|
5
5
|
displayName: z.string().optional(),
|
|
@@ -11,4 +11,5 @@ export const zAgentRunnerParams = z.object({
|
|
|
11
11
|
tools: z.array(zToolType).optional(),
|
|
12
12
|
mcps: z.array(McpConfigSchema).optional(),
|
|
13
13
|
hooks: z.array(HookConfigSchema).optional(),
|
|
14
|
+
initialMessage: InitialMessageSchema.optional(),
|
|
14
15
|
});
|
|
@@ -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)
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { MultiServerMCPClient } from "@langchain/mcp-adapters";
|
|
2
2
|
import { context, propagation, trace } from "@opentelemetry/api";
|
|
3
|
-
import {
|
|
3
|
+
import { loadAuthCredentials } from "@townco/core/auth";
|
|
4
4
|
import { AIMessageChunk, createAgent, ToolMessage, tool, } from "langchain";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { SUBAGENT_MODE_KEY } from "../../acp-server/adapter";
|
|
7
|
+
import { createLogger } from "../../logger.js";
|
|
7
8
|
import { telemetry } from "../../telemetry/index.js";
|
|
8
9
|
import { loadCustomToolModule, } from "../tool-loader.js";
|
|
9
10
|
import { createModelFromString, detectProvider } from "./model-factory.js";
|
|
10
11
|
import { makeOtelCallbacks } from "./otel-callbacks.js";
|
|
12
|
+
import { makeBrowserTools } from "./tools/browser";
|
|
11
13
|
import { makeFilesystemTools } from "./tools/filesystem";
|
|
12
14
|
import { makeGenerateImageTool } from "./tools/generate_image";
|
|
13
15
|
import { SUBAGENT_TOOL_NAME } from "./tools/subagent";
|
|
@@ -29,6 +31,7 @@ export const TOOL_REGISTRY = {
|
|
|
29
31
|
web_search: () => makeWebSearchTools(),
|
|
30
32
|
filesystem: () => makeFilesystemTools(process.cwd()),
|
|
31
33
|
generate_image: () => makeGenerateImageTool(),
|
|
34
|
+
browser: () => makeBrowserTools(),
|
|
32
35
|
};
|
|
33
36
|
// ============================================================================
|
|
34
37
|
// Custom tool loading
|
|
@@ -78,11 +81,14 @@ export class LangchainAgent {
|
|
|
78
81
|
const countedMessageIds = new Set();
|
|
79
82
|
// Track tool calls for which we've emitted preliminary notifications (from early tool_use blocks)
|
|
80
83
|
const preliminaryToolCallIds = new Set();
|
|
84
|
+
// Set session_id as a base attribute so all spans in this invocation include it
|
|
85
|
+
telemetry.setBaseAttributes({
|
|
86
|
+
"agent.session_id": req.sessionId,
|
|
87
|
+
});
|
|
81
88
|
// Start telemetry span for entire invocation
|
|
82
89
|
const invocationSpan = telemetry.startSpan("agent.invoke", {
|
|
83
90
|
"agent.model": this.definition.model,
|
|
84
91
|
"agent.subagent": meta?.[SUBAGENT_MODE_KEY] === true,
|
|
85
|
-
"agent.session_id": req.sessionId,
|
|
86
92
|
"agent.message_id": req.messageId,
|
|
87
93
|
}, parentContext);
|
|
88
94
|
// Create a context with the invocation span as active
|
|
@@ -294,7 +300,7 @@ export class LangchainAgent {
|
|
|
294
300
|
: wrappedTools;
|
|
295
301
|
// Wrap tools with tracing so each tool executes within its own span context.
|
|
296
302
|
// This ensures subagent spans are children of the Task tool span.
|
|
297
|
-
const finalTools = filteredTools.map((t) => wrapToolWithTracing(t
|
|
303
|
+
const finalTools = filteredTools.map((t) => wrapToolWithTracing(t));
|
|
298
304
|
// Create the model instance using the factory
|
|
299
305
|
// This detects the provider from the model string:
|
|
300
306
|
// - "gemini-2.0-flash" → Google Generative AI
|
|
@@ -776,12 +782,19 @@ const modelRequestSchema = z.object({
|
|
|
776
782
|
const makeMcpToolsClient = (mcpConfigs) => {
|
|
777
783
|
const mcpServers = mcpConfigs?.map((config) => {
|
|
778
784
|
if (typeof config === "string") {
|
|
779
|
-
//
|
|
780
|
-
const
|
|
785
|
+
// String configs use the centralized MCP proxy with auth
|
|
786
|
+
const credentials = loadAuthCredentials();
|
|
787
|
+
if (!credentials) {
|
|
788
|
+
throw new Error("Not logged in. Run 'town login' first to use cloud MCP servers.");
|
|
789
|
+
}
|
|
790
|
+
const proxyUrl = process.env.MCP_PROXY_URL ?? `${credentials.shed_url}/mcp_proxy`;
|
|
781
791
|
return [
|
|
782
792
|
config,
|
|
783
793
|
{
|
|
784
794
|
url: `${proxyUrl}?server=${config}`,
|
|
795
|
+
headers: {
|
|
796
|
+
Authorization: `Bearer ${credentials.access_token}`,
|
|
797
|
+
},
|
|
785
798
|
},
|
|
786
799
|
];
|
|
787
800
|
}
|
|
@@ -874,13 +887,12 @@ export { makeSubagentsTool } from "./tools/subagent.js";
|
|
|
874
887
|
* so any child operations (like subagent spawning) become children
|
|
875
888
|
* of the tool span rather than the parent invocation span.
|
|
876
889
|
*/
|
|
877
|
-
function wrapToolWithTracing(originalTool
|
|
890
|
+
function wrapToolWithTracing(originalTool) {
|
|
878
891
|
const wrappedFunc = async (input) => {
|
|
879
892
|
const toolInputJson = JSON.stringify(input);
|
|
880
893
|
const toolSpan = telemetry.startSpan("agent.tool_call", {
|
|
881
894
|
"tool.name": originalTool.name,
|
|
882
895
|
"tool.input": toolInputJson,
|
|
883
|
-
"agent.session_id": sessionId,
|
|
884
896
|
});
|
|
885
897
|
// Create a context with the tool span as active
|
|
886
898
|
const spanContext = toolSpan
|
|
@@ -4,6 +4,7 @@ import type { BaseChatModel } from "@langchain/core/language_models/chat_models"
|
|
|
4
4
|
* LangChain chat model instance.
|
|
5
5
|
*
|
|
6
6
|
* Detection logic:
|
|
7
|
+
* - If model starts with "town-" → Proxied via shed (strips prefix, uses TOWN_SHED_URL)
|
|
7
8
|
* - If model starts with "vertex-" → Google Vertex AI (strips prefix)
|
|
8
9
|
* - If model contains "gemini" (unprefixed) → Google Generative AI
|
|
9
10
|
* - If model contains "gpt" → OpenAI (future support)
|
|
@@ -12,6 +13,7 @@ import type { BaseChatModel } from "@langchain/core/language_models/chat_models"
|
|
|
12
13
|
* Supported formats:
|
|
13
14
|
* - Direct model name: "gemini-2.0-flash", "vertex-gemini-2.0-flash", "claude-sonnet-4-5-20250929"
|
|
14
15
|
* - Provider prefix: "google_vertexai:gemini-2.0-flash", "google_genai:gemini-2.0-flash", "anthropic:claude-3-5-sonnet"
|
|
16
|
+
* - Proxied: "town-claude-sonnet-4-5-20250929" (uses TOWN_SHED_URL or defaults to localhost:3000)
|
|
15
17
|
*/
|
|
16
18
|
export declare function createModelFromString(modelString: string): BaseChatModel;
|
|
17
19
|
/**
|
|
@@ -1,13 +1,15 @@
|
|
|
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 {
|
|
4
|
+
import { loadAuthCredentials } from "@townco/core/auth";
|
|
5
|
+
import { createLogger } from "../../logger.js";
|
|
5
6
|
const logger = createLogger("model-factory");
|
|
6
7
|
/**
|
|
7
8
|
* Detects the provider from a model string and returns the appropriate
|
|
8
9
|
* LangChain chat model instance.
|
|
9
10
|
*
|
|
10
11
|
* Detection logic:
|
|
12
|
+
* - If model starts with "town-" → Proxied via shed (strips prefix, uses TOWN_SHED_URL)
|
|
11
13
|
* - If model starts with "vertex-" → Google Vertex AI (strips prefix)
|
|
12
14
|
* - If model contains "gemini" (unprefixed) → Google Generative AI
|
|
13
15
|
* - If model contains "gpt" → OpenAI (future support)
|
|
@@ -16,8 +18,25 @@ const logger = createLogger("model-factory");
|
|
|
16
18
|
* Supported formats:
|
|
17
19
|
* - Direct model name: "gemini-2.0-flash", "vertex-gemini-2.0-flash", "claude-sonnet-4-5-20250929"
|
|
18
20
|
* - Provider prefix: "google_vertexai:gemini-2.0-flash", "google_genai:gemini-2.0-flash", "anthropic:claude-3-5-sonnet"
|
|
21
|
+
* - Proxied: "town-claude-sonnet-4-5-20250929" (uses TOWN_SHED_URL or defaults to localhost:3000)
|
|
19
22
|
*/
|
|
20
23
|
export function createModelFromString(modelString) {
|
|
24
|
+
// Check for town- prefix for proxied models via shed
|
|
25
|
+
if (modelString.startsWith("town-")) {
|
|
26
|
+
const actualModel = modelString.slice(5); // strip "town-"
|
|
27
|
+
const credentials = loadAuthCredentials();
|
|
28
|
+
if (!credentials) {
|
|
29
|
+
throw new Error("Not logged in. Run 'town login' first.");
|
|
30
|
+
}
|
|
31
|
+
const shedUrl = credentials.shed_url ??
|
|
32
|
+
process.env.TOWN_SHED_URL ??
|
|
33
|
+
"http://localhost:3000";
|
|
34
|
+
return new ChatAnthropic({
|
|
35
|
+
model: actualModel,
|
|
36
|
+
anthropicApiUrl: `${shedUrl}/api/anthropic`,
|
|
37
|
+
apiKey: credentials.access_token,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
21
40
|
// Check if the model string uses provider prefix format
|
|
22
41
|
const parts = modelString.split(":", 2);
|
|
23
42
|
const maybeProvider = parts[0];
|