@townco/agent 0.1.34 → 0.1.36

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.
@@ -1,5 +1,5 @@
1
1
  import * as acp from "@agentclientprotocol/sdk";
2
- import { SessionStorage } from "./session-storage.js";
2
+ import { SessionStorage, } from "./session-storage.js";
3
3
  /**
4
4
  * ACP extension key for subagent mode indicator
5
5
  * Following ACP extensibility pattern with namespaced key
@@ -62,20 +62,76 @@ export class AgentAcpAdapter {
62
62
  messages: storedSession.messages,
63
63
  requestParams: { cwd: process.cwd(), mcpServers: [] },
64
64
  });
65
- // Replay conversation history to client
65
+ // Replay conversation history to client with ordered content blocks
66
66
  console.log(`[adapter] Replaying ${storedSession.messages.length} messages for session ${params.sessionId}`);
67
67
  for (const msg of storedSession.messages) {
68
- console.log(`[adapter] Replaying message: ${msg.role} - ${msg.content.substring(0, 50)}...`);
69
- this.connection.sessionUpdate({
70
- sessionId: params.sessionId,
71
- update: {
72
- sessionUpdate: msg.role === "user" ? "user_message_chunk" : "agent_message_chunk",
73
- content: {
74
- type: "text",
75
- text: msg.content,
76
- },
77
- },
78
- });
68
+ console.log(`[adapter] Replaying message: ${msg.role} with ${msg.content.length} content blocks`);
69
+ // Iterate through content blocks in order
70
+ for (const block of msg.content) {
71
+ if (block.type === "text") {
72
+ // Replay text block
73
+ this.connection.sessionUpdate({
74
+ sessionId: params.sessionId,
75
+ update: {
76
+ sessionUpdate: msg.role === "user"
77
+ ? "user_message_chunk"
78
+ : "agent_message_chunk",
79
+ content: {
80
+ type: "text",
81
+ text: block.text,
82
+ },
83
+ },
84
+ });
85
+ }
86
+ else if (block.type === "tool_call") {
87
+ // Replay tool call directly in final state (skip pending → completed transitions)
88
+ this.connection.sessionUpdate({
89
+ sessionId: params.sessionId,
90
+ update: {
91
+ sessionUpdate: "tool_call",
92
+ toolCallId: block.id,
93
+ title: block.title,
94
+ kind: block.kind,
95
+ status: block.status, // Use final status directly
96
+ rawInput: block.rawInput,
97
+ },
98
+ });
99
+ // If there's output, emit tool_output event for the UI to display
100
+ if (block.rawOutput) {
101
+ const outputUpdate = {
102
+ sessionUpdate: "tool_output",
103
+ toolCallId: block.id,
104
+ rawOutput: block.rawOutput,
105
+ content: [
106
+ {
107
+ type: "content",
108
+ content: {
109
+ type: "text",
110
+ text: typeof block.rawOutput.content === "string"
111
+ ? block.rawOutput.content
112
+ : JSON.stringify(block.rawOutput),
113
+ },
114
+ },
115
+ ],
116
+ };
117
+ this.connection.sessionUpdate({
118
+ sessionId: params.sessionId,
119
+ update: outputUpdate,
120
+ });
121
+ }
122
+ // Also emit tool_call_update with final status for consistency
123
+ this.connection.sessionUpdate({
124
+ sessionId: params.sessionId,
125
+ update: {
126
+ sessionUpdate: "tool_call_update",
127
+ toolCallId: block.id,
128
+ status: block.status,
129
+ rawOutput: block.rawOutput,
130
+ error: block.error,
131
+ },
132
+ });
133
+ }
134
+ }
79
135
  }
80
136
  return {};
81
137
  }
@@ -104,7 +160,7 @@ export class AgentAcpAdapter {
104
160
  // Generate a unique messageId for this assistant response
105
161
  const messageId = Math.random().toString(36).substring(2);
106
162
  // Extract and store the user message
107
- const userMessageContent = params.prompt
163
+ const userMessageText = params.prompt
108
164
  .filter((p) => p.type === "text")
109
165
  .map((p) => p.text)
110
166
  .join("\n");
@@ -112,13 +168,21 @@ export class AgentAcpAdapter {
112
168
  if (!this.noSession) {
113
169
  const userMessage = {
114
170
  role: "user",
115
- content: userMessageContent,
171
+ content: [{ type: "text", text: userMessageText }],
116
172
  timestamp: new Date().toISOString(),
117
173
  };
118
174
  session.messages.push(userMessage);
119
175
  }
120
- // Accumulate assistant response content
121
- let assistantContent = "";
176
+ // Build ordered content blocks for the assistant response
177
+ const contentBlocks = [];
178
+ let pendingText = "";
179
+ // Helper function to flush pending text as a TextBlock
180
+ const flushPendingText = () => {
181
+ if (pendingText.length > 0) {
182
+ contentBlocks.push({ type: "text", text: pendingText });
183
+ pendingText = "";
184
+ }
185
+ };
122
186
  try {
123
187
  const invokeParams = {
124
188
  prompt: params.prompt,
@@ -132,7 +196,7 @@ export class AgentAcpAdapter {
132
196
  invokeParams.sessionMeta = session.requestParams._meta;
133
197
  }
134
198
  for await (const msg of this.agent.invoke(invokeParams)) {
135
- // Accumulate assistant content from message chunks
199
+ // Accumulate text content from message chunks
136
200
  if ("sessionUpdate" in msg &&
137
201
  msg.sessionUpdate === "agent_message_chunk") {
138
202
  if ("content" in msg &&
@@ -140,7 +204,7 @@ export class AgentAcpAdapter {
140
204
  typeof msg.content === "object") {
141
205
  const content = msg.content;
142
206
  if (content.type === "text" && typeof content.text === "string") {
143
- assistantContent += content.text;
207
+ pendingText += content.text;
144
208
  }
145
209
  }
146
210
  // Debug: log if this chunk has tokenUsage in _meta
@@ -151,6 +215,62 @@ export class AgentAcpAdapter {
151
215
  console.log("DEBUG adapter: sending agent_message_chunk with tokenUsage in _meta:", JSON.stringify(msg._meta.tokenUsage));
152
216
  }
153
217
  }
218
+ // Handle tool_call - flush pending text and add ToolCallBlock
219
+ if ("sessionUpdate" in msg && msg.sessionUpdate === "tool_call") {
220
+ flushPendingText();
221
+ const toolCallMsg = msg;
222
+ const toolCall = {
223
+ type: "tool_call",
224
+ id: toolCallMsg.toolCallId || `tool_${Date.now()}`,
225
+ title: toolCallMsg.title || "Tool",
226
+ kind: toolCallMsg.kind || "other",
227
+ status: toolCallMsg.status || "pending",
228
+ startedAt: Date.now(),
229
+ };
230
+ // Only add rawInput if it exists
231
+ if (toolCallMsg.rawInput) {
232
+ toolCall.rawInput = toolCallMsg.rawInput;
233
+ }
234
+ contentBlocks.push(toolCall);
235
+ }
236
+ // Handle tool_call_update - update existing ToolCallBlock
237
+ if ("sessionUpdate" in msg &&
238
+ msg.sessionUpdate === "tool_call_update") {
239
+ const updateMsg = msg;
240
+ const toolCallBlock = contentBlocks.find((block) => block.type === "tool_call" && block.id === updateMsg.toolCallId);
241
+ if (toolCallBlock) {
242
+ if (updateMsg.status) {
243
+ toolCallBlock.status =
244
+ updateMsg.status;
245
+ }
246
+ if (updateMsg.rawOutput) {
247
+ toolCallBlock.rawOutput = updateMsg.rawOutput;
248
+ }
249
+ if (updateMsg.error) {
250
+ toolCallBlock.error = updateMsg.error;
251
+ }
252
+ if (toolCallBlock.status === "completed" ||
253
+ toolCallBlock.status === "failed") {
254
+ toolCallBlock.completedAt = Date.now();
255
+ }
256
+ }
257
+ }
258
+ // Handle tool_output - update ToolCallBlock with output content
259
+ if ("sessionUpdate" in msg && msg.sessionUpdate === "tool_output") {
260
+ const outputMsg = msg;
261
+ const toolCallBlock = contentBlocks.find((block) => block.type === "tool_call" && block.id === outputMsg.toolCallId);
262
+ if (toolCallBlock) {
263
+ // Store both rawOutput and output from tool_output
264
+ if (outputMsg.rawOutput) {
265
+ toolCallBlock.rawOutput = outputMsg.rawOutput;
266
+ }
267
+ else if (outputMsg.output) {
268
+ toolCallBlock.rawOutput = outputMsg.output;
269
+ }
270
+ // Note: content blocks are handled by the transport for display
271
+ // We store the raw output here for session persistence
272
+ }
273
+ }
154
274
  // The agent may emit extended types (like tool_output) that aren't in ACP SDK yet
155
275
  // The http transport will handle routing these appropriately
156
276
  this.connection.sessionUpdate({
@@ -158,6 +278,8 @@ export class AgentAcpAdapter {
158
278
  update: msg,
159
279
  });
160
280
  }
281
+ // Flush any remaining pending text
282
+ flushPendingText();
161
283
  }
162
284
  catch (err) {
163
285
  if (session.pendingPrompt.signal.aborted) {
@@ -167,10 +289,10 @@ export class AgentAcpAdapter {
167
289
  }
168
290
  // Store the complete assistant response in session messages
169
291
  // Only store if session persistence is enabled
170
- if (!this.noSession && assistantContent.length > 0) {
292
+ if (!this.noSession && contentBlocks.length > 0) {
171
293
  const assistantMessage = {
172
294
  role: "assistant",
173
- content: assistantContent,
295
+ content: contentBlocks,
174
296
  timestamp: new Date().toISOString(),
175
297
  };
176
298
  session.messages.push(assistantMessage);
@@ -1,9 +1,29 @@
1
+ /**
2
+ * Content block types for messages
3
+ */
4
+ export interface TextBlock {
5
+ type: "text";
6
+ text: string;
7
+ }
8
+ export interface ToolCallBlock {
9
+ type: "tool_call";
10
+ id: string;
11
+ title: string;
12
+ kind: "read" | "edit" | "delete" | "move" | "search" | "execute" | "think" | "fetch" | "switch_mode" | "other";
13
+ status: "pending" | "in_progress" | "completed" | "failed";
14
+ rawInput?: Record<string, unknown> | undefined;
15
+ rawOutput?: Record<string, unknown> | undefined;
16
+ error?: string | undefined;
17
+ startedAt?: number | undefined;
18
+ completedAt?: number | undefined;
19
+ }
20
+ export type ContentBlock = TextBlock | ToolCallBlock;
1
21
  /**
2
22
  * Session message format stored in files
3
23
  */
