@poncho-ai/harness 0.22.1 → 0.24.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 +12 -0
- package/dist/index.d.ts +14 -1
- package/dist/index.js +87 -8
- package/package.json +1 -1
- package/src/agent-parser.ts +7 -0
- package/src/config.ts +1 -0
- package/src/default-tools.ts +53 -0
- package/src/harness.ts +12 -3
- package/src/state.ts +25 -0
- package/test/harness.test.ts +64 -1
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @poncho-ai/harness@0.
|
|
2
|
+
> @poncho-ai/harness@0.24.0 build /home/runner/work/poncho-ai/poncho-ai/packages/harness
|
|
3
3
|
> node scripts/embed-docs.js && tsup src/index.ts --format esm --dts
|
|
4
4
|
|
|
5
5
|
[embed-docs] Generated poncho-docs.ts with 4 topics
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
[34mCLI[39m tsup v8.5.1
|
|
9
9
|
[34mCLI[39m Target: es2022
|
|
10
10
|
[34mESM[39m Build start
|
|
11
|
-
[32mESM[39m [1mdist/index.js [22m[
|
|
12
|
-
[32mESM[39m ⚡️ Build success in
|
|
11
|
+
[32mESM[39m [1mdist/index.js [22m[32m263.89 KB[39m
|
|
12
|
+
[32mESM[39m ⚡️ Build success in 136ms
|
|
13
13
|
[34mDTS[39m Build start
|
|
14
|
-
[32mDTS[39m ⚡️ Build success in
|
|
15
|
-
[32mDTS[39m [1mdist/index.d.ts [22m[32m27.
|
|
14
|
+
[32mDTS[39m ⚡️ Build success in 6918ms
|
|
15
|
+
[32mDTS[39m [1mdist/index.d.ts [22m[32m27.50 KB[39m
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @poncho-ai/harness
|
|
2
2
|
|
|
3
|
+
## 0.24.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [`aee4f17`](https://github.com/cesr/poncho-ai/commit/aee4f17237d33b2cc134ed9934b709d967ca3f10) Thanks [@cesr](https://github.com/cesr)! - Add `edit_file` built-in tool with str_replace semantics for targeted file edits. The tool takes `path`, `old_str`, and `new_str` parameters, enforces uniqueness of the match, and is write-gated like `write_file` (disabled in production by default). Also improves browser SSE frame streaming with backpressure handling and auto-stops screencast when all listeners disconnect.
|
|
8
|
+
|
|
9
|
+
## 0.23.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- [`d1e1bfb`](https://github.com/cesr/poncho-ai/commit/d1e1bfbf35b18788ab79231ca675774e949f5116) Thanks [@cesr](https://github.com/cesr)! - Add proactive scheduled messaging via channel-targeted cron jobs. Cron jobs with `channel: telegram` (or `slack`) now automatically discover known conversations and send the agent's response directly to each chat, continuing the existing conversation history.
|
|
14
|
+
|
|
3
15
|
## 0.22.1
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/dist/index.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ interface CronJobConfig {
|
|
|
19
19
|
schedule: string;
|
|
20
20
|
task: string;
|
|
21
21
|
timezone?: string;
|
|
22
|
+
channel?: string;
|
|
22
23
|
}
|
|
23
24
|
interface AgentFrontmatter {
|
|
24
25
|
name: string;
|
|
@@ -155,6 +156,11 @@ interface Conversation {
|
|
|
155
156
|
result?: _poncho_ai_sdk.RunResult;
|
|
156
157
|
error?: _poncho_ai_sdk.AgentFailure;
|
|
157
158
|
};
|
|
159
|
+
channelMeta?: {
|
|
160
|
+
platform: string;
|
|
161
|
+
channelId: string;
|
|
162
|
+
platformThreadId: string;
|
|
163
|
+
};
|
|
158
164
|
createdAt: number;
|
|
159
165
|
updatedAt: number;
|
|
160
166
|
}
|
|
@@ -208,6 +214,11 @@ type ConversationSummary = {
|
|
|
208
214
|
parentConversationId?: string;
|
|
209
215
|
messageCount?: number;
|
|
210
216
|
hasPendingApprovals?: boolean;
|
|
217
|
+
channelMeta?: {
|
|
218
|
+
platform: string;
|
|
219
|
+
channelId: string;
|
|
220
|
+
platformThreadId: string;
|
|
221
|
+
};
|
|
211
222
|
};
|
|
212
223
|
declare const createStateStore: (config?: StateConfig, options?: {
|
|
213
224
|
workingDir?: string;
|
|
@@ -317,6 +328,7 @@ type BuiltInToolToggles = {
|
|
|
317
328
|
list_directory?: boolean;
|
|
318
329
|
read_file?: boolean;
|
|
319
330
|
write_file?: boolean;
|
|
331
|
+
edit_file?: boolean;
|
|
320
332
|
delete_file?: boolean;
|
|
321
333
|
delete_directory?: boolean;
|
|
322
334
|
};
|
|
@@ -423,6 +435,7 @@ declare const loadPonchoConfig: (workingDir: string) => Promise<PonchoConfig | u
|
|
|
423
435
|
|
|
424
436
|
declare const createDefaultTools: (workingDir: string) => ToolDefinition[];
|
|
425
437
|
declare const createWriteTool: (workingDir: string) => ToolDefinition;
|
|
438
|
+
declare const createEditTool: (workingDir: string) => ToolDefinition;
|
|
426
439
|
declare const createDeleteTool: (workingDir: string) => ToolDefinition;
|
|
427
440
|
declare const createDeleteDirectoryTool: (workingDir: string) => ToolDefinition;
|
|
428
441
|
declare const ponchoDocsTool: ToolDefinition;
|
|
@@ -751,4 +764,4 @@ declare class TelemetryEmitter {
|
|
|
751
764
|
|
|
752
765
|
declare const createSubagentTools: (manager: SubagentManager, getConversationId: () => string | undefined, getOwnerId: () => string) => ToolDefinition[];
|
|
753
766
|
|
|
754
|
-
export { type AgentFrontmatter, AgentHarness, type AgentIdentity, type AgentLimitsConfig, type AgentModelConfig, type BuiltInToolToggles, type CompactMessagesOptions, type CompactResult, type CompactionConfig, 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, compactMessages, createConversationStore, createDefaultTools, createDeleteDirectoryTool, createDeleteTool, createMemoryStore, createMemoryTools, createModelProvider, createSkillTools, createStateStore, createSubagentTools, createUploadStore, createWriteTool, deriveUploadKey, ensureAgentIdentity, estimateTokens, estimateTotalTokens, findSafeSplitPoint, generateAgentId, getAgentStoreDirectory, getModelContextWindow, getPonchoStoreRoot, jsonSchemaToZod, loadPonchoConfig, loadSkillContext, loadSkillInstructions, loadSkillMetadata, normalizeScriptPolicyPath, parseAgentFile, parseAgentMarkdown, ponchoDocsTool, readSkillResource, renderAgentPrompt, resolveAgentIdentity, resolveCompactionConfig, resolveMemoryConfig, resolveSkillDirs, resolveStateConfig, slugifyStorageComponent };
|
|
767
|
+
export { type AgentFrontmatter, AgentHarness, type AgentIdentity, type AgentLimitsConfig, type AgentModelConfig, type BuiltInToolToggles, type CompactMessagesOptions, type CompactResult, type CompactionConfig, 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, compactMessages, createConversationStore, createDefaultTools, createDeleteDirectoryTool, createDeleteTool, createEditTool, createMemoryStore, createMemoryTools, createModelProvider, createSkillTools, createStateStore, createSubagentTools, createUploadStore, createWriteTool, deriveUploadKey, ensureAgentIdentity, estimateTokens, estimateTotalTokens, findSafeSplitPoint, generateAgentId, getAgentStoreDirectory, getModelContextWindow, getPonchoStoreRoot, jsonSchemaToZod, loadPonchoConfig, loadSkillContext, loadSkillInstructions, loadSkillMetadata, normalizeScriptPolicyPath, parseAgentFile, parseAgentMarkdown, ponchoDocsTool, readSkillResource, renderAgentPrompt, resolveAgentIdentity, resolveCompactionConfig, resolveMemoryConfig, resolveSkillDirs, resolveStateConfig, slugifyStorageComponent };
|
package/dist/index.js
CHANGED
|
@@ -112,10 +112,12 @@ var parseCronJobs = (value) => {
|
|
|
112
112
|
if (timezone) {
|
|
113
113
|
validateTimezone(timezone, path);
|
|
114
114
|
}
|
|
115
|
+
const channel = typeof jobValue.channel === "string" && jobValue.channel.trim() ? jobValue.channel.trim() : void 0;
|
|
115
116
|
jobs[jobName] = {
|
|
116
117
|
schedule: jobValue.schedule.trim(),
|
|
117
118
|
task: jobValue.task,
|
|
118
|
-
timezone
|
|
119
|
+
timezone,
|
|
120
|
+
channel
|
|
119
121
|
};
|
|
120
122
|
}
|
|
121
123
|
return jobs;
|
|
@@ -1030,6 +1032,22 @@ messaging: [
|
|
|
1030
1032
|
]
|
|
1031
1033
|
\`\`\`
|
|
1032
1034
|
|
|
1035
|
+
#### Proactive scheduled messages
|
|
1036
|
+
|
|
1037
|
+
You can have the agent proactively message Telegram chats on a cron schedule. Add \`channel: telegram\` to any cron job in your \`AGENT.md\` frontmatter:
|
|
1038
|
+
|
|
1039
|
+
\`\`\`yaml
|
|
1040
|
+
cron:
|
|
1041
|
+
daily-checkin:
|
|
1042
|
+
schedule: "0 9 * * *"
|
|
1043
|
+
task: "Check in with the user about their plans for today"
|
|
1044
|
+
channel: telegram
|
|
1045
|
+
\`\`\`
|
|
1046
|
+
|
|
1047
|
+
The system auto-discovers all Telegram chats the bot has interacted with and sends the agent's response to each one. No chat IDs need to be configured -- filtering is handled by \`allowedUserIds\` if set. The agent runs with the full conversation history for each chat, so it has context from prior interactions.
|
|
1048
|
+
|
|
1049
|
+
The bot must have received at least one message from a user before it can send proactive messages to that chat (Telegram API requirement).
|
|
1050
|
+
|
|
1033
1051
|
### Email (Resend)
|
|
1034
1052
|
|
|
1035
1053
|
#### 1. Set up Resend
|
|
@@ -1890,6 +1908,52 @@ var createWriteTool = (workingDir) => defineTool({
|
|
|
1890
1908
|
return { path, written: true };
|
|
1891
1909
|
}
|
|
1892
1910
|
});
|
|
1911
|
+
var createEditTool = (workingDir) => defineTool({
|
|
1912
|
+
name: "edit_file",
|
|
1913
|
+
description: "Edit a file by replacing an exact string match with new content. The old_str must match exactly one location in the file (including whitespace and indentation). Use an empty new_str to delete matched content.",
|
|
1914
|
+
inputSchema: {
|
|
1915
|
+
type: "object",
|
|
1916
|
+
properties: {
|
|
1917
|
+
path: {
|
|
1918
|
+
type: "string",
|
|
1919
|
+
description: "File path relative to working directory"
|
|
1920
|
+
},
|
|
1921
|
+
old_str: {
|
|
1922
|
+
type: "string",
|
|
1923
|
+
description: "The exact text to find and replace (must be unique in the file). Include surrounding context lines if needed to ensure uniqueness."
|
|
1924
|
+
},
|
|
1925
|
+
new_str: {
|
|
1926
|
+
type: "string",
|
|
1927
|
+
description: "The replacement text (use empty string to delete the matched content)"
|
|
1928
|
+
}
|
|
1929
|
+
},
|
|
1930
|
+
required: ["path", "old_str", "new_str"],
|
|
1931
|
+
additionalProperties: false
|
|
1932
|
+
},
|
|
1933
|
+
handler: async (input) => {
|
|
1934
|
+
const path = typeof input.path === "string" ? input.path : "";
|
|
1935
|
+
const oldStr = typeof input.old_str === "string" ? input.old_str : "";
|
|
1936
|
+
const newStr = typeof input.new_str === "string" ? input.new_str : "";
|
|
1937
|
+
if (!oldStr) throw new Error("old_str must not be empty.");
|
|
1938
|
+
const resolved = resolveSafePath(workingDir, path);
|
|
1939
|
+
const content = await readFile3(resolved, "utf8");
|
|
1940
|
+
const first = content.indexOf(oldStr);
|
|
1941
|
+
if (first === -1) {
|
|
1942
|
+
throw new Error(
|
|
1943
|
+
"old_str not found in file. Make sure it matches exactly, including whitespace and line breaks."
|
|
1944
|
+
);
|
|
1945
|
+
}
|
|
1946
|
+
const last = content.lastIndexOf(oldStr);
|
|
1947
|
+
if (first !== last) {
|
|
1948
|
+
throw new Error(
|
|
1949
|
+
"old_str appears multiple times in the file. Please provide more context to ensure a unique match."
|
|
1950
|
+
);
|
|
1951
|
+
}
|
|
1952
|
+
const newContent = content.slice(0, first) + newStr + content.slice(first + oldStr.length);
|
|
1953
|
+
await writeFile2(resolved, newContent, "utf8");
|
|
1954
|
+
return { path, edited: true };
|
|
1955
|
+
}
|
|
1956
|
+
});
|
|
1893
1957
|
var createDeleteTool = (workingDir) => defineTool({
|
|
1894
1958
|
name: "delete_file",
|
|
1895
1959
|
description: "Delete a file at a path inside the working directory",
|
|
@@ -4309,7 +4373,7 @@ You are running locally in development mode. Treat this as an editable agent wor
|
|
|
4309
4373
|
## Understanding Your Environment
|
|
4310
4374
|
|
|
4311
4375
|
- Built-in tools: \`list_directory\` and \`read_file\`
|
|
4312
|
-
- \`write_file\`
|
|
4376
|
+
- \`write_file\` and \`edit_file\` are available in development (disabled by default in production)
|
|
4313
4377
|
- A starter local skill is included (\`starter-echo\`)
|
|
4314
4378
|
- Bash/shell commands are **not** available unless you install and enable a shell tool/skill
|
|
4315
4379
|
- Git operations are only available if a git-capable tool/skill is configured
|
|
@@ -4365,6 +4429,10 @@ cron:
|
|
|
4365
4429
|
schedule: "0 9 * * *" # Standard 5-field cron expression
|
|
4366
4430
|
timezone: "America/New_York" # Optional IANA timezone (default: UTC)
|
|
4367
4431
|
task: "Generate the daily sales report"
|
|
4432
|
+
telegram-checkin:
|
|
4433
|
+
schedule: "0 18 * * 1-5"
|
|
4434
|
+
channel: telegram # Proactive message to all known Telegram chats
|
|
4435
|
+
task: "Send an end-of-day summary to the user"
|
|
4368
4436
|
\`\`\`
|
|
4369
4437
|
|
|
4370
4438
|
- Each cron job triggers an autonomous agent run with the specified task, creating a fresh conversation.
|
|
@@ -4373,6 +4441,7 @@ cron:
|
|
|
4373
4441
|
- Jobs can also be triggered manually: \`GET /api/cron/<jobName>\`.
|
|
4374
4442
|
- To carry context across cron runs, enable memory.
|
|
4375
4443
|
- **IMPORTANT**: When adding a new cron job, always PRESERVE all existing cron jobs. Never remove or overwrite existing jobs unless the user explicitly asks you to replace or delete them. Read the full current \`cron:\` block before editing, and append the new job alongside the existing ones.
|
|
4444
|
+
- **Proactive channel messaging**: Adding \`channel: telegram\` (or \`slack\`) makes the cron job send its response directly to all known conversations on that platform, instead of creating a standalone conversation. The agent continues the existing conversation history for context. A chat must have at least one prior user message for auto-discovery to find it.
|
|
4376
4445
|
|
|
4377
4446
|
## Messaging Integrations (Slack, Telegram, Email)
|
|
4378
4447
|
|
|
@@ -4565,6 +4634,7 @@ Since all fields have defaults, you only need to specify \`*Env\` when your env
|
|
|
4565
4634
|
- If shell/CLI access is unavailable, ask the user to run needed commands and provide exact copy-paste commands.
|
|
4566
4635
|
- For setup, skills, MCP, auth, storage, telemetry, or "how do I..." questions, proactively read \`README.md\` with \`read_file\` before answering.
|
|
4567
4636
|
- Prefer quoting concrete commands and examples from \`README.md\` over guessing.
|
|
4637
|
+
- Prefer \`edit_file\` for targeted changes to existing files (uses exact string matching); use \`write_file\` only for creating new files or full rewrites.
|
|
4568
4638
|
- Keep edits minimal, preserve unrelated settings/code, and summarize what changed.
|
|
4569
4639
|
|
|
4570
4640
|
## Detailed Documentation
|
|
@@ -4635,7 +4705,7 @@ var AgentHarness = class _AgentHarness {
|
|
|
4635
4705
|
isToolEnabled(name) {
|
|
4636
4706
|
const access3 = this.resolveToolAccess(name);
|
|
4637
4707
|
if (access3 === false) return false;
|
|
4638
|
-
if (name === "write_file" || name === "delete_file" || name === "delete_directory") {
|
|
4708
|
+
if (name === "write_file" || name === "edit_file" || name === "delete_file" || name === "delete_directory") {
|
|
4639
4709
|
return this.shouldEnableWriteTool();
|
|
4640
4710
|
}
|
|
4641
4711
|
return true;
|
|
@@ -4678,6 +4748,9 @@ var AgentHarness = class _AgentHarness {
|
|
|
4678
4748
|
if (this.isToolEnabled("write_file")) {
|
|
4679
4749
|
this.registerIfMissing(createWriteTool(this.workingDir));
|
|
4680
4750
|
}
|
|
4751
|
+
if (this.isToolEnabled("edit_file")) {
|
|
4752
|
+
this.registerIfMissing(createEditTool(this.workingDir));
|
|
4753
|
+
}
|
|
4681
4754
|
if (this.isToolEnabled("delete_file")) {
|
|
4682
4755
|
this.registerIfMissing(createDeleteTool(this.workingDir));
|
|
4683
4756
|
}
|
|
@@ -6350,7 +6423,8 @@ var InMemoryConversationStore = class {
|
|
|
6350
6423
|
ownerId: c.ownerId,
|
|
6351
6424
|
parentConversationId: c.parentConversationId,
|
|
6352
6425
|
messageCount: c.messages.length,
|
|
6353
|
-
hasPendingApprovals: Array.isArray(c.pendingApprovals) && c.pendingApprovals.length > 0
|
|
6426
|
+
hasPendingApprovals: Array.isArray(c.pendingApprovals) && c.pendingApprovals.length > 0,
|
|
6427
|
+
channelMeta: c.channelMeta
|
|
6354
6428
|
}));
|
|
6355
6429
|
}
|
|
6356
6430
|
async get(conversationId) {
|
|
@@ -6512,7 +6586,8 @@ var FileConversationStore = class {
|
|
|
6512
6586
|
fileName,
|
|
6513
6587
|
parentConversationId: conversation.parentConversationId,
|
|
6514
6588
|
messageCount: conversation.messages.length,
|
|
6515
|
-
hasPendingApprovals: Array.isArray(conversation.pendingApprovals) && conversation.pendingApprovals.length > 0
|
|
6589
|
+
hasPendingApprovals: Array.isArray(conversation.pendingApprovals) && conversation.pendingApprovals.length > 0,
|
|
6590
|
+
channelMeta: conversation.channelMeta
|
|
6516
6591
|
});
|
|
6517
6592
|
await this.writeIndex();
|
|
6518
6593
|
});
|
|
@@ -6540,7 +6615,8 @@ var FileConversationStore = class {
|
|
|
6540
6615
|
ownerId: c.ownerId,
|
|
6541
6616
|
parentConversationId: c.parentConversationId,
|
|
6542
6617
|
messageCount: c.messageCount,
|
|
6543
|
-
hasPendingApprovals: c.hasPendingApprovals
|
|
6618
|
+
hasPendingApprovals: c.hasPendingApprovals,
|
|
6619
|
+
channelMeta: c.channelMeta
|
|
6544
6620
|
}));
|
|
6545
6621
|
}
|
|
6546
6622
|
async get(conversationId) {
|
|
@@ -6806,7 +6882,8 @@ var KeyValueConversationStoreBase = class {
|
|
|
6806
6882
|
ownerId: meta.ownerId,
|
|
6807
6883
|
parentConversationId: meta.parentConversationId,
|
|
6808
6884
|
messageCount: meta.messageCount,
|
|
6809
|
-
hasPendingApprovals: meta.hasPendingApprovals
|
|
6885
|
+
hasPendingApprovals: meta.hasPendingApprovals,
|
|
6886
|
+
channelMeta: meta.channelMeta
|
|
6810
6887
|
});
|
|
6811
6888
|
}
|
|
6812
6889
|
} catch {
|
|
@@ -6867,7 +6944,8 @@ var KeyValueConversationStoreBase = class {
|
|
|
6867
6944
|
ownerId: nextConversation.ownerId,
|
|
6868
6945
|
parentConversationId: nextConversation.parentConversationId,
|
|
6869
6946
|
messageCount: nextConversation.messages.length,
|
|
6870
|
-
hasPendingApprovals: Array.isArray(nextConversation.pendingApprovals) && nextConversation.pendingApprovals.length > 0
|
|
6947
|
+
hasPendingApprovals: Array.isArray(nextConversation.pendingApprovals) && nextConversation.pendingApprovals.length > 0,
|
|
6948
|
+
channelMeta: nextConversation.channelMeta
|
|
6871
6949
|
}),
|
|
6872
6950
|
this.ttl
|
|
6873
6951
|
);
|
|
@@ -7384,6 +7462,7 @@ export {
|
|
|
7384
7462
|
createDefaultTools,
|
|
7385
7463
|
createDeleteDirectoryTool,
|
|
7386
7464
|
createDeleteTool,
|
|
7465
|
+
createEditTool,
|
|
7387
7466
|
createMemoryStore,
|
|
7388
7467
|
createMemoryTools,
|
|
7389
7468
|
createModelProvider,
|
package/package.json
CHANGED
package/src/agent-parser.ts
CHANGED
|
@@ -27,6 +27,7 @@ export interface CronJobConfig {
|
|
|
27
27
|
schedule: string;
|
|
28
28
|
task: string;
|
|
29
29
|
timezone?: string;
|
|
30
|
+
channel?: string;
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
export interface AgentFrontmatter {
|
|
@@ -138,10 +139,16 @@ const parseCronJobs = (
|
|
|
138
139
|
validateTimezone(timezone, path);
|
|
139
140
|
}
|
|
140
141
|
|
|
142
|
+
const channel =
|
|
143
|
+
typeof jobValue.channel === "string" && jobValue.channel.trim()
|
|
144
|
+
? jobValue.channel.trim()
|
|
145
|
+
: undefined;
|
|
146
|
+
|
|
141
147
|
jobs[jobName] = {
|
|
142
148
|
schedule: jobValue.schedule.trim(),
|
|
143
149
|
task: jobValue.task,
|
|
144
150
|
timezone,
|
|
151
|
+
channel,
|
|
145
152
|
};
|
|
146
153
|
}
|
|
147
154
|
return jobs;
|
package/src/config.ts
CHANGED
package/src/default-tools.ts
CHANGED
|
@@ -89,6 +89,59 @@ export const createWriteTool = (workingDir: string): ToolDefinition =>
|
|
|
89
89
|
},
|
|
90
90
|
});
|
|
91
91
|
|
|
92
|
+
export const createEditTool = (workingDir: string): ToolDefinition =>
|
|
93
|
+
defineTool({
|
|
94
|
+
name: "edit_file",
|
|
95
|
+
description:
|
|
96
|
+
"Edit a file by replacing an exact string match with new content. " +
|
|
97
|
+
"The old_str must match exactly one location in the file (including whitespace and indentation). " +
|
|
98
|
+
"Use an empty new_str to delete matched content.",
|
|
99
|
+
inputSchema: {
|
|
100
|
+
type: "object",
|
|
101
|
+
properties: {
|
|
102
|
+
path: {
|
|
103
|
+
type: "string",
|
|
104
|
+
description: "File path relative to working directory",
|
|
105
|
+
},
|
|
106
|
+
old_str: {
|
|
107
|
+
type: "string",
|
|
108
|
+
description:
|
|
109
|
+
"The exact text to find and replace (must be unique in the file). " +
|
|
110
|
+
"Include surrounding context lines if needed to ensure uniqueness.",
|
|
111
|
+
},
|
|
112
|
+
new_str: {
|
|
113
|
+
type: "string",
|
|
114
|
+
description: "The replacement text (use empty string to delete the matched content)",
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
required: ["path", "old_str", "new_str"],
|
|
118
|
+
additionalProperties: false,
|
|
119
|
+
},
|
|
120
|
+
handler: async (input) => {
|
|
121
|
+
const path = typeof input.path === "string" ? input.path : "";
|
|
122
|
+
const oldStr = typeof input.old_str === "string" ? input.old_str : "";
|
|
123
|
+
const newStr = typeof input.new_str === "string" ? input.new_str : "";
|
|
124
|
+
if (!oldStr) throw new Error("old_str must not be empty.");
|
|
125
|
+
const resolved = resolveSafePath(workingDir, path);
|
|
126
|
+
const content = await readFile(resolved, "utf8");
|
|
127
|
+
const first = content.indexOf(oldStr);
|
|
128
|
+
if (first === -1) {
|
|
129
|
+
throw new Error(
|
|
130
|
+
"old_str not found in file. Make sure it matches exactly, including whitespace and line breaks.",
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
const last = content.lastIndexOf(oldStr);
|
|
134
|
+
if (first !== last) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
"old_str appears multiple times in the file. Please provide more context to ensure a unique match.",
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
const newContent = content.slice(0, first) + newStr + content.slice(first + oldStr.length);
|
|
140
|
+
await writeFile(resolved, newContent, "utf8");
|
|
141
|
+
return { path, edited: true };
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
|
|
92
145
|
export const createDeleteTool = (workingDir: string): ToolDefinition =>
|
|
93
146
|
defineTool({
|
|
94
147
|
name: "delete_file",
|
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, createDeleteDirectoryTool, createDeleteTool, createWriteTool, ponchoDocsTool } from "./default-tools.js";
|
|
18
|
+
import { createDefaultTools, createDeleteDirectoryTool, createDeleteTool, createEditTool, createWriteTool, ponchoDocsTool } from "./default-tools.js";
|
|
19
19
|
import {
|
|
20
20
|
createMemoryStore,
|
|
21
21
|
createMemoryTools,
|
|
@@ -224,7 +224,7 @@ You are running locally in development mode. Treat this as an editable agent wor
|
|
|
224
224
|
## Understanding Your Environment
|
|
225
225
|
|
|
226
226
|
- Built-in tools: \`list_directory\` and \`read_file\`
|
|
227
|
-
- \`write_file\`
|
|
227
|
+
- \`write_file\` and \`edit_file\` are available in development (disabled by default in production)
|
|
228
228
|
- A starter local skill is included (\`starter-echo\`)
|
|
229
229
|
- Bash/shell commands are **not** available unless you install and enable a shell tool/skill
|
|
230
230
|
- Git operations are only available if a git-capable tool/skill is configured
|
|
@@ -280,6 +280,10 @@ cron:
|
|
|
280
280
|
schedule: "0 9 * * *" # Standard 5-field cron expression
|
|
281
281
|
timezone: "America/New_York" # Optional IANA timezone (default: UTC)
|
|
282
282
|
task: "Generate the daily sales report"
|
|
283
|
+
telegram-checkin:
|
|
284
|
+
schedule: "0 18 * * 1-5"
|
|
285
|
+
channel: telegram # Proactive message to all known Telegram chats
|
|
286
|
+
task: "Send an end-of-day summary to the user"
|
|
283
287
|
\`\`\`
|
|
284
288
|
|
|
285
289
|
- Each cron job triggers an autonomous agent run with the specified task, creating a fresh conversation.
|
|
@@ -288,6 +292,7 @@ cron:
|
|
|
288
292
|
- Jobs can also be triggered manually: \`GET /api/cron/<jobName>\`.
|
|
289
293
|
- To carry context across cron runs, enable memory.
|
|
290
294
|
- **IMPORTANT**: When adding a new cron job, always PRESERVE all existing cron jobs. Never remove or overwrite existing jobs unless the user explicitly asks you to replace or delete them. Read the full current \`cron:\` block before editing, and append the new job alongside the existing ones.
|
|
295
|
+
- **Proactive channel messaging**: Adding \`channel: telegram\` (or \`slack\`) makes the cron job send its response directly to all known conversations on that platform, instead of creating a standalone conversation. The agent continues the existing conversation history for context. A chat must have at least one prior user message for auto-discovery to find it.
|
|
291
296
|
|
|
292
297
|
## Messaging Integrations (Slack, Telegram, Email)
|
|
293
298
|
|
|
@@ -480,6 +485,7 @@ Since all fields have defaults, you only need to specify \`*Env\` when your env
|
|
|
480
485
|
- If shell/CLI access is unavailable, ask the user to run needed commands and provide exact copy-paste commands.
|
|
481
486
|
- For setup, skills, MCP, auth, storage, telemetry, or "how do I..." questions, proactively read \`README.md\` with \`read_file\` before answering.
|
|
482
487
|
- Prefer quoting concrete commands and examples from \`README.md\` over guessing.
|
|
488
|
+
- Prefer \`edit_file\` for targeted changes to existing files (uses exact string matching); use \`write_file\` only for creating new files or full rewrites.
|
|
483
489
|
- Keep edits minimal, preserve unrelated settings/code, and summarize what changed.
|
|
484
490
|
|
|
485
491
|
## Detailed Documentation
|
|
@@ -580,7 +586,7 @@ export class AgentHarness {
|
|
|
580
586
|
private isToolEnabled(name: string): boolean {
|
|
581
587
|
const access = this.resolveToolAccess(name);
|
|
582
588
|
if (access === false) return false;
|
|
583
|
-
if (name === "write_file" || name === "delete_file" || name === "delete_directory") {
|
|
589
|
+
if (name === "write_file" || name === "edit_file" || name === "delete_file" || name === "delete_directory") {
|
|
584
590
|
return this.shouldEnableWriteTool();
|
|
585
591
|
}
|
|
586
592
|
return true;
|
|
@@ -628,6 +634,9 @@ export class AgentHarness {
|
|
|
628
634
|
if (this.isToolEnabled("write_file")) {
|
|
629
635
|
this.registerIfMissing(createWriteTool(this.workingDir));
|
|
630
636
|
}
|
|
637
|
+
if (this.isToolEnabled("edit_file")) {
|
|
638
|
+
this.registerIfMissing(createEditTool(this.workingDir));
|
|
639
|
+
}
|
|
631
640
|
if (this.isToolEnabled("delete_file")) {
|
|
632
641
|
this.registerIfMissing(createDeleteTool(this.workingDir));
|
|
633
642
|
}
|
package/src/state.ts
CHANGED
|
@@ -50,6 +50,11 @@ export interface Conversation {
|
|
|
50
50
|
result?: import("@poncho-ai/sdk").RunResult;
|
|
51
51
|
error?: import("@poncho-ai/sdk").AgentFailure;
|
|
52
52
|
};
|
|
53
|
+
channelMeta?: {
|
|
54
|
+
platform: string;
|
|
55
|
+
channelId: string;
|
|
56
|
+
platformThreadId: string;
|
|
57
|
+
};
|
|
53
58
|
createdAt: number;
|
|
54
59
|
updatedAt: number;
|
|
55
60
|
}
|
|
@@ -247,6 +252,7 @@ export class InMemoryConversationStore implements ConversationStore {
|
|
|
247
252
|
parentConversationId: c.parentConversationId,
|
|
248
253
|
messageCount: c.messages.length,
|
|
249
254
|
hasPendingApprovals: Array.isArray(c.pendingApprovals) && c.pendingApprovals.length > 0,
|
|
255
|
+
channelMeta: c.channelMeta,
|
|
250
256
|
}));
|
|
251
257
|
}
|
|
252
258
|
|
|
@@ -305,6 +311,11 @@ export type ConversationSummary = {
|
|
|
305
311
|
parentConversationId?: string;
|
|
306
312
|
messageCount?: number;
|
|
307
313
|
hasPendingApprovals?: boolean;
|
|
314
|
+
channelMeta?: {
|
|
315
|
+
platform: string;
|
|
316
|
+
channelId: string;
|
|
317
|
+
platformThreadId: string;
|
|
318
|
+
};
|
|
308
319
|
};
|
|
309
320
|
|
|
310
321
|
type ConversationStoreFile = {
|
|
@@ -319,6 +330,11 @@ type ConversationStoreFile = {
|
|
|
319
330
|
parentConversationId?: string;
|
|
320
331
|
messageCount?: number;
|
|
321
332
|
hasPendingApprovals?: boolean;
|
|
333
|
+
channelMeta?: {
|
|
334
|
+
platform: string;
|
|
335
|
+
channelId: string;
|
|
336
|
+
platformThreadId: string;
|
|
337
|
+
};
|
|
322
338
|
}>;
|
|
323
339
|
};
|
|
324
340
|
|
|
@@ -451,6 +467,7 @@ class FileConversationStore implements ConversationStore {
|
|
|
451
467
|
parentConversationId: conversation.parentConversationId,
|
|
452
468
|
messageCount: conversation.messages.length,
|
|
453
469
|
hasPendingApprovals: Array.isArray(conversation.pendingApprovals) && conversation.pendingApprovals.length > 0,
|
|
470
|
+
channelMeta: conversation.channelMeta,
|
|
454
471
|
});
|
|
455
472
|
await this.writeIndex();
|
|
456
473
|
});
|
|
@@ -486,6 +503,7 @@ class FileConversationStore implements ConversationStore {
|
|
|
486
503
|
parentConversationId: c.parentConversationId,
|
|
487
504
|
messageCount: c.messageCount,
|
|
488
505
|
hasPendingApprovals: c.hasPendingApprovals,
|
|
506
|
+
channelMeta: c.channelMeta,
|
|
489
507
|
}));
|
|
490
508
|
}
|
|
491
509
|
|
|
@@ -660,6 +678,11 @@ type ConversationMeta = {
|
|
|
660
678
|
parentConversationId?: string;
|
|
661
679
|
messageCount?: number;
|
|
662
680
|
hasPendingApprovals?: boolean;
|
|
681
|
+
channelMeta?: {
|
|
682
|
+
platform: string;
|
|
683
|
+
channelId: string;
|
|
684
|
+
platformThreadId: string;
|
|
685
|
+
};
|
|
663
686
|
};
|
|
664
687
|
|
|
665
688
|
abstract class KeyValueConversationStoreBase implements ConversationStore {
|
|
@@ -804,6 +827,7 @@ abstract class KeyValueConversationStoreBase implements ConversationStore {
|
|
|
804
827
|
parentConversationId: meta.parentConversationId,
|
|
805
828
|
messageCount: meta.messageCount,
|
|
806
829
|
hasPendingApprovals: meta.hasPendingApprovals,
|
|
830
|
+
channelMeta: meta.channelMeta,
|
|
807
831
|
});
|
|
808
832
|
}
|
|
809
833
|
} catch { /* skip invalid records */ }
|
|
@@ -867,6 +891,7 @@ abstract class KeyValueConversationStoreBase implements ConversationStore {
|
|
|
867
891
|
parentConversationId: nextConversation.parentConversationId,
|
|
868
892
|
messageCount: nextConversation.messages.length,
|
|
869
893
|
hasPendingApprovals: Array.isArray(nextConversation.pendingApprovals) && nextConversation.pendingApprovals.length > 0,
|
|
894
|
+
channelMeta: nextConversation.channelMeta,
|
|
870
895
|
} satisfies ConversationMeta),
|
|
871
896
|
this.ttl,
|
|
872
897
|
);
|
package/test/harness.test.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
1
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
2
|
import { createServer } from "node:http";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "path";
|
|
5
5
|
import { describe, expect, it } from "vitest";
|
|
6
6
|
import type { ToolContext } from "@poncho-ai/sdk";
|
|
7
7
|
import { AgentHarness } from "../src/harness.js";
|
|
8
|
+
import { createEditTool } from "../src/default-tools.js";
|
|
8
9
|
import { loadSkillMetadata } from "../src/skill-context.js";
|
|
9
10
|
|
|
10
11
|
const stubContext: ToolContext = {
|
|
@@ -39,6 +40,7 @@ model:
|
|
|
39
40
|
expect(names).toContain("list_directory");
|
|
40
41
|
expect(names).toContain("read_file");
|
|
41
42
|
expect(names).toContain("write_file");
|
|
43
|
+
expect(names).toContain("edit_file");
|
|
42
44
|
});
|
|
43
45
|
|
|
44
46
|
it("disables write_file by default in production environment", async () => {
|
|
@@ -64,6 +66,7 @@ model:
|
|
|
64
66
|
expect(names).toContain("list_directory");
|
|
65
67
|
expect(names).toContain("read_file");
|
|
66
68
|
expect(names).not.toContain("write_file");
|
|
69
|
+
expect(names).not.toContain("edit_file");
|
|
67
70
|
});
|
|
68
71
|
|
|
69
72
|
it("allows disabling built-in tools via poncho.config.js", async () => {
|
|
@@ -1230,3 +1233,63 @@ allowed-tools:
|
|
|
1230
1233
|
});
|
|
1231
1234
|
|
|
1232
1235
|
});
|
|
1236
|
+
|
|
1237
|
+
describe("edit_file tool", () => {
|
|
1238
|
+
it("replaces a unique string match in a file", async () => {
|
|
1239
|
+
const dir = await mkdtemp(join(tmpdir(), "poncho-edit-tool-"));
|
|
1240
|
+
const filePath = join(dir, "test.txt");
|
|
1241
|
+
await writeFile(filePath, "hello world\nfoo bar\nbaz qux\n", "utf8");
|
|
1242
|
+
|
|
1243
|
+
const tool = createEditTool(dir);
|
|
1244
|
+
const result = await tool.handler(
|
|
1245
|
+
{ path: "test.txt", old_str: "foo bar", new_str: "replaced" },
|
|
1246
|
+
stubContext,
|
|
1247
|
+
);
|
|
1248
|
+
|
|
1249
|
+
expect(result).toEqual({ path: "test.txt", edited: true });
|
|
1250
|
+
const content = await readFile(filePath, "utf8");
|
|
1251
|
+
expect(content).toBe("hello world\nreplaced\nbaz qux\n");
|
|
1252
|
+
});
|
|
1253
|
+
|
|
1254
|
+
it("errors when old_str is not found in the file", async () => {
|
|
1255
|
+
const dir = await mkdtemp(join(tmpdir(), "poncho-edit-tool-notfound-"));
|
|
1256
|
+
await writeFile(join(dir, "test.txt"), "hello world\n", "utf8");
|
|
1257
|
+
|
|
1258
|
+
const tool = createEditTool(dir);
|
|
1259
|
+
await expect(
|
|
1260
|
+
tool.handler({ path: "test.txt", old_str: "nonexistent", new_str: "x" }, stubContext),
|
|
1261
|
+
).rejects.toThrow("old_str not found in file");
|
|
1262
|
+
});
|
|
1263
|
+
|
|
1264
|
+
it("errors when old_str matches multiple locations", async () => {
|
|
1265
|
+
const dir = await mkdtemp(join(tmpdir(), "poncho-edit-tool-multi-"));
|
|
1266
|
+
await writeFile(join(dir, "test.txt"), "aaa\nbbb\naaa\n", "utf8");
|
|
1267
|
+
|
|
1268
|
+
const tool = createEditTool(dir);
|
|
1269
|
+
await expect(
|
|
1270
|
+
tool.handler({ path: "test.txt", old_str: "aaa", new_str: "ccc" }, stubContext),
|
|
1271
|
+
).rejects.toThrow("old_str appears multiple times");
|
|
1272
|
+
});
|
|
1273
|
+
|
|
1274
|
+
it("deletes matched content when new_str is empty", async () => {
|
|
1275
|
+
const dir = await mkdtemp(join(tmpdir(), "poncho-edit-tool-delete-"));
|
|
1276
|
+
const filePath = join(dir, "test.txt");
|
|
1277
|
+
await writeFile(filePath, "keep this\nremove this\nkeep this too\n", "utf8");
|
|
1278
|
+
|
|
1279
|
+
const tool = createEditTool(dir);
|
|
1280
|
+
await tool.handler({ path: "test.txt", old_str: "remove this\n", new_str: "" }, stubContext);
|
|
1281
|
+
|
|
1282
|
+
const content = await readFile(filePath, "utf8");
|
|
1283
|
+
expect(content).toBe("keep this\nkeep this too\n");
|
|
1284
|
+
});
|
|
1285
|
+
|
|
1286
|
+
it("errors when old_str is empty", async () => {
|
|
1287
|
+
const dir = await mkdtemp(join(tmpdir(), "poncho-edit-tool-empty-"));
|
|
1288
|
+
await writeFile(join(dir, "test.txt"), "content\n", "utf8");
|
|
1289
|
+
|
|
1290
|
+
const tool = createEditTool(dir);
|
|
1291
|
+
await expect(
|
|
1292
|
+
tool.handler({ path: "test.txt", old_str: "", new_str: "x" }, stubContext),
|
|
1293
|
+
).rejects.toThrow("old_str must not be empty");
|
|
1294
|
+
});
|
|
1295
|
+
});
|