@agentled/cli 0.1.6 → 0.5.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/README.md +136 -0
- package/dist/builtin-tools-catalog.d.ts +37 -0
- package/dist/builtin-tools-catalog.js +96 -0
- package/dist/builtin-tools-catalog.js.map +1 -0
- package/dist/commands/auth.js +30 -0
- package/dist/commands/auth.js.map +1 -1
- package/dist/commands/examples.d.ts +15 -0
- package/dist/commands/examples.js +100 -0
- package/dist/commands/examples.js.map +1 -0
- package/dist/commands/scaffold.d.ts +14 -0
- package/dist/commands/scaffold.js +103 -0
- package/dist/commands/scaffold.js.map +1 -0
- package/dist/commands/schema.d.ts +10 -0
- package/dist/commands/schema.js +107 -0
- package/dist/commands/schema.js.map +1 -0
- package/dist/commands/skills.d.ts +9 -0
- package/dist/commands/skills.js +94 -0
- package/dist/commands/skills.js.map +1 -0
- package/dist/commands/tools.d.ts +10 -0
- package/dist/commands/tools.js +53 -0
- package/dist/commands/tools.js.map +1 -0
- package/dist/commands/workflows.js +227 -9
- package/dist/commands/workflows.js.map +1 -1
- package/dist/context-schema.d.ts +37 -0
- package/dist/context-schema.js +108 -0
- package/dist/context-schema.js.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/utils/preflight.d.ts +25 -0
- package/dist/utils/preflight.js +293 -0
- package/dist/utils/preflight.js.map +1 -0
- package/dist/utils/skills.d.ts +49 -0
- package/dist/utils/skills.js +214 -0
- package/dist/utils/skills.js.map +1 -0
- package/package.json +4 -1
- package/patterns/v1/00-why-agentic-ops.md +107 -0
- package/patterns/v1/01-trigger-design.md +107 -0
- package/patterns/v1/02-dedup-gates.md +135 -0
- package/patterns/v1/03-credit-efficiency.md +130 -0
- package/patterns/v1/04-loop-patterns.md +147 -0
- package/patterns/v1/05-child-workflow-contracts.md +151 -0
- package/patterns/v1/06-conditional-routing.md +151 -0
- package/patterns/v1/07-error-handling.md +157 -0
- package/patterns/v1/08-composed-email-approval.md +130 -0
- package/patterns/v1/09-reports-and-knowledge-storage.md +166 -0
- package/scaffolds/README.md +62 -0
- package/scaffolds/ai-with-tools.json +49 -0
- package/scaffolds/email-polling-dedup.json +71 -0
- package/scaffolds/extract-threshold-alert.json +131 -0
- package/scaffolds/lead-scoring-kg.json +84 -0
- package/scaffolds/list-match-email.json +131 -0
- package/scaffolds/minimal.json +20 -0
- package/skills/agentled/SKILL.md +573 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skills installer — copies the bundled Agentled Claude Code skills into the
|
|
3
|
+
* user's `~/.claude/skills/` (global) or `./.claude/skills/` (project) dir.
|
|
4
|
+
*
|
|
5
|
+
* Why: When an agent drives the CLI with a fresh Claude Code session, it does
|
|
6
|
+
* not have the Agentled skill loaded unless the skill is present on disk in a
|
|
7
|
+
* Claude-discoverable location. Without the skill, the LLM invents invalid
|
|
8
|
+
* step types (`type: "ai"`, `knowledge_graph_query`, …) — the exact class of
|
|
9
|
+
* silent CLI failure that MCP-025 is closing.
|
|
10
|
+
*
|
|
11
|
+
* The installer is version-aware:
|
|
12
|
+
* - Reads a `version:` frontmatter field from each `SKILL.md`
|
|
13
|
+
* - Leaves newer or hand-edited skills alone (unless --force)
|
|
14
|
+
* - Reports status so we can print a one-line summary after `auth login`
|
|
15
|
+
*/
|
|
16
|
+
export type SkillInstallOutcome = 'installed' | 'updated' | 'up-to-date' | 'newer-local' | 'hand-edited' | 'forced';
|
|
17
|
+
export interface SkillInstallResult {
|
|
18
|
+
skill: string;
|
|
19
|
+
outcome: SkillInstallOutcome;
|
|
20
|
+
bundledVersion: string;
|
|
21
|
+
installedVersion: string | null;
|
|
22
|
+
targetPath: string;
|
|
23
|
+
}
|
|
24
|
+
export interface InstallSkillsOptions {
|
|
25
|
+
/** true = install to `~/.claude/skills/` (global); false = `./.claude/skills/` (project) */
|
|
26
|
+
global?: boolean;
|
|
27
|
+
/** overwrite regardless of version comparison or local edits */
|
|
28
|
+
force?: boolean;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Locate the `skills/` directory that ships with @agentled/cli.
|
|
32
|
+
*
|
|
33
|
+
* When running from source (`node dist/index.js`) the layout is:
|
|
34
|
+
* packages/cli/dist/utils/skills.js → packages/cli/skills/…
|
|
35
|
+
* When installed globally via npm, the package layout is the same relative
|
|
36
|
+
* structure (`dist/utils/skills.js` → `skills/`).
|
|
37
|
+
*/
|
|
38
|
+
export declare function resolveBundledSkillsDir(): string;
|
|
39
|
+
export declare function getSkillsTargetDir(global: boolean): string;
|
|
40
|
+
export declare function installSkills(options?: InstallSkillsOptions): SkillInstallResult[];
|
|
41
|
+
export declare function describeSkillsInstall(results: SkillInstallResult[], targetDir: string): string;
|
|
42
|
+
/**
|
|
43
|
+
* Produce a short one-line hint for the `auth login` summary, e.g.
|
|
44
|
+
* "Skill installed: agentled v0.2.0 → ~/.claude/skills/"
|
|
45
|
+
* "Skill already installed (v0.1.0), latest is v0.2.0 — run `agentled skills update` to refresh."
|
|
46
|
+
*
|
|
47
|
+
* Returns null if there is nothing worth showing.
|
|
48
|
+
*/
|
|
49
|
+
export declare function summarizeForLoginBanner(results: SkillInstallResult[], targetDir: string): string | null;
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skills installer — copies the bundled Agentled Claude Code skills into the
|
|
3
|
+
* user's `~/.claude/skills/` (global) or `./.claude/skills/` (project) dir.
|
|
4
|
+
*
|
|
5
|
+
* Why: When an agent drives the CLI with a fresh Claude Code session, it does
|
|
6
|
+
* not have the Agentled skill loaded unless the skill is present on disk in a
|
|
7
|
+
* Claude-discoverable location. Without the skill, the LLM invents invalid
|
|
8
|
+
* step types (`type: "ai"`, `knowledge_graph_query`, …) — the exact class of
|
|
9
|
+
* silent CLI failure that MCP-025 is closing.
|
|
10
|
+
*
|
|
11
|
+
* The installer is version-aware:
|
|
12
|
+
* - Reads a `version:` frontmatter field from each `SKILL.md`
|
|
13
|
+
* - Leaves newer or hand-edited skills alone (unless --force)
|
|
14
|
+
* - Reports status so we can print a one-line summary after `auth login`
|
|
15
|
+
*/
|
|
16
|
+
import { existsSync, readFileSync, mkdirSync, readdirSync, statSync, cpSync } from 'node:fs';
|
|
17
|
+
import { join, dirname, resolve } from 'node:path';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
import { homedir } from 'node:os';
|
|
20
|
+
/**
|
|
21
|
+
* Locate the `skills/` directory that ships with @agentled/cli.
|
|
22
|
+
*
|
|
23
|
+
* When running from source (`node dist/index.js`) the layout is:
|
|
24
|
+
* packages/cli/dist/utils/skills.js → packages/cli/skills/…
|
|
25
|
+
* When installed globally via npm, the package layout is the same relative
|
|
26
|
+
* structure (`dist/utils/skills.js` → `skills/`).
|
|
27
|
+
*/
|
|
28
|
+
export function resolveBundledSkillsDir() {
|
|
29
|
+
const here = fileURLToPath(import.meta.url);
|
|
30
|
+
// dist/utils/skills.js → package root is two levels up
|
|
31
|
+
const pkgRoot = resolve(dirname(here), '..', '..');
|
|
32
|
+
return join(pkgRoot, 'skills');
|
|
33
|
+
}
|
|
34
|
+
export function getSkillsTargetDir(global) {
|
|
35
|
+
return global
|
|
36
|
+
? join(homedir(), '.claude', 'skills')
|
|
37
|
+
: join(process.cwd(), '.claude', 'skills');
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Parse a `version:` field out of the YAML-style frontmatter at the top of a
|
|
41
|
+
* SKILL.md file. Returns null if no frontmatter or no version field is present.
|
|
42
|
+
*/
|
|
43
|
+
function parseSkillVersion(skillMdPath) {
|
|
44
|
+
try {
|
|
45
|
+
const content = readFileSync(skillMdPath, 'utf-8');
|
|
46
|
+
// Only inspect frontmatter (between the first two --- delimiters)
|
|
47
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
48
|
+
if (!fmMatch)
|
|
49
|
+
return null;
|
|
50
|
+
const versionMatch = fmMatch[1].match(/^version:\s*(.+)$/m);
|
|
51
|
+
return versionMatch ? versionMatch[1].trim() : null;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Compare two semver-ish version strings. Returns -1, 0, or 1.
|
|
59
|
+
* Handles missing prereleases by treating undefined segments as 0.
|
|
60
|
+
*/
|
|
61
|
+
function compareVersions(a, b) {
|
|
62
|
+
const parse = (v) => v.replace(/^v/, '').split('.').map(n => parseInt(n, 10) || 0);
|
|
63
|
+
const [a1, a2 = 0, a3 = 0] = parse(a);
|
|
64
|
+
const [b1, b2 = 0, b3 = 0] = parse(b);
|
|
65
|
+
if (a1 !== b1)
|
|
66
|
+
return a1 < b1 ? -1 : 1;
|
|
67
|
+
if (a2 !== b2)
|
|
68
|
+
return a2 < b2 ? -1 : 1;
|
|
69
|
+
if (a3 !== b3)
|
|
70
|
+
return a3 < b3 ? -1 : 1;
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Recursively hash-compare two directories by content. Returns true if the
|
|
75
|
+
* byte contents of every file match (directory structure + file bytes).
|
|
76
|
+
*
|
|
77
|
+
* Used to detect hand-edited installs: if the installed version matches the
|
|
78
|
+
* bundled version but the bytes differ, the user (or a prior agent session)
|
|
79
|
+
* has customised the skill and we should not overwrite without --force.
|
|
80
|
+
*/
|
|
81
|
+
function directoriesIdentical(a, b) {
|
|
82
|
+
try {
|
|
83
|
+
const aEntries = readdirSync(a).sort();
|
|
84
|
+
const bEntries = readdirSync(b).sort();
|
|
85
|
+
if (aEntries.length !== bEntries.length)
|
|
86
|
+
return false;
|
|
87
|
+
for (let i = 0; i < aEntries.length; i++) {
|
|
88
|
+
if (aEntries[i] !== bEntries[i])
|
|
89
|
+
return false;
|
|
90
|
+
const aChild = join(a, aEntries[i]);
|
|
91
|
+
const bChild = join(b, bEntries[i]);
|
|
92
|
+
const aStat = statSync(aChild);
|
|
93
|
+
const bStat = statSync(bChild);
|
|
94
|
+
if (aStat.isDirectory() !== bStat.isDirectory())
|
|
95
|
+
return false;
|
|
96
|
+
if (aStat.isDirectory()) {
|
|
97
|
+
if (!directoriesIdentical(aChild, bChild))
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
const aBuf = readFileSync(aChild);
|
|
102
|
+
const bBuf = readFileSync(bChild);
|
|
103
|
+
if (!aBuf.equals(bBuf))
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
export function installSkills(options = {}) {
|
|
114
|
+
const sourceDir = resolveBundledSkillsDir();
|
|
115
|
+
if (!existsSync(sourceDir)) {
|
|
116
|
+
throw new Error(`Bundled skills directory not found at ${sourceDir}`);
|
|
117
|
+
}
|
|
118
|
+
const targetBase = getSkillsTargetDir(options.global ?? false);
|
|
119
|
+
const results = [];
|
|
120
|
+
const skills = readdirSync(sourceDir, { withFileTypes: true })
|
|
121
|
+
.filter(d => d.isDirectory())
|
|
122
|
+
.map(d => d.name);
|
|
123
|
+
for (const skill of skills) {
|
|
124
|
+
const src = join(sourceDir, skill);
|
|
125
|
+
const dest = join(targetBase, skill);
|
|
126
|
+
const bundledVersion = parseSkillVersion(join(src, 'SKILL.md')) ?? '0.0.0';
|
|
127
|
+
const installedVersion = existsSync(dest) ? parseSkillVersion(join(dest, 'SKILL.md')) : null;
|
|
128
|
+
let outcome;
|
|
129
|
+
if (!existsSync(dest)) {
|
|
130
|
+
outcome = 'installed';
|
|
131
|
+
}
|
|
132
|
+
else if (options.force) {
|
|
133
|
+
outcome = 'forced';
|
|
134
|
+
}
|
|
135
|
+
else if (installedVersion && compareVersions(installedVersion, bundledVersion) > 0) {
|
|
136
|
+
outcome = 'newer-local';
|
|
137
|
+
}
|
|
138
|
+
else if (installedVersion && compareVersions(installedVersion, bundledVersion) === 0) {
|
|
139
|
+
outcome = directoriesIdentical(src, dest) ? 'up-to-date' : 'hand-edited';
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
outcome = 'updated';
|
|
143
|
+
}
|
|
144
|
+
if (outcome === 'installed' || outcome === 'updated' || outcome === 'forced') {
|
|
145
|
+
mkdirSync(dest, { recursive: true });
|
|
146
|
+
cpSync(src, dest, { recursive: true, force: true });
|
|
147
|
+
}
|
|
148
|
+
results.push({
|
|
149
|
+
skill,
|
|
150
|
+
outcome,
|
|
151
|
+
bundledVersion,
|
|
152
|
+
installedVersion,
|
|
153
|
+
targetPath: dest,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
return results;
|
|
157
|
+
}
|
|
158
|
+
export function describeSkillsInstall(results, targetDir) {
|
|
159
|
+
if (results.length === 0)
|
|
160
|
+
return 'No skills to install.';
|
|
161
|
+
const lines = [];
|
|
162
|
+
for (const r of results) {
|
|
163
|
+
switch (r.outcome) {
|
|
164
|
+
case 'installed':
|
|
165
|
+
lines.push(` ✓ ${r.skill} v${r.bundledVersion} installed`);
|
|
166
|
+
break;
|
|
167
|
+
case 'updated':
|
|
168
|
+
lines.push(` ✓ ${r.skill} v${r.installedVersion} → v${r.bundledVersion} updated`);
|
|
169
|
+
break;
|
|
170
|
+
case 'forced':
|
|
171
|
+
lines.push(` ✓ ${r.skill} v${r.bundledVersion} installed (forced)`);
|
|
172
|
+
break;
|
|
173
|
+
case 'up-to-date':
|
|
174
|
+
lines.push(` • ${r.skill} v${r.bundledVersion} already installed`);
|
|
175
|
+
break;
|
|
176
|
+
case 'newer-local':
|
|
177
|
+
lines.push(` • ${r.skill} local v${r.installedVersion} is newer than bundled v${r.bundledVersion} — left as-is`);
|
|
178
|
+
break;
|
|
179
|
+
case 'hand-edited':
|
|
180
|
+
lines.push(` • ${r.skill} v${r.bundledVersion} already installed with local edits — left as-is (run with --force to overwrite)`);
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
lines.push(` Skills directory: ${targetDir}`);
|
|
185
|
+
return lines.join('\n');
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Produce a short one-line hint for the `auth login` summary, e.g.
|
|
189
|
+
* "Skill installed: agentled v0.2.0 → ~/.claude/skills/"
|
|
190
|
+
* "Skill already installed (v0.1.0), latest is v0.2.0 — run `agentled skills update` to refresh."
|
|
191
|
+
*
|
|
192
|
+
* Returns null if there is nothing worth showing.
|
|
193
|
+
*/
|
|
194
|
+
export function summarizeForLoginBanner(results, targetDir) {
|
|
195
|
+
if (results.length === 0)
|
|
196
|
+
return null;
|
|
197
|
+
const fresh = results.filter(r => r.outcome === 'installed');
|
|
198
|
+
const stale = results.filter(r => r.outcome === 'newer-local' || r.outcome === 'hand-edited');
|
|
199
|
+
const updatable = results.filter(r => r.outcome === 'up-to-date' && r.installedVersion && r.bundledVersion && compareVersions(r.installedVersion, r.bundledVersion) < 0);
|
|
200
|
+
if (fresh.length > 0) {
|
|
201
|
+
const names = fresh.map(r => `${r.skill} v${r.bundledVersion}`).join(', ');
|
|
202
|
+
return `Installed Claude Code skill: ${names} → ${targetDir}`;
|
|
203
|
+
}
|
|
204
|
+
if (stale.length > 0) {
|
|
205
|
+
const r = stale[0];
|
|
206
|
+
return `Skill "${r.skill}" already installed (v${r.installedVersion ?? '?'}), bundled is v${r.bundledVersion} — run \`agentled skills update\` to refresh.`;
|
|
207
|
+
}
|
|
208
|
+
if (updatable.length > 0) {
|
|
209
|
+
const r = updatable[0];
|
|
210
|
+
return `Skill "${r.skill}" v${r.installedVersion} installed, v${r.bundledVersion} available — run \`agentled skills update\` to refresh.`;
|
|
211
|
+
}
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
//# sourceMappingURL=skills.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skills.js","sourceRoot":"","sources":["../../src/utils/skills.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentled/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "CLI for Agentled — manage workflows, apps, and knowledge from the command line. Zero context-window cost for AI agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -28,6 +28,9 @@
|
|
|
28
28
|
},
|
|
29
29
|
"files": [
|
|
30
30
|
"dist",
|
|
31
|
+
"skills",
|
|
32
|
+
"patterns",
|
|
33
|
+
"scaffolds",
|
|
31
34
|
"README.md",
|
|
32
35
|
"llms.txt",
|
|
33
36
|
"llms-full.txt"
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# 00 — Why agentic-ops
|
|
2
|
+
|
|
3
|
+
> The ops discipline that makes AI agents production-ready.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## The pitch deck demo works. Production doesn't.
|
|
8
|
+
|
|
9
|
+
You've seen it: an AI agent that looks brilliant in a demo — processes a lead, writes a personalized email, updates the CRM. Then you run it on 500 leads a week and the wheels fall off. Duplicates. Missing records. Costs 3× what you expected. No way to know what actually went through.
|
|
10
|
+
|
|
11
|
+
This isn't an AI problem. It's an **architecture problem**.
|
|
12
|
+
|
|
13
|
+
The fix has a name in every other engineering discipline: operations. CI/CD, observability, idempotency, retries, caching, audit trails — these didn't exist on day one of web development either. Developers invented them because raw execution didn't scale. Agentic workflows are at the same inflection point.
|
|
14
|
+
|
|
15
|
+
This is **agentic-ops**: the patterns that make the gap between "demo" and "production" crossable.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 1. Unit economics collapse without dedup discipline
|
|
20
|
+
|
|
21
|
+
This is the argument that lands before developers hit the wall themselves.
|
|
22
|
+
|
|
23
|
+
You're processing 500 inbound leads per week. Your workflow enriches each one (5 credits). No dedup gate — the workflow polls every hour, no label marking what's been processed.
|
|
24
|
+
|
|
25
|
+
- Average lead appears in 3 polls before it's handled: **1,500 enrichments instead of 500**
|
|
26
|
+
- At 5 credits each: **5,000 wasted credits per week**
|
|
27
|
+
- At scale, you're paying for work you already did
|
|
28
|
+
|
|
29
|
+
This isn't hypothetical. It's the default outcome of any polling workflow without a dedup gate. See [02-dedup-gates](02-dedup-gates.md) for the fix.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## 2. Non-determinism at scale
|
|
34
|
+
|
|
35
|
+
A one-off prompt works once. Run the same prompt 1,000 times against your pipeline and you get 1,000 slightly different outputs — different field names, different score ranges, different JSON shapes. No two downstream steps can reliably consume the previous one.
|
|
36
|
+
|
|
37
|
+
Structured workflows define a **response contract**: the output schema is declared upfront, validated at runtime, and consistent across every execution. The AI fills in the values; the workflow enforces the shape.
|
|
38
|
+
|
|
39
|
+
Without this contract, you're parsing surprises, not outputs.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## 3. No audit trail, no trust
|
|
44
|
+
|
|
45
|
+
"The AI processed our deal flow last week" — can you tell me exactly which companies it looked at, what data it used, what score it assigned, and why three were dropped? If not, you can't trust it in any context that matters: regulated industries, board reporting, customer-facing processes.
|
|
46
|
+
|
|
47
|
+
Structured workflows produce **per-step execution records**: inputs, outputs, duration, status, timestamp. You can replay any run. You can compare two runs on the same input. You can show an auditor exactly what happened.
|
|
48
|
+
|
|
49
|
+
Ad-hoc prompts produce a chat message and nothing else.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 4. Retries without idempotency destroy partial state
|
|
54
|
+
|
|
55
|
+
An AI agent that crashes halfway through is worse than one that never started. You have partial writes: some records enriched, some not; some CRM entries created, some missing. You don't know where it stopped.
|
|
56
|
+
|
|
57
|
+
Structured workflows solve this at two levels:
|
|
58
|
+
- **Step-level retry**: resume from the exact failed step with the same inputs — no re-running the work that succeeded
|
|
59
|
+
- **Idempotency gates**: dedup keys, label markers, and processed flags ensure a retried step produces the same outcome as the original, not a duplicate
|
|
60
|
+
|
|
61
|
+
See [07-error-handling](07-error-handling.md) and [02-dedup-gates](02-dedup-gates.md).
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## 5. Caching requires stable step boundaries
|
|
66
|
+
|
|
67
|
+
Enriching the same LinkedIn company URL twice in the same workflow shouldn't hit the API twice. But caching requires a **stable cache key**: a defined input that uniquely identifies the computation.
|
|
68
|
+
|
|
69
|
+
Ad-hoc prompts have no stable key — the prompt is the key, and it changes with every rephrasing. Structured steps have explicit inputs, which means the platform can cache at the step level, deduplicate in-flight requests, and short-circuit repeated work.
|
|
70
|
+
|
|
71
|
+
No step boundary = no cache key = no caching.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## 6. Production requirements don't exist in raw prompting
|
|
76
|
+
|
|
77
|
+
Running AI agents at production scale without structured execution is like running a web server without a framework. You'll reinvent every wheel:
|
|
78
|
+
|
|
79
|
+
- **Observability**: knowing which step is running, how long it's taking, when it silently failed
|
|
80
|
+
- **Rate limiting**: preventing a burst of executions from hammering a downstream API
|
|
81
|
+
- **Concurrency control**: ensuring two parallel runs don't write conflicting records to the same CRM entry
|
|
82
|
+
- **Backpressure**: slowing intake when the processing queue backs up
|
|
83
|
+
|
|
84
|
+
These are solved problems in structured workflow systems. They don't exist as primitives in a prompt-and-hope architecture.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## 7. Human-in-the-loop gates need a first-class abstraction
|
|
89
|
+
|
|
90
|
+
"Have a human review the AI's output before it sends an email" sounds simple. In practice it requires: a UI to surface the output for review, a way to approve or reject, a mechanism to block the next step until the human acts, and a timeout path if they don't.
|
|
91
|
+
|
|
92
|
+
This is a first-class primitive in workflow systems — an approval gate with configurable behavior. In raw prompting, you build it from scratch every time.
|
|
93
|
+
|
|
94
|
+
Any workflow that touches customers, sends communications, or writes to a system of record should have a human-in-the-loop gate. See [09-human-in-the-loop](../v2/09-human-in-the-loop.md) (v2).
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## The pattern
|
|
99
|
+
|
|
100
|
+
Every production failure in agentic workflows traces back to one of these seven gaps. The patterns in this repo address each one directly — not as theoretical advice, but as the specific implementations that fix them.
|
|
101
|
+
|
|
102
|
+
Start with the one that matches your current pain:
|
|
103
|
+
- Burning credits → [03-credit-efficiency](03-credit-efficiency.md)
|
|
104
|
+
- Processing duplicates → [02-dedup-gates](02-dedup-gates.md)
|
|
105
|
+
- Steps silently skipping → [06-conditional-routing](06-conditional-routing.md)
|
|
106
|
+
- Crashes with partial state → [07-error-handling](07-error-handling.md)
|
|
107
|
+
- Wrong trigger choice → [01-trigger-design](01-trigger-design.md)
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# 01 — Trigger design: polling vs event triggers
|
|
2
|
+
|
|
3
|
+
**Problem**: Developers default to event triggers for email/document intake workflows, creating fragile pipelines that drop records, can't backfill, and are hard to debug.
|
|
4
|
+
|
|
5
|
+
**Why it fails silently**: Event triggers appear to work in testing (low volume, reliable delivery). At production scale, re-deliveries cause duplicates, Pub/Sub TTL loses events during outages, and there's no way to backfill records missed during downtime — without any visible error.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Decision framework
|
|
10
|
+
|
|
11
|
+
| | Schedule (polling) | App Event (real-time) |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| **Latency** | minutes–hours | seconds |
|
|
14
|
+
| **Idempotency** | trivial — label/flag marks processed | must dedupe on messageId; re-deliveries happen |
|
|
15
|
+
| **Backfill** | built-in — widen the query window | doesn't exist; needs a separate bootstrap run |
|
|
16
|
+
| **Replay after outage** | automatic on next scheduled run | events can be permanently lost (TTL) |
|
|
17
|
+
| **Debugging** | read last execution log | subscription status + delivery + filter + dedupe all need checking |
|
|
18
|
+
| **Infrastructure** | none | webhook receiver, watch renewal, Pub/Sub |
|
|
19
|
+
|
|
20
|
+
**Default rule: polling for intake, events for reactions.**
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Anti-pattern
|
|
25
|
+
|
|
26
|
+
Using an event trigger for email intake because it "feels more real-time":
|
|
27
|
+
|
|
28
|
+
```yaml
|
|
29
|
+
# Wrong: event trigger for deal flow email intake
|
|
30
|
+
trigger:
|
|
31
|
+
type: app_event
|
|
32
|
+
app: gmail
|
|
33
|
+
event: GMAIL_NEW_MESSAGE_RECEIVED
|
|
34
|
+
filters:
|
|
35
|
+
query: "from:investor subject:pitch"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Problems:
|
|
39
|
+
- Duplicate delivery means the same email gets processed 2-3× with no dedup mechanism
|
|
40
|
+
- Gmail watch tokens expire — you need a renewal job or emails stop arriving silently
|
|
41
|
+
- No backfill: if the workflow is down for 2 days, those emails are gone
|
|
42
|
+
- Debugging requires checking: is the watch active? Did the webhook fire? Did the filter match? Did the dedup run?
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Correct pattern
|
|
47
|
+
|
|
48
|
+
Schedule trigger with label-based dedup:
|
|
49
|
+
|
|
50
|
+
```yaml
|
|
51
|
+
# Correct: scheduled polling with dedup gate
|
|
52
|
+
trigger:
|
|
53
|
+
type: schedule
|
|
54
|
+
config:
|
|
55
|
+
frequency: daily
|
|
56
|
+
time: "08:00"
|
|
57
|
+
|
|
58
|
+
steps:
|
|
59
|
+
- id: fetch-emails
|
|
60
|
+
action: GMAIL_FETCH_EMAILS
|
|
61
|
+
input:
|
|
62
|
+
query: "-label:processed newer_than:1d"
|
|
63
|
+
max_results: 50
|
|
64
|
+
|
|
65
|
+
- id: process-email
|
|
66
|
+
type: loop
|
|
67
|
+
over: "{{steps.fetch-emails.messages}}"
|
|
68
|
+
# ... processing steps ...
|
|
69
|
+
|
|
70
|
+
- id: mark-processed
|
|
71
|
+
action: GMAIL_ADD_LABEL
|
|
72
|
+
input:
|
|
73
|
+
message_id: "{{currentItem.id}}"
|
|
74
|
+
label_id: "{{steps.ensure-label.id}}" # resolved ID, not display name
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The `-label:processed` filter does the dedup work. Each email is processed exactly once. If the workflow goes down for a week, widen to `newer_than:7d` on the next run to backfill.
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## When to use event triggers
|
|
82
|
+
|
|
83
|
+
Event triggers are correct when:
|
|
84
|
+
- The user explicitly requires sub-minute latency ("alert within 30 seconds", "as soon as", "real-time")
|
|
85
|
+
- The workflow is a **side effect** (fire-and-forget notification), not a record-of-truth producer
|
|
86
|
+
- Missed events are acceptable (or you have a separate reconciliation job)
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Trigger type cheatsheet
|
|
91
|
+
|
|
92
|
+
| User says | Trigger |
|
|
93
|
+
|---|---|
|
|
94
|
+
| "process inbound pitch emails" | Schedule (daily) |
|
|
95
|
+
| "triage support emails every morning" | Schedule (daily 08:00) |
|
|
96
|
+
| "every Monday summarize last week's emails" | Schedule (weekly) |
|
|
97
|
+
| "analyze my inbox and create Notion entries" | Schedule (daily) |
|
|
98
|
+
| "page oncall within 30s of an escalation email" | App event |
|
|
99
|
+
| "create a ticket the moment a customer emails" | App event |
|
|
100
|
+
| "run every time a form is submitted" | Webhook |
|
|
101
|
+
| "user clicks Run" | Manual |
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## One-line rule
|
|
106
|
+
|
|
107
|
+
> Default to Schedule + label-based dedup for email and document intake; use event triggers only when the user explicitly states a latency requirement under one minute.
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# 02 — Dedup gates: idempotency for agentic workflows
|
|
2
|
+
|
|
3
|
+
**Problem**: Without a dedup gate, every record in a polling or webhook workflow gets processed multiple times — silently, expensively, and often with conflicting writes.
|
|
4
|
+
|
|
5
|
+
**Why it fails silently**: The first few runs look correct. Duplicates only surface when you notice your CRM has 3 entries for the same company, your enrichment bill is 2× expected, or your outreach tool sent the same email twice. By then, the damage is done.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## The Gmail label-ID bug (the most common dedup failure)
|
|
10
|
+
|
|
11
|
+
This is the one that wastes 2 hours and isn't documented anywhere.
|
|
12
|
+
|
|
13
|
+
You build an email polling workflow. You add a step to mark each email as processed. You pass the label name:
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"action": "GMAIL_ADD_LABEL",
|
|
18
|
+
"input": {
|
|
19
|
+
"message_id": "{{currentItem.id}}",
|
|
20
|
+
"label_id": "processed"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Result: `400 Bad Request: Invalid label: processed`
|
|
26
|
+
|
|
27
|
+
The Gmail API does not accept label **display names**. It requires internal label **IDs** — strings that look like `Label_3456789012345678`. The display name "processed" is what you see in Gmail's UI. The ID is what the API needs.
|
|
28
|
+
|
|
29
|
+
Same bug with any user-created label: `"agentled"`, `"reviewed"`, `"done"` — all invalid.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Anti-pattern
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
// Wrong: passing label display name
|
|
37
|
+
{
|
|
38
|
+
"action": "GMAIL_ADD_LABEL",
|
|
39
|
+
"input": {
|
|
40
|
+
"message_id": "{{currentItem.id}}",
|
|
41
|
+
"label_id": "processed"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// → 400: Invalid label: processed
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Correct pattern
|
|
50
|
+
|
|
51
|
+
Always resolve the label ID first using a create-or-get-label step:
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
// Step 1: create label if it doesn't exist, or get existing (idempotent)
|
|
55
|
+
{
|
|
56
|
+
"id": "ensure-label",
|
|
57
|
+
"action": "GMAIL_CREATE_LABEL",
|
|
58
|
+
"input": { "name": "processed" }
|
|
59
|
+
}
|
|
60
|
+
// Returns: { "id": "Label_3456789012345678", "name": "processed" }
|
|
61
|
+
|
|
62
|
+
// Step 2: fetch unprocessed emails
|
|
63
|
+
{
|
|
64
|
+
"id": "fetch-emails",
|
|
65
|
+
"action": "GMAIL_FETCH_EMAILS",
|
|
66
|
+
"input": {
|
|
67
|
+
"query": "-label:processed newer_than:1d",
|
|
68
|
+
"max_results": 50
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Step 3 (inside loop): mark each email processed using the resolved ID
|
|
73
|
+
{
|
|
74
|
+
"id": "mark-processed",
|
|
75
|
+
"action": "GMAIL_ADD_LABEL",
|
|
76
|
+
"input": {
|
|
77
|
+
"message_id": "{{currentItem.id}}",
|
|
78
|
+
"label_id": "{{steps.ensure-label.id}}" // ← resolved ID, not display name
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`GMAIL_CREATE_LABEL` is idempotent — if the label already exists, it returns the existing label's ID. Run it every time with no side effects.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## How label-based dedup works
|
|
88
|
+
|
|
89
|
+
The `-label:processed` filter in the fetch query does the dedup work:
|
|
90
|
+
|
|
91
|
+
1. First run: fetches 50 emails. Processes each. Adds `processed` label to each.
|
|
92
|
+
2. Second run: fetches emails without `processed` label. Those 50 are now excluded. Only new emails are returned.
|
|
93
|
+
3. Outage for 3 days: widen to `newer_than:7d` on the next run. All unprocessed emails in the window are caught. Processed ones are excluded.
|
|
94
|
+
|
|
95
|
+
This gives you **exactly-once processing** with no database, no external state store, and no coordination overhead.
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Dedup for webhook triggers
|
|
100
|
+
|
|
101
|
+
Webhooks re-deliver. Always. Your endpoint will receive the same event 2–5× under normal conditions (retries on timeout, delivery confirmation failures). Without dedup:
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
Webhook fires → workflow starts → enrichment call × 3 duplicates → 3 CRM entries
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
The fix: use a unique event ID as an idempotency key and check before processing:
|
|
108
|
+
|
|
109
|
+
```javascript
|
|
110
|
+
// Code step at workflow entry
|
|
111
|
+
const eventId = input.webhookPayload.id; // or messageId, leadId, etc.
|
|
112
|
+
const alreadyProcessed = await kv.get(`processed:${eventId}`);
|
|
113
|
+
if (alreadyProcessed) {
|
|
114
|
+
return { skipped: true, reason: "duplicate" };
|
|
115
|
+
}
|
|
116
|
+
await kv.set(`processed:${eventId}`, true, { ttl: 86400 });
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Dedup patterns by source
|
|
122
|
+
|
|
123
|
+
| Source | Dedup mechanism |
|
|
124
|
+
|---|---|
|
|
125
|
+
| Gmail polling | `-label:processed` query + `GMAIL_ADD_LABEL` after processing |
|
|
126
|
+
| Webhook | Idempotency key from event ID, stored in KV or DB |
|
|
127
|
+
| Scheduled API poll | Cursor / `since_id` / `updated_at` timestamp stored in persistent memory |
|
|
128
|
+
| File/S3 intake | Move to `processed/` prefix after reading |
|
|
129
|
+
| Form submissions | Unique submission ID checked before processing |
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## One-line rule
|
|
134
|
+
|
|
135
|
+
> Always resolve label IDs before passing them to the Gmail API — display names cause a silent 400 error — and always add the processed label as the final step in every email intake loop.
|