4
24
  export interface SessionMessage {
5
25
  role: "user" | "assistant";
6
- content: string;
26
+ content: ContentBlock[];
7
27
  timestamp: string;
8
28
  }
9
29
  /**
@@ -4,9 +4,40 @@ import { z } from "zod";
4
4
  /**
5
5
  * Zod schema for validating session files
6
6
  */
7
+ const textBlockSchema = z.object({
8
+ type: z.literal("text"),
9
+ text: z.string(),
10
+ });
11
+ const toolCallBlockSchema = z.object({
12
+ type: z.literal("tool_call"),
13
+ id: z.string(),
14
+ title: z.string(),
15
+ kind: z.enum([
16
+ "read",
17
+ "edit",
18
+ "delete",
19
+ "move",
20
+ "search",
21
+ "execute",
22
+ "think",
23
+ "fetch",
24
+ "switch_mode",
25
+ "other",
26
+ ]),
27
+ status: z.enum(["pending", "in_progress", "completed", "failed"]),
28
+ rawInput: z.record(z.string(), z.unknown()).optional(),
29
+ rawOutput: z.record(z.string(), z.unknown()).optional(),
30
+ error: z.string().optional(),
31
+ startedAt: z.number().optional(),
32
+ completedAt: z.number().optional(),
33
+ });
34
+ const contentBlockSchema = z.discriminatedUnion("type", [
35
+ textBlockSchema,
36
+ toolCallBlockSchema,
37
+ ]);
7
38
  const sessionMessageSchema = z.object({
8
39
  role: z.enum(["user", "assistant"]),
9
- content: z.string(),
40
+ content: z.array(contentBlockSchema),
10
41
  timestamp: z.string(),
11
42
  });
