loopengine 0.1.92 → 0.1.94
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/README.md +14 -5
- package/dist/core/agent-config.d.ts +19 -7
- package/dist/core/email-senders.d.ts +26 -0
- package/dist/core/email-senders.js +108 -0
- package/dist/core/http-notifier.js +6 -6
- package/dist/core/http-notify-triggers/email.d.ts +14 -8
- package/dist/core/http-notify-triggers/email.js +70 -28
- package/dist/core/notifier-file.d.ts +11 -3
- package/dist/core/notifier-file.js +37 -5
- package/dist/core/scheduler.d.ts +22 -0
- package/dist/core/scheduler.js +111 -12
- package/dist/core/system-agents/templates.d.ts +43 -0
- package/dist/core/system-agents/templates.js +290 -48
- package/dist/core/system-tools/schedule.js +16 -6
- package/dist/core/time-zone.d.ts +40 -0
- package/dist/core/time-zone.js +138 -0
- package/dist/index.d.ts +6 -5
- package/dist/index.js +5 -4
- package/dist/web/agents-config-page.js +49 -11
- package/dist/web/global-config-page.js +65 -0
- package/dist/web/global-config.d.ts +24 -0
- package/dist/web/global-config.js +30 -0
- package/dist/web/notifier-admin.js +11 -2
- package/dist/web/templates-admin.d.ts +82 -7
- package/dist/web/templates-admin.js +207 -16
- package/dist/web/templates-page.js +31 -6
- package/package.json +5 -1
|
@@ -1,53 +1,135 @@
|
|
|
1
|
+
import { nextTimeOfDay, parseTimeOfDay, workspaceTimeZone } from '../time-zone.js';
|
|
2
|
+
const GMAIL_KEYS = ['GMAIL_CLIENT_ID', 'GMAIL_CLIENT_SECRET', 'GMAIL_REFRESH_TOKEN'];
|
|
3
|
+
const SHOPIFY_KEYS = ['SHOPIFY_STORE_DOMAIN', 'SHOPIFY_ADMIN_TOKEN'];
|
|
4
|
+
const GITHUB_KEYS = ['GITHUB_TOKEN', 'GITHUB_DEFAULT_REPO'];
|
|
5
|
+
const SLACK_KEYS = ['SLACK_BOT_TOKEN', 'SLACK_DEFAULT_CHANNEL'];
|
|
6
|
+
const WEB_SEARCH_KEYS = [['TAVILY_API_KEY', 'BRAVE_API_KEY', 'FIRECRAWL_API_KEY']];
|
|
7
|
+
const DOCS_NOTE = 'Put your policies and help articles in the project\'s docs folder, as Markdown or text files (or point DOCS_SEARCH_PATH at another folder)';
|
|
1
8
|
export const STARTER_TEMPLATES = [
|
|
2
9
|
{
|
|
3
10
|
id: 'customer-email-replies',
|
|
4
11
|
category: 'Customer service',
|
|
5
12
|
candidate: {
|
|
6
13
|
title: 'Answer customer emails',
|
|
7
|
-
summary: '
|
|
8
|
-
trigger: { kind: '
|
|
14
|
+
summary: 'Checks the support inbox every 10 minutes, looks up each customer\'s order, and replies. Every reply waits for your approval until you trust it; refund requests are left for a person.',
|
|
15
|
+
trigger: { kind: 'schedule', description: 'every 10 minutes, for new emails', every: '10m' },
|
|
9
16
|
hoursPerWeek: 5,
|
|
10
17
|
fit: 7,
|
|
11
18
|
risk: 'medium',
|
|
12
|
-
asksFirst: ['sending a reply'
|
|
19
|
+
asksFirst: ['sending a reply'],
|
|
13
20
|
needs: [
|
|
14
|
-
{ kind: '
|
|
15
|
-
{ kind: 'ability', name: 'lp-
|
|
16
|
-
{ kind: '
|
|
21
|
+
{ kind: 'ability', name: 'lp-gmail', description: 'your support inbox' },
|
|
22
|
+
{ kind: 'ability', name: 'lp-shopify', description: 'your store\'s orders' },
|
|
23
|
+
{ kind: 'ability', name: 'lp-docs-search', description: 'your help articles and policies' },
|
|
17
24
|
],
|
|
18
25
|
agent: {
|
|
19
26
|
name: 'customer-email-replies',
|
|
20
|
-
systemPrompt: `You answer customer emails for our business.
|
|
27
|
+
systemPrompt: `You answer customer emails for our business, from our support inbox.
|
|
21
28
|
|
|
22
|
-
|
|
23
|
-
1.
|
|
24
|
-
2.
|
|
25
|
-
3.
|
|
26
|
-
4.
|
|
29
|
+
When asked to handle new emails:
|
|
30
|
+
1. Find them with gmail_search, query "is:unread in:inbox". Skip newsletters, receipts, automatic notifications and "no-reply" senders: mark those read and move on.
|
|
31
|
+
2. For each customer email, read the whole conversation with gmail_get_thread first.
|
|
32
|
+
3. Work out what they want: order status, a product question, a return or refund, a complaint, or something else.
|
|
33
|
+
4. Look up what you need before answering: the order with shopify_find_orders (by the order number they give, or their email) and shopify_get_order, and our policies with search_docs. Never guess an order detail, a date or a policy.
|
|
34
|
+
5. Reply with gmail_send, passing reply_to_message_id so it stays in the same thread: short, friendly, in the customer's language, using their name, answering the question first and saying what happens next.
|
|
35
|
+
6. Mark the email read with gmail_modify_labels (remove "UNREAD").
|
|
27
36
|
|
|
28
37
|
Rules:
|
|
29
|
-
- Refunds,
|
|
30
|
-
- If the customer is angry, mentions a lawyer, a chargeback or a safety problem, don't promise anything;
|
|
31
|
-
- If you can't find the order or the answer, say so
|
|
32
|
-
- Never share
|
|
33
|
-
|
|
38
|
+
- Refunds, returns and replacements are handled by a person: reply that their request has been passed to the team, then star the email (add "STARRED") instead of acting on it.
|
|
39
|
+
- If the customer is angry, mentions a lawyer, a chargeback or a safety problem, don't promise anything; save a calm reply with gmail_create_draft and star the email for a person.
|
|
40
|
+
- If you can't find the order or the answer, say so and ask for the order number; don't invent one.
|
|
41
|
+
- Never share one customer's information with anyone else.`,
|
|
42
|
+
scheduleMessage: 'Handle new customer emails in the support inbox.',
|
|
43
|
+
exampleMessage: 'Handle new customer emails in the support inbox.',
|
|
34
44
|
},
|
|
35
45
|
},
|
|
46
|
+
setup: {
|
|
47
|
+
abilities: ['lp-gmail', 'lp-shopify', 'lp-docs-search'],
|
|
48
|
+
keys: [...GMAIL_KEYS, ...SHOPIFY_KEYS],
|
|
49
|
+
env: { DOCS_SEARCH_PATH: 'docs' },
|
|
50
|
+
rules: [
|
|
51
|
+
{ name: 'template-read-mail-allowed', tool: 'gmail_search', decision: 'allow' },
|
|
52
|
+
{ name: 'template-read-thread-allowed', tool: 'gmail_get_thread', decision: 'allow' },
|
|
53
|
+
{ name: 'template-drafts-allowed', tool: 'gmail_create_draft', decision: 'allow' },
|
|
54
|
+
{ name: 'template-labels-allowed', tool: 'gmail_modify_labels', decision: 'allow' },
|
|
55
|
+
{ name: 'template-replies-need-approval', tool: 'gmail_send', decision: 'ask' },
|
|
56
|
+
{ name: 'template-no-refunds', tool: 'shopify_create_refund', decision: 'deny' },
|
|
57
|
+
],
|
|
58
|
+
schedule: { every: '10m', name: 'Check the support inbox', message: 'Handle new customer emails in the support inbox.' },
|
|
59
|
+
needsApprovals: true,
|
|
60
|
+
notes: [DOCS_NOTE],
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: 'refund-approvals',
|
|
65
|
+
category: 'Customer service',
|
|
66
|
+
candidate: {
|
|
67
|
+
title: 'Refund approvals',
|
|
68
|
+
summary: 'Checks the inbox for refund and return requests, checks each against the order and your return policy, and proposes the refund and the reply. Both wait for your approval.',
|
|
69
|
+
trigger: { kind: 'schedule', description: 'every 10 minutes, for new refund requests', every: '10m' },
|
|
70
|
+
hoursPerWeek: 3,
|
|
71
|
+
fit: 7,
|
|
72
|
+
risk: 'high',
|
|
73
|
+
asksFirst: ['issuing a refund', 'sending the reply'],
|
|
74
|
+
needs: [
|
|
75
|
+
{ kind: 'ability', name: 'lp-gmail', description: 'your support inbox' },
|
|
76
|
+
{ kind: 'ability', name: 'lp-shopify', description: 'your store\'s orders and refunds' },
|
|
77
|
+
{ kind: 'ability', name: 'lp-docs-search', description: 'your return policy' },
|
|
78
|
+
],
|
|
79
|
+
agent: {
|
|
80
|
+
name: 'refund-approvals',
|
|
81
|
+
systemPrompt: `You handle refund and return requests for our store, from our support inbox.
|
|
82
|
+
|
|
83
|
+
When asked to handle new refund requests:
|
|
84
|
+
1. Find them with gmail_search, query "is:unread in:inbox (refund OR return OR damaged OR broken OR "money back")". Read each conversation with gmail_get_thread. If an email isn't really a refund or return request, leave it unread for the support team.
|
|
85
|
+
2. Find the order with shopify_find_orders (the order number they give, or their email) and read it with shopify_get_order: what was bought, when, how much, what's already been refunded, whether it was delivered.
|
|
86
|
+
3. Check the request against our return policy with search_docs: the return window, whether the item is eligible, and whether the reason is covered.
|
|
87
|
+
4. Decide what to propose: a full refund, a partial refund, refunding shipping, or no refund (and which part of the policy says so).
|
|
88
|
+
5. Work out the exact amount with shopify_preview_refund. Never state an amount you haven't previewed.
|
|
89
|
+
6. Issue it with shopify_create_refund, with a note giving the policy rule and the facts from the order. A person approves it first.
|
|
90
|
+
7. Once the refund is done, reply with gmail_send (reply_to_message_id set): what was refunded, to which payment method, and that it takes 5–10 business days to arrive. Then mark the email read with gmail_modify_labels.
|
|
91
|
+
|
|
92
|
+
Rules:
|
|
93
|
+
- Never refund more than was paid, and never refund an order twice: check refunds on the order first.
|
|
94
|
+
- If the refund is denied, don't try again; save a reply with gmail_create_draft explaining what you'd suggest instead, and star the email for a person.
|
|
95
|
+
- If the order can't be found, ask the customer for the order number; don't guess.
|
|
96
|
+
- If the customer mentions a chargeback, a lawyer or a safety problem, don't act; star the email for a person.`,
|
|
97
|
+
scheduleMessage: 'Handle new refund and return requests in the support inbox.',
|
|
98
|
+
exampleMessage: 'Handle new refund and return requests in the support inbox.',
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
setup: {
|
|
102
|
+
abilities: ['lp-gmail', 'lp-shopify', 'lp-docs-search'],
|
|
103
|
+
keys: [...GMAIL_KEYS, ...SHOPIFY_KEYS],
|
|
104
|
+
env: { DOCS_SEARCH_PATH: 'docs' },
|
|
105
|
+
rules: [
|
|
106
|
+
{ name: 'template-read-mail-allowed', tool: 'gmail_search', decision: 'allow' },
|
|
107
|
+
{ name: 'template-read-thread-allowed', tool: 'gmail_get_thread', decision: 'allow' },
|
|
108
|
+
{ name: 'template-drafts-allowed', tool: 'gmail_create_draft', decision: 'allow' },
|
|
109
|
+
{ name: 'template-labels-allowed', tool: 'gmail_modify_labels', decision: 'allow' },
|
|
110
|
+
{ name: 'template-preview-allowed', tool: 'shopify_preview_refund', decision: 'allow' },
|
|
111
|
+
{ name: 'template-refunds-need-approval', tool: 'shopify_create_refund', decision: 'ask' },
|
|
112
|
+
{ name: 'template-replies-need-approval', tool: 'gmail_send', decision: 'ask' },
|
|
113
|
+
],
|
|
114
|
+
schedule: { every: '10m', name: 'Check for refund requests', message: 'Handle new refund and return requests in the support inbox.' },
|
|
115
|
+
needsApprovals: true,
|
|
116
|
+
notes: [DOCS_NOTE + ' — especially your return policy'],
|
|
117
|
+
},
|
|
36
118
|
},
|
|
37
119
|
{
|
|
38
120
|
id: 'incident-triage',
|
|
39
121
|
category: 'Operations',
|
|
40
122
|
candidate: {
|
|
41
123
|
title: 'Triage incidents',
|
|
42
|
-
summary: 'Takes an alert or a bug report, works out what broke and how bad it is, finds
|
|
43
|
-
trigger: { kind: 'on_request', description: 'when an alert or report is passed to it in Chat or
|
|
124
|
+
summary: 'Takes an alert or a bug report, works out what broke and how bad it is, finds the deploys and changes that likely caused it, and posts a summary to Slack. Opening or commenting on issues waits for approval.',
|
|
125
|
+
trigger: { kind: 'on_request', description: 'when an alert or report is passed to it in Chat, Slack or over HTTP' },
|
|
44
126
|
hoursPerWeek: 3,
|
|
45
127
|
fit: 6,
|
|
46
128
|
risk: 'medium',
|
|
47
|
-
asksFirst: ['opening or
|
|
129
|
+
asksFirst: ['opening or commenting on issues'],
|
|
48
130
|
needs: [
|
|
49
|
-
{ kind: '
|
|
50
|
-
{ kind: '
|
|
131
|
+
{ kind: 'ability', name: 'lp-github', description: 'your repository\'s commits, deploys and issues' },
|
|
132
|
+
{ kind: 'ability', name: 'lp-slack-notifier', description: 'posting the summary to Slack' },
|
|
51
133
|
],
|
|
52
134
|
agent: {
|
|
53
135
|
name: 'incident-triage',
|
|
@@ -56,69 +138,208 @@ Rules:
|
|
|
56
138
|
When you get an alert, error or bug report:
|
|
57
139
|
1. Summarise it in two sentences: what is failing and who is affected.
|
|
58
140
|
2. Rate the severity: SEV1 (customers can't use the product), SEV2 (a major feature is broken), SEV3 (degraded or a workaround exists), SEV4 (minor).
|
|
59
|
-
3.
|
|
60
|
-
4.
|
|
141
|
+
3. Find what changed shortly before it started: github_list_workflow_runs for recent deploys and failed builds, github_list_pull_requests for recently merged changes, and github_list_commits (narrowed with since_hours and path) for detail. Match times carefully.
|
|
142
|
+
4. Check whether it's already known with github_search_issues; read a match with github_get_issue.
|
|
143
|
+
5. Post a short summary to Slack with post_to_slack: what's broken, severity, the most likely cause with links, and one next step (who should look, what to check, or whether to roll back).
|
|
144
|
+
6. If an issue already tracks it, add your findings with github_comment_issue; otherwise, for SEV1 or SEV2, open one with github_create_issue. Both ask a person first.
|
|
61
145
|
|
|
62
146
|
Rules:
|
|
63
|
-
- Only read on your own
|
|
147
|
+
- Only read on your own; never roll anything back yourself.
|
|
64
148
|
- Say how sure you are. If the evidence is thin, say what would confirm it instead of guessing.
|
|
65
|
-
- Keep the summary short enough to read on a phone.`,
|
|
149
|
+
- Keep the Slack summary short enough to read on a phone.`,
|
|
66
150
|
exampleMessage: 'Alert: checkout-api error rate 18% (normally under 1%) since 14:05 UTC. Errors: "timeout connecting to payments".',
|
|
67
151
|
},
|
|
68
152
|
},
|
|
153
|
+
setup: {
|
|
154
|
+
abilities: ['lp-github', 'lp-slack-notifier'],
|
|
155
|
+
keys: [...GITHUB_KEYS, ...SLACK_KEYS],
|
|
156
|
+
rules: [
|
|
157
|
+
{ name: 'template-slack-summary-allowed', tool: 'post_to_slack', decision: 'allow' },
|
|
158
|
+
{ name: 'template-new-issues-need-approval', tool: 'github_create_issue', decision: 'ask' },
|
|
159
|
+
{ name: 'template-comments-need-approval', tool: 'github_comment_issue', decision: 'ask' },
|
|
160
|
+
],
|
|
161
|
+
needsApprovals: true,
|
|
162
|
+
notes: ['Send it alerts: paste one into Chat, have your alerting tool call it over HTTP, or reply to it in Slack'],
|
|
163
|
+
},
|
|
69
164
|
},
|
|
70
165
|
{
|
|
71
166
|
id: 'expense-approvals',
|
|
72
167
|
category: 'Finance',
|
|
73
168
|
candidate: {
|
|
74
169
|
title: 'Check expense claims',
|
|
75
|
-
summary: '
|
|
76
|
-
trigger: { kind: '
|
|
170
|
+
summary: 'Checks the claims inbox, reads each claim and its receipt, checks it against your expense policy, and sends the approver a one-line verdict. Replies to claimants wait for your approval.',
|
|
171
|
+
trigger: { kind: 'schedule', description: 'every 15 minutes, for new claims', every: '15m' },
|
|
77
172
|
hoursPerWeek: 3,
|
|
78
173
|
fit: 7,
|
|
79
174
|
risk: 'high',
|
|
80
|
-
asksFirst: ['
|
|
175
|
+
asksFirst: ['replying to the claimant'],
|
|
81
176
|
needs: [
|
|
82
|
-
{ kind: '
|
|
83
|
-
{ kind: 'ability', name: 'lp-docs-search', description: 'your expense policy
|
|
84
|
-
{ kind: 'messaging', name: 'Slack', description: 'Slack, so approvers decide in one click' },
|
|
177
|
+
{ kind: 'ability', name: 'lp-gmail', description: 'the inbox where claims arrive' },
|
|
178
|
+
{ kind: 'ability', name: 'lp-docs-search', description: 'your expense policy' },
|
|
85
179
|
],
|
|
86
180
|
agent: {
|
|
87
181
|
name: 'expense-approvals',
|
|
88
182
|
systemPrompt: `You check expense claims against our expense policy.
|
|
89
183
|
|
|
90
|
-
|
|
91
|
-
1.
|
|
92
|
-
2.
|
|
93
|
-
3.
|
|
94
|
-
4.
|
|
184
|
+
When asked to check new claims:
|
|
185
|
+
1. Find them with gmail_search, query "is:unread in:inbox". Read each with gmail_get_thread.
|
|
186
|
+
2. Read the amount, currency, date, merchant, category and who submitted it, from the email and its receipt (the attachment names show whether a receipt is there).
|
|
187
|
+
3. Check it against the expense policy with search_docs: the category is allowed, it's under the limit, the receipt matches the amount and date, and it isn't a duplicate of an earlier claim (search the inbox for the same merchant and amount).
|
|
188
|
+
4. Write a one-line verdict: "Within policy" or "Outside policy: <reason>", then the details you checked.
|
|
189
|
+
5. Email the verdict to the approver at the address in the EXPENSE_APPROVER_EMAIL setting with gmail_send, then mark the claim read with gmail_modify_labels.
|
|
95
190
|
|
|
96
191
|
Rules:
|
|
97
192
|
- Never approve or pay a claim yourself. A person always decides.
|
|
98
|
-
- If the receipt is missing or unreadable,
|
|
193
|
+
- If the receipt is missing or unreadable, draft a reply to the claimant asking for it with gmail_create_draft; don't fill in values.
|
|
99
194
|
- If the policy doesn't cover the case, say which part is unclear rather than deciding.`,
|
|
100
|
-
|
|
195
|
+
scheduleMessage: 'Check the new expense claims in the inbox.',
|
|
196
|
+
exampleMessage: 'Check the new expense claims in the inbox.',
|
|
101
197
|
},
|
|
102
198
|
},
|
|
199
|
+
setup: {
|
|
200
|
+
abilities: ['lp-gmail', 'lp-docs-search'],
|
|
201
|
+
keys: [...GMAIL_KEYS],
|
|
202
|
+
env: { DOCS_SEARCH_PATH: 'docs' },
|
|
203
|
+
rules: [
|
|
204
|
+
{ name: 'template-read-mail-allowed', tool: 'gmail_search', decision: 'allow' },
|
|
205
|
+
{ name: 'template-read-thread-allowed', tool: 'gmail_get_thread', decision: 'allow' },
|
|
206
|
+
{ name: 'template-drafts-allowed', tool: 'gmail_create_draft', decision: 'allow' },
|
|
207
|
+
{ name: 'template-labels-allowed', tool: 'gmail_modify_labels', decision: 'allow' },
|
|
208
|
+
{ name: 'template-emails-need-approval', tool: 'gmail_send', decision: 'ask' },
|
|
209
|
+
],
|
|
210
|
+
schedule: { every: '15m', name: 'Check for new claims', message: 'Check the new expense claims in the inbox.' },
|
|
211
|
+
needsApprovals: true,
|
|
212
|
+
notes: [DOCS_NOTE + ' — especially your expense policy', 'Set EXPENSE_APPROVER_EMAIL (Environment tab) to the approver\'s address'],
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
id: 'lead-follow-up',
|
|
217
|
+
category: 'Sales',
|
|
218
|
+
candidate: {
|
|
219
|
+
title: 'Lead follow-up',
|
|
220
|
+
summary: 'Every hour, picks up new leads in HubSpot, researches each on the web, and drafts a short, personal first email. Sending and updating the CRM wait for your approval.',
|
|
221
|
+
trigger: { kind: 'schedule', description: 'every hour, for new leads', every: '1h' },
|
|
222
|
+
hoursPerWeek: 3,
|
|
223
|
+
fit: 6,
|
|
224
|
+
risk: 'medium',
|
|
225
|
+
asksFirst: ['sending the first email', 'updating the lead'],
|
|
226
|
+
needs: [
|
|
227
|
+
{ kind: 'ability', name: 'lp-hubspot', description: 'your leads in HubSpot' },
|
|
228
|
+
{ kind: 'ability', name: 'lp-web-search', description: 'researching each lead' },
|
|
229
|
+
{ kind: 'ability', name: 'lp-gmail', description: 'the inbox to send from' },
|
|
230
|
+
],
|
|
231
|
+
agent: {
|
|
232
|
+
name: 'lead-follow-up',
|
|
233
|
+
systemPrompt: `You follow up with new leads for our business.
|
|
234
|
+
|
|
235
|
+
What we sell (edit this):
|
|
236
|
+
- [What your product does, in one sentence]
|
|
237
|
+
- [Who it is for]
|
|
238
|
+
- [The one result customers care about most]
|
|
239
|
+
|
|
240
|
+
When asked to follow up with new leads:
|
|
241
|
+
1. Find them with hubspot_find_contacts: lead_status "NEW", created_in_last_days 2.
|
|
242
|
+
2. For each, read hubspot_get_contact. Skip anyone with an owner, recent notes, or a last-contacted date: someone is already on it.
|
|
243
|
+
3. Research them with web_search and web_fetch: what their company does, its size, and anything recent that matters (funding, hiring, a launch). Note your sources.
|
|
244
|
+
4. Work out what they most likely need from us, in one sentence.
|
|
245
|
+
5. Send a short first email (under 120 words) with gmail_send: a specific opening based on your research, one sentence on how we help with that need, and one easy question to reply to. A person approves it first.
|
|
246
|
+
6. Log what you sent and found with hubspot_add_note, and set the lead to "ATTEMPTED_TO_CONTACT" with hubspot_update_contact.
|
|
247
|
+
|
|
248
|
+
Rules:
|
|
249
|
+
- Never invent facts about a lead. If research finds nothing reliable, write a simple, honest email instead.
|
|
250
|
+
- No pressure tactics, fake urgency or discounts you weren't told about.
|
|
251
|
+
- If the lead looks like a competitor, a student or spam, add a note saying so and don't email them.`,
|
|
252
|
+
scheduleMessage: 'Follow up with the new leads in HubSpot.',
|
|
253
|
+
exampleMessage: 'Follow up with the new leads in HubSpot.',
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
setup: {
|
|
257
|
+
abilities: ['lp-hubspot', 'lp-web-search', 'lp-gmail'],
|
|
258
|
+
keys: ['HUBSPOT_ACCESS_TOKEN', ...WEB_SEARCH_KEYS, ...GMAIL_KEYS],
|
|
259
|
+
rules: [
|
|
260
|
+
{ name: 'template-notes-allowed', tool: 'hubspot_add_note', decision: 'allow' },
|
|
261
|
+
{ name: 'template-lead-updates-need-approval', tool: 'hubspot_update_contact', decision: 'ask' },
|
|
262
|
+
{ name: 'template-no-new-contacts', tool: 'hubspot_create_contact', decision: 'deny' },
|
|
263
|
+
{ name: 'template-emails-need-approval', tool: 'gmail_send', decision: 'ask' },
|
|
264
|
+
],
|
|
265
|
+
schedule: { every: '1h', name: 'Follow up with new leads', message: 'Follow up with the new leads in HubSpot.' },
|
|
266
|
+
needsApprovals: true,
|
|
267
|
+
notes: ['Edit "What we sell" at the top of its instructions (Overview tab)'],
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
id: 'inbox-to-tasks',
|
|
272
|
+
category: 'Operations',
|
|
273
|
+
candidate: {
|
|
274
|
+
title: 'Inbox to tasks',
|
|
275
|
+
summary: 'Checks a shared inbox every 10 minutes and turns each request into a task (a GitHub issue) with an owner and a due date, then tells the sender who is handling it.',
|
|
276
|
+
trigger: { kind: 'schedule', description: 'every 10 minutes, for new emails', every: '10m' },
|
|
277
|
+
hoursPerWeek: 3,
|
|
278
|
+
fit: 7,
|
|
279
|
+
risk: 'low',
|
|
280
|
+
asksFirst: ['replying to the sender'],
|
|
281
|
+
needs: [
|
|
282
|
+
{ kind: 'ability', name: 'lp-gmail', description: 'the shared inbox' },
|
|
283
|
+
{ kind: 'ability', name: 'lp-github', description: 'tasks, as GitHub issues' },
|
|
284
|
+
],
|
|
285
|
+
agent: {
|
|
286
|
+
name: 'inbox-to-tasks',
|
|
287
|
+
systemPrompt: `You turn requests in our shared inbox into tasks, kept as GitHub issues.
|
|
288
|
+
|
|
289
|
+
Who handles what (edit this; GitHub usernames):
|
|
290
|
+
- Billing and invoices: [username]
|
|
291
|
+
- Technical problems: [username]
|
|
292
|
+
- Everything else: [username]
|
|
293
|
+
|
|
294
|
+
When asked to handle new emails:
|
|
295
|
+
1. Find them with gmail_search, query "is:unread in:inbox". Read each with gmail_get_thread.
|
|
296
|
+
2. Decide whether it's a request someone needs to act on. Newsletters, receipts, out-of-office replies and spam are not: mark them read and move on.
|
|
297
|
+
3. Check whether a task already exists with github_search_issues (the sender and subject). If so, add the new details with github_comment_issue instead.
|
|
298
|
+
4. Otherwise open one with github_create_issue: a short title starting with a verb, a two-line summary, the sender, the due date (the date they asked for, or 2 working days if they didn't say), and the owner from the list above as the assignee.
|
|
299
|
+
5. Reply to the sender with gmail_send (reply_to_message_id set): who is handling it and by when. A person approves it first.
|
|
300
|
+
6. Mark the email read with gmail_modify_labels.
|
|
301
|
+
|
|
302
|
+
Rules:
|
|
303
|
+
- If an email holds several requests, make one task for each.
|
|
304
|
+
- Never promise a fix or a refund in the reply; only say who is handling it and by when.`,
|
|
305
|
+
scheduleMessage: 'Turn the new emails in the shared inbox into tasks.',
|
|
306
|
+
exampleMessage: 'Turn the new emails in the shared inbox into tasks.',
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
setup: {
|
|
310
|
+
abilities: ['lp-gmail', 'lp-github'],
|
|
311
|
+
keys: [...GMAIL_KEYS, ...GITHUB_KEYS],
|
|
312
|
+
rules: [
|
|
313
|
+
{ name: 'template-read-mail-allowed', tool: 'gmail_search', decision: 'allow' },
|
|
314
|
+
{ name: 'template-read-thread-allowed', tool: 'gmail_get_thread', decision: 'allow' },
|
|
315
|
+
{ name: 'template-labels-allowed', tool: 'gmail_modify_labels', decision: 'allow' },
|
|
316
|
+
{ name: 'template-tasks-allowed', tool: 'github_create_issue', decision: 'allow' },
|
|
317
|
+
{ name: 'template-task-updates-allowed', tool: 'github_comment_issue', decision: 'allow' },
|
|
318
|
+
{ name: 'template-replies-need-approval', tool: 'gmail_send', decision: 'ask' },
|
|
319
|
+
],
|
|
320
|
+
schedule: { every: '10m', name: 'Check the shared inbox', message: 'Turn the new emails in the shared inbox into tasks.' },
|
|
321
|
+
needsApprovals: true,
|
|
322
|
+
notes: ['Edit "Who handles what" at the top of its instructions (Overview tab)', 'Point GITHUB_DEFAULT_REPO at the repository your team keeps tasks in'],
|
|
323
|
+
},
|
|
103
324
|
},
|
|
104
325
|
{
|
|
105
326
|
id: 'weekly-research-digest',
|
|
106
327
|
category: 'Research',
|
|
107
328
|
candidate: {
|
|
108
329
|
title: 'Weekly research digest',
|
|
109
|
-
summary: '
|
|
110
|
-
trigger: { kind: 'schedule', description: '
|
|
330
|
+
summary: 'Every Monday morning, searches the web for news on the topics you choose and posts a short digest with links to Slack.',
|
|
331
|
+
trigger: { kind: 'schedule', description: 'Mondays at 9:00', every: '7d' },
|
|
111
332
|
hoursPerWeek: 2,
|
|
112
333
|
fit: 6,
|
|
113
334
|
risk: 'low',
|
|
114
|
-
asksFirst: [
|
|
335
|
+
asksFirst: [],
|
|
115
336
|
needs: [
|
|
116
337
|
{ kind: 'ability', name: 'lp-web-search', description: 'web search' },
|
|
117
|
-
{ kind: '
|
|
338
|
+
{ kind: 'ability', name: 'lp-slack-notifier', description: 'posting the digest to Slack' },
|
|
118
339
|
],
|
|
119
340
|
agent: {
|
|
120
341
|
name: 'weekly-research-digest',
|
|
121
|
-
systemPrompt: `You write a weekly research digest.
|
|
342
|
+
systemPrompt: `You write a weekly research digest and post it to Slack.
|
|
122
343
|
|
|
123
344
|
Topics to follow (edit this list):
|
|
124
345
|
- [Your industry or market]
|
|
@@ -126,21 +347,42 @@ Topics to follow (edit this list):
|
|
|
126
347
|
- [Technologies or regulations you care about]
|
|
127
348
|
|
|
128
349
|
Each week:
|
|
129
|
-
1. Search the web for what happened in the last 7 days on each topic.
|
|
130
|
-
2. Open the most relevant pages and read them; don't rely on search snippets.
|
|
350
|
+
1. Search the web with web_search for what happened in the last 7 days on each topic.
|
|
351
|
+
2. Open the most relevant pages with web_fetch and read them; don't rely on search snippets.
|
|
131
352
|
3. Write the digest: for each topic, up to three items, each with a one-sentence summary, why it matters to us, and the link.
|
|
132
353
|
4. End with "Worth a closer look": the single most important item, and why.
|
|
354
|
+
5. Post it with post_to_slack.
|
|
133
355
|
|
|
134
356
|
Rules:
|
|
135
357
|
- Only include things published in the last 7 days, and give each item's date.
|
|
136
358
|
- Prefer original sources over aggregators. Never invent a link.
|
|
137
359
|
- If a topic had no real news, say so in one line.`,
|
|
138
|
-
scheduleMessage: 'Write this week\'s research digest.',
|
|
139
|
-
exampleMessage: 'Write this week\'s research digest.',
|
|
360
|
+
scheduleMessage: 'Write and post this week\'s research digest.',
|
|
361
|
+
exampleMessage: 'Write and post this week\'s research digest.',
|
|
140
362
|
},
|
|
141
363
|
},
|
|
364
|
+
setup: {
|
|
365
|
+
abilities: ['lp-web-search', 'lp-slack-notifier'],
|
|
366
|
+
keys: [...WEB_SEARCH_KEYS, ...SLACK_KEYS],
|
|
367
|
+
rules: [
|
|
368
|
+
{ name: 'template-search-allowed', tool: 'web_search', decision: 'allow' },
|
|
369
|
+
{ name: 'template-fetch-allowed', tool: 'web_fetch', decision: 'allow' },
|
|
370
|
+
{ name: 'template-digest-post-allowed', tool: 'post_to_slack', decision: 'allow' },
|
|
371
|
+
],
|
|
372
|
+
schedule: { every: '7d', at: '09:00', weekday: 1, name: 'Weekly research digest', message: 'Write and post this week\'s research digest.' },
|
|
373
|
+
needsApprovals: false,
|
|
374
|
+
notes: ['Edit "Topics to follow" at the top of its instructions (Overview tab)'],
|
|
375
|
+
},
|
|
142
376
|
},
|
|
143
377
|
];
|
|
144
378
|
export function starterTemplate(id) {
|
|
145
379
|
return STARTER_TEMPLATES.find((t) => t.id === id);
|
|
146
380
|
}
|
|
381
|
+
/** The first run of a template's schedule: its time of day (and weekday)
|
|
382
|
+
* next, in the workspace's time zone; undefined when it has none. */
|
|
383
|
+
export function templateScheduleStart(schedule, nowMs = Date.now(), timeZone = workspaceTimeZone()) {
|
|
384
|
+
const at = schedule.at ? parseTimeOfDay(schedule.at) : undefined;
|
|
385
|
+
if (!at)
|
|
386
|
+
return undefined;
|
|
387
|
+
return new Date(nextTimeOfDay(at, nowMs, timeZone, schedule.weekday)).toISOString();
|
|
388
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { cancelScheduledTask, createScheduledTask, getScheduledTask, listScheduledTasks, } from '../scheduler.js';
|
|
2
|
+
import { formatWallTime, workspaceTimeZone } from '../time-zone.js';
|
|
2
3
|
/** How many active (not done or cancelled) tasks one agent may create
|
|
3
4
|
* through these tools — each run is a model call, so a runaway loop of
|
|
4
5
|
* "schedule another one" stops here. The console and API aren't capped. */
|
|
@@ -6,7 +7,12 @@ function maxActivePerAgent() {
|
|
|
6
7
|
const n = Number(process.env.LOOPENGINE_SCHEDULER_MAX_PER_AGENT);
|
|
7
8
|
return Number.isInteger(n) && n > 0 ? n : 25;
|
|
8
9
|
}
|
|
9
|
-
|
|
10
|
+
/** next_run_at as the workspace's wall-clock time, so the model can
|
|
11
|
+
* confirm "tomorrow at 9:00" without converting from UTC. */
|
|
12
|
+
function nextRunLocal(t, timeZone) {
|
|
13
|
+
return t.next_run_at ? `${formatWallTime(Date.parse(t.next_run_at), timeZone)} (${timeZone})` : undefined;
|
|
14
|
+
}
|
|
15
|
+
function summary(t, timeZone) {
|
|
10
16
|
return {
|
|
11
17
|
task_id: t.task_id,
|
|
12
18
|
task_name: t.task_name,
|
|
@@ -14,11 +20,15 @@ function summary(t) {
|
|
|
14
20
|
schedule: t.schedule,
|
|
15
21
|
status: t.status,
|
|
16
22
|
next_run_at: t.next_run_at,
|
|
23
|
+
next_run_local: nextRunLocal(t, timeZone),
|
|
17
24
|
run_count: t.run_count,
|
|
18
25
|
last_status: t.last_status,
|
|
19
26
|
};
|
|
20
27
|
}
|
|
21
28
|
export function createScheduleTools(agentName) {
|
|
29
|
+
// Read once per run: the time zone is in the tool description, which
|
|
30
|
+
// should stay the same from call to call.
|
|
31
|
+
const timeZone = workspaceTimeZone();
|
|
22
32
|
async function ownTask(taskId, tool) {
|
|
23
33
|
const task = await getScheduledTask(taskId);
|
|
24
34
|
// Another agent's task reads exactly like a missing one.
|
|
@@ -28,7 +38,7 @@ export function createScheduleTools(agentName) {
|
|
|
28
38
|
}
|
|
29
39
|
const scheduleTask = {
|
|
30
40
|
name: 'system_schedule_task',
|
|
31
|
-
description: 'Schedule a message to yourself for later — once at a set time, or repeatedly on an interval — like setting a reminder or a cron job. When it fires you receive the message as a new conversation (none of the current one carries over, so make the message self-contained) and handle it with your tools as usual, with no person live. A tool call that needs approval goes wherever your approval requests are sent and waits; if nowhere is set, it is denied. Use only when the user asks for something to happen later or regularly, never on your own initiative.
|
|
41
|
+
description: 'Schedule a message to yourself for later — once at a set time, or repeatedly on an interval — like setting a reminder or a cron job. When it fires you receive the message as a new conversation (none of the current one carries over, so make the message self-contained) and handle it with your tools as usual, with no person live. A tool call that needs approval goes wherever your approval requests are sent and waits; if nowhere is set, it is denied. Use only when the user asks for something to happen later or regularly, never on your own initiative. Times are in this workspace\'s time zone, ' + timeZone + ', unless you give an offset, so when the user means another time zone, give its offset. Prefer the longest interval that serves them since each run is a model call, and confirm the schedule back in plain words. Returns task_id, next_run_at and next_run_local (the workspace\'s time).',
|
|
32
42
|
input_schema: {
|
|
33
43
|
type: 'object',
|
|
34
44
|
properties: {
|
|
@@ -38,8 +48,8 @@ export function createScheduleTools(agentName) {
|
|
|
38
48
|
},
|
|
39
49
|
schedule: {
|
|
40
50
|
type: 'object',
|
|
41
|
-
description: 'Either { "run_at": "<ISO 8601 timestamp
|
|
42
|
-
properties: { run_at: { type: 'string' }, every: { type: 'string' } },
|
|
51
|
+
description: 'Either { "run_at": "<ISO 8601 timestamp>" } for once, or { "every": "<interval>", ... } to repeat until cancelled — a number plus s/m/h/d, e.g. "30m", "1h", "1d"; 30s at the shortest. A timestamp without an offset, like "2026-11-02T09:00", is in the workspace\'s time zone (' + timeZone + '). A repeating task keeps its first slot: started at 10:07 with "1h", it fires at 11:07, 12:07, ... For a time of day or day of week, add "at" (24-hour "HH:MM") and for weekly "weekday": "every day at 9am" is { "every": "1d", "at": "09:00" }; "every Monday at 9am" is { "every": "7d", "at": "09:00", "weekday": "monday" }. Daily and weekly tasks keep their time of day when the clocks change. Or give "start_at", a timestamp, for the first run.',
|
|
52
|
+
properties: { run_at: { type: 'string' }, every: { type: 'string' }, at: { type: 'string' }, weekday: { type: 'string' }, start_at: { type: 'string' } },
|
|
43
53
|
},
|
|
44
54
|
task_name: { type: 'string', description: 'Optional short label, shown when listing tasks and in the console.' },
|
|
45
55
|
notify: { type: 'boolean', description: 'Optional — also send each run\'s result through your notifier (where your approval requests go), so a person sees it.' },
|
|
@@ -58,7 +68,7 @@ export function createScheduleTools(agentName) {
|
|
|
58
68
|
task_name: typeof input.task_name === 'string' ? input.task_name : undefined,
|
|
59
69
|
notify: input.notify === true,
|
|
60
70
|
});
|
|
61
|
-
return JSON.stringify({ task_id: task.task_id, next_run_at: task.next_run_at, schedule: task.schedule });
|
|
71
|
+
return JSON.stringify({ task_id: task.task_id, next_run_at: task.next_run_at, next_run_local: nextRunLocal(task, timeZone), schedule: task.schedule });
|
|
62
72
|
},
|
|
63
73
|
};
|
|
64
74
|
const listTasks = {
|
|
@@ -76,7 +86,7 @@ export function createScheduleTools(agentName) {
|
|
|
76
86
|
},
|
|
77
87
|
execute: async (input) => {
|
|
78
88
|
const status = typeof input.status === 'string' ? input.status : undefined;
|
|
79
|
-
return JSON.stringify((await listScheduledTasks({ agent: agentName, status })).map(summary));
|
|
89
|
+
return JSON.stringify((await listScheduledTasks({ agent: agentName, status })).map((t) => summary(t, timeZone)));
|
|
80
90
|
},
|
|
81
91
|
safe: true,
|
|
82
92
|
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export declare const TIME_ZONE_ENV = "LOOPENGINE_TIME_ZONE";
|
|
2
|
+
export interface WallTime {
|
|
3
|
+
year: number;
|
|
4
|
+
/** 1–12 */
|
|
5
|
+
month: number;
|
|
6
|
+
day: number;
|
|
7
|
+
hour: number;
|
|
8
|
+
minute: number;
|
|
9
|
+
second: number;
|
|
10
|
+
}
|
|
11
|
+
export declare function isValidTimeZone(timeZone: unknown): timeZone is string;
|
|
12
|
+
/** The server's own time zone. */
|
|
13
|
+
export declare function serverTimeZone(): string;
|
|
14
|
+
/** The workspace's time zone: LOOPENGINE_TIME_ZONE when it's a real one,
|
|
15
|
+
* else the server's. */
|
|
16
|
+
export declare function workspaceTimeZone(): string;
|
|
17
|
+
/** Every time zone this server knows, for the console's picker. */
|
|
18
|
+
export declare function listTimeZones(): string[];
|
|
19
|
+
/** The wall-clock time (and weekday, 0 = Sunday) at `ms` in `timeZone`. */
|
|
20
|
+
export declare function wallTimeAt(ms: number, timeZone: string): WallTime & {
|
|
21
|
+
weekday: number;
|
|
22
|
+
};
|
|
23
|
+
/** The instant a wall-clock time in `timeZone` happens. Fields may
|
|
24
|
+
* overflow (day 32, hour 25), as with Date.UTC. */
|
|
25
|
+
export declare function instantOf(t: WallTime, timeZone: string): number;
|
|
26
|
+
/** An ISO 8601 timestamp as ms. One with a "Z" or an offset is exact;
|
|
27
|
+
* one without ("2026-11-02T09:00") is a wall-clock time in `timeZone`.
|
|
28
|
+
* NaN when it isn't a timestamp. */
|
|
29
|
+
export declare function parseTimestamp(value: string, timeZone: string): number;
|
|
30
|
+
/** "09:00" as [hours, minutes], or undefined when it isn't a time. */
|
|
31
|
+
export declare function parseTimeOfDay(value: string): [number, number] | undefined;
|
|
32
|
+
/** The next time it's `at` ("09:00") in `timeZone`, strictly after
|
|
33
|
+
* `nowMs`, on `weekday` (0 = Sunday) when given. */
|
|
34
|
+
export declare function nextTimeOfDay(at: [number, number], nowMs: number, timeZone: string, weekday?: number): number;
|
|
35
|
+
/** `ms` moved by whole calendar days in `timeZone`, keeping its time of
|
|
36
|
+
* day across a daylight-saving change. */
|
|
37
|
+
export declare function addCalendarDays(ms: number, days: number, timeZone: string): number;
|
|
38
|
+
/** `ms` as "2026-11-02 09:00" in `timeZone`, for tool results the model
|
|
39
|
+
* reads back to a person. */
|
|
40
|
+
export declare function formatWallTime(ms: number, timeZone: string): string;
|