@poncho-ai/harness 0.18.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.
@@ -1,5 +1,5 @@
1
1
 
2
- > @poncho-ai/harness@0.18.0 build /home/runner/work/poncho-ai/poncho-ai/packages/harness
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
  CLI Building entry: src/index.ts
@@ -7,8 +7,8 @@
7
7
  CLI tsup v8.5.1
8
8
  CLI Target: es2022
9
9
  ESM Build start
10
- ESM dist/index.js 196.78 KB
11
- ESM ⚡️ Build success in 133ms
10
+ ESM dist/index.js 198.85 KB
11
+ ESM ⚡️ Build success in 137ms
12
12
  DTS Build start
13
- DTS ⚡️ Build success in 6813ms
14
- DTS dist/index.d.ts 24.01 KB
13
+ DTS ⚡️ Build success in 7145ms
14
+ DTS dist/index.d.ts 24.33 KB
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
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
+
3
17
  ## 0.18.0
4
18
 
5
19
  ### 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;
@@ -268,6 +269,8 @@ type BuiltInToolToggles = {
268
269
  list_directory?: boolean;
269
270
  read_file?: boolean;
270
271
  write_file?: boolean;
272
+ delete_file?: boolean;
273
+ delete_directory?: boolean;
271
274
  };
272
275
  interface MessagingChannelConfig {
273
276
  platform: "slack" | "resend";
@@ -276,6 +279,7 @@ interface MessagingChannelConfig {
276
279
  apiKeyEnv?: string;
277
280
  webhookSecretEnv?: string;
278
281
  fromEnv?: string;
282
+ replyToEnv?: string;
279
283
  allowedSenders?: string[];
280
284
  mode?: "auto-reply" | "tool";
281
285
  allowedRecipients?: string[];
@@ -359,6 +363,8 @@ declare const loadPonchoConfig: (workingDir: string) => Promise<PonchoConfig | u
359
363
 
360
364
  declare const createDefaultTools: (workingDir: string) => ToolDefinition[];
361
365
  declare const createWriteTool: (workingDir: string) => ToolDefinition;
366
+ declare const createDeleteTool: (workingDir: string) => ToolDefinition;
367
+ declare const createDeleteDirectoryTool: (workingDir: string) => ToolDefinition;
362
368
 
363
369
  declare const PONCHO_UPLOAD_SCHEME = "poncho-upload://";
364
370
  interface UploadStore {
@@ -681,4 +687,4 @@ declare class TelemetryEmitter {
681
687
 
682
688
  declare const createSubagentTools: (manager: SubagentManager, getConversationId: () => string | undefined, getOwnerId: () => string) => ToolDefinition[];
683
689
 
684
- 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 rm(resolve5(this.uploadsDir, key), { force: true });
626
+ await rm2(resolve5(this.uploadsDir, key), { force: true });
581
627
  }
582
628
  };
583
629
  var VercelBlobUploadStore = class {
@@ -2908,6 +2954,7 @@ The agent will respond in Slack threads when @mentioned. Each Slack thread maps
2908
2954
  RESEND_API_KEY=re_...
2909
2955
  RESEND_WEBHOOK_SECRET=whsec_...
2910
2956
  RESEND_FROM=Agent <agent@yourdomain.com>
2957
+ RESEND_REPLY_TO=support@yourdomain.com # optional
2911
2958
  \`\`\`
2912
2959
  5. Add to \`poncho.config.js\`:
2913
2960
  \`\`\`javascript
@@ -3080,7 +3127,7 @@ var AgentHarness = class {
3080
3127
  isToolEnabled(name) {
3081
3128
  const access3 = this.resolveToolAccess(name);
3082
3129
  if (access3 === false) return false;
3083
- if (name === "write_file") {
3130
+ if (name === "write_file" || name === "delete_file" || name === "delete_directory") {
3084
3131
  return this.shouldEnableWriteTool();
3085
3132
  }
3086
3133
  return true;
@@ -3123,6 +3170,12 @@ var AgentHarness = class {
3123
3170
  if (this.isToolEnabled("write_file")) {
3124
3171
  this.registerIfMissing(createWriteTool(this.workingDir));
3125
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
+ }
3126
3179
  }
3127
3180
  shouldEnableWriteTool() {
3128
3181
  const override = process.env.PONCHO_FS_WRITE?.toLowerCase();
@@ -4242,6 +4295,7 @@ ${textContent}` };
4242
4295
  const toolResultsForModel = [];
4243
4296
  const richToolResults = [];
4244
4297
  const approvedCalls = [];
4298
+ const approvalNeeded = [];
4245
4299
  for (const call of toolCalls) {
4246
4300
  if (isCancelled()) {
4247
4301
  yield emitCancellation();
@@ -4249,52 +4303,60 @@ ${textContent}` };
4249
4303
  }
4250
4304
  const runtimeToolName = exposedToolNames.get(call.name) ?? call.name;
4251
4305
  yield pushEvent({ type: "tool:started", tool: runtimeToolName, input: call.input });
4252
- const requiresApproval = this.requiresApprovalForToolCall(
4253
- runtimeToolName,
4254
- call.input
4255
- );
4256
- if (requiresApproval) {
4257
- const approvalId = `approval_${randomUUID3()}`;
4258
- yield pushEvent({
4259
- type: "tool:approval:required",
4260
- tool: runtimeToolName,
4261
- input: call.input,
4262
- 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
4263
4312
  });
4264
- const assistantContent2 = JSON.stringify({
4265
- text: fullText,
4266
- tool_calls: toolCalls.map((tc) => ({
4267
- id: tc.id,
4268
- name: exposedToolNames.get(tc.name) ?? tc.name,
4269
- input: tc.input
4270
- }))
4313
+ } else {
4314
+ approvedCalls.push({
4315
+ id: call.id,
4316
+ name: runtimeToolName,
4317
+ input: call.input
4271
4318
  });
4272
- const assistantMsg = {
4273
- role: "assistant",
4274
- content: assistantContent2,
4275
- metadata: { timestamp: now(), id: randomUUID3(), step }
4276
- };
4277
- const deltaMessages = [...messages.slice(inputMessageCount), assistantMsg];
4319
+ }
4320
+ }
4321
+ if (approvalNeeded.length > 0) {
4322
+ for (const an of approvalNeeded) {
4278
4323
  yield pushEvent({
4279
- type: "tool:approval:checkpoint",
4280
- approvalId,
4281
- tool: runtimeToolName,
4282
- toolCallId: call.id,
4283
- input: call.input,
4284
- checkpointMessages: deltaMessages,
4285
- pendingToolCalls: toolCalls.map((tc) => ({
4286
- id: tc.id,
4287
- name: exposedToolNames.get(tc.name) ?? tc.name,
4288
- input: tc.input
4289
- }))
4324
+ type: "tool:approval:required",
4325
+ tool: an.name,
4326
+ input: an.input,
4327
+ approvalId: an.approvalId
4290
4328
  });
4291
- return;
4292
4329
  }
4293
- approvedCalls.push({
4294
- id: call.id,
4295
- name: runtimeToolName,
4296
- input: call.input
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
+ }))
4297
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
+ }))
4358
+ });
4359
+ return;
4298
4360
  }
4299
4361
  const batchStart = now();
4300
4362
  if (isCancelled()) {
@@ -4588,7 +4650,7 @@ var LatitudeCapture = class {
4588
4650
 
4589
4651
  // src/state.ts
4590
4652
  import { randomUUID as randomUUID4 } from "crypto";
4591
- import { mkdir as mkdir4, readFile as readFile7, readdir as readdir4, rename as rename2, rm as rm2, writeFile as writeFile5 } from "fs/promises";
4653
+ import { mkdir as mkdir4, readFile as readFile7, readdir as readdir4, rename as rename2, rm as rm3, writeFile as writeFile5 } from "fs/promises";
4592
4654
  import { dirname as dirname4, resolve as resolve9 } from "path";
4593
4655
  var DEFAULT_OWNER = "local-owner";
4594
4656
  var LOCAL_STATE_FILE = "state.json";
@@ -4963,7 +5025,7 @@ var FileConversationStore = class {
4963
5025
  if (removed) {
4964
5026
  this.writing = this.writing.then(async () => {
4965
5027
  if (existing) {
4966
- await rm2(resolve9(conversationsDir, existing.fileName), { force: true });
5028
+ await rm3(resolve9(conversationsDir, existing.fileName), { force: true });
4967
5029
  }
4968
5030
  await this.writeIndex();
4969
5031
  });
@@ -5711,6 +5773,8 @@ export {
5711
5773
  buildSkillContextWindow,
5712
5774
  createConversationStore,
5713
5775
  createDefaultTools,
5776
+ createDeleteDirectoryTool,
5777
+ createDeleteTool,
5714
5778
  createMemoryStore,
5715
5779
  createMemoryTools,
5716
5780
  createModelProvider,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poncho-ai/harness",
3
- "version": "0.18.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.3.0"
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[];
@@ -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 {
@@ -1906,7 +1913,14 @@ ${boundedMainMemory.trim()}`
1906
1913
  name: string;
1907
1914
  input: Record<string, unknown>;
1908
1915
  }> = [];
1916
+ const approvalNeeded: Array<{
1917
+ approvalId: string;
1918
+ id: string;
1919
+ name: string;
1920
+ input: Record<string, unknown>;
1921
+ }> = [];
1909
1922
 
1923
+ // Phase 1: classify all tool calls
1910
1924
  for (const call of toolCalls) {
1911
1925
  if (isCancelled()) {
1912
1926
  yield emitCancellation();
@@ -1914,54 +1928,66 @@ ${boundedMainMemory.trim()}`
1914
1928
  }
1915
1929
  const runtimeToolName = exposedToolNames.get(call.name) ?? call.name;
1916
1930
  yield pushEvent({ type: "tool:started", tool: runtimeToolName, input: call.input });
1917
- const requiresApproval = this.requiresApprovalForToolCall(
1918
- runtimeToolName,
1919
- call.input,
1920
- );
1921
- if (requiresApproval) {
1922
- const approvalId = `approval_${randomUUID()}`;
1923
- yield pushEvent({
1924
- type: "tool:approval:required",
1925
- tool: runtimeToolName,
1931
+ if (this.requiresApprovalForToolCall(runtimeToolName, call.input)) {
1932
+ approvalNeeded.push({
1933
+ approvalId: `approval_${randomUUID()}`,
1934
+ id: call.id,
1935
+ name: runtimeToolName,
1926
1936
  input: call.input,
1927
- approvalId,
1928
1937
  });
1929
-
1930
- const assistantContent = JSON.stringify({
1931
- text: fullText,
1932
- tool_calls: toolCalls.map(tc => ({
1933
- id: tc.id,
1934
- name: exposedToolNames.get(tc.name) ?? tc.name,
1935
- input: tc.input,
1936
- })),
1938
+ } else {
1939
+ approvedCalls.push({
1940
+ id: call.id,
1941
+ name: runtimeToolName,
1942
+ input: call.input,
1937
1943
  });
1938
- const assistantMsg: Message = {
1939
- role: "assistant",
1940
- content: assistantContent,
1941
- metadata: { timestamp: now(), id: randomUUID(), step },
1942
- };
1943
- const deltaMessages = [...messages.slice(inputMessageCount), assistantMsg];
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) {
1944
1950
  yield pushEvent({
1945
- type: "tool:approval:checkpoint",
1946
- approvalId,
1947
- tool: runtimeToolName,
1948
- toolCallId: call.id,
1949
- input: call.input,
1950
- checkpointMessages: deltaMessages,
1951
- pendingToolCalls: toolCalls.map(tc => ({
1952
- id: tc.id,
1953
- name: exposedToolNames.get(tc.name) ?? tc.name,
1954
- input: tc.input,
1955
- })),
1951
+ type: "tool:approval:required",
1952
+ tool: an.name,
1953
+ input: an.input,
1954
+ approvalId: an.approvalId,
1956
1955
  });
1957
- return;
1958
1956
  }
1959
- approvedCalls.push({
1960
- id: call.id,
1961
- name: runtimeToolName,
1962
- input: call.input,
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
+ })),
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
+ })),
1963
1986
  });
1987
+ return;
1964
1988
  }
1989
+
1990
+ // Phase 2b: no approvals needed — execute all auto-approved calls
1965
1991
  const batchStart = now();
1966
1992
  if (isCancelled()) {
1967
1993
  yield emitCancellation();
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;