12
43
  const sessionMetadataSchema = z.object({
@@ -1,5 +1,6 @@
1
1
  import type { PromptRequest, PromptResponse, SessionNotification } from "@agentclientprotocol/sdk";
2
2
  import { z } from "zod";
3
+ import type { ContentBlock } from "../acp-server/session-storage.js";
3
4
  export declare const zAgentRunnerParams: z.ZodObject<{
4
5
  systemPrompt: z.ZodNullable<z.ZodString>;
5
6
  model: z.ZodString;
@@ -31,7 +32,7 @@ export declare const zAgentRunnerParams: z.ZodObject<{
31
32
  export type CreateAgentRunnerParams = z.infer<typeof zAgentRunnerParams>;
32
33
  export interface SessionMessage {
33
34
  role: "user" | "assistant";
34
- content: string;
35
+ content: ContentBlock[];
35
36
  timestamp: string;
36
37
  }
37
38
  export type InvokeRequest = Omit<PromptRequest, "_meta"> & {
@@ -145,7 +145,11 @@ export class LangchainAgent {
145
145
  const historyMessages = req.sessionMessages.slice(0, -1);
146
146
  messages = historyMessages.map((msg) => ({
147
147
  type: msg.role === "user" ? "human" : "ai",
148
- content: msg.content,
148
+ // Extract text from content blocks (ignore tool call blocks for LLM context)
149
+ content: msg.content
150
+ .filter((block) => block.type === "text")
151
+ .map((block) => block.text)
152
+ .join(""),
149
153
  }));
150
154
  // Add the current prompt as the final human message
151
155
  const currentPromptText = req.prompt
@@ -301,7 +305,6 @@ export class LangchainAgent {
301
305
  text: aiMessage.content,
302
306
  },
303
307
  };
304
- console.log("DEBUG agent: yielding message (string content):", JSON.stringify(msgToYield));
305
308
  yield msgToYield;
306
309
  }
307
310
  else if (Array.isArray(aiMessage.content)) {
@@ -325,7 +328,6 @@ export class LangchainAgent {
325
328
  text: part.text,
326
329
  },
327
330
  };
328
- console.log("DEBUG agent: yielding message (array content):", JSON.stringify(msgToYield));
329
331
  yield msgToYield;
330
332
  }
331
333
  else if (part.type === "tool_use") {