@poncho-ai/harness 0.12.0 → 0.13.1
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 +15 -0
- package/dist/index.d.ts +14 -1
- package/dist/index.js +102 -1
- package/package.json +2 -2
- package/src/agent-parser.ts +76 -0
- package/src/config.ts +7 -0
- package/src/harness.ts +49 -1
- package/test/agent-parser.test.ts +118 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @poncho-ai/harness@0.
|
|
2
|
+
> @poncho-ai/harness@0.13.1 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[32m162.92 KB[39m
|
|
11
|
+
[32mESM[39m ⚡️ Build success in 81ms
|
|
12
12
|
[34mDTS[39m Build start
|
|
13
|
-
[32mDTS[39m ⚡️ Build success in
|
|
14
|
-
[32mDTS[39m [1mdist/index.d.ts [22m[32m19.
|
|
13
|
+
[32mDTS[39m ⚡️ Build success in 5035ms
|
|
14
|
+
[32mDTS[39m [1mdist/index.d.ts [22m[32m19.73 KB[39m
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# @poncho-ai/harness
|
|
2
2
|
|
|
3
|
+
## 0.13.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#10](https://github.com/cesr/poncho-ai/pull/10) [`d5bce7b`](https://github.com/cesr/poncho-ai/commit/d5bce7be5890c657bea915eb0926feb6de66b218) Thanks [@cesr](https://github.com/cesr)! - Add generic messaging layer with Slack as the first adapter. Agents can now respond to @mentions in Slack by adding `messaging: [{ platform: 'slack' }]` to `poncho.config.js`. Includes signature verification, threaded conversations, processing indicators, and Vercel `waitUntil` support.
|
|
8
|
+
|
|
9
|
+
- Updated dependencies [[`d5bce7b`](https://github.com/cesr/poncho-ai/commit/d5bce7be5890c657bea915eb0926feb6de66b218)]:
|
|
10
|
+
- @poncho-ai/sdk@1.0.1
|
|
11
|
+
|
|
12
|
+
## 0.13.0
|
|
13
|
+
|
|
14
|
+
### Minor Changes
|
|
15
|
+
|
|
16
|
+
- [#8](https://github.com/cesr/poncho-ai/pull/8) [`658bc54`](https://github.com/cesr/poncho-ai/commit/658bc54d391cb0b58aa678a2b86cd617eebdd8aa) Thanks [@cesr](https://github.com/cesr)! - Add cron job support for scheduled agent tasks. Define recurring jobs in AGENT.md frontmatter with schedule, task, and optional timezone. Includes in-process scheduler for local dev with hot-reload, HTTP endpoint for Vercel/serverless with self-continuation, Vercel scaffold generation with drift detection, and full tool activity tracking in cron conversations.
|
|
17
|
+
|
|
3
18
|
## 0.12.0
|
|
4
19
|
|
|
5
20
|
### Minor Changes
|
package/dist/index.d.ts
CHANGED
|
@@ -13,6 +13,11 @@ interface AgentLimitsConfig {
|
|
|
13
13
|
maxSteps?: number;
|
|
14
14
|
timeout?: number;
|
|
15
15
|
}
|
|
16
|
+
interface CronJobConfig {
|
|
17
|
+
schedule: string;
|
|
18
|
+
task: string;
|
|
19
|
+
timezone?: string;
|
|
20
|
+
}
|
|
16
21
|
interface AgentFrontmatter {
|
|
17
22
|
name: string;
|
|
18
23
|
id?: string;
|
|
@@ -27,6 +32,7 @@ interface AgentFrontmatter {
|
|
|
27
32
|
mcp?: string[];
|
|
28
33
|
scripts?: string[];
|
|
29
34
|
};
|
|
35
|
+
cron?: Record<string, CronJobConfig>;
|
|
30
36
|
}
|
|
31
37
|
interface ParsedAgent {
|
|
32
38
|
frontmatter: AgentFrontmatter;
|
|
@@ -229,8 +235,14 @@ type BuiltInToolToggles = {
|
|
|
229
235
|
read_file?: boolean;
|
|
230
236
|
write_file?: boolean;
|
|
231
237
|
};
|
|
238
|
+
interface MessagingChannelConfig {
|
|
239
|
+
platform: "slack";
|
|
240
|
+
botTokenEnv?: string;
|
|
241
|
+
signingSecretEnv?: string;
|
|
242
|
+
}
|
|
232
243
|
interface PonchoConfig extends McpConfig {
|
|
233
244
|
harness?: string;
|
|
245
|
+
messaging?: MessagingChannelConfig[];
|
|
234
246
|
tools?: {
|
|
235
247
|
defaults?: BuiltInToolToggles;
|
|
236
248
|
byEnvironment?: {
|
|
@@ -374,6 +386,7 @@ declare class AgentHarness {
|
|
|
374
386
|
private registerConfiguredBuiltInTools;
|
|
375
387
|
private shouldEnableWriteTool;
|
|
376
388
|
constructor(options?: HarnessOptions);
|
|
389
|
+
get frontmatter(): AgentFrontmatter | undefined;
|
|
377
390
|
private listActiveSkills;
|
|
378
391
|
private getAgentMcpIntent;
|
|
379
392
|
private getAgentScriptIntent;
|
|
@@ -547,4 +560,4 @@ declare class ToolDispatcher {
|
|
|
547
560
|
executeBatch(calls: ToolCall[], context: ToolContext): Promise<ToolExecutionResult[]>;
|
|
548
561
|
}
|
|
549
562
|
|
|
550
|
-
export { type AgentFrontmatter, AgentHarness, type AgentIdentity, type AgentLimitsConfig, type AgentModelConfig, type BuiltInToolToggles, type Conversation, type ConversationState, type ConversationStore, type HarnessOptions, type HarnessRunOutput, InMemoryConversationStore, InMemoryStateStore, LatitudeCapture, type LatitudeCaptureConfig, LocalMcpBridge, LocalUploadStore, type MainMemory, type McpConfig, type MemoryConfig, type MemoryStore, type ModelProviderFactory, PONCHO_UPLOAD_SCHEME, type ParsedAgent, type PonchoConfig, type RemoteMcpServerConfig, type RuntimeRenderContext, S3UploadStore, STORAGE_SCHEMA_VERSION, type SkillContextEntry, type SkillMetadata, type StateConfig, type StateProviderName, type StateStore, type StorageConfig, type TelemetryConfig, TelemetryEmitter, type ToolCall, ToolDispatcher, type ToolExecutionResult, type UploadStore, type UploadsConfig, VercelBlobUploadStore, buildAgentDirectoryName, buildSkillContextWindow, createConversationStore, createDefaultTools, createMemoryStore, createMemoryTools, createModelProvider, createSkillTools, createStateStore, createUploadStore, createWriteTool, deriveUploadKey, ensureAgentIdentity, generateAgentId, getAgentStoreDirectory, getPonchoStoreRoot, jsonSchemaToZod, loadPonchoConfig, loadSkillContext, loadSkillInstructions, loadSkillMetadata, normalizeScriptPolicyPath, parseAgentFile, parseAgentMarkdown, readSkillResource, renderAgentPrompt, resolveAgentIdentity, resolveMemoryConfig, resolveSkillDirs, resolveStateConfig, slugifyStorageComponent };
|
|
563
|
+
export { type AgentFrontmatter, AgentHarness, type AgentIdentity, type AgentLimitsConfig, type AgentModelConfig, type BuiltInToolToggles, type Conversation, type ConversationState, type ConversationStore, 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 RemoteMcpServerConfig, type RuntimeRenderContext, S3UploadStore, STORAGE_SCHEMA_VERSION, type SkillContextEntry, type SkillMetadata, type StateConfig, type StateProviderName, type StateStore, type StorageConfig, type TelemetryConfig, TelemetryEmitter, type ToolCall, ToolDispatcher, type ToolExecutionResult, type UploadStore, type UploadsConfig, VercelBlobUploadStore, buildAgentDirectoryName, buildSkillContextWindow, createConversationStore, createDefaultTools, createMemoryStore, createMemoryTools, createModelProvider, createSkillTools, createStateStore, createUploadStore, createWriteTool, deriveUploadKey, ensureAgentIdentity, generateAgentId, getAgentStoreDirectory, getPonchoStoreRoot, jsonSchemaToZod, loadPonchoConfig, loadSkillContext, loadSkillInstructions, loadSkillMetadata, normalizeScriptPolicyPath, parseAgentFile, parseAgentMarkdown, readSkillResource, renderAgentPrompt, resolveAgentIdentity, resolveMemoryConfig, resolveSkillDirs, resolveStateConfig, slugifyStorageComponent };
|
package/dist/index.js
CHANGED
|
@@ -67,6 +67,59 @@ var matchesSlashPattern = (value, pattern) => {
|
|
|
67
67
|
var FRONTMATTER_PATTERN = /^---\s*\n([\s\S]*?)\n---\s*\n?([\s\S]*)$/;
|
|
68
68
|
var asRecord = (value) => typeof value === "object" && value !== null ? value : {};
|
|
69
69
|
var asNumberOrUndefined = (value) => typeof value === "number" ? value : void 0;
|
|
70
|
+
var CRON_EXPRESSION_PATTERN = /^(\S+\s+){4}\S+$/;
|
|
71
|
+
var validateCronExpression = (expr, path) => {
|
|
72
|
+
if (!CRON_EXPRESSION_PATTERN.test(expr.trim())) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
`Invalid cron expression at ${path}: "${expr}". Expected 5-field cron format (minute hour day month weekday).`
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
var KNOWN_TIMEZONES = (() => {
|
|
79
|
+
try {
|
|
80
|
+
return new Set(Intl.supportedValuesOf("timeZone"));
|
|
81
|
+
} catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
})();
|
|
85
|
+
var validateTimezone = (tz, path) => {
|
|
86
|
+
if (KNOWN_TIMEZONES && !KNOWN_TIMEZONES.has(tz)) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`Invalid timezone at ${path}: "${tz}". Expected an IANA timezone string (e.g. "America/New_York", "UTC").`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
var parseCronJobs = (value) => {
|
|
93
|
+
const raw = asRecord(value);
|
|
94
|
+
const keys = Object.keys(raw);
|
|
95
|
+
if (keys.length === 0) return void 0;
|
|
96
|
+
const jobs = {};
|
|
97
|
+
for (const jobName of keys) {
|
|
98
|
+
const jobValue = asRecord(raw[jobName]);
|
|
99
|
+
const path = `AGENT.md frontmatter cron.${jobName}`;
|
|
100
|
+
if (typeof jobValue.schedule !== "string" || jobValue.schedule.trim() === "") {
|
|
101
|
+
throw new Error(
|
|
102
|
+
`Invalid ${path}: "schedule" is required and must be a non-empty string.`
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
if (typeof jobValue.task !== "string" || jobValue.task.trim() === "") {
|
|
106
|
+
throw new Error(
|
|
107
|
+
`Invalid ${path}: "task" is required and must be a non-empty string.`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
validateCronExpression(jobValue.schedule, path);
|
|
111
|
+
const timezone = typeof jobValue.timezone === "string" && jobValue.timezone.trim() ? jobValue.timezone.trim() : void 0;
|
|
112
|
+
if (timezone) {
|
|
113
|
+
validateTimezone(timezone, path);
|
|
114
|
+
}
|
|
115
|
+
jobs[jobName] = {
|
|
116
|
+
schedule: jobValue.schedule.trim(),
|
|
117
|
+
task: jobValue.task,
|
|
118
|
+
timezone
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
return jobs;
|
|
122
|
+
};
|
|
70
123
|
var parseAgentMarkdown = (content) => {
|
|
71
124
|
const match = content.match(FRONTMATTER_PATTERN);
|
|
72
125
|
if (!match) {
|
|
@@ -144,7 +197,8 @@ var parseAgentMarkdown = (content) => {
|
|
|
144
197
|
approvalRequired: approvalRequired.mcp.length > 0 || approvalRequired.scripts.length > 0 ? {
|
|
145
198
|
mcp: approvalRequired.mcp.length > 0 ? approvalRequired.mcp : void 0,
|
|
146
199
|
scripts: approvalRequired.scripts.length > 0 ? approvalRequired.scripts : void 0
|
|
147
|
-
} : void 0
|
|
200
|
+
} : void 0,
|
|
201
|
+
cron: parseCronJobs(parsed.cron)
|
|
148
202
|
};
|
|
149
203
|
return {
|
|
150
204
|
frontmatter,
|
|
@@ -2559,6 +2613,50 @@ You can extend your own capabilities by creating custom JavaScript/TypeScript sc
|
|
|
2559
2613
|
- Script entries outside \`./scripts/\` must also appear in \`allowed-tools\`.
|
|
2560
2614
|
- Keep MCP server connection details (\`url\`, auth env vars) in \`poncho.config.js\` only.
|
|
2561
2615
|
|
|
2616
|
+
## Cron Jobs
|
|
2617
|
+
|
|
2618
|
+
Users can define scheduled tasks in \`AGENT.md\` frontmatter:
|
|
2619
|
+
|
|
2620
|
+
\`\`\`yaml
|
|
2621
|
+
cron:
|
|
2622
|
+
daily-report:
|
|
2623
|
+
schedule: "0 9 * * *" # Standard 5-field cron expression
|
|
2624
|
+
timezone: "America/New_York" # Optional IANA timezone (default: UTC)
|
|
2625
|
+
task: "Generate the daily sales report"
|
|
2626
|
+
\`\`\`
|
|
2627
|
+
|
|
2628
|
+
- Each cron job triggers an autonomous agent run with the specified task, creating a fresh conversation.
|
|
2629
|
+
- In \`poncho dev\`, jobs run via an in-process scheduler and appear in the web UI sidebar (prefixed with \`[cron]\`).
|
|
2630
|
+
- For Vercel: \`poncho build vercel\` generates \`vercel.json\` cron entries. Set \`CRON_SECRET\` = \`PONCHO_AUTH_TOKEN\`.
|
|
2631
|
+
- Jobs can also be triggered manually: \`GET /api/cron/<jobName>\`.
|
|
2632
|
+
- To carry context across cron runs, enable memory.
|
|
2633
|
+
- **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.
|
|
2634
|
+
|
|
2635
|
+
## Messaging Integrations (Slack, etc.)
|
|
2636
|
+
|
|
2637
|
+
Users can connect this agent to messaging platforms so it responds to @mentions.
|
|
2638
|
+
|
|
2639
|
+
### Slack Setup
|
|
2640
|
+
|
|
2641
|
+
1. Create a Slack App at https://api.slack.com/apps ("From scratch")
|
|
2642
|
+
2. Under **OAuth & Permissions**, add Bot Token Scopes: \`app_mentions:read\`, \`chat:write\`, \`reactions:write\`
|
|
2643
|
+
3. Under **Event Subscriptions**, enable events, set the Request URL to \`https://<deployed-url>/api/messaging/slack\`, and subscribe to \`app_mention\`
|
|
2644
|
+
4. Install the app to the workspace (generates Bot Token \`xoxb-...\`)
|
|
2645
|
+
5. Copy the **Signing Secret** from the Basic Information page
|
|
2646
|
+
6. Add env vars:
|
|
2647
|
+
\`\`\`
|
|
2648
|
+
SLACK_BOT_TOKEN=xoxb-...
|
|
2649
|
+
SLACK_SIGNING_SECRET=...
|
|
2650
|
+
\`\`\`
|
|
2651
|
+
7. Add to \`poncho.config.js\`:
|
|
2652
|
+
\`\`\`javascript
|
|
2653
|
+
messaging: [{ platform: 'slack' }]
|
|
2654
|
+
\`\`\`
|
|
2655
|
+
8. Deploy (or use a tunnel like ngrok for local dev)
|
|
2656
|
+
9. **Vercel only:** install \`@vercel/functions\` so the serverless function stays alive while processing messages (\`npm install @vercel/functions\`)
|
|
2657
|
+
|
|
2658
|
+
The agent will respond in Slack threads when @mentioned. Each Slack thread maps to a separate Poncho conversation.
|
|
2659
|
+
|
|
2562
2660
|
## When users ask about customization:
|
|
2563
2661
|
|
|
2564
2662
|
- Explain and edit \`poncho.config.js\` for model/provider, storage+memory, auth, telemetry, and MCP settings.
|
|
@@ -2654,6 +2752,9 @@ var AgentHarness = class {
|
|
|
2654
2752
|
this.dispatcher.registerMany(options.toolDefinitions);
|
|
2655
2753
|
}
|
|
2656
2754
|
}
|
|
2755
|
+
get frontmatter() {
|
|
2756
|
+
return this.parsedAgent?.frontmatter;
|
|
2757
|
+
}
|
|
2657
2758
|
listActiveSkills() {
|
|
2658
2759
|
return [...this.activeSkillNames].sort();
|
|
2659
2760
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@poncho-ai/harness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.1",
|
|
4
4
|
"description": "Agent execution runtime - conversation loop, tool dispatch, streaming",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"redis": "^5.10.0",
|
|
31
31
|
"yaml": "^2.4.0",
|
|
32
32
|
"zod": "^3.22.0",
|
|
33
|
-
"@poncho-ai/sdk": "1.0.
|
|
33
|
+
"@poncho-ai/sdk": "1.0.1"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@types/mustache": "^4.2.6",
|
package/src/agent-parser.ts
CHANGED
|
@@ -22,6 +22,12 @@ export interface AgentLimitsConfig {
|
|
|
22
22
|
timeout?: number;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
export interface CronJobConfig {
|
|
26
|
+
schedule: string;
|
|
27
|
+
task: string;
|
|
28
|
+
timezone?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
25
31
|
export interface AgentFrontmatter {
|
|
26
32
|
name: string;
|
|
27
33
|
id?: string;
|
|
@@ -36,6 +42,7 @@ export interface AgentFrontmatter {
|
|
|
36
42
|
mcp?: string[];
|
|
37
43
|
scripts?: string[];
|
|
38
44
|
};
|
|
45
|
+
cron?: Record<string, CronJobConfig>;
|
|
39
46
|
}
|
|
40
47
|
|
|
41
48
|
export interface ParsedAgent {
|
|
@@ -63,6 +70,74 @@ const asRecord = (value: unknown): Record<string, unknown> =>
|
|
|
63
70
|
const asNumberOrUndefined = (value: unknown): number | undefined =>
|
|
64
71
|
typeof value === "number" ? value : undefined;
|
|
65
72
|
|
|
73
|
+
const CRON_EXPRESSION_PATTERN = /^(\S+\s+){4}\S+$/;
|
|
74
|
+
|
|
75
|
+
const validateCronExpression = (expr: string, path: string): void => {
|
|
76
|
+
if (!CRON_EXPRESSION_PATTERN.test(expr.trim())) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
`Invalid cron expression at ${path}: "${expr}". Expected 5-field cron format (minute hour day month weekday).`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const KNOWN_TIMEZONES: Set<string> | null = (() => {
|
|
84
|
+
try {
|
|
85
|
+
return new Set(Intl.supportedValuesOf("timeZone"));
|
|
86
|
+
} catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
})();
|
|
90
|
+
|
|
91
|
+
const validateTimezone = (tz: string, path: string): void => {
|
|
92
|
+
if (KNOWN_TIMEZONES && !KNOWN_TIMEZONES.has(tz)) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
`Invalid timezone at ${path}: "${tz}". Expected an IANA timezone string (e.g. "America/New_York", "UTC").`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const parseCronJobs = (
|
|
100
|
+
value: unknown,
|
|
101
|
+
): Record<string, CronJobConfig> | undefined => {
|
|
102
|
+
const raw = asRecord(value);
|
|
103
|
+
const keys = Object.keys(raw);
|
|
104
|
+
if (keys.length === 0) return undefined;
|
|
105
|
+
|
|
106
|
+
const jobs: Record<string, CronJobConfig> = {};
|
|
107
|
+
for (const jobName of keys) {
|
|
108
|
+
const jobValue = asRecord(raw[jobName]);
|
|
109
|
+
const path = `AGENT.md frontmatter cron.${jobName}`;
|
|
110
|
+
|
|
111
|
+
if (typeof jobValue.schedule !== "string" || jobValue.schedule.trim() === "") {
|
|
112
|
+
throw new Error(
|
|
113
|
+
`Invalid ${path}: "schedule" is required and must be a non-empty string.`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
if (typeof jobValue.task !== "string" || jobValue.task.trim() === "") {
|
|
117
|
+
throw new Error(
|
|
118
|
+
`Invalid ${path}: "task" is required and must be a non-empty string.`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
validateCronExpression(jobValue.schedule, path);
|
|
123
|
+
|
|
124
|
+
const timezone =
|
|
125
|
+
typeof jobValue.timezone === "string" && jobValue.timezone.trim()
|
|
126
|
+
? jobValue.timezone.trim()
|
|
127
|
+
: undefined;
|
|
128
|
+
if (timezone) {
|
|
129
|
+
validateTimezone(timezone, path);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
jobs[jobName] = {
|
|
133
|
+
schedule: jobValue.schedule.trim(),
|
|
134
|
+
task: jobValue.task,
|
|
135
|
+
timezone,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
return jobs;
|
|
139
|
+
};
|
|
140
|
+
|
|
66
141
|
export const parseAgentMarkdown = (content: string): ParsedAgent => {
|
|
67
142
|
const match = content.match(FRONTMATTER_PATTERN);
|
|
68
143
|
|
|
@@ -175,6 +250,7 @@ export const parseAgentMarkdown = (content: string): ParsedAgent => {
|
|
|
175
250
|
: undefined,
|
|
176
251
|
}
|
|
177
252
|
: undefined,
|
|
253
|
+
cron: parseCronJobs(parsed.cron),
|
|
178
254
|
};
|
|
179
255
|
|
|
180
256
|
return {
|
package/src/config.ts
CHANGED
|
@@ -38,8 +38,15 @@ export type BuiltInToolToggles = {
|
|
|
38
38
|
write_file?: boolean;
|
|
39
39
|
};
|
|
40
40
|
|
|
41
|
+
export interface MessagingChannelConfig {
|
|
42
|
+
platform: "slack";
|
|
43
|
+
botTokenEnv?: string;
|
|
44
|
+
signingSecretEnv?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
41
47
|
export interface PonchoConfig extends McpConfig {
|
|
42
48
|
harness?: string;
|
|
49
|
+
messaging?: MessagingChannelConfig[];
|
|
43
50
|
tools?: {
|
|
44
51
|
defaults?: BuiltInToolToggles;
|
|
45
52
|
byEnvironment?: {
|
package/src/harness.ts
CHANGED
|
@@ -13,7 +13,7 @@ import type {
|
|
|
13
13
|
import { getTextContent } from "@poncho-ai/sdk";
|
|
14
14
|
import type { UploadStore } from "./upload-store.js";
|
|
15
15
|
import { PONCHO_UPLOAD_SCHEME, deriveUploadKey } from "./upload-store.js";
|
|
16
|
-
import { parseAgentFile, renderAgentPrompt, type ParsedAgent } from "./agent-parser.js";
|
|
16
|
+
import { parseAgentFile, renderAgentPrompt, type ParsedAgent, type AgentFrontmatter } from "./agent-parser.js";
|
|
17
17
|
import { loadPonchoConfig, resolveMemoryConfig, type PonchoConfig } from "./config.js";
|
|
18
18
|
import { createDefaultTools, createWriteTool } from "./default-tools.js";
|
|
19
19
|
import {
|
|
@@ -236,6 +236,50 @@ You can extend your own capabilities by creating custom JavaScript/TypeScript sc
|
|
|
236
236
|
- Script entries outside \`./scripts/\` must also appear in \`allowed-tools\`.
|
|
237
237
|
- Keep MCP server connection details (\`url\`, auth env vars) in \`poncho.config.js\` only.
|
|
238
238
|
|
|
239
|
+
## Cron Jobs
|
|
240
|
+
|
|
241
|
+
Users can define scheduled tasks in \`AGENT.md\` frontmatter:
|
|
242
|
+
|
|
243
|
+
\`\`\`yaml
|
|
244
|
+
cron:
|
|
245
|
+
daily-report:
|
|
246
|
+
schedule: "0 9 * * *" # Standard 5-field cron expression
|
|
247
|
+
timezone: "America/New_York" # Optional IANA timezone (default: UTC)
|
|
248
|
+
task: "Generate the daily sales report"
|
|
249
|
+
\`\`\`
|
|
250
|
+
|
|
251
|
+
- Each cron job triggers an autonomous agent run with the specified task, creating a fresh conversation.
|
|
252
|
+
- In \`poncho dev\`, jobs run via an in-process scheduler and appear in the web UI sidebar (prefixed with \`[cron]\`).
|
|
253
|
+
- For Vercel: \`poncho build vercel\` generates \`vercel.json\` cron entries. Set \`CRON_SECRET\` = \`PONCHO_AUTH_TOKEN\`.
|
|
254
|
+
- Jobs can also be triggered manually: \`GET /api/cron/<jobName>\`.
|
|
255
|
+
- To carry context across cron runs, enable memory.
|
|
256
|
+
- **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.
|
|
257
|
+
|
|
258
|
+
## Messaging Integrations (Slack, etc.)
|
|
259
|
+
|
|
260
|
+
Users can connect this agent to messaging platforms so it responds to @mentions.
|
|
261
|
+
|
|
262
|
+
### Slack Setup
|
|
263
|
+
|
|
264
|
+
1. Create a Slack App at https://api.slack.com/apps ("From scratch")
|
|
265
|
+
2. Under **OAuth & Permissions**, add Bot Token Scopes: \`app_mentions:read\`, \`chat:write\`, \`reactions:write\`
|
|
266
|
+
3. Under **Event Subscriptions**, enable events, set the Request URL to \`https://<deployed-url>/api/messaging/slack\`, and subscribe to \`app_mention\`
|
|
267
|
+
4. Install the app to the workspace (generates Bot Token \`xoxb-...\`)
|
|
268
|
+
5. Copy the **Signing Secret** from the Basic Information page
|
|
269
|
+
6. Add env vars:
|
|
270
|
+
\`\`\`
|
|
271
|
+
SLACK_BOT_TOKEN=xoxb-...
|
|
272
|
+
SLACK_SIGNING_SECRET=...
|
|
273
|
+
\`\`\`
|
|
274
|
+
7. Add to \`poncho.config.js\`:
|
|
275
|
+
\`\`\`javascript
|
|
276
|
+
messaging: [{ platform: 'slack' }]
|
|
277
|
+
\`\`\`
|
|
278
|
+
8. Deploy (or use a tunnel like ngrok for local dev)
|
|
279
|
+
9. **Vercel only:** install \`@vercel/functions\` so the serverless function stays alive while processing messages (\`npm install @vercel/functions\`)
|
|
280
|
+
|
|
281
|
+
The agent will respond in Slack threads when @mentioned. Each Slack thread maps to a separate Poncho conversation.
|
|
282
|
+
|
|
239
283
|
## When users ask about customization:
|
|
240
284
|
|
|
241
285
|
- Explain and edit \`poncho.config.js\` for model/provider, storage+memory, auth, telemetry, and MCP settings.
|
|
@@ -344,6 +388,10 @@ export class AgentHarness {
|
|
|
344
388
|
}
|
|
345
389
|
}
|
|
346
390
|
|
|
391
|
+
get frontmatter(): AgentFrontmatter | undefined {
|
|
392
|
+
return this.parsedAgent?.frontmatter;
|
|
393
|
+
}
|
|
394
|
+
|
|
347
395
|
private listActiveSkills(): string[] {
|
|
348
396
|
return [...this.activeSkillNames].sort();
|
|
349
397
|
}
|
|
@@ -37,6 +37,124 @@ Env: {{runtime.environment}}
|
|
|
37
37
|
expect(prompt).toContain("Env: development");
|
|
38
38
|
});
|
|
39
39
|
|
|
40
|
+
describe("cron jobs", () => {
|
|
41
|
+
it("parses cron jobs from frontmatter", () => {
|
|
42
|
+
const parsed = parseAgentMarkdown(`---
|
|
43
|
+
name: test-agent
|
|
44
|
+
cron:
|
|
45
|
+
daily-report:
|
|
46
|
+
schedule: "0 9 * * *"
|
|
47
|
+
task: "Generate the daily report"
|
|
48
|
+
health-check:
|
|
49
|
+
schedule: "*/30 * * * *"
|
|
50
|
+
timezone: "America/New_York"
|
|
51
|
+
task: "Check all APIs"
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
# Agent
|
|
55
|
+
`);
|
|
56
|
+
expect(parsed.frontmatter.cron).toBeDefined();
|
|
57
|
+
expect(Object.keys(parsed.frontmatter.cron!)).toEqual([
|
|
58
|
+
"daily-report",
|
|
59
|
+
"health-check",
|
|
60
|
+
]);
|
|
61
|
+
expect(parsed.frontmatter.cron!["daily-report"]).toEqual({
|
|
62
|
+
schedule: "0 9 * * *",
|
|
63
|
+
task: "Generate the daily report",
|
|
64
|
+
timezone: undefined,
|
|
65
|
+
});
|
|
66
|
+
expect(parsed.frontmatter.cron!["health-check"]).toEqual({
|
|
67
|
+
schedule: "*/30 * * * *",
|
|
68
|
+
task: "Check all APIs",
|
|
69
|
+
timezone: "America/New_York",
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("returns undefined cron when not defined", () => {
|
|
74
|
+
const parsed = parseAgentMarkdown(`---
|
|
75
|
+
name: test-agent
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
# Agent
|
|
79
|
+
`);
|
|
80
|
+
expect(parsed.frontmatter.cron).toBeUndefined();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("throws on missing schedule", () => {
|
|
84
|
+
expect(() =>
|
|
85
|
+
parseAgentMarkdown(`---
|
|
86
|
+
name: test-agent
|
|
87
|
+
cron:
|
|
88
|
+
bad-job:
|
|
89
|
+
task: "Do something"
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
# Agent
|
|
93
|
+
`),
|
|
94
|
+
).toThrow(/"schedule" is required/);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("throws on missing task", () => {
|
|
98
|
+
expect(() =>
|
|
99
|
+
parseAgentMarkdown(`---
|
|
100
|
+
name: test-agent
|
|
101
|
+
cron:
|
|
102
|
+
bad-job:
|
|
103
|
+
schedule: "0 9 * * *"
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
# Agent
|
|
107
|
+
`),
|
|
108
|
+
).toThrow(/"task" is required/);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("throws on invalid cron expression", () => {
|
|
112
|
+
expect(() =>
|
|
113
|
+
parseAgentMarkdown(`---
|
|
114
|
+
name: test-agent
|
|
115
|
+
cron:
|
|
116
|
+
bad-job:
|
|
117
|
+
schedule: "every day"
|
|
118
|
+
task: "Do something"
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
# Agent
|
|
122
|
+
`),
|
|
123
|
+
).toThrow(/Invalid cron expression/);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("throws on invalid timezone", () => {
|
|
127
|
+
expect(() =>
|
|
128
|
+
parseAgentMarkdown(`---
|
|
129
|
+
name: test-agent
|
|
130
|
+
cron:
|
|
131
|
+
bad-job:
|
|
132
|
+
schedule: "0 9 * * *"
|
|
133
|
+
timezone: "Fake/Zone"
|
|
134
|
+
task: "Do something"
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
# Agent
|
|
138
|
+
`),
|
|
139
|
+
).toThrow(/Invalid timezone/);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("accepts valid timezone", () => {
|
|
143
|
+
const parsed = parseAgentMarkdown(`---
|
|
144
|
+
name: test-agent
|
|
145
|
+
cron:
|
|
146
|
+
job:
|
|
147
|
+
schedule: "0 9 * * *"
|
|
148
|
+
timezone: "Europe/London"
|
|
149
|
+
task: "Do something"
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
# Agent
|
|
153
|
+
`);
|
|
154
|
+
expect(parsed.frontmatter.cron!["job"]!.timezone).toBe("Europe/London");
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
40
158
|
it("parses approval-required with relative script paths", () => {
|
|
41
159
|
const parsed = parseAgentMarkdown(`---
|
|
42
160
|
name: test-agent
|