@poncho-ai/harness 0.17.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +5 -5
- package/CHANGELOG.md +28 -0
- package/dist/index.d.ts +8 -1
- package/dist/index.js +149 -69
- package/package.json +2 -2
- package/src/config.ts +3 -0
- package/src/default-tools.ts +51 -1
- package/src/harness.ts +89 -66
- package/src/mcp.ts +11 -1
- package/src/state.ts +18 -10
- package/test/mcp.test.ts +83 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @poncho-ai/harness@0.
|
|
2
|
+
> @poncho-ai/harness@0.19.0 build /home/runner/work/poncho-ai/poncho-ai/packages/harness
|
|
3
3
|
> tsup src/index.ts --format esm --dts
|
|
4
4
|
|
|
5
5
|
[34mCLI[39m Building entry: src/index.ts
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
[34mCLI[39m tsup v8.5.1
|
|
8
8
|
[34mCLI[39m Target: es2022
|
|
9
9
|
[34mESM[39m Build start
|
|
10
|
-
[32mESM[39m [1mdist/index.js [22m[
|
|
11
|
-
[32mESM[39m ⚡️ Build success in
|
|
10
|
+
[32mESM[39m [1mdist/index.js [22m[32m198.85 KB[39m
|
|
11
|
+
[32mESM[39m ⚡️ Build success in 137ms
|
|
12
12
|
[34mDTS[39m Build start
|
|
13
|
-
[32mDTS[39m ⚡️ Build success in
|
|
14
|
-
[32mDTS[39m [1mdist/index.d.ts [22m[
|
|
13
|
+
[32mDTS[39m ⚡️ Build success in 7145ms
|
|
14
|
+
[32mDTS[39m [1mdist/index.d.ts [22m[32m24.33 KB[39m
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# @poncho-ai/harness
|
|
2
2
|
|
|
3
|
+
## 0.19.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [`075b9ac`](https://github.com/cesr/poncho-ai/commit/075b9ac3556847af913bf2b58f030575c3b99852) Thanks [@cesr](https://github.com/cesr)! - Batch tool approvals, fix serverless session persistence and adapter init
|
|
8
|
+
- Batch tool approvals: all approval-requiring tool calls in a single step are now collected and presented together instead of one at a time.
|
|
9
|
+
- Fix messaging adapter route registration: routes are only registered after successful initialization, preventing "Adapter not initialised" errors on Vercel.
|
|
10
|
+
- Add stateless signed-cookie sessions so web UI auth survives serverless cold starts.
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- Updated dependencies [[`075b9ac`](https://github.com/cesr/poncho-ai/commit/075b9ac3556847af913bf2b58f030575c3b99852)]:
|
|
15
|
+
- @poncho-ai/sdk@1.4.0
|
|
16
|
+
|
|
17
|
+
## 0.18.0
|
|
18
|
+
|
|
19
|
+
### Minor Changes
|
|
20
|
+
|
|
21
|
+
- [`cd6ccd7`](https://github.com/cesr/poncho-ai/commit/cd6ccd7846e16fbaf17167617666796320ec29ce) Thanks [@cesr](https://github.com/cesr)! - Add MCP custom headers support, tool:generating streaming feedback, and cross-owner subagent recovery
|
|
22
|
+
- **MCP custom headers**: `poncho mcp add --header "Name: value"` and `headers` config field let servers like Arcade receive extra HTTP headers alongside bearer auth.
|
|
23
|
+
- **tool:generating event**: the harness now emits `tool:generating` events when the model begins writing tool-call arguments, so the web UI shows real-time "preparing <tool>" feedback instead of appearing stuck during large tool calls.
|
|
24
|
+
- **Subagent recovery**: `list`/`listSummaries` accept optional `ownerId` so stale-subagent recovery on server restart scans across all owners.
|
|
25
|
+
|
|
26
|
+
### Patch Changes
|
|
27
|
+
|
|
28
|
+
- Updated dependencies [[`cd6ccd7`](https://github.com/cesr/poncho-ai/commit/cd6ccd7846e16fbaf17167617666796320ec29ce)]:
|
|
29
|
+
- @poncho-ai/sdk@1.3.0
|
|
30
|
+
|
|
3
31
|
## 0.17.0
|
|
4
32
|
|
|
5
33
|
### Minor Changes
|
package/dist/index.d.ts
CHANGED
|
@@ -94,6 +94,7 @@ interface Conversation {
|
|
|
94
94
|
name: string;
|
|
95
95
|
input: Record<string, unknown>;
|
|
96
96
|
}>;
|
|
97
|
+
decision?: "approved" | "denied";
|
|
97
98
|
}>;
|
|
98
99
|
ownerId: string;
|
|
99
100
|
tenantId: string | null;
|
|
@@ -205,6 +206,7 @@ interface RemoteMcpServerConfig {
|
|
|
205
206
|
type: "bearer";
|
|
206
207
|
tokenEnv?: string;
|
|
207
208
|
};
|
|
209
|
+
headers?: Record<string, string>;
|
|
208
210
|
timeoutMs?: number;
|
|
209
211
|
reconnectAttempts?: number;
|
|
210
212
|
reconnectDelayMs?: number;
|
|
@@ -267,6 +269,8 @@ type BuiltInToolToggles = {
|
|
|
267
269
|
list_directory?: boolean;
|
|
268
270
|
read_file?: boolean;
|
|
269
271
|
write_file?: boolean;
|
|
272
|
+
delete_file?: boolean;
|
|
273
|
+
delete_directory?: boolean;
|
|
270
274
|
};
|
|
271
275
|
interface MessagingChannelConfig {
|
|
272
276
|
platform: "slack" | "resend";
|
|
@@ -275,6 +279,7 @@ interface MessagingChannelConfig {
|
|
|
275
279
|
apiKeyEnv?: string;
|
|
276
280
|
webhookSecretEnv?: string;
|
|
277
281
|
fromEnv?: string;
|
|
282
|
+
replyToEnv?: string;
|
|
278
283
|
allowedSenders?: string[];
|
|
279
284
|
mode?: "auto-reply" | "tool";
|
|
280
285
|
allowedRecipients?: string[];
|
|
@@ -358,6 +363,8 @@ declare const loadPonchoConfig: (workingDir: string) => Promise<PonchoConfig | u
|
|
|
358
363
|
|
|
359
364
|
declare const createDefaultTools: (workingDir: string) => ToolDefinition[];
|
|
360
365
|
declare const createWriteTool: (workingDir: string) => ToolDefinition;
|
|
366
|
+
declare const createDeleteTool: (workingDir: string) => ToolDefinition;
|
|
367
|
+
declare const createDeleteDirectoryTool: (workingDir: string) => ToolDefinition;
|
|
361
368
|
|
|
362
369
|
declare const PONCHO_UPLOAD_SCHEME = "poncho-upload://";
|
|
363
370
|
interface UploadStore {
|
|
@@ -680,4 +687,4 @@ declare class TelemetryEmitter {
|
|
|
680
687
|
|
|
681
688
|
declare const createSubagentTools: (manager: SubagentManager, getConversationId: () => string | undefined, getOwnerId: () => string) => ToolDefinition[];
|
|
682
689
|
|
|
683
|
-
export { type AgentFrontmatter, AgentHarness, type AgentIdentity, type AgentLimitsConfig, type AgentModelConfig, type BuiltInToolToggles, type Conversation, type ConversationState, type ConversationStore, type ConversationSummary, type CronJobConfig, type HarnessOptions, type HarnessRunOutput, InMemoryConversationStore, InMemoryStateStore, LatitudeCapture, type LatitudeCaptureConfig, LocalMcpBridge, LocalUploadStore, type MainMemory, type McpConfig, type MemoryConfig, type MemoryStore, type MessagingChannelConfig, type ModelProviderFactory, PONCHO_UPLOAD_SCHEME, type ParsedAgent, type PonchoConfig, type ProviderConfig, type RemoteMcpServerConfig, type RuntimeRenderContext, S3UploadStore, STORAGE_SCHEMA_VERSION, type SkillContextEntry, type SkillMetadata, type StateConfig, type StateProviderName, type StateStore, type StorageConfig, type SubagentManager, type SubagentResult, type SubagentSummary, type TelemetryConfig, TelemetryEmitter, type ToolAccess, type ToolCall, ToolDispatcher, type ToolExecutionResult, type UploadStore, type UploadsConfig, VercelBlobUploadStore, buildAgentDirectoryName, buildSkillContextWindow, createConversationStore, createDefaultTools, createMemoryStore, createMemoryTools, createModelProvider, createSkillTools, createStateStore, createSubagentTools, createUploadStore, createWriteTool, deriveUploadKey, ensureAgentIdentity, generateAgentId, getAgentStoreDirectory, getModelContextWindow, getPonchoStoreRoot, jsonSchemaToZod, loadPonchoConfig, loadSkillContext, loadSkillInstructions, loadSkillMetadata, normalizeScriptPolicyPath, parseAgentFile, parseAgentMarkdown, readSkillResource, renderAgentPrompt, resolveAgentIdentity, resolveMemoryConfig, resolveSkillDirs, resolveStateConfig, slugifyStorageComponent };
|
|
690
|
+
export { type AgentFrontmatter, AgentHarness, type AgentIdentity, type AgentLimitsConfig, type AgentModelConfig, type BuiltInToolToggles, type Conversation, type ConversationState, type ConversationStore, type ConversationSummary, type CronJobConfig, type HarnessOptions, type HarnessRunOutput, InMemoryConversationStore, InMemoryStateStore, LatitudeCapture, type LatitudeCaptureConfig, LocalMcpBridge, LocalUploadStore, type MainMemory, type McpConfig, type MemoryConfig, type MemoryStore, type MessagingChannelConfig, type ModelProviderFactory, PONCHO_UPLOAD_SCHEME, type ParsedAgent, type PonchoConfig, type ProviderConfig, type RemoteMcpServerConfig, type RuntimeRenderContext, S3UploadStore, STORAGE_SCHEMA_VERSION, type SkillContextEntry, type SkillMetadata, type StateConfig, type StateProviderName, type StateStore, type StorageConfig, type SubagentManager, type SubagentResult, type SubagentSummary, type TelemetryConfig, TelemetryEmitter, type ToolAccess, type ToolCall, ToolDispatcher, type ToolExecutionResult, type UploadStore, type UploadsConfig, VercelBlobUploadStore, buildAgentDirectoryName, buildSkillContextWindow, createConversationStore, createDefaultTools, createDeleteDirectoryTool, createDeleteTool, createMemoryStore, createMemoryTools, createModelProvider, createSkillTools, createStateStore, createSubagentTools, createUploadStore, createWriteTool, deriveUploadKey, ensureAgentIdentity, generateAgentId, getAgentStoreDirectory, getModelContextWindow, getPonchoStoreRoot, jsonSchemaToZod, loadPonchoConfig, loadSkillContext, loadSkillInstructions, loadSkillMetadata, normalizeScriptPolicyPath, parseAgentFile, parseAgentMarkdown, readSkillResource, renderAgentPrompt, resolveAgentIdentity, resolveMemoryConfig, resolveSkillDirs, resolveStateConfig, slugifyStorageComponent };
|
package/dist/index.js
CHANGED
|
@@ -378,7 +378,7 @@ var loadPonchoConfig = async (workingDir) => {
|
|
|
378
378
|
};
|
|
379
379
|
|
|
380
380
|
// src/default-tools.ts
|
|
381
|
-
import { mkdir, readdir, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
|
|
381
|
+
import { mkdir, readdir, readFile as readFile3, rm, unlink, writeFile as writeFile2 } from "fs/promises";
|
|
382
382
|
import { dirname, resolve as resolve4, sep } from "path";
|
|
383
383
|
import { defineTool } from "@poncho-ai/sdk";
|
|
384
384
|
var resolveSafePath = (workingDir, inputPath) => {
|
|
@@ -463,6 +463,52 @@ var createWriteTool = (workingDir) => defineTool({
|
|
|
463
463
|
return { path, written: true };
|
|
464
464
|
}
|
|
465
465
|
});
|
|
466
|
+
var createDeleteTool = (workingDir) => defineTool({
|
|
467
|
+
name: "delete_file",
|
|
468
|
+
description: "Delete a file at a path inside the working directory",
|
|
469
|
+
inputSchema: {
|
|
470
|
+
type: "object",
|
|
471
|
+
properties: {
|
|
472
|
+
path: {
|
|
473
|
+
type: "string",
|
|
474
|
+
description: "File path relative to working directory"
|
|
475
|
+
}
|
|
476
|
+
},
|
|
477
|
+
required: ["path"],
|
|
478
|
+
additionalProperties: false
|
|
479
|
+
},
|
|
480
|
+
handler: async (input) => {
|
|
481
|
+
const path = typeof input.path === "string" ? input.path : "";
|
|
482
|
+
const resolved = resolveSafePath(workingDir, path);
|
|
483
|
+
await unlink(resolved);
|
|
484
|
+
return { path, deleted: true };
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
var createDeleteDirectoryTool = (workingDir) => defineTool({
|
|
488
|
+
name: "delete_directory",
|
|
489
|
+
description: "Recursively delete a directory and all its contents inside the working directory",
|
|
490
|
+
inputSchema: {
|
|
491
|
+
type: "object",
|
|
492
|
+
properties: {
|
|
493
|
+
path: {
|
|
494
|
+
type: "string",
|
|
495
|
+
description: "Directory path relative to working directory"
|
|
496
|
+
}
|
|
497
|
+
},
|
|
498
|
+
required: ["path"],
|
|
499
|
+
additionalProperties: false
|
|
500
|
+
},
|
|
501
|
+
handler: async (input) => {
|
|
502
|
+
const path = typeof input.path === "string" ? input.path : "";
|
|
503
|
+
if (!path) throw new Error("Path must not be empty.");
|
|
504
|
+
const resolved = resolveSafePath(workingDir, path);
|
|
505
|
+
if (resolved === resolve4(workingDir)) {
|
|
506
|
+
throw new Error("Cannot delete the working directory root.");
|
|
507
|
+
}
|
|
508
|
+
await rm(resolved, { recursive: true });
|
|
509
|
+
return { path, deleted: true };
|
|
510
|
+
}
|
|
511
|
+
});
|
|
466
512
|
|
|
467
513
|
// src/harness.ts
|
|
468
514
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
@@ -470,7 +516,7 @@ import { getTextContent as getTextContent2 } from "@poncho-ai/sdk";
|
|
|
470
516
|
|
|
471
517
|
// src/upload-store.ts
|
|
472
518
|
import { createHash as createHash2 } from "crypto";
|
|
473
|
-
import { mkdir as mkdir2, readFile as readFile4, writeFile as writeFile3, rm } from "fs/promises";
|
|
519
|
+
import { mkdir as mkdir2, readFile as readFile4, writeFile as writeFile3, rm as rm2 } from "fs/promises";
|
|
474
520
|
import { createRequire } from "module";
|
|
475
521
|
import { resolve as resolve5 } from "path";
|
|
476
522
|
var tryImport = async (mod, workingDir) => {
|
|
@@ -577,7 +623,7 @@ var LocalUploadStore = class {
|
|
|
577
623
|
}
|
|
578
624
|
async delete(urlOrKey) {
|
|
579
625
|
const key = urlOrKey.startsWith(PONCHO_UPLOAD_SCHEME) ? urlOrKey.slice(PONCHO_UPLOAD_SCHEME.length) : urlOrKey;
|
|
580
|
-
await
|
|
626
|
+
await rm2(resolve5(this.uploadsDir, key), { force: true });
|
|
581
627
|
}
|
|
582
628
|
};
|
|
583
629
|
var VercelBlobUploadStore = class {
|
|
@@ -1306,16 +1352,19 @@ var StreamableHttpMcpRpcClient = class {
|
|
|
1306
1352
|
endpoint;
|
|
1307
1353
|
timeoutMs;
|
|
1308
1354
|
bearerToken;
|
|
1355
|
+
customHeaders;
|
|
1309
1356
|
idCounter = 1;
|
|
1310
1357
|
initialized = false;
|
|
1311
1358
|
sessionId;
|
|
1312
|
-
constructor(endpoint, timeoutMs = 1e4, bearerToken) {
|
|
1359
|
+
constructor(endpoint, timeoutMs = 1e4, bearerToken, customHeaders) {
|
|
1313
1360
|
this.endpoint = endpoint;
|
|
1314
1361
|
this.timeoutMs = timeoutMs;
|
|
1315
1362
|
this.bearerToken = bearerToken;
|
|
1363
|
+
this.customHeaders = customHeaders ?? {};
|
|
1316
1364
|
}
|
|
1317
1365
|
buildHeaders(accept) {
|
|
1318
1366
|
const headers = {
|
|
1367
|
+
...this.customHeaders,
|
|
1319
1368
|
"Content-Type": "application/json",
|
|
1320
1369
|
Accept: accept
|
|
1321
1370
|
};
|
|
@@ -1596,7 +1645,8 @@ var LocalMcpBridge = class {
|
|
|
1596
1645
|
new StreamableHttpMcpRpcClient(
|
|
1597
1646
|
server.url,
|
|
1598
1647
|
server.timeoutMs ?? 1e4,
|
|
1599
|
-
server.auth?.tokenEnv ? process.env[server.auth.tokenEnv] : void 0
|
|
1648
|
+
server.auth?.tokenEnv ? process.env[server.auth.tokenEnv] : void 0,
|
|
1649
|
+
server.headers
|
|
1600
1650
|
)
|
|
1601
1651
|
);
|
|
1602
1652
|
}
|
|
@@ -2904,6 +2954,7 @@ The agent will respond in Slack threads when @mentioned. Each Slack thread maps
|
|
|
2904
2954
|
RESEND_API_KEY=re_...
|
|
2905
2955
|
RESEND_WEBHOOK_SECRET=whsec_...
|
|
2906
2956
|
RESEND_FROM=Agent <agent@yourdomain.com>
|
|
2957
|
+
RESEND_REPLY_TO=support@yourdomain.com # optional
|
|
2907
2958
|
\`\`\`
|
|
2908
2959
|
5. Add to \`poncho.config.js\`:
|
|
2909
2960
|
\`\`\`javascript
|
|
@@ -3076,7 +3127,7 @@ var AgentHarness = class {
|
|
|
3076
3127
|
isToolEnabled(name) {
|
|
3077
3128
|
const access3 = this.resolveToolAccess(name);
|
|
3078
3129
|
if (access3 === false) return false;
|
|
3079
|
-
if (name === "write_file") {
|
|
3130
|
+
if (name === "write_file" || name === "delete_file" || name === "delete_directory") {
|
|
3080
3131
|
return this.shouldEnableWriteTool();
|
|
3081
3132
|
}
|
|
3082
3133
|
return true;
|
|
@@ -3119,6 +3170,12 @@ var AgentHarness = class {
|
|
|
3119
3170
|
if (this.isToolEnabled("write_file")) {
|
|
3120
3171
|
this.registerIfMissing(createWriteTool(this.workingDir));
|
|
3121
3172
|
}
|
|
3173
|
+
if (this.isToolEnabled("delete_file")) {
|
|
3174
|
+
this.registerIfMissing(createDeleteTool(this.workingDir));
|
|
3175
|
+
}
|
|
3176
|
+
if (this.isToolEnabled("delete_directory")) {
|
|
3177
|
+
this.registerIfMissing(createDeleteDirectoryTool(this.workingDir));
|
|
3178
|
+
}
|
|
3122
3179
|
}
|
|
3123
3180
|
shouldEnableWriteTool() {
|
|
3124
3181
|
const override = process.env.PONCHO_FS_WRITE?.toLowerCase();
|
|
@@ -4063,7 +4120,7 @@ ${textContent}` };
|
|
|
4063
4120
|
let chunkCount = 0;
|
|
4064
4121
|
const hasRunTimeout = timeoutMs > 0;
|
|
4065
4122
|
const streamDeadline = hasRunTimeout ? start + timeoutMs : 0;
|
|
4066
|
-
const
|
|
4123
|
+
const fullStreamIterator = result.fullStream[Symbol.asyncIterator]();
|
|
4067
4124
|
try {
|
|
4068
4125
|
while (true) {
|
|
4069
4126
|
if (isCancelled()) {
|
|
@@ -4089,20 +4146,20 @@ ${textContent}` };
|
|
|
4089
4146
|
}
|
|
4090
4147
|
const remaining = hasRunTimeout ? streamDeadline - now() : Infinity;
|
|
4091
4148
|
const timeout = chunkCount === 0 ? Math.min(remaining, FIRST_CHUNK_TIMEOUT_MS) : hasRunTimeout ? remaining : 0;
|
|
4092
|
-
let
|
|
4149
|
+
let nextPart;
|
|
4093
4150
|
if (timeout <= 0 && chunkCount > 0) {
|
|
4094
|
-
|
|
4151
|
+
nextPart = await fullStreamIterator.next();
|
|
4095
4152
|
} else {
|
|
4096
4153
|
let timer;
|
|
4097
|
-
|
|
4098
|
-
|
|
4154
|
+
nextPart = await Promise.race([
|
|
4155
|
+
fullStreamIterator.next(),
|
|
4099
4156
|
new Promise((resolve10) => {
|
|
4100
4157
|
timer = setTimeout(() => resolve10(null), timeout);
|
|
4101
4158
|
})
|
|
4102
4159
|
]);
|
|
4103
4160
|
clearTimeout(timer);
|
|
4104
4161
|
}
|
|
4105
|
-
if (
|
|
4162
|
+
if (nextPart === null) {
|
|
4106
4163
|
const isFirstChunk = chunkCount === 0;
|
|
4107
4164
|
console.error(
|
|
4108
4165
|
`[poncho][harness] Stream timeout waiting for ${isFirstChunk ? "first" : "next"} chunk: model="${modelName}", step=${step}, chunks=${chunkCount}, elapsed=${now() - start}ms`
|
|
@@ -4120,13 +4177,19 @@ ${textContent}` };
|
|
|
4120
4177
|
});
|
|
4121
4178
|
return;
|
|
4122
4179
|
}
|
|
4123
|
-
if (
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4180
|
+
if (nextPart.done) break;
|
|
4181
|
+
const part = nextPart.value;
|
|
4182
|
+
if (part.type === "text-delta") {
|
|
4183
|
+
chunkCount += 1;
|
|
4184
|
+
fullText += part.text;
|
|
4185
|
+
yield pushEvent({ type: "model:chunk", content: part.text });
|
|
4186
|
+
} else if (part.type === "tool-input-start") {
|
|
4187
|
+
chunkCount += 1;
|
|
4188
|
+
yield pushEvent({ type: "tool:generating", tool: part.toolName, toolCallId: part.id });
|
|
4189
|
+
}
|
|
4127
4190
|
}
|
|
4128
4191
|
} finally {
|
|
4129
|
-
|
|
4192
|
+
fullStreamIterator.return?.(void 0)?.catch?.(() => {
|
|
4130
4193
|
});
|
|
4131
4194
|
}
|
|
4132
4195
|
if (isCancelled()) {
|
|
@@ -4232,6 +4295,7 @@ ${textContent}` };
|
|
|
4232
4295
|
const toolResultsForModel = [];
|
|
4233
4296
|
const richToolResults = [];
|
|
4234
4297
|
const approvedCalls = [];
|
|
4298
|
+
const approvalNeeded = [];
|
|
4235
4299
|
for (const call of toolCalls) {
|
|
4236
4300
|
if (isCancelled()) {
|
|
4237
4301
|
yield emitCancellation();
|
|
@@ -4239,52 +4303,60 @@ ${textContent}` };
|
|
|
4239
4303
|
}
|
|
4240
4304
|
const runtimeToolName = exposedToolNames.get(call.name) ?? call.name;
|
|
4241
4305
|
yield pushEvent({ type: "tool:started", tool: runtimeToolName, input: call.input });
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
yield pushEvent({
|
|
4249
|
-
type: "tool:approval:required",
|
|
4250
|
-
tool: runtimeToolName,
|
|
4251
|
-
input: call.input,
|
|
4252
|
-
approvalId
|
|
4306
|
+
if (this.requiresApprovalForToolCall(runtimeToolName, call.input)) {
|
|
4307
|
+
approvalNeeded.push({
|
|
4308
|
+
approvalId: `approval_${randomUUID3()}`,
|
|
4309
|
+
id: call.id,
|
|
4310
|
+
name: runtimeToolName,
|
|
4311
|
+
input: call.input
|
|
4253
4312
|
});
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
input: tc.input
|
|
4260
|
-
}))
|
|
4313
|
+
} else {
|
|
4314
|
+
approvedCalls.push({
|
|
4315
|
+
id: call.id,
|
|
4316
|
+
name: runtimeToolName,
|
|
4317
|
+
input: call.input
|
|
4261
4318
|
});
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
};
|
|
4267
|
-
const deltaMessages = [...messages.slice(inputMessageCount), assistantMsg];
|
|
4319
|
+
}
|
|
4320
|
+
}
|
|
4321
|
+
if (approvalNeeded.length > 0) {
|
|
4322
|
+
for (const an of approvalNeeded) {
|
|
4268
4323
|
yield pushEvent({
|
|
4269
|
-
type: "tool:approval:
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
input: call.input,
|
|
4274
|
-
checkpointMessages: deltaMessages,
|
|
4275
|
-
pendingToolCalls: toolCalls.map((tc) => ({
|
|
4276
|
-
id: tc.id,
|
|
4277
|
-
name: exposedToolNames.get(tc.name) ?? tc.name,
|
|
4278
|
-
input: tc.input
|
|
4279
|
-
}))
|
|
4324
|
+
type: "tool:approval:required",
|
|
4325
|
+
tool: an.name,
|
|
4326
|
+
input: an.input,
|
|
4327
|
+
approvalId: an.approvalId
|
|
4280
4328
|
});
|
|
4281
|
-
return;
|
|
4282
4329
|
}
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4330
|
+
const assistantContent2 = JSON.stringify({
|
|
4331
|
+
text: fullText,
|
|
4332
|
+
tool_calls: toolCalls.map((tc) => ({
|
|
4333
|
+
id: tc.id,
|
|
4334
|
+
name: exposedToolNames.get(tc.name) ?? tc.name,
|
|
4335
|
+
input: tc.input
|
|
4336
|
+
}))
|
|
4337
|
+
});
|
|
4338
|
+
const assistantMsg = {
|
|
4339
|
+
role: "assistant",
|
|
4340
|
+
content: assistantContent2,
|
|
4341
|
+
metadata: { timestamp: now(), id: randomUUID3(), step }
|
|
4342
|
+
};
|
|
4343
|
+
const deltaMessages = [...messages.slice(inputMessageCount), assistantMsg];
|
|
4344
|
+
yield pushEvent({
|
|
4345
|
+
type: "tool:approval:checkpoint",
|
|
4346
|
+
approvals: approvalNeeded.map((an) => ({
|
|
4347
|
+
approvalId: an.approvalId,
|
|
4348
|
+
tool: an.name,
|
|
4349
|
+
toolCallId: an.id,
|
|
4350
|
+
input: an.input
|
|
4351
|
+
})),
|
|
4352
|
+
checkpointMessages: deltaMessages,
|
|
4353
|
+
pendingToolCalls: toolCalls.map((tc) => ({
|
|
4354
|
+
id: tc.id,
|
|
4355
|
+
name: exposedToolNames.get(tc.name) ?? tc.name,
|
|
4356
|
+
input: tc.input
|
|
4357
|
+
}))
|
|
4287
4358
|
});
|
|
4359
|
+
return;
|
|
4288
4360
|
}
|
|
4289
4361
|
const batchStart = now();
|
|
4290
4362
|
if (isCancelled()) {
|
|
@@ -4578,7 +4650,7 @@ var LatitudeCapture = class {
|
|
|
4578
4650
|
|
|
4579
4651
|
// src/state.ts
|
|
4580
4652
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
4581
|
-
import { mkdir as mkdir4, readFile as readFile7, readdir as readdir4, rename as rename2, rm as
|
|
4653
|
+
import { mkdir as mkdir4, readFile as readFile7, readdir as readdir4, rename as rename2, rm as rm3, writeFile as writeFile5 } from "fs/promises";
|
|
4582
4654
|
import { dirname as dirname4, resolve as resolve9 } from "path";
|
|
4583
4655
|
var DEFAULT_OWNER = "local-owner";
|
|
4584
4656
|
var LOCAL_STATE_FILE = "state.json";
|
|
@@ -4693,13 +4765,13 @@ var InMemoryConversationStore = class {
|
|
|
4693
4765
|
}
|
|
4694
4766
|
}
|
|
4695
4767
|
}
|
|
4696
|
-
async list(ownerId
|
|
4768
|
+
async list(ownerId) {
|
|
4697
4769
|
this.purgeExpired();
|
|
4698
|
-
return Array.from(this.conversations.values()).filter((conversation) => conversation.ownerId === ownerId).sort((a, b) => b.updatedAt - a.updatedAt);
|
|
4770
|
+
return Array.from(this.conversations.values()).filter((conversation) => !ownerId || conversation.ownerId === ownerId).sort((a, b) => b.updatedAt - a.updatedAt);
|
|
4699
4771
|
}
|
|
4700
|
-
async listSummaries(ownerId
|
|
4772
|
+
async listSummaries(ownerId) {
|
|
4701
4773
|
this.purgeExpired();
|
|
4702
|
-
return Array.from(this.conversations.values()).filter((c) => c.ownerId === ownerId).sort((a, b) => b.updatedAt - a.updatedAt).map((c) => ({
|
|
4774
|
+
return Array.from(this.conversations.values()).filter((c) => !ownerId || c.ownerId === ownerId).sort((a, b) => b.updatedAt - a.updatedAt).map((c) => ({
|
|
4703
4775
|
conversationId: c.conversationId,
|
|
4704
4776
|
title: c.title,
|
|
4705
4777
|
updatedAt: c.updatedAt,
|
|
@@ -4875,9 +4947,9 @@ var FileConversationStore = class {
|
|
|
4875
4947
|
});
|
|
4876
4948
|
await this.writing;
|
|
4877
4949
|
}
|
|
4878
|
-
async list(ownerId
|
|
4950
|
+
async list(ownerId) {
|
|
4879
4951
|
await this.ensureLoaded();
|
|
4880
|
-
const summaries = Array.from(this.conversations.values()).filter((conversation) => conversation.ownerId === ownerId).sort((a, b) => b.updatedAt - a.updatedAt);
|
|
4952
|
+
const summaries = Array.from(this.conversations.values()).filter((conversation) => !ownerId || conversation.ownerId === ownerId).sort((a, b) => b.updatedAt - a.updatedAt);
|
|
4881
4953
|
const conversations = [];
|
|
4882
4954
|
for (const summary of summaries) {
|
|
4883
4955
|
const loaded = await this.readConversationFile(summary.fileName);
|
|
@@ -4887,9 +4959,9 @@ var FileConversationStore = class {
|
|
|
4887
4959
|
}
|
|
4888
4960
|
return conversations;
|
|
4889
4961
|
}
|
|
4890
|
-
async listSummaries(ownerId
|
|
4962
|
+
async listSummaries(ownerId) {
|
|
4891
4963
|
await this.ensureLoaded();
|
|
4892
|
-
return Array.from(this.conversations.values()).filter((c) => c.ownerId === ownerId).sort((a, b) => b.updatedAt - a.updatedAt).map((c) => ({
|
|
4964
|
+
return Array.from(this.conversations.values()).filter((c) => !ownerId || c.ownerId === ownerId).sort((a, b) => b.updatedAt - a.updatedAt).map((c) => ({
|
|
4893
4965
|
conversationId: c.conversationId,
|
|
4894
4966
|
title: c.title,
|
|
4895
4967
|
updatedAt: c.updatedAt,
|
|
@@ -4953,7 +5025,7 @@ var FileConversationStore = class {
|
|
|
4953
5025
|
if (removed) {
|
|
4954
5026
|
this.writing = this.writing.then(async () => {
|
|
4955
5027
|
if (existing) {
|
|
4956
|
-
await
|
|
5028
|
+
await rm3(resolve9(conversationsDir, existing.fileName), { force: true });
|
|
4957
5029
|
}
|
|
4958
5030
|
await this.writeIndex();
|
|
4959
5031
|
});
|
|
@@ -5115,11 +5187,14 @@ var KeyValueConversationStoreBase = class {
|
|
|
5115
5187
|
return void 0;
|
|
5116
5188
|
}
|
|
5117
5189
|
}
|
|
5118
|
-
async list(ownerId
|
|
5190
|
+
async list(ownerId) {
|
|
5119
5191
|
const kv = await this.client();
|
|
5120
5192
|
if (!kv) {
|
|
5121
5193
|
return await this.memoryFallback.list(ownerId);
|
|
5122
5194
|
}
|
|
5195
|
+
if (!ownerId) {
|
|
5196
|
+
return [];
|
|
5197
|
+
}
|
|
5123
5198
|
const ids = await this.getOwnerConversationIds(ownerId);
|
|
5124
5199
|
const conversations = [];
|
|
5125
5200
|
for (const id of ids) {
|
|
@@ -5134,11 +5209,14 @@ var KeyValueConversationStoreBase = class {
|
|
|
5134
5209
|
}
|
|
5135
5210
|
return conversations.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
5136
5211
|
}
|
|
5137
|
-
async listSummaries(ownerId
|
|
5212
|
+
async listSummaries(ownerId) {
|
|
5138
5213
|
const kv = await this.client();
|
|
5139
5214
|
if (!kv) {
|
|
5140
5215
|
return await this.memoryFallback.listSummaries(ownerId);
|
|
5141
5216
|
}
|
|
5217
|
+
if (!ownerId) {
|
|
5218
|
+
return [];
|
|
5219
|
+
}
|
|
5142
5220
|
const ids = await this.getOwnerConversationIds(ownerId);
|
|
5143
5221
|
const summaries = [];
|
|
5144
5222
|
for (const id of ids) {
|
|
@@ -5695,6 +5773,8 @@ export {
|
|
|
5695
5773
|
buildSkillContextWindow,
|
|
5696
5774
|
createConversationStore,
|
|
5697
5775
|
createDefaultTools,
|
|
5776
|
+
createDeleteDirectoryTool,
|
|
5777
|
+
createDeleteTool,
|
|
5698
5778
|
createMemoryStore,
|
|
5699
5779
|
createMemoryTools,
|
|
5700
5780
|
createModelProvider,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@poncho-ai/harness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"description": "Agent execution runtime - conversation loop, tool dispatch, streaming",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"redis": "^5.10.0",
|
|
32
32
|
"yaml": "^2.4.0",
|
|
33
33
|
"zod": "^3.22.0",
|
|
34
|
-
"@poncho-ai/sdk": "1.
|
|
34
|
+
"@poncho-ai/sdk": "1.4.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/mustache": "^4.2.6",
|
package/src/config.ts
CHANGED
|
@@ -39,6 +39,8 @@ export type BuiltInToolToggles = {
|
|
|
39
39
|
list_directory?: boolean;
|
|
40
40
|
read_file?: boolean;
|
|
41
41
|
write_file?: boolean;
|
|
42
|
+
delete_file?: boolean;
|
|
43
|
+
delete_directory?: boolean;
|
|
42
44
|
};
|
|
43
45
|
|
|
44
46
|
export interface MessagingChannelConfig {
|
|
@@ -50,6 +52,7 @@ export interface MessagingChannelConfig {
|
|
|
50
52
|
apiKeyEnv?: string;
|
|
51
53
|
webhookSecretEnv?: string;
|
|
52
54
|
fromEnv?: string;
|
|
55
|
+
replyToEnv?: string;
|
|
53
56
|
allowedSenders?: string[];
|
|
54
57
|
mode?: "auto-reply" | "tool";
|
|
55
58
|
allowedRecipients?: string[];
|
package/src/default-tools.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
1
|
+
import { mkdir, readdir, readFile, rm, unlink, writeFile } from "node:fs/promises";
|
|
2
2
|
import { dirname, resolve, sep } from "node:path";
|
|
3
3
|
import { defineTool, type ToolDefinition } from "@poncho-ai/sdk";
|
|
4
4
|
|
|
@@ -87,3 +87,53 @@ export const createWriteTool = (workingDir: string): ToolDefinition =>
|
|
|
87
87
|
return { path, written: true };
|
|
88
88
|
},
|
|
89
89
|
});
|
|
90
|
+
|
|
91
|
+
export const createDeleteTool = (workingDir: string): ToolDefinition =>
|
|
92
|
+
defineTool({
|
|
93
|
+
name: "delete_file",
|
|
94
|
+
description: "Delete a file at a path inside the working directory",
|
|
95
|
+
inputSchema: {
|
|
96
|
+
type: "object",
|
|
97
|
+
properties: {
|
|
98
|
+
path: {
|
|
99
|
+
type: "string",
|
|
100
|
+
description: "File path relative to working directory",
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
required: ["path"],
|
|
104
|
+
additionalProperties: false,
|
|
105
|
+
},
|
|
106
|
+
handler: async (input) => {
|
|
107
|
+
const path = typeof input.path === "string" ? input.path : "";
|
|
108
|
+
const resolved = resolveSafePath(workingDir, path);
|
|
109
|
+
await unlink(resolved);
|
|
110
|
+
return { path, deleted: true };
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
export const createDeleteDirectoryTool = (workingDir: string): ToolDefinition =>
|
|
115
|
+
defineTool({
|
|
116
|
+
name: "delete_directory",
|
|
117
|
+
description: "Recursively delete a directory and all its contents inside the working directory",
|
|
118
|
+
inputSchema: {
|
|
119
|
+
type: "object",
|
|
120
|
+
properties: {
|
|
121
|
+
path: {
|
|
122
|
+
type: "string",
|
|
123
|
+
description: "Directory path relative to working directory",
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
required: ["path"],
|
|
127
|
+
additionalProperties: false,
|
|
128
|
+
},
|
|
129
|
+
handler: async (input) => {
|
|
130
|
+
const path = typeof input.path === "string" ? input.path : "";
|
|
131
|
+
if (!path) throw new Error("Path must not be empty.");
|
|
132
|
+
const resolved = resolveSafePath(workingDir, path);
|
|
133
|
+
if (resolved === resolve(workingDir)) {
|
|
134
|
+
throw new Error("Cannot delete the working directory root.");
|
|
135
|
+
}
|
|
136
|
+
await rm(resolved, { recursive: true });
|
|
137
|
+
return { path, deleted: true };
|
|
138
|
+
},
|
|
139
|
+
});
|
package/src/harness.ts
CHANGED
|
@@ -15,7 +15,7 @@ import type { UploadStore } from "./upload-store.js";
|
|
|
15
15
|
import { PONCHO_UPLOAD_SCHEME, deriveUploadKey } from "./upload-store.js";
|
|
16
16
|
import { parseAgentFile, renderAgentPrompt, type ParsedAgent, type AgentFrontmatter } from "./agent-parser.js";
|
|
17
17
|
import { loadPonchoConfig, resolveMemoryConfig, type PonchoConfig, type ToolAccess, type BuiltInToolToggles } from "./config.js";
|
|
18
|
-
import { createDefaultTools, createWriteTool } from "./default-tools.js";
|
|
18
|
+
import { createDefaultTools, createDeleteDirectoryTool, createDeleteTool, createWriteTool } from "./default-tools.js";
|
|
19
19
|
import {
|
|
20
20
|
createMemoryStore,
|
|
21
21
|
createMemoryTools,
|
|
@@ -324,6 +324,7 @@ The agent will respond in Slack threads when @mentioned. Each Slack thread maps
|
|
|
324
324
|
RESEND_API_KEY=re_...
|
|
325
325
|
RESEND_WEBHOOK_SECRET=whsec_...
|
|
326
326
|
RESEND_FROM=Agent <agent@yourdomain.com>
|
|
327
|
+
RESEND_REPLY_TO=support@yourdomain.com # optional
|
|
327
328
|
\`\`\`
|
|
328
329
|
5. Add to \`poncho.config.js\`:
|
|
329
330
|
\`\`\`javascript
|
|
@@ -526,7 +527,7 @@ export class AgentHarness {
|
|
|
526
527
|
private isToolEnabled(name: string): boolean {
|
|
527
528
|
const access = this.resolveToolAccess(name);
|
|
528
529
|
if (access === false) return false;
|
|
529
|
-
if (name === "write_file") {
|
|
530
|
+
if (name === "write_file" || name === "delete_file" || name === "delete_directory") {
|
|
530
531
|
return this.shouldEnableWriteTool();
|
|
531
532
|
}
|
|
532
533
|
return true;
|
|
@@ -574,6 +575,12 @@ export class AgentHarness {
|
|
|
574
575
|
if (this.isToolEnabled("write_file")) {
|
|
575
576
|
this.registerIfMissing(createWriteTool(this.workingDir));
|
|
576
577
|
}
|
|
578
|
+
if (this.isToolEnabled("delete_file")) {
|
|
579
|
+
this.registerIfMissing(createDeleteTool(this.workingDir));
|
|
580
|
+
}
|
|
581
|
+
if (this.isToolEnabled("delete_directory")) {
|
|
582
|
+
this.registerIfMissing(createDeleteDirectoryTool(this.workingDir));
|
|
583
|
+
}
|
|
577
584
|
}
|
|
578
585
|
|
|
579
586
|
private shouldEnableWriteTool(): boolean {
|
|
@@ -1682,15 +1689,14 @@ ${boundedMainMemory.trim()}`
|
|
|
1682
1689
|
isEnabled: telemetryEnabled && !!this.latitudeTelemetry,
|
|
1683
1690
|
},
|
|
1684
1691
|
});
|
|
1685
|
-
// Stream
|
|
1686
|
-
//
|
|
1687
|
-
//
|
|
1688
|
-
// each next() call against the remaining time budget.
|
|
1692
|
+
// Stream full response — use fullStream to get visibility into
|
|
1693
|
+
// tool-call generation (tool-input-start) in addition to text deltas.
|
|
1694
|
+
// Enforce overall run timeout per part.
|
|
1689
1695
|
let fullText = "";
|
|
1690
1696
|
let chunkCount = 0;
|
|
1691
1697
|
const hasRunTimeout = timeoutMs > 0;
|
|
1692
1698
|
const streamDeadline = hasRunTimeout ? start + timeoutMs : 0;
|
|
1693
|
-
const
|
|
1699
|
+
const fullStreamIterator = result.fullStream[Symbol.asyncIterator]();
|
|
1694
1700
|
try {
|
|
1695
1701
|
while (true) {
|
|
1696
1702
|
if (isCancelled()) {
|
|
@@ -1714,21 +1720,17 @@ ${boundedMainMemory.trim()}`
|
|
|
1714
1720
|
return;
|
|
1715
1721
|
}
|
|
1716
1722
|
}
|
|
1717
|
-
// Use a shorter timeout for the first chunk to detect
|
|
1718
|
-
// non-responsive models quickly instead of waiting minutes.
|
|
1719
|
-
// When no run timeout is set, only the first chunk is time-bounded.
|
|
1720
1723
|
const remaining = hasRunTimeout ? streamDeadline - now() : Infinity;
|
|
1721
1724
|
const timeout = chunkCount === 0
|
|
1722
1725
|
? Math.min(remaining, FIRST_CHUNK_TIMEOUT_MS)
|
|
1723
1726
|
: hasRunTimeout ? remaining : 0;
|
|
1724
|
-
let
|
|
1727
|
+
let nextPart: IteratorResult<(typeof result.fullStream) extends AsyncIterable<infer T> ? T : never> | null;
|
|
1725
1728
|
if (timeout <= 0 && chunkCount > 0) {
|
|
1726
|
-
|
|
1727
|
-
nextChunk = await textIterator.next();
|
|
1729
|
+
nextPart = await fullStreamIterator.next();
|
|
1728
1730
|
} else {
|
|
1729
1731
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
+
nextPart = await Promise.race([
|
|
1733
|
+
fullStreamIterator.next(),
|
|
1732
1734
|
new Promise<null>((resolve) => {
|
|
1733
1735
|
timer = setTimeout(() => resolve(null), timeout);
|
|
1734
1736
|
}),
|
|
@@ -1736,16 +1738,14 @@ ${boundedMainMemory.trim()}`
|
|
|
1736
1738
|
clearTimeout(timer);
|
|
1737
1739
|
}
|
|
1738
1740
|
|
|
1739
|
-
if (
|
|
1741
|
+
if (nextPart === null) {
|
|
1740
1742
|
const isFirstChunk = chunkCount === 0;
|
|
1741
1743
|
console.error(
|
|
1742
1744
|
`[poncho][harness] Stream timeout waiting for ${isFirstChunk ? "first" : "next"} chunk: model="${modelName}", step=${step}, chunks=${chunkCount}, elapsed=${now() - start}ms`,
|
|
1743
1745
|
);
|
|
1744
1746
|
if (isFirstChunk) {
|
|
1745
|
-
// Throw so the step-level retry logic can handle it.
|
|
1746
1747
|
throw new FirstChunkTimeoutError(modelName, FIRST_CHUNK_TIMEOUT_MS);
|
|
1747
1748
|
}
|
|
1748
|
-
// Mid-stream timeout: not retryable (partial response would be lost)
|
|
1749
1749
|
yield pushEvent({
|
|
1750
1750
|
type: "run:error",
|
|
1751
1751
|
runId,
|
|
@@ -1757,14 +1757,20 @@ ${boundedMainMemory.trim()}`
|
|
|
1757
1757
|
return;
|
|
1758
1758
|
}
|
|
1759
1759
|
|
|
1760
|
-
if (
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1760
|
+
if (nextPart.done) break;
|
|
1761
|
+
const part = nextPart.value;
|
|
1762
|
+
|
|
1763
|
+
if (part.type === "text-delta") {
|
|
1764
|
+
chunkCount += 1;
|
|
1765
|
+
fullText += part.text;
|
|
1766
|
+
yield pushEvent({ type: "model:chunk", content: part.text });
|
|
1767
|
+
} else if (part.type === "tool-input-start") {
|
|
1768
|
+
chunkCount += 1;
|
|
1769
|
+
yield pushEvent({ type: "tool:generating", tool: part.toolName, toolCallId: part.id });
|
|
1770
|
+
}
|
|
1764
1771
|
}
|
|
1765
1772
|
} finally {
|
|
1766
|
-
|
|
1767
|
-
textIterator.return?.(undefined)?.catch?.(() => {});
|
|
1773
|
+
fullStreamIterator.return?.(undefined)?.catch?.(() => {});
|
|
1768
1774
|
}
|
|
1769
1775
|
|
|
1770
1776
|
if (isCancelled()) {
|
|
@@ -1773,8 +1779,6 @@ ${boundedMainMemory.trim()}`
|
|
|
1773
1779
|
}
|
|
1774
1780
|
|
|
1775
1781
|
// Check finish reason for error / abnormal completions.
|
|
1776
|
-
// textStream silently swallows model-level errors – they only
|
|
1777
|
-
// surface through finishReason (or fullStream, which we don't use).
|
|
1778
1782
|
const finishReason = await result.finishReason;
|
|
1779
1783
|
|
|
1780
1784
|
if (finishReason === "error") {
|
|
@@ -1909,7 +1913,14 @@ ${boundedMainMemory.trim()}`
|
|
|
1909
1913
|
name: string;
|
|
1910
1914
|
input: Record<string, unknown>;
|
|
1911
1915
|
}> = [];
|
|
1916
|
+
const approvalNeeded: Array<{
|
|
1917
|
+
approvalId: string;
|
|
1918
|
+
id: string;
|
|
1919
|
+
name: string;
|
|
1920
|
+
input: Record<string, unknown>;
|
|
1921
|
+
}> = [];
|
|
1912
1922
|
|
|
1923
|
+
// Phase 1: classify all tool calls
|
|
1913
1924
|
for (const call of toolCalls) {
|
|
1914
1925
|
if (isCancelled()) {
|
|
1915
1926
|
yield emitCancellation();
|
|
@@ -1917,54 +1928,66 @@ ${boundedMainMemory.trim()}`
|
|
|
1917
1928
|
}
|
|
1918
1929
|
const runtimeToolName = exposedToolNames.get(call.name) ?? call.name;
|
|
1919
1930
|
yield pushEvent({ type: "tool:started", tool: runtimeToolName, input: call.input });
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
const approvalId = `approval_${randomUUID()}`;
|
|
1926
|
-
yield pushEvent({
|
|
1927
|
-
type: "tool:approval:required",
|
|
1928
|
-
tool: runtimeToolName,
|
|
1931
|
+
if (this.requiresApprovalForToolCall(runtimeToolName, call.input)) {
|
|
1932
|
+
approvalNeeded.push({
|
|
1933
|
+
approvalId: `approval_${randomUUID()}`,
|
|
1934
|
+
id: call.id,
|
|
1935
|
+
name: runtimeToolName,
|
|
1929
1936
|
input: call.input,
|
|
1930
|
-
approvalId,
|
|
1931
1937
|
});
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
name: exposedToolNames.get(tc.name) ?? tc.name,
|
|
1938
|
-
input: tc.input,
|
|
1939
|
-
})),
|
|
1938
|
+
} else {
|
|
1939
|
+
approvedCalls.push({
|
|
1940
|
+
id: call.id,
|
|
1941
|
+
name: runtimeToolName,
|
|
1942
|
+
input: call.input,
|
|
1940
1943
|
});
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
// Phase 2a: if any tools need approval, emit events for ALL of them and checkpoint
|
|
1948
|
+
if (approvalNeeded.length > 0) {
|
|
1949
|
+
for (const an of approvalNeeded) {
|
|
1947
1950
|
yield pushEvent({
|
|
1948
|
-
type: "tool:approval:
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
input: call.input,
|
|
1953
|
-
checkpointMessages: deltaMessages,
|
|
1954
|
-
pendingToolCalls: toolCalls.map(tc => ({
|
|
1955
|
-
id: tc.id,
|
|
1956
|
-
name: exposedToolNames.get(tc.name) ?? tc.name,
|
|
1957
|
-
input: tc.input,
|
|
1958
|
-
})),
|
|
1951
|
+
type: "tool:approval:required",
|
|
1952
|
+
tool: an.name,
|
|
1953
|
+
input: an.input,
|
|
1954
|
+
approvalId: an.approvalId,
|
|
1959
1955
|
});
|
|
1960
|
-
return;
|
|
1961
1956
|
}
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1957
|
+
|
|
1958
|
+
const assistantContent = JSON.stringify({
|
|
1959
|
+
text: fullText,
|
|
1960
|
+
tool_calls: toolCalls.map(tc => ({
|
|
1961
|
+
id: tc.id,
|
|
1962
|
+
name: exposedToolNames.get(tc.name) ?? tc.name,
|
|
1963
|
+
input: tc.input,
|
|
1964
|
+
})),
|
|
1966
1965
|
});
|
|
1966
|
+
const assistantMsg: Message = {
|
|
1967
|
+
role: "assistant",
|
|
1968
|
+
content: assistantContent,
|
|
1969
|
+
metadata: { timestamp: now(), id: randomUUID(), step },
|
|
1970
|
+
};
|
|
1971
|
+
const deltaMessages = [...messages.slice(inputMessageCount), assistantMsg];
|
|
1972
|
+
yield pushEvent({
|
|
1973
|
+
type: "tool:approval:checkpoint",
|
|
1974
|
+
approvals: approvalNeeded.map(an => ({
|
|
1975
|
+
approvalId: an.approvalId,
|
|
1976
|
+
tool: an.name,
|
|
1977
|
+
toolCallId: an.id,
|
|
1978
|
+
input: an.input,
|
|
1979
|
+
})),
|
|
1980
|
+
checkpointMessages: deltaMessages,
|
|
1981
|
+
pendingToolCalls: toolCalls.map(tc => ({
|
|
1982
|
+
id: tc.id,
|
|
1983
|
+
name: exposedToolNames.get(tc.name) ?? tc.name,
|
|
1984
|
+
input: tc.input,
|
|
1985
|
+
})),
|
|
1986
|
+
});
|
|
1987
|
+
return;
|
|
1967
1988
|
}
|
|
1989
|
+
|
|
1990
|
+
// Phase 2b: no approvals needed — execute all auto-approved calls
|
|
1968
1991
|
const batchStart = now();
|
|
1969
1992
|
if (isCancelled()) {
|
|
1970
1993
|
yield emitCancellation();
|
package/src/mcp.ts
CHANGED
|
@@ -12,6 +12,7 @@ export interface RemoteMcpServerConfig {
|
|
|
12
12
|
type: "bearer";
|
|
13
13
|
tokenEnv?: string;
|
|
14
14
|
};
|
|
15
|
+
headers?: Record<string, string>;
|
|
15
16
|
timeoutMs?: number;
|
|
16
17
|
reconnectAttempts?: number;
|
|
17
18
|
reconnectDelayMs?: number;
|
|
@@ -46,18 +47,26 @@ class StreamableHttpMcpRpcClient implements McpRpcClient {
|
|
|
46
47
|
private readonly endpoint: string;
|
|
47
48
|
private readonly timeoutMs: number;
|
|
48
49
|
private readonly bearerToken?: string;
|
|
50
|
+
private readonly customHeaders: Record<string, string>;
|
|
49
51
|
private idCounter = 1;
|
|
50
52
|
private initialized = false;
|
|
51
53
|
private sessionId?: string;
|
|
52
54
|
|
|
53
|
-
constructor(
|
|
55
|
+
constructor(
|
|
56
|
+
endpoint: string,
|
|
57
|
+
timeoutMs = 10_000,
|
|
58
|
+
bearerToken?: string,
|
|
59
|
+
customHeaders?: Record<string, string>,
|
|
60
|
+
) {
|
|
54
61
|
this.endpoint = endpoint;
|
|
55
62
|
this.timeoutMs = timeoutMs;
|
|
56
63
|
this.bearerToken = bearerToken;
|
|
64
|
+
this.customHeaders = customHeaders ?? {};
|
|
57
65
|
}
|
|
58
66
|
|
|
59
67
|
private buildHeaders(accept: string): Record<string, string> {
|
|
60
68
|
const headers: Record<string, string> = {
|
|
69
|
+
...this.customHeaders,
|
|
61
70
|
"Content-Type": "application/json",
|
|
62
71
|
Accept: accept,
|
|
63
72
|
};
|
|
@@ -367,6 +376,7 @@ export class LocalMcpBridge {
|
|
|
367
376
|
server.url,
|
|
368
377
|
server.timeoutMs ?? 10_000,
|
|
369
378
|
server.auth?.tokenEnv ? process.env[server.auth.tokenEnv] : undefined,
|
|
379
|
+
server.headers,
|
|
370
380
|
),
|
|
371
381
|
);
|
|
372
382
|
}
|
package/src/state.ts
CHANGED
|
@@ -35,6 +35,7 @@ export interface Conversation {
|
|
|
35
35
|
checkpointMessages?: Message[];
|
|
36
36
|
baseMessageCount?: number;
|
|
37
37
|
pendingToolCalls?: Array<{ id: string; name: string; input: Record<string, unknown> }>;
|
|
38
|
+
decision?: "approved" | "denied";
|
|
38
39
|
}>;
|
|
39
40
|
ownerId: string;
|
|
40
41
|
tenantId: string | null;
|
|
@@ -223,17 +224,17 @@ export class InMemoryConversationStore implements ConversationStore {
|
|
|
223
224
|
}
|
|
224
225
|
}
|
|
225
226
|
|
|
226
|
-
async list(ownerId
|
|
227
|
+
async list(ownerId?: string): Promise<Conversation[]> {
|
|
227
228
|
this.purgeExpired();
|
|
228
229
|
return Array.from(this.conversations.values())
|
|
229
|
-
.filter((conversation) => conversation.ownerId === ownerId)
|
|
230
|
+
.filter((conversation) => !ownerId || conversation.ownerId === ownerId)
|
|
230
231
|
.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
231
232
|
}
|
|
232
233
|
|
|
233
|
-
async listSummaries(ownerId
|
|
234
|
+
async listSummaries(ownerId?: string): Promise<ConversationSummary[]> {
|
|
234
235
|
this.purgeExpired();
|
|
235
236
|
return Array.from(this.conversations.values())
|
|
236
|
-
.filter((c) => c.ownerId === ownerId)
|
|
237
|
+
.filter((c) => !ownerId || c.ownerId === ownerId)
|
|
237
238
|
.sort((a, b) => b.updatedAt - a.updatedAt)
|
|
238
239
|
.map((c) => ({
|
|
239
240
|
conversationId: c.conversationId,
|
|
@@ -454,10 +455,10 @@ class FileConversationStore implements ConversationStore {
|
|
|
454
455
|
await this.writing;
|
|
455
456
|
}
|
|
456
457
|
|
|
457
|
-
async list(ownerId
|
|
458
|
+
async list(ownerId?: string): Promise<Conversation[]> {
|
|
458
459
|
await this.ensureLoaded();
|
|
459
460
|
const summaries = Array.from(this.conversations.values())
|
|
460
|
-
.filter((conversation) => conversation.ownerId === ownerId)
|
|
461
|
+
.filter((conversation) => !ownerId || conversation.ownerId === ownerId)
|
|
461
462
|
.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
462
463
|
const conversations: Conversation[] = [];
|
|
463
464
|
for (const summary of summaries) {
|
|
@@ -469,10 +470,10 @@ class FileConversationStore implements ConversationStore {
|
|
|
469
470
|
return conversations;
|
|
470
471
|
}
|
|
471
472
|
|
|
472
|
-
async listSummaries(ownerId
|
|
473
|
+
async listSummaries(ownerId?: string): Promise<ConversationSummary[]> {
|
|
473
474
|
await this.ensureLoaded();
|
|
474
475
|
return Array.from(this.conversations.values())
|
|
475
|
-
.filter((c) => c.ownerId === ownerId)
|
|
476
|
+
.filter((c) => !ownerId || c.ownerId === ownerId)
|
|
476
477
|
.sort((a, b) => b.updatedAt - a.updatedAt)
|
|
477
478
|
.map((c) => ({
|
|
478
479
|
conversationId: c.conversationId,
|
|
@@ -751,11 +752,15 @@ abstract class KeyValueConversationStoreBase implements ConversationStore {
|
|
|
751
752
|
}
|
|
752
753
|
}
|
|
753
754
|
|
|
754
|
-
async list(ownerId
|
|
755
|
+
async list(ownerId?: string): Promise<Conversation[]> {
|
|
755
756
|
const kv = await this.client();
|
|
756
757
|
if (!kv) {
|
|
757
758
|
return await this.memoryFallback.list(ownerId);
|
|
758
759
|
}
|
|
760
|
+
if (!ownerId) {
|
|
761
|
+
// KV stores index per-owner; cross-owner listing not supported
|
|
762
|
+
return [];
|
|
763
|
+
}
|
|
759
764
|
const ids = await this.getOwnerConversationIds(ownerId);
|
|
760
765
|
const conversations: Conversation[] = [];
|
|
761
766
|
for (const id of ids) {
|
|
@@ -772,11 +777,14 @@ abstract class KeyValueConversationStoreBase implements ConversationStore {
|
|
|
772
777
|
return conversations.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
773
778
|
}
|
|
774
779
|
|
|
775
|
-
async listSummaries(ownerId
|
|
780
|
+
async listSummaries(ownerId?: string): Promise<ConversationSummary[]> {
|
|
776
781
|
const kv = await this.client();
|
|
777
782
|
if (!kv) {
|
|
778
783
|
return await this.memoryFallback.listSummaries(ownerId);
|
|
779
784
|
}
|
|
785
|
+
if (!ownerId) {
|
|
786
|
+
return [];
|
|
787
|
+
}
|
|
780
788
|
const ids = await this.getOwnerConversationIds(ownerId);
|
|
781
789
|
const summaries: ConversationSummary[] = [];
|
|
782
790
|
for (const id of ids) {
|
package/test/mcp.test.ts
CHANGED
|
@@ -126,6 +126,89 @@ describe("mcp bridge protocol transports", () => {
|
|
|
126
126
|
expect(deleteSeen).toBe(true);
|
|
127
127
|
});
|
|
128
128
|
|
|
129
|
+
it("sends custom headers alongside bearer token", async () => {
|
|
130
|
+
process.env.LINEAR_TOKEN = "token-123";
|
|
131
|
+
let capturedHeaders: Record<string, string | undefined> = {};
|
|
132
|
+
const server = createServer(async (req, res) => {
|
|
133
|
+
if (req.method === "DELETE") {
|
|
134
|
+
res.statusCode = 200;
|
|
135
|
+
res.end();
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const chunks: Buffer[] = [];
|
|
139
|
+
for await (const chunk of req) chunks.push(Buffer.from(chunk));
|
|
140
|
+
const body = Buffer.concat(chunks).toString("utf8");
|
|
141
|
+
const payload = body.trim().length > 0 ? (JSON.parse(body) as any) : {};
|
|
142
|
+
capturedHeaders = {
|
|
143
|
+
authorization: req.headers.authorization,
|
|
144
|
+
"x-custom-id": req.headers["x-custom-id"] as string | undefined,
|
|
145
|
+
"x-another": req.headers["x-another"] as string | undefined,
|
|
146
|
+
};
|
|
147
|
+
if (payload.method === "initialize") {
|
|
148
|
+
res.setHeader("Content-Type", "application/json");
|
|
149
|
+
res.setHeader("Mcp-Session-Id", "s");
|
|
150
|
+
res.end(
|
|
151
|
+
JSON.stringify({
|
|
152
|
+
jsonrpc: "2.0",
|
|
153
|
+
id: payload.id,
|
|
154
|
+
result: {
|
|
155
|
+
protocolVersion: "2025-03-26",
|
|
156
|
+
capabilities: { tools: { listChanged: true } },
|
|
157
|
+
serverInfo: { name: "remote", version: "1.0.0" },
|
|
158
|
+
},
|
|
159
|
+
}),
|
|
160
|
+
);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (payload.method === "notifications/initialized") {
|
|
164
|
+
res.statusCode = 202;
|
|
165
|
+
res.end();
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (payload.method === "tools/list") {
|
|
169
|
+
res.setHeader("Content-Type", "application/json");
|
|
170
|
+
res.end(
|
|
171
|
+
JSON.stringify({
|
|
172
|
+
jsonrpc: "2.0",
|
|
173
|
+
id: payload.id,
|
|
174
|
+
result: {
|
|
175
|
+
tools: [
|
|
176
|
+
{ name: "ping", inputSchema: { type: "object", properties: {} } },
|
|
177
|
+
],
|
|
178
|
+
},
|
|
179
|
+
}),
|
|
180
|
+
);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
res.statusCode = 404;
|
|
184
|
+
res.end();
|
|
185
|
+
});
|
|
186
|
+
await new Promise<void>((r) => server.listen(0, () => r()));
|
|
187
|
+
const address = server.address();
|
|
188
|
+
if (!address || typeof address === "string") throw new Error("Unexpected address");
|
|
189
|
+
|
|
190
|
+
const bridge = new LocalMcpBridge({
|
|
191
|
+
mcp: [
|
|
192
|
+
{
|
|
193
|
+
name: "custom-headers",
|
|
194
|
+
url: `http://127.0.0.1:${address.port}/mcp`,
|
|
195
|
+
auth: { type: "bearer", tokenEnv: "LINEAR_TOKEN" },
|
|
196
|
+
headers: { "X-Custom-Id": "user-42", "X-Another": "value" },
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
await bridge.startLocalServers();
|
|
202
|
+
await bridge.discoverTools();
|
|
203
|
+
|
|
204
|
+
expect(capturedHeaders.authorization).toBe("Bearer token-123");
|
|
205
|
+
expect(capturedHeaders["x-custom-id"]).toBe("user-42");
|
|
206
|
+
expect(capturedHeaders["x-another"]).toBe("value");
|
|
207
|
+
|
|
208
|
+
await bridge.stopLocalServers();
|
|
209
|
+
await new Promise<void>((r) => server.close(() => r()));
|
|
210
|
+
});
|
|
211
|
+
|
|
129
212
|
it("fails fast on duplicate server names", () => {
|
|
130
213
|
expect(
|
|
131
214
|
() =>
|