@audienti/cli 0.2.0-exora.24 → 0.2.0-exora.26
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/cli/account-replies.mjs +16 -2
- package/cli/account-reply-operations.mjs +3 -2
- package/cli/audienti.mjs +8 -1
- package/cli/decision-operations.mjs +35 -0
- package/cli/decision.mjs +51 -0
- package/cli/discovery-operations.mjs +1 -1
- package/cli/intent-operations.mjs +1 -1
- package/cli/playbook-operations.mjs +1 -1
- package/cli/workspaces.mjs +9 -1
- package/package.json +1 -1
- package/skills/getting-started/references/replies.md +29 -4
- package/skills/playbook-design/references/fields.md +2 -2
- package/source-manifest.json +13 -11
package/cli/account-replies.mjs
CHANGED
|
@@ -14,7 +14,7 @@ export async function accountRepliesCli(args) {
|
|
|
14
14
|
if (positions[0] !== 'prospects' || positions[1] !== 'replies' || !['set', 'get', 'list'].includes(command) || positions.length !== 4) throw Error('Use prospects replies set|get|list PERSON --workspace ID.');
|
|
15
15
|
const allowed = ['workspace', 'profile', ...(command === 'set' ? ['file', 'evidence'] : command === 'get' ? ['evidence', 'history-cursor', 'history-limit'] : ['cursor', 'limit', 'status'])];
|
|
16
16
|
if (Object.keys(flags).some(k => !allowed.includes(k))) throw Error('Option does not apply to this reply command.');
|
|
17
|
-
if (command
|
|
17
|
+
if (command === 'set' && !flags.evidence) throw Error('--evidence EVIDENCE is required.');
|
|
18
18
|
let input = {};
|
|
19
19
|
if (command === 'set') {
|
|
20
20
|
if (!flags.file) throw Error('--file INPUT.json is required.');
|
|
@@ -31,5 +31,19 @@ export async function accountRepliesCli(args) {
|
|
|
31
31
|
if (!/^\d+$/.test(flags[flag])) throw Error('invalid_fields');
|
|
32
32
|
input[key] = Number(flags[flag]);
|
|
33
33
|
}
|
|
34
|
-
|
|
34
|
+
const config = configuration(flags.profile);
|
|
35
|
+
if (command === 'get' && !input.evidenceId) {
|
|
36
|
+
// No --evidence: read the person's latest reply. The list runs oldest first, so walk to its last page.
|
|
37
|
+
let last = null, cursor;
|
|
38
|
+
for (;;) {
|
|
39
|
+
const page = await callWorkspace('workspace_reply_list', { workspaceId: input.workspaceId, prospectId: input.prospectId, limit: 100, ...(cursor ? { cursor } : {}) }, config);
|
|
40
|
+
const data = page.data ?? page;
|
|
41
|
+
last = data.items?.at(-1)?.evidenceId ?? last;
|
|
42
|
+
if (!data.nextCursor) break;
|
|
43
|
+
cursor = data.nextCursor;
|
|
44
|
+
}
|
|
45
|
+
if (!last) throw Error('This person has no stored reply yet. Run prospects timeline PERSON to see their messages.');
|
|
46
|
+
input.evidenceId = last;
|
|
47
|
+
}
|
|
48
|
+
console.log(JSON.stringify(await callWorkspace(`workspace_reply_${command}`, input, config)));
|
|
35
49
|
}
|
|
@@ -5,7 +5,7 @@ const item = { ...scope, evidenceId: uuid };
|
|
|
5
5
|
const status = z.enum(['needs_handling', 'handled', 'not_reply']);
|
|
6
6
|
const outcome = z.enum(['response_recorded', 'closed_without_response', 'suppressed']);
|
|
7
7
|
const limit = z.number().int().min(1).max(100);
|
|
8
|
-
const classification = z.enum(['interested', 'question', 'later', 'not_interested', 'out_of_office', 'stop', 'bounce', 'referral', 'wrong_person']);
|
|
8
|
+
const classification = z.enum(['interested', 'question', 'later', 'not_interested', 'out_of_office', 'stop', 'bounce', 'referral', 'wrong_person', 'unclear']);
|
|
9
9
|
const slot = z.object({ start: z.string().describe('UTC start, ending in Z.'), end: z.string().describe('UTC end, ending in Z.'), timeZone: z.string().describe("The user's IANA time zone, for display.") }).strict();
|
|
10
10
|
const draft = z.object({
|
|
11
11
|
type: z.enum(['email.send', 'linkedin.send']),
|
|
@@ -34,7 +34,7 @@ export const accountReplyOperations = {
|
|
|
34
34
|
workspace_reply_set: {
|
|
35
35
|
schema: z.object({ ...item, requestId: uuid, expectedVersion: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER), status,
|
|
36
36
|
outcome: outcome.nullable().describe("Null for needs_handling/not_reply; a handling outcome is required for handled."), reason: z.string().meta({ minLength: 1, maxLength: 2000 }).describe("Required trimmed NFC reason, 1–2000 codepoints. Interior control characters including newline and tab are rejected."), responseEvidenceId: uuid.nullable().describe("Required for response_recorded; null otherwise. Must identify matching outbound account evidence."),
|
|
37
|
-
classification: classification.optional().describe("What the reply is. Records a replied event on the person's active campaigns, except for bounce and out_of_office. An out_of_office reply snoozes the person instead; a wrong_person reply with no name given stops the sends and asks who the right person is. Someone who names the right person is a referral."),
|
|
37
|
+
classification: classification.optional().describe("What the reply is. Records a replied event on the person's active campaigns, except for bounce and out_of_office. An out_of_office reply snoozes the person instead; an unclear reply holds the person for a decision; a wrong_person reply with no name given stops the sends and asks who the right person is. Someone who names the right person is a referral."),
|
|
38
38
|
returnsOn: z.string().datetime().optional().describe("Only with out_of_office: the date the person is back (UTC, ending in Z). The snooze lasts until then; with no date it follows the playbook's out-of-office rule, 7 days by default."),
|
|
39
39
|
invite: z.literal(true).optional().describe("The prospect sent a calendar invite you have not processed yet. Requires a classification; a meetings.confirm task follows."),
|
|
40
40
|
draft: draft.optional().describe("Your drafted answer. Saved as a reply plan revision the user reviews in Operator; requires status needs_handling. Never sent until approved."),
|
|
@@ -46,6 +46,7 @@ export const accountReplyOperations = {
|
|
|
46
46
|
body: ({ workspaceId, prospectId, evidenceId, ...rest }) => rest,
|
|
47
47
|
},
|
|
48
48
|
workspace_reply_get: {
|
|
49
|
+
description: 'Read one inbound message: its reply state and history, and the `labels` an agent or person recorded on it (label, title, quote, reason, confidence, who recorded it, when), newest first. Free CRM read. The CLI reads the latest reply of the person when `--evidence` is left out.',
|
|
49
50
|
schema: z.object({ ...item, historyCursor: uuid.optional(), historyLimit: limit.optional() }).strict(),
|
|
50
51
|
method: 'GET', scoped: true, path: a => query(detail(a), a, ['historyCursor', 'historyLimit']),
|
|
51
52
|
},
|
package/cli/audienti.mjs
CHANGED
|
@@ -83,6 +83,11 @@ Discovery: prospects list|get ID|timeline ID [--limit N]|observation create|cita
|
|
|
83
83
|
intent list [--workspace ID] (the closed set of message labels: name, direction, event, channels, typed detail; output is JSON)
|
|
84
84
|
intent suggest (--text TEXT | --text-file PATH) --channel email|linkedin [--direction inbound|outbound] [--context TEXT] [--request-id UUID] [--workspace ID] (paid: what a message may mean, read by a model; prints the price per 1M input tokens first; suggests labels with a quote and reason, records nothing; keep one with intent record)
|
|
85
85
|
intent record --person ID --evidence ID --label NAME --quote TEXT --reason TEXT [--confidence high|medium|low] [--request-id UUID] [--participation ID] [--workspace ID] (what a stored message means; get EVIDENCE from prospects conversation append|list; --file LABELS.json for several labels or a typed detail such as a stop result or a meeting time; every label is checked and a refusal says what to fix; a message you sent that asks for a meeting takes --label meeting_requested on its outbound evidence)
|
|
86
|
+
decision list [--state open|decided|all] [--person ID] [--participation ID] [--limit N] [--workspace ID] (what Operator asks the person when the rules do not decide; an open decision holds its person)
|
|
87
|
+
decision options ID [--workspace ID] (one decision with the options the playbook allows)
|
|
88
|
+
decision ask --participation ID [--reason TEXT] [--request-id UUID] [--workspace ID] (ask the person to decide about one running person)
|
|
89
|
+
decision prepare ID --recommended OPTION_ID --option OPTION_ID=ANALYSIS ... [--file PREPARE.json] [--request-id UUID] [--workspace ID] (an agent adds its view: two to five options, one line each)
|
|
90
|
+
decision answer ID --option OPTION_ID [--workspace ID] (a person picks; the move runs once)
|
|
86
91
|
audit list [--workspace ID] [--since 7d] [--actor agent|person] [--category NAME] [--format json|csv]
|
|
87
92
|
budgets set --workspace ID [--agent ID --cap N|none] [--new-agent-cap N|none] [--at-limit pause|ask] [--alert80 on|off] (alert80 covers Operator and email at 80%)
|
|
88
93
|
Qualification: campaigns qualification submit --file INPUT.json
|
|
@@ -123,6 +128,7 @@ Tool names follow the commands: playbook_get for playbook show, playbook_draft_g
|
|
|
123
128
|
Errors name the MCP tool to call next, not the command.`;
|
|
124
129
|
// Example inputs for the commands that save prospects, so `--help` shows a working shape.
|
|
125
130
|
const examples = {
|
|
131
|
+
'decision prepare': { recommended: 'snooze_30', options: [{ id: 'snooze_30', analysis: 'They said ask again in March, so wait a month.' }, { id: 'continue', analysis: 'Keep the plan; a follow-up may go this week.' }] },
|
|
126
132
|
'prospects conversation append': { requestId: 'UUID', item: { source: { provider: 'email', accountRef: 'you@yourcompany.com', itemId: 'PROVIDER_MESSAGE_ID', threadId: null }, payload: { direction: 'outbound', conversationType: 'direct', occurredAt: '2026-11-06T14:00:00Z', subject: 'Ten minutes on late invoices?', body: 'Hi Jordan, could we find 20 minutes next week?', participants: null } } },
|
|
127
133
|
'intent record': [{ label: 'stop', quote: 'please stop writing to me', reason: 'They ask us to stop.', result: { type: 'reply.stop', scope: 'person' } }],
|
|
128
134
|
'prospects observation create': { requestId: 'UUID', kind: 'person', source: { provider: 'linkedin', url: 'https://www.linkedin.com/in/jane-doe' }, fields: { name: 'Jane Doe', title: 'VP Sales', imageUrl: 'https://media.licdn.com/dms/image/PHOTO_ID' }, provenance: { url: 'https://example.com/team/jane' }, observedAt: '2026-01-01T00:00:00Z', skill: { id: 'signal-prospect-research', version: '1' } },
|
|
@@ -142,7 +148,7 @@ function commandHelp() {
|
|
|
142
148
|
}
|
|
143
149
|
console.log(shown.length ? shown.map(line => line.trim()).join('\n') : usage);
|
|
144
150
|
const example = Object.entries(examples).find(([command]) => words.join(' ').startsWith(command) || (words.length > 0 && command.startsWith(words.join(' '))));
|
|
145
|
-
if (example) console.log(`\nExample --file INPUT.json for ${example[0]}:\n${JSON.stringify(example[1], null, 1)}\n${example[0].startsWith('intent') ? `\nA message you sent that asks for a meeting (store it first with prospects conversation append, direction outbound):\naudienti intent record --person PERSON --evidence EVIDENCE --label meeting_requested --quote "find 20 minutes next week" --reason "We ask for a call next week."\nRules: the "Label a message with its words" and "Label a message you sent" sections of the getting-started replies reference.` : 'Steps and rules: the "Save prospects and contacts" section of llms.txt.'}`);
|
|
151
|
+
if (example) console.log(`\nExample --file INPUT.json for ${example[0]}:\n${JSON.stringify(example[1], null, 1)}\n${example[0].startsWith('decision') ? 'Choose option ids from `audienti decision options ID`. One plain line each, at most 140 characters, no IDs or error codes.' : example[0].startsWith('intent') ? `\nA message you sent that asks for a meeting (store it first with prospects conversation append, direction outbound):\naudienti intent record --person PERSON --evidence EVIDENCE --label meeting_requested --quote "find 20 minutes next week" --reason "We ask for a call next week."\nRules: the "Label a message with its words" and "Label a message you sent" sections of the getting-started replies reference.` : 'Steps and rules: the "Save prospects and contacts" section of llms.txt.'}`);
|
|
146
152
|
}
|
|
147
153
|
|
|
148
154
|
/** Says what is wrong with a command no handler took, instead of one message for every case. */
|
|
@@ -164,6 +170,7 @@ async function main() {
|
|
|
164
170
|
if(args[0]==='prospects'&&args[1]==='replies'){const {accountRepliesCli}=await import('./account-replies.mjs');await accountRepliesCli(args);return;}
|
|
165
171
|
if(args[0]==='prospects'&&args[1]==='contacts'){const {recipientsCli}=await import('./recipients.mjs');await recipientsCli(args);return;}
|
|
166
172
|
if(args[0]==='usage'){const {usageCli}=await import('./usage.mjs');await usageCli(args);return;}
|
|
173
|
+
if(args[0]==='decision'){const {decisionCli}=await import('./decision.mjs');await decisionCli(args);return;}
|
|
167
174
|
if(args[0]==='intent'){const {intentCli}=await import('./intent.mjs');await intentCli(args);return;}
|
|
168
175
|
if(args[0]==='audit'){const {auditCli}=await import('./audit.mjs');await auditCli(args);return;}
|
|
169
176
|
if(args[0]==='secret'||args[0]==='secrets'){const {secretsCli}=await import('./secrets.mjs');await secretsCli(args);return;}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
const uuid = z.string().uuid(), scope = { workspaceId: uuid.optional() };
|
|
3
|
+
const base = a => `/workspaces/${a.workspaceId}/decisions`;
|
|
4
|
+
const hold = 'An open decision holds its person: nothing is sent to them until the decision is answered.';
|
|
5
|
+
export const decisionOperations = {
|
|
6
|
+
decision_list: {
|
|
7
|
+
schema: z.object({ ...scope, state: z.enum(['open', 'decided', 'all']).optional(), prospectId: uuid.optional(), participationId: uuid.optional(), limit: z.number().int().min(1).max(100).optional() }).strict(),
|
|
8
|
+
method: 'GET', path: a => { const q = new URLSearchParams(Object.entries({ state: a.state, prospectId: a.prospectId, participationId: a.participationId, limit: a.limit }).filter(([, v]) => v !== undefined).map(([k, v]) => [k, String(v)])).toString(); return `${base(a)}${q ? `?${q}` : ''}`; }, scoped: true,
|
|
9
|
+
description: `List the decisions Operator asks the person when the rules do not decide: an unclear or low-confidence reply, a reply nobody labeled for 24 hours, or an agent that asked. Open ones come first. Each has the options the playbook allows, the agent's view when it added one, and a stale flag after 7 days. ${hold}`,
|
|
10
|
+
},
|
|
11
|
+
decision_get: {
|
|
12
|
+
schema: z.object({ ...scope, decisionId: uuid }).strict(),
|
|
13
|
+
method: 'GET', path: a => `${base(a)}/${a.decisionId}`, scoped: true,
|
|
14
|
+
description: 'Read one decision with the options a person may pick from. Use these option ids with decision_prepare and decision_answer.',
|
|
15
|
+
},
|
|
16
|
+
decision_ask: {
|
|
17
|
+
schema: z.object({ ...scope, participationId: uuid, requestId: uuid, reason: z.string().trim().max(200).optional().describe('Optional. One plain sentence on why the rules cannot decide.') }).strict(),
|
|
18
|
+
method: 'POST', path: base, body: ({ workspaceId, ...rest }) => rest, scoped: true,
|
|
19
|
+
description: `Ask the person to decide about one running participation. If a decision is already open for the person, the reason is added to it. ${hold} The same requestId with the same fields returns the first answer.`,
|
|
20
|
+
},
|
|
21
|
+
decision_prepare: {
|
|
22
|
+
schema: z.object({
|
|
23
|
+
...scope, decisionId: uuid, requestId: uuid,
|
|
24
|
+
recommended: z.string().min(1).max(80).describe('The id of the option you recommend. It must be one of the options you chose.'),
|
|
25
|
+
options: z.array(z.object({ id: z.string().min(1).max(80).describe('An option id from decision_get.'), analysis: z.string().describe('One plain line, at most 140 characters: what happens and why it may fit. No IDs or error codes.') }).strict()).min(2).max(5),
|
|
26
|
+
}).strict(),
|
|
27
|
+
method: 'POST', path: a => `${base(a)}/${a.decisionId}/prepare`, body: ({ workspaceId, decisionId, ...rest }) => rest, scoped: true,
|
|
28
|
+
description: 'Add your view to a decision: choose two to five of its options, write a one-line analysis for each and recommend one. Audienti refuses an option the playbook does not allow from the stage and lists the allowed ids. Take over and hold always stay open to the person. Your recommendation is never applied without the person\'s pick.',
|
|
29
|
+
},
|
|
30
|
+
decision_answer: {
|
|
31
|
+
schema: z.object({ ...scope, decisionId: uuid, option: z.string().min(1).max(80).describe('An option id from decision_get.') }).strict(),
|
|
32
|
+
method: 'POST', path: a => `${base(a)}/${a.decisionId}/answer`, body: ({ workspaceId, decisionId, ...rest }) => rest, scoped: true,
|
|
33
|
+
description: 'People only: an agent cannot pick. Pick an option of a decision. The pick is recorded and the option\'s move runs once; sending it again returns the first answer. Picking hold changes nothing and the person stays held.',
|
|
34
|
+
},
|
|
35
|
+
};
|
package/cli/decision.mjs
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { callWorkspace, configuration } from './workspaces.mjs';
|
|
3
|
+
import { readJsonFile } from './json-file.mjs';
|
|
4
|
+
|
|
5
|
+
const FLAGS = ['workspace', 'profile', 'state', 'person', 'participation', 'limit', 'reason', 'request-id', 'recommended', 'option', 'file'];
|
|
6
|
+
const USAGE = 'Use decision list [--state open|decided|all] [--person ID] [--participation ID] [--limit N], decision options ID, decision ask --participation ID [--reason TEXT] [--request-id UUID], decision prepare ID (--recommended OPTION_ID --option OPTION_ID=ANALYSIS ... | --file PREPARE.json) [--request-id UUID], or decision answer ID --option OPTION_ID. Add --workspace ID to any of them.';
|
|
7
|
+
|
|
8
|
+
export async function decisionCli(args) {
|
|
9
|
+
const flags = {}, many = [], positions = [];
|
|
10
|
+
for (let i = 0; i < args.length; i++) {
|
|
11
|
+
const arg = args[i];
|
|
12
|
+
if (arg === '--json') continue;
|
|
13
|
+
if (!arg.startsWith('--')) { positions.push(arg); continue; }
|
|
14
|
+
const key = arg.slice(2);
|
|
15
|
+
if (!FLAGS.includes(key) || (key !== 'option' && Object.hasOwn(flags, key)) || i + 1 >= args.length) throw Error(`Invalid flag --${key}. ${USAGE}`);
|
|
16
|
+
if (key === 'option') many.push(args[++i]); else flags[key] = args[++i];
|
|
17
|
+
}
|
|
18
|
+
const [, command, id] = positions;
|
|
19
|
+
const call = async (name, input) => console.log(JSON.stringify(await callWorkspace(name, { workspaceId: flags.workspace, ...input }, configuration(flags.profile))));
|
|
20
|
+
const only = (...allowed) => { const bad = Object.keys(flags).find(key => !['workspace', 'profile', ...allowed].includes(key)); if (bad || (many.length && !allowed.includes('option'))) throw Error(`Option does not apply to decision ${command}. ${USAGE}`); };
|
|
21
|
+
if (positions[0] !== 'decision' || !['list', 'options', 'ask', 'prepare', 'answer'].includes(command)) throw Error(USAGE);
|
|
22
|
+
if (command === 'list') {
|
|
23
|
+
only('state', 'person', 'participation', 'limit');
|
|
24
|
+
if (positions.length !== 2) throw Error(USAGE);
|
|
25
|
+
return call('decision_list', { ...(flags.state ? { state: flags.state } : {}), ...(flags.person ? { prospectId: flags.person } : {}), ...(flags.participation ? { participationId: flags.participation } : {}), ...(flags.limit ? { limit: Number(flags.limit) } : {}) });
|
|
26
|
+
}
|
|
27
|
+
if (command === 'ask') {
|
|
28
|
+
only('participation', 'reason', 'request-id');
|
|
29
|
+
if (positions.length !== 2 || !flags.participation) throw Error(`--participation ID is required. ${USAGE}`);
|
|
30
|
+
return call('decision_ask', { participationId: flags.participation, requestId: flags['request-id'] ?? randomUUID(), ...(flags.reason ? { reason: flags.reason } : {}) });
|
|
31
|
+
}
|
|
32
|
+
if (positions.length !== 3 || !id) throw Error(`Give the decision ID. ${USAGE}`);
|
|
33
|
+
if (command === 'options') { only(); return call('decision_get', { decisionId: id }); }
|
|
34
|
+
if (command === 'answer') {
|
|
35
|
+
only('option');
|
|
36
|
+
if (many.length !== 1) throw Error(`Give the option once: --option OPTION_ID. ${USAGE}`);
|
|
37
|
+
return call('decision_answer', { decisionId: id, option: many[0] });
|
|
38
|
+
}
|
|
39
|
+
only('recommended', 'option', 'file', 'request-id');
|
|
40
|
+
let body;
|
|
41
|
+
if (flags.file) {
|
|
42
|
+
if (many.length || flags.recommended) throw Error('Give the options in --file, or with --recommended and --option. Not both.');
|
|
43
|
+
body = readJsonFile(flags.file, 65536, 'Prepare file');
|
|
44
|
+
if (!body || typeof body !== 'object' || Array.isArray(body)) throw Error('The prepare file is { "recommended": "OPTION_ID", "options": [{ "id": "OPTION_ID", "analysis": "One plain line." }] }.');
|
|
45
|
+
} else {
|
|
46
|
+
if (!flags.recommended || !many.length) throw Error(`--recommended OPTION_ID and at least two --option OPTION_ID=ANALYSIS are required without --file. ${USAGE}`);
|
|
47
|
+
body = { recommended: flags.recommended, options: many.map(item => { const at = item.indexOf('='); if (at < 1) throw Error(`--option takes OPTION_ID=ANALYSIS, and "${item}" has no "=".`); return { id: item.slice(0, at), analysis: item.slice(at + 1) }; }) };
|
|
48
|
+
}
|
|
49
|
+
const { requestId, ...rest } = body;
|
|
50
|
+
return call('decision_prepare', { decisionId: id, requestId: flags['request-id'] ?? requestId ?? randomUUID(), ...rest });
|
|
51
|
+
}
|
|
@@ -20,7 +20,7 @@ export const discoveryOperations={
|
|
|
20
20
|
|
|
21
21
|
prospect_list:{schema:z.object({...workspace,kind:z.enum(['person','company']).optional(),search:z.string().optional(),cursor:uuid.optional(),limit:z.number().int().min(1).max(100).optional()}).strict(),method:'GET',path:a=>root(a)+'?'+qs(a),scoped:true},
|
|
22
22
|
prospect_get:{schema:z.object({...workspace,prospectId:uuid,profiles:page.optional(),observations:page.optional(),citations:page.optional(),qualifications:page.optional()}).strict(),method:'GET',path:a=>root(a)+`/${a.prospectId}?`+qs(a),scoped:true},
|
|
23
|
-
prospect_timeline:{description:'Read what happened to a person, newest first: profile reads with the fields each set, changed or confirmed; signal evidence; qualification checks; campaign stage events; your conversations with agents about the person. Free CRM read; no provider call.',schema:z.object({...workspace,prospectId:uuid,limit:z.number().int().min(1).max(100).optional()}).strict(),method:'GET',path:a=>root(a)+`/${a.prospectId}/timeline?`+qs(a),scoped:true},
|
|
23
|
+
prospect_timeline:{description:'Read what happened to a person, newest first: profile reads with the fields each set, changed or confirmed; signal evidence; qualification checks; campaign stage events; your conversations with agents about the person. A recorded message carries `labels` (label, title, quote, reason, confidence, who recorded it, when), newest first, and a label event carries its `label` once (its `detail` does not repeat it). Free CRM read; no provider call.',schema:z.object({...workspace,prospectId:uuid,limit:z.number().int().min(1).max(100).optional()}).strict(),method:'GET',path:a=>root(a)+`/${a.prospectId}/timeline?`+qs(a),scoped:true},
|
|
24
24
|
prospect_record_observation:{schema:z.object({...request,kind:z.enum(['person','company']),prospectId:uuid.optional(),source:z.object({provider:z.string(),providerId:z.string().optional(),url:z.string().optional()}).strict(),fields,provenance:z.object({url:z.string(),excerpt:z.string().optional()}).strict().optional(),observedAt:z.string().nullable().optional(),skill:skill.optional()}).strict(),method:'POST',path:a=>root(a)+'/observations',body,scoped:true},
|
|
25
25
|
prospect_record_citation:{schema:z.object({...request,source:endpoint,target:endpoint,edgeType:z.enum(['works_for','works_at','worked_at','references']),roles:z.array(z.object({roleId:z.string(),title:z.string(),current:z.boolean(),startDate:text,endDate:text,employmentType:text,observedAt:text}).strict()).optional(),evidenceIds:z.array(uuid).optional(),employmentSelection:z.object({action:z.enum(['set_primary','clear']),roleId:z.string().optional(),evidenceHash:z.string().optional()}).strict().optional()}).strict(),method:'POST',path:a=>root(a)+'/citations',body,scoped:true},
|
|
26
26
|
prospect_record_evidence:{schema:z.object({...request,subject,campaignId:uuid.optional(),campaignRevision:z.number().int().positive().optional(),kind:z.enum(['signal','interpretation']),data:z.record(z.string(),z.unknown()),sourceUrl:text,observedAt:text,skill}).strict(),method:'POST',path:a=>root(a)+'/evidence',body,scoped:true},
|
|
@@ -11,7 +11,7 @@ const label = z.object({
|
|
|
11
11
|
label: z.string().describe('A name from intent_labels_list that fits the message direction and channel.'),
|
|
12
12
|
quote: z.string().meta({ minLength: 1, maxLength: 300 }).describe('The words in the message that support the label. Copy them from the message; case and spacing do not matter.'),
|
|
13
13
|
reason: z.string().meta({ minLength: 1, maxLength: 200 }).describe('One plain sentence: why the quote means this label. A person reads it, so no IDs, error codes or status numbers.'),
|
|
14
|
-
confidence: z.enum(['high', 'medium', 'low']).optional().describe('How sure you are. Optional. It is stored with the label when given
|
|
14
|
+
confidence: z.enum(['high', 'medium', 'low']).optional().describe('How sure you are. Optional. It is stored with the label when given. A low-confidence label does not fire its rule: it holds the person and opens a decision, except stop and bounce, which always act.'),
|
|
15
15
|
result: resultInput.optional().describe('The typed detail a stop (reply.stop), bounce (email.bounce) or referral (reply.referral) needs. Same shape as replies set; a stop or referral takes the label quote as its own when it has none.'),
|
|
16
16
|
meeting: z.object({ at: z.string().datetime(), source: z.enum(['sent_invite', 'received_invite']) }).strict().optional().describe('Only for meeting_booked: when the meeting is (UTC, ending in Z) and who sent the invite.'),
|
|
17
17
|
returnsOn: z.string().datetime().optional().describe('Only for out_of_office: the day the person is back (UTC, ending in Z).'),
|
|
@@ -31,7 +31,7 @@ export const playbookOperations={
|
|
|
31
31
|
playbook_draft_save:{schema:z.object({...scope,id:uuid,baseRevision:z.number().int().positive().describe('The version the edits start from: currentRevision from playbook_get, or the baseRevision of the draft you read. A value above the current version is refused with revision_conflict, which carries currentRevision. A lower value is kept; playbook_draft_publish checks it and refuses a draft that started from an older version.'),expectedUpdatedAt:z.string().datetime().nullable().describe('The updatedAt of the draft you read with playbook_draft_get, or null when it returned no draft. If the draft changed since (another tab, your agent or the person), the save is refused with draft_conflict and currentUpdatedAt, and nothing is replaced. Read the draft again, combine the changes, then save.'),fields}).strict(),method:'PUT',path:a=>`${base(a)}/playbooks/${a.id}/draft`,body:({workspaceId,id,...a})=>a,scoped:true,description:'Keep your edits as the draft of a playbook. The draft is replaced as a whole, so send every field: read it with playbook_draft_get (or playbook_get when there is no draft) and change what you need. It never makes a version and uses no requestId. The person who owns the draft sees the change in the editor within a few seconds. A stale expectedUpdatedAt is refused with draft_conflict (HTTP 409). Same field rules as playbook_save, but problems do not stop a draft save: the answer carries check (the problems and warnings a publish would see). The draft is not fully checked until publish. Starter and archived playbooks are refused (starter_playbook, playbook_archived).'},
|
|
32
32
|
playbook_draft_discard:{schema:z.object({...scope,id:uuid,expectedUpdatedAt:z.string().datetime().optional().describe('The updatedAt of the draft you read. When given and the draft changed since, the draft is kept and the call is refused with draft_conflict and currentUpdatedAt. Leave it out to discard whatever the draft holds.')}).strict(),method:'DELETE',path:a=>`${base(a)}/playbooks/${a.id}/draft${a.expectedUpdatedAt?`?expectedUpdatedAt=${encodeURIComponent(a.expectedUpdatedAt)}`:''}`,scoped:true,description:'Throw your draft of a playbook away. The saved versions stay. Safe to repeat: {id, discarded} says whether a draft existed. Pass expectedUpdatedAt to keep a draft that was saved since you read it (draft_conflict).'},
|
|
33
33
|
playbook_draft_publish:{schema:z.object({...scope,id:uuid,requestId:uuid.describe('A fresh UUID. Reuse it only to retry the same call.')}).strict(),method:'POST',path:a=>`${base(a)}/playbooks/${a.id}/draft/versions`,body:({workspaceId,id,...a})=>a,scoped:true,description:'Save your draft of a playbook as the next version and remove the draft, in one step. Returns the new version, as playbook_save does, with the same checks (including 422 playbook_cannot_work). If a newer version was saved after the draft started, nothing is saved and the call is refused with 409 revision_conflict and currentRevision; read the draft and the new version, then save the draft again with the new baseRevision. No draft gives 404 no_draft. A retry with the same requestId returns the same version.'},
|
|
34
|
-
workspace_pipeline_get:{schema:z.object({...scope,campaignId:uuid.optional(),pinId:uuid.optional()}).strict(),method:'GET',path:a=>`${base(a)}/pipeline?${query(a)}`,scoped:true,description:'Read the pipeline board: stages of one playbook version, the people currently in each stage, and counts of people closed out through each off-ramp. Free CRM read.'},
|
|
34
|
+
workspace_pipeline_get:{schema:z.object({...scope,campaignId:uuid.optional(),pinId:uuid.optional()}).strict(),method:'GET',path:a=>`${base(a)}/pipeline?${query(a)}`,scoped:true,description:'Read the pipeline board: stages of one playbook version, the people currently in each stage, and counts of people closed out through each off-ramp. Each card carries the latest three `labels` on the person\'s messages (label, quote, reason, confidence, who recorded it) and `labelsTotal`. Free CRM read.'},
|
|
35
35
|
playbook_progress_get:{schema:z.object({...scope,participationId:uuid.describe('A participation ID (one person in one campaign, from the pipeline), not a playbook ID.')}).strict(),method:'GET',path:a=>`${base(a)}/participations/${a.participationId}/progress`,scoped:true,description:'Read the participation’s admitted playbook pin and recorded stage/outcome history. Takes a participation ID (a person in a campaign; each person in the pipeline has one), not a playbook ID.'},
|
|
36
36
|
playbook_progress_record:{schema:z.object({...scope,participationId:uuid,expectedVersion:z.number().int().nonnegative(),requestId:uuid,stageKey:uuid.optional(),offRampKey:uuid.optional(),finishStage:z.literal(true).optional(),reason:z.string().trim().min(1).max(2000),evidence:z.array(z.object({kind:z.enum(['observation','conversation']),id:uuid}).strict()).max(20)}).strict(),method:'POST',path:a=>`${base(a)}/participations/${a.participationId}/progress`,body:({workspaceId,participationId,...a})=>a,scoped:true,description:'Report a stage, off-ramp, or finishStage:true after externally completing the current play and waits. Finish resolves next/stay/off-ramp from the admitted version. Stored evidence required for agents; human-review and backward-move rules apply. Assigned owner/Admin only. Goal ends prospecting; post-goal forward reporting never reactivates. No send or provider call.'},
|
|
37
37
|
playbook_event_record:{schema:z.object({...scope,participationId:uuid.describe('A participation ID (one person in one campaign, from the pipeline), not a playbook ID.'),requestId:uuid,kind:z.enum(['meeting_requested','meeting_accepted','meeting_declined','meeting_booked','not_a_fit','steps_done']).describe('meeting_requested: a meeting was asked for. meeting_accepted: agreed to, no time yet. meeting_declined: turned down. meeting_booked: a meeting has a time. not_a_fit: the person is not a fit. steps_done: the steps of the current stage are done.'),reason:z.string().trim().min(1).max(300).describe('Why, in one or two sentences. It shows in the person’s history.'),evidence:z.array(z.object({kind:z.enum(['observation','conversation']),id:uuid}).strict()).max(20).optional(),at:z.string().datetime({offset:true}).optional().describe('When it happened, ISO 8601. Defaults to now and is never later than now.')}).strict(),method:'POST',path:a=>`${base(a)}/participations/${a.participationId}/events`,body:({workspaceId,participationId,...a})=>a,scoped:true,description:'Record one event for a person: a meeting asked for, agreed to, turned down or booked, not a fit, or the steps of the current stage done. The playbook then settles: stages that list the event as an entry event take the person forward, and the rules decide the rest (not_a_fit ends the person in the not_fit off-ramp when the playbook has one). Safe to repeat: a second call writes nothing and answers recorded false. Ended participation is refused with participation_ended. Assigned owner/Admin only. No send or provider call.'},
|
package/cli/workspaces.mjs
CHANGED
|
@@ -23,6 +23,7 @@ import { suppressionOperations } from './suppression-operations.mjs';
|
|
|
23
23
|
import { guardrailsOperations } from './guardrails-operations.mjs';
|
|
24
24
|
import { auditOperations } from './audit-operations.mjs';
|
|
25
25
|
import { intentOperations } from './intent-operations.mjs';
|
|
26
|
+
import { decisionOperations } from './decision-operations.mjs';
|
|
26
27
|
import { usageOperations } from './usage-operations.mjs';
|
|
27
28
|
import { outboundOperations } from './outbound-operations.mjs';
|
|
28
29
|
import { cohortOperations } from './cohort-operations.mjs';
|
|
@@ -63,6 +64,7 @@ export const operations = {
|
|
|
63
64
|
...guardrailsOperations,
|
|
64
65
|
...auditOperations,
|
|
65
66
|
...intentOperations,
|
|
67
|
+
...decisionOperations,
|
|
66
68
|
...usageOperations,
|
|
67
69
|
...outboundOperations,
|
|
68
70
|
...cohortOperations,
|
|
@@ -161,6 +163,10 @@ export function localGuidance(name, issues, args) {
|
|
|
161
163
|
const more = playbook && last === 'actionId' ? ' List every action id with its channel: audienti playbook actions.' : playbook && last === 'when' ? ' List the event names: audienti playbook actions.' : '';
|
|
162
164
|
return {field, ...(allowed.length ? {allowed} : {}), next: `Use one of the allowed values in place of ${shown}.${phone}${more}`};
|
|
163
165
|
}
|
|
166
|
+
if(issue.code === 'invalid_union' && issue.discriminator && Array.isArray(issue.options)) {
|
|
167
|
+
const allowed = issue.options.filter(value => typeof value === 'string' && value.length <= 80).slice(0, 60);
|
|
168
|
+
return {field, ...(allowed.length ? {allowed} : {}), next: `Use one of the allowed values in place of ${shown}, spelled exactly.${playbook ? ` Field rules: ${FIELD_RULES}` : ''}`};
|
|
169
|
+
}
|
|
164
170
|
if(issue.code === 'unrecognized_keys') return {field, next: `Remove ${issue.keys.map(key => `"${key}"`).join(', ')} or fix the spelling. ${playbook ? `The fields a playbook has are in the example of: audienti playbook save --help. Field rules: ${FIELD_RULES}` : 'Run audienti --help for the options of this command.'}`};
|
|
165
171
|
if(issue.code === 'invalid_type' && given === undefined && last === 'expectedUpdatedAt') return {field, next: 'Add expectedUpdatedAt (null when there is no draft; otherwise the updatedAt that playbook draft show returned).'};
|
|
166
172
|
if(issue.code === 'invalid_type') return {field, next: given === undefined ? `Add ${last || 'the value'}.${playbook ? ` Field rules: ${FIELD_RULES}` : ''}` : `Send a ${issue.expected} for ${last || field}.${playbook ? ` Field rules: ${FIELD_RULES}` : ''}`};
|
|
@@ -169,6 +175,8 @@ export function localGuidance(name, issues, args) {
|
|
|
169
175
|
// The next step an error gives an agent (#483): one sentence, the path of the wrong value and the values that would have worked.
|
|
170
176
|
// Checked here too, so a response never puts the token or control text into the agent's context.
|
|
171
177
|
// #519: a refused label already says in plain words what is wrong; the code and the next step travel beside it.
|
|
178
|
+
// Refusals of the decision calls (#522). Their messages tell the person what to do next, so the CLI shows them.
|
|
179
|
+
const DECISION_REFUSALS = new Set(['agent_required', 'human_only', 'not_assigned_owner', 'participation_ended', 'decision_not_found', 'decision_closed', 'decision_superseded', 'already_decided', 'option_not_allowed', 'option_count', 'duplicate_option', 'analysis_required', 'analysis_too_long', 'analysis_not_plain', 'workspace_not_found']);
|
|
172
180
|
const INTENT_REFUSALS = new Set(['prospect_not_found', 'conversation_evidence_not_found', 'participation_not_found', 'payload_too_large', 'idempotency_conflict', 'evidence_has_no_body', 'too_many_labels', 'unknown_label', 'already_recorded', 'label_not_available', 'label_direction', 'label_channel', 'duplicate_label', 'must_stand_alone', 'labels_conflict', 'result_required', 'result_mismatch', 'quote_required', 'quote_not_in_message', 'reason_required', 'reason_not_plain', 'detail_not_allowed']);
|
|
173
181
|
export function errorGuidance(error, token, config) {
|
|
174
182
|
const clean = text => typeof text === 'string' && !/[\u0000-\u001f\u007f]/.test(text) && !(token && text.includes(token));
|
|
@@ -202,7 +210,7 @@ export async function callWorkspace(name, args, config) {
|
|
|
202
210
|
if(!response.ok) {
|
|
203
211
|
const code = typeof result?.error?.code==='string' && /^[a-z_]+$/.test(result.error.code) ? result.error.code : 'request_failed';
|
|
204
212
|
const retryAt = typeof result?.error?.retryAt === 'string' && /^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d\.\d{3}Z$/.test(result.error.retryAt) ? result.error.retryAt : undefined;
|
|
205
|
-
const detail = (INTENT_REFUSALS.has(code) || (response.status===400 && (code==='invalid_fields' || code==='validation_error' || code==='secret_rejected')) || (response.status===404 && (code==='not_found' || code==='no_draft')) || (response.status===422 && code==='playbook_cannot_work') || (response.status===409 && (code==='revision_conflict' || code==='draft_conflict' || code==='playbook_in_use' || code==='definition_archived' || code==='playbook_archived' || code==='starter_playbook' || code==='identity_conflict' || code==='task_not_for_you' || code==='version_conflict' || code==='request_conflict' || code==='campaign_not_ready' || code==='campaign_not_paced' || code==='wrong_event_name' || code==='mark_task_done_first' || code==='no_event' || code==='prepare_first' || code==='task_closed' || code==='not_claimed'))) && typeof result?.error?.message==='string' && result.error.message.length<=(code==='playbook_cannot_work'?2000:1000) && !/[\u0000-\u001f\u007f]/.test(result.error.message) && !/^Workspace request failed/.test(result.error.message) && !(config.token && result.error.message.includes(config.token)) ? result.error.message.trim() : '';
|
|
213
|
+
const detail = (INTENT_REFUSALS.has(code) || (name.startsWith('decision_') && DECISION_REFUSALS.has(code)) || (response.status===400 && (code==='invalid_fields' || code==='validation_error' || code==='secret_rejected')) || (response.status===404 && (code==='not_found' || code==='no_draft')) || (response.status===422 && code==='playbook_cannot_work') || (response.status===409 && (code==='revision_conflict' || code==='draft_conflict' || code==='playbook_in_use' || code==='definition_archived' || code==='playbook_archived' || code==='starter_playbook' || code==='identity_conflict' || code==='task_not_for_you' || code==='version_conflict' || code==='request_conflict' || code==='campaign_not_ready' || code==='campaign_not_paced' || code==='wrong_event_name' || code==='mark_task_done_first' || code==='no_event' || code==='prepare_first' || code==='task_closed' || code==='not_claimed'))) && typeof result?.error?.message==='string' && result.error.message.length<=(code==='playbook_cannot_work'?2000:1000) && !/[\u0000-\u001f\u007f]/.test(result.error.message) && !/^Workspace request failed/.test(result.error.message) && !(config.token && result.error.message.includes(config.token)) ? result.error.message.trim() : '';
|
|
206
214
|
const blockers = (code==='execution_blocked' || code==='campaign_not_ready') && Array.isArray(result?.error?.blockers) && result.error.blockers.length<=28 && result.error.blockers.every(v=>typeof v==='string' && /^[a-z_]+$/.test(v)) ? result.error.blockers : undefined;
|
|
207
215
|
const error = new WorkspaceRequestError(response.status, code, name==='intent_record' && (INTENT_REFUSALS.has(code) || code==='invalid_fields') && detail ? surfaceWording(detail, config) : `Workspace request failed: ${code} (HTTP ${response.status}).${detail ? ` ${surfaceWording(detail, config)}` : ''}${retryAt ? ` Retry after ${retryAt}.` : ''}`, retryAt, blockers, requestTraceId, errorGuidance(result?.error, config.token, config));
|
|
208
216
|
if(code==='playbook_cannot_work'&&Array.isArray(result?.error?.errors)&&result.error.errors.length<=30)error.errors=result.error.errors.filter(item=>item&&typeof item.id==='string'&&typeof item.message==='string').map(item=>({id:item.id,level:item.level,message:item.message,...(typeof item.stageKey==='string'?{stageKey:item.stageKey}:{}),...(item.fix&&typeof item.fix.label==='string'?{fix:{id:item.fix.id,label:item.fix.label}}:{})}));
|
package/package.json
CHANGED
|
@@ -7,8 +7,8 @@ approved: inbound wins, and you record it. What the user approves is your answer
|
|
|
7
7
|
|
|
8
8
|
1. Read the conversation: `audienti prospects conversation list PERSON --workspace ID`.
|
|
9
9
|
2. Classify the reply as one of `interested`, `question`, `later`, `not_interested` (a clear no thanks),
|
|
10
|
-
`out_of_office`, `stop`, `bounce`, `referral
|
|
11
|
-
names no one else). A stop, a bounce or a referral must carry its typed
|
|
10
|
+
`out_of_office`, `stop`, `bounce`, `referral`, `wrong_person` (the prospect is the wrong person and
|
|
11
|
+
names no one else) or `unclear` (you cannot tell what they mean; it holds the person for a decision, see below). A stop, a bounce or a referral must carry its typed
|
|
12
12
|
`result`, or the report is refused; see [Stop, bounce and referral](#stop-bounce-and-referral).
|
|
13
13
|
3. Draft the answer from the mailbox or LinkedIn account the reply came to (the assignee's).
|
|
14
14
|
- Bind the recipient and the sender, or the answer waits in Blocked: the recipient's
|
|
@@ -98,8 +98,9 @@ asks for everything to end looks like this:
|
|
|
98
98
|
```
|
|
99
99
|
|
|
100
100
|
`audienti intent --help` prints this example. Give the same `--request-id` to repeat a call safely. Add
|
|
101
|
-
`confidence` (`high`, `medium` or `low`) when you are unsure
|
|
102
|
-
|
|
101
|
+
`confidence` (`high`, `medium` or `low`) when you are unsure. A low-confidence label does not fire its rule:
|
|
102
|
+
the person is held and a decision opens for your user to pick from (see below). `stop` and `bounce` always
|
|
103
|
+
act, whatever the confidence. A message keeps the labels it has: a second request that repeats
|
|
103
104
|
one is refused with `already_recorded`, and one that would give the message a second reading is refused.
|
|
104
105
|
|
|
105
106
|
Audienti checks every label and writes nothing if one fails. A refusal names the `field`, the `allowed`
|
|
@@ -113,6 +114,10 @@ values and the `next` step. The checks:
|
|
|
113
114
|
reply and at most one meeting label.
|
|
114
115
|
- The typed detail is present: a `stop`, `bounce` or `referral` needs its `result`; `meeting_booked` needs `meeting`.
|
|
115
116
|
|
|
117
|
+
A recorded label shows to the user as a chip with the quote, the reason, how sure you were and who recorded it, on the
|
|
118
|
+
reply card in Operator, the person's page and the Pipeline card. Write the `quote` and `reason` for a person to read. The same
|
|
119
|
+
labels come back as `labels` on `audienti prospects timeline PERSON`, `audienti prospects replies get PERSON` (the latest reply, or one message with `--evidence`) and `audienti pipeline show`. A label event on the timeline carries the label once, under `event.label`. Mark a guess with `confidence: low`: the chip says so.
|
|
120
|
+
|
|
116
121
|
To get a second reading before you record, `audienti intent suggest --channel email --text "..."` asks a model
|
|
117
122
|
to suggest labels. It costs credits: the call prints the price per million input tokens first, and a message
|
|
118
123
|
uses a small part of that. It returns up to two suggestions, each with a `quote`, a `reason`, a `confidence` and
|
|
@@ -157,6 +162,26 @@ The same checks apply: a message with no stored text, a quote that is not in it,
|
|
|
157
162
|
- The label goes on the person's running campaign. A person runs in one campaign per workspace, so you need no option; `--participation ID` names the run if you want it explicit. The answer to `work execution send` or `report` carries that `participationId` when the send started a playbook step and the person still runs in the campaign run the send started, also when you repeat the call. A person who left and rejoined has a new run: a repeat of the old send names none. The label works without it.
|
|
158
163
|
- An answer to a reply that offers times is labeled the same way, with the evidence of the answer you sent.
|
|
159
164
|
|
|
165
|
+
## When the rules do not decide: decisions
|
|
166
|
+
|
|
167
|
+
A reply the rules cannot act on holds the person, and a decision asks your user to pick. It opens when a
|
|
168
|
+
label is `unclear`, when a label has low confidence (not `stop` or `bounce`), when a reply has had no label
|
|
169
|
+
for 24 hours, or when you ask for one with `audienti decision ask`. While it is open nothing is sent to the
|
|
170
|
+
person. A `decisions.prepare` task tells you to add your view:
|
|
171
|
+
|
|
172
|
+
1. Read the options the playbook allows: `audienti decision options DECISION_ID`. Only these ids are valid.
|
|
173
|
+
A stage option names its stage (`stageName`), a snooze option reads "Snooze 30 days, then continue", and each
|
|
174
|
+
trigger says who recorded the event behind it (`recordedBy`: kind and name). The clock raises a trigger with none.
|
|
175
|
+
2. Choose two to five. Write one plain line for each (at most 140 characters, no IDs or error codes) and
|
|
176
|
+
recommend one: `audienti decision prepare DECISION_ID --recommended ID --option ID=ANALYSIS ...`.
|
|
177
|
+
Audienti refuses an option outside the list and names the allowed ids.
|
|
178
|
+
3. Report the task done. Only your user picks, in Operator or with `audienti decision answer` under their own
|
|
179
|
+
sign-in. Audienti never applies your recommendation. After 24 hours without your view the user sees the
|
|
180
|
+
options without analyses. An open decision turns stale after 7 days; the person stays held.
|
|
181
|
+
|
|
182
|
+
A reply you can label clearly needs no decision: record the label with `audienti intent record` and the rules
|
|
183
|
+
act. A label recorded later ends the hold.
|
|
184
|
+
|
|
160
185
|
## Stop, bounce and referral
|
|
161
186
|
|
|
162
187
|
These three need no decision from the user. Classify the reply, report the typed
|
|
@@ -35,7 +35,7 @@ When you change a playbook you already have, a part you leave out (`stages`, `go
|
|
|
35
35
|
| a stage `completion` | A stage with steps that an agent works, other than the goal, closes people out as Unresponsive. Other stages stay. |
|
|
36
36
|
| a step `condition` | `Only if connected` for a LinkedIn message, else `Always`. |
|
|
37
37
|
| a step `instructions` | One coaching sentence for the kind of action. |
|
|
38
|
-
| a main step `wait` | Only for a step that is new in this save. A step that sends waits 3 days for a reply; a connection request waits 7 days for the accept; a step that sends nothing has no wait. A step the playbook already has keeps what it has, and `"wait": null` means no wait. |
|
|
38
|
+
| a main step `wait` | Only for a step that is new in this save. A step that sends waits 3 days for a reply; a connection request waits 7 days for the accept; a step that sends nothing has no wait. A step the playbook already has keeps what it has, and `"wait": null` means no wait. The saved step carries the wait it got, so `playbook show` and the API read give the actual days. |
|
|
39
39
|
|
|
40
40
|
## The Playbook check
|
|
41
41
|
|
|
@@ -66,7 +66,7 @@ The other items are warnings; read them after each save and clear them:
|
|
|
66
66
|
| `goalDays` | 0 to 365. The days after which a prospect in this stage is overdue (the pipeline card turns orange). 0 means never overdue. |
|
|
67
67
|
| `enabled` | Defaults to true. `false` skips the stage: no one is moved into it. The first stage and the goal stage cannot be skipped. |
|
|
68
68
|
| `steps` | Up to 20 steps. |
|
|
69
|
-
| `completion` | `{ "kind": "next" }`, `{ "kind": "stay" }` or `{ "kind": "off_ramp", "offRampKey": KEY }`. The last enabled stage cannot use `next`. |
|
|
69
|
+
| `completion` | `{ "kind": "next" }`, `{ "kind": "stay" }` or `{ "kind": "off_ramp", "offRampKey": KEY }`. The last enabled stage cannot use `next`. Spell the kind exactly: `off_ramp`, not `offRamp`. A kind that matches none is refused with the list of kinds that work. |
|
|
70
70
|
|
|
71
71
|
## Step
|
|
72
72
|
|
package/source-manifest.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"repository": "https://github.com/omalab/exora",
|
|
3
|
-
"sourceCommit": "
|
|
3
|
+
"sourceCommit": "12b6349e33300c2867a516871532b009361e2af3",
|
|
4
4
|
"files": {
|
|
5
5
|
"LICENSE": "220f297cfcd5c38eb64e6b4b4a77cab4ebc7f4686d4d99c6260a475d02ca11e5",
|
|
6
6
|
"README.md": "7ff32cc8ac89dd54b64d7b39c0b41b1f37e154b7ea10ad5a7bceefc3df77d87f",
|
|
7
7
|
"bin/audienti-mcp.mjs": "6df1d6eb3a5bc65e7592e5b8f73cd4c98c159f2583723d042338633b356301df",
|
|
8
8
|
"cli/account-intake-operations.mjs": "b43d764c628beabafad753d711ddb2af2c64276e4a6506ed0b854839091ea5db",
|
|
9
9
|
"cli/account-intake.mjs": "aa77f2f96fd030f8830d6de278932186aea7c82cfda086a5a6cf569202feebc8",
|
|
10
|
-
"cli/account-replies.mjs": "
|
|
11
|
-
"cli/account-reply-operations.mjs": "
|
|
10
|
+
"cli/account-replies.mjs": "80059e8cec767a3e33aa971acf467603ca83f05efe42c59cd57a19defc37c07e",
|
|
11
|
+
"cli/account-reply-operations.mjs": "12c3fa1533b09b7f5a376f24c9ef140cfe14211927d9780bf532503367b132da",
|
|
12
12
|
"cli/agents.mjs": "e311dd08d1dc09648881a37eb9765bf6d155786f0b713204f51de29bc8134293",
|
|
13
13
|
"cli/assignment-operations.mjs": "fc74e3fd79975cfcbac14891d5dec5f25e662777050fc46a8ad3aae5cf361c59",
|
|
14
14
|
"cli/assignment.mjs": "90cb85e0df8aadca2ff8391c99e778b41261183b5f38e8c6c32afee2f98f871a",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"cli/attachments.mjs": "2fee6b4abbf82da9847d4a927a3d4d3ca947a198a903a78927596a2ea858cbf4",
|
|
17
17
|
"cli/audience-operations.mjs": "133b72dc6b9e3a6b4f173ce28f1524163f9c7b049505b885115f83b9e4af7170",
|
|
18
18
|
"cli/audiences.mjs": "3c377346efc1a8bdde432a16c2d19a3f51f34f3333d344d3a2bfa94c188a017d",
|
|
19
|
-
"cli/audienti.mjs": "
|
|
19
|
+
"cli/audienti.mjs": "fa7a7e479eb95d60678fa7b24dbbbbc84aacddcfdcd8edc7373f1859b86c8ba4",
|
|
20
20
|
"cli/audit-operations.mjs": "afaf87d592a1e5f6bff2a8649a6a64a8f471950b7d99e98995d9205aa5910398",
|
|
21
21
|
"cli/audit.mjs": "a7becaed67c58b063e668b5e66844b6dc15c11a63b428faa171b542ad922c001",
|
|
22
22
|
"cli/budget-operations.mjs": "d74916551ef393e29734c6e13cf9021251e0ed126eeaa529e18cf2332146e1e0",
|
|
@@ -28,7 +28,9 @@
|
|
|
28
28
|
"cli/cohorts.mjs": "c71940903430f3e6834608f113534910b75a98b3e410a5942f04051607488485",
|
|
29
29
|
"cli/connection-registry-operations.mjs": "888ea440d00ffc9ed68d4cc084ff73a5a00d0e60bd6293baba8bb4539a25ae62",
|
|
30
30
|
"cli/content.mjs": "139dee5fd37fd89669c39e2120ac70a2e0286b207b055ff666f54ba3512ab061",
|
|
31
|
-
"cli/
|
|
31
|
+
"cli/decision-operations.mjs": "0c4c2134c9615678def2b50f2a5254218b14f1174f1ce4e8ba7db78c751661af",
|
|
32
|
+
"cli/decision.mjs": "3d89d48b333e2620bce0191cf528ef25541ee841a7f8ae6d60249aead434b651",
|
|
33
|
+
"cli/discovery-operations.mjs": "9b6873d586d27bf67106a592db8ced75da1abf1a1330f192e69d34f81b2d080f",
|
|
32
34
|
"cli/discovery.mjs": "34b3794ee6ca539dbbccef77ec2085fd5411023046bdfb9b83e2537414542a41",
|
|
33
35
|
"cli/file-operations.mjs": "564f334dd9d52e9c56f44d58b5c3b0bda59ff72f54126b755bc472144069349e",
|
|
34
36
|
"cli/files.mjs": "2221b067d6ade7ef5cc8965929aea2eb485781953d3f2b50600995b65fbdedae",
|
|
@@ -36,7 +38,7 @@
|
|
|
36
38
|
"cli/guardrails.mjs": "e8b9c0a19956a9eab84475d3c56a4b6c77baf7ed9a55e8686d660ef1ad562aa8",
|
|
37
39
|
"cli/hand-task-operations.mjs": "473473fe95324c8c8733e037feab5ac468c07b0658fabd739cfd7d400a7faa83",
|
|
38
40
|
"cli/hand-tasks.mjs": "2c895cf6b30c594a7e27cc4ec4b7eddeb39f941cd195b9255c6e0d6b4a56371f",
|
|
39
|
-
"cli/intent-operations.mjs": "
|
|
41
|
+
"cli/intent-operations.mjs": "63871dbc0262353a12663df2642fa0f7b4166d688f95f0b729ec01e9f1e10857",
|
|
40
42
|
"cli/intent.mjs": "5aa62593f65f58f2714bb402365624005ae17f73f65da3952b06f45e3c109450",
|
|
41
43
|
"cli/json-file.mjs": "96431b18215834c2531f5901e2526875c634dd2e85a9465fcffc937fbec52150",
|
|
42
44
|
"cli/kv-operations.mjs": "505d79d0d535d3441883f8a37817c30c73367216a35dddb07181f0f0e799df79",
|
|
@@ -51,7 +53,7 @@
|
|
|
51
53
|
"cli/outcomes.mjs": "765ccb112a90fc4d228b2a41e5e19c5e1f3e2fe14bc1d8128b0090fc95cdbf3f",
|
|
52
54
|
"cli/owner-account-client.mjs": "aa2790fd3c16714b1a82922fe13012f261cced99025d692bca5d0c605a717cd1",
|
|
53
55
|
"cli/playbook-help.mjs": "dad00cae19eb107f84c7934812800431535c6cf1dbcdcdd22c16522cfaddadf3",
|
|
54
|
-
"cli/playbook-operations.mjs": "
|
|
56
|
+
"cli/playbook-operations.mjs": "5b0680b69c2589c5e2b83aeb97f350b3c694de121a8307a76bd220a394088b76",
|
|
55
57
|
"cli/profiles.mjs": "45929b224ffc916b6dbd7a94e06ac09a1ff7feec430633b10d160c1608fc2d32",
|
|
56
58
|
"cli/progress.mjs": "6908f2937adb87a83ebefc1f4324ed18adac65228547b0524a474ac3513b5c67",
|
|
57
59
|
"cli/prospecting-settings-operations.mjs": "1829eb2fe7208f58601988143756072429adbfe00f8deb5a52613bf650a9eacc",
|
|
@@ -83,8 +85,8 @@
|
|
|
83
85
|
"cli/work-feedback.mjs": "f3b77ecfd7ea1aba9c9f03c9d894137547e9c5575d9daa359ed94505ad025852",
|
|
84
86
|
"cli/work-operations.mjs": "8b3a84ca15e8e4ad79ef92ced4c5adaffd5129f630e3bfc2c74a844a90867600",
|
|
85
87
|
"cli/work.mjs": "751107ecc3de5a6be9db4a068cddc41f3ad8e77c87e59686eaf522862caf9e9f",
|
|
86
|
-
"cli/workspaces.mjs": "
|
|
87
|
-
"package.json": "
|
|
88
|
+
"cli/workspaces.mjs": "fb1b809eafa0c761d0e6124a172acf55056488e7d8a0e25ceeeb1cd6a036fb5c",
|
|
89
|
+
"package.json": "91e96eb6bc639bea60b9741498f268220ef56ef70f3d6ce8baf07f82f2082f6c",
|
|
88
90
|
"skills/agent-rooms/SKILL.md": "08f917ff85fee1d23499a4499056ce845b39ce6727029b104819dad41ee87cc2",
|
|
89
91
|
"skills/agent-rooms/references/commands.md": "ef5afa5543ca9da967a147f638774647998ea1c1a2a821959f3bf87d54c292bd",
|
|
90
92
|
"skills/audienti/SKILL.md": "8f2124065bfc3a005b44e194e7dcce9bbf14062ab332e7db66d441eeccb518ed",
|
|
@@ -100,9 +102,9 @@
|
|
|
100
102
|
"skills/getting-started/references/capabilities.md": "982e3583a47ba4ac832efae2f6af3a4d8c0c18cace159c76b2a4ad1a8d8e910e",
|
|
101
103
|
"skills/getting-started/references/mailbox-check.md": "113574bb5a573125620dcde4e65ebf44164f7b0727cc936fb41234f5d57bba73",
|
|
102
104
|
"skills/getting-started/references/outcome-checks.md": "094d09d231b38d4935aa9eb8ac089e46ba8e698489852fa97040e6b5eac77a9b",
|
|
103
|
-
"skills/getting-started/references/replies.md": "
|
|
105
|
+
"skills/getting-started/references/replies.md": "a372a01078f7c0a750b0fb98aff23ca9e7d9155c6b4ac549dd621444325e402b",
|
|
104
106
|
"skills/getting-started/references/reply-checks.md": "85fe692ad6b7bc73e204de313b27d225be3da1a0af71e8e7bb9945ae511a0dc3",
|
|
105
107
|
"skills/playbook-design/SKILL.md": "56aa5692c1bbe3843f11819f0f9270988ddd0424b16f38db72a854864155407f",
|
|
106
|
-
"skills/playbook-design/references/fields.md": "
|
|
108
|
+
"skills/playbook-design/references/fields.md": "c7ba7a0bf8ac1325e70a5e4d98734716d752c311588d064bf2aba849925357aa"
|
|
107
109
|
}
|
|
108
110
|
}
|