@audienti/cli 0.2.0-exora.12 → 0.2.0-exora.13
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-reply-operations.mjs +3 -2
- package/cli/audienti.mjs +4 -2
- package/cli/intent-operations.mjs +29 -2
- package/cli/intent.mjs +30 -3
- package/cli/playbook-operations.mjs +4 -3
- package/cli/workspaces.mjs +4 -2
- package/package.json +1 -1
- package/skills/getting-started/references/replies.md +41 -0
- package/skills/playbook-design/references/fields.md +3 -4
- package/source-manifest.json +10 -10
|
@@ -18,7 +18,7 @@ const draft = z.object({
|
|
|
18
18
|
}).strict();
|
|
19
19
|
// Brief 12: what a stop, bounce or referral means, as a typed detail next to the classification it matches.
|
|
20
20
|
const quote = z.string().meta({ minLength: 1, maxLength: 500 }).describe("The prospect's words, verbatim.");
|
|
21
|
-
const
|
|
21
|
+
export const replyResultInput = z.discriminatedUnion('type', [
|
|
22
22
|
z.object({ type: z.literal('email.bounce'), address: z.string().meta({ minLength: 3, maxLength: 320 }).describe('The address that bounced.'), kind: z.enum(['hard', 'soft']).describe('hard: the address does not exist or refuses mail. soft: a full mailbox or a temporary failure.'), evidence: z.string().meta({ minLength: 1, maxLength: 2000 }).describe('The bounce message, its status code and diagnostic.') }).strict().describe('Classification bounce. A hard bounce, or a second bounce, marks the address bounced and blocks it in every campaign.'),
|
|
23
23
|
z.object({ type: z.literal('reply.stop'), scope: z.enum(['person', 'company']).describe('company only when the prospect asked to stop for their whole company.'), quote }).strict().describe('Classification stop, with status handled and outcome suppressed. The person, or everyone at the company, is suppressed for good.'),
|
|
24
24
|
z.object({ type: z.literal('reply.referral'), name: z.string().meta({ minLength: 1, maxLength: 200 }).describe('The person the prospect named.'), title: z.string().meta({ maxLength: 200 }).optional(), email: z.string().meta({ maxLength: 320 }).optional(), linkedinUrl: z.string().meta({ maxLength: 500 }).optional(), quote }).strict().describe('Classification referral, with status needs_handling and the thank-you as the draft. The named person is checked against the audience before any contact.'),
|
|
@@ -38,8 +38,9 @@ export const accountReplyOperations = {
|
|
|
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."),
|
|
41
|
-
result:
|
|
41
|
+
result: replyResultInput.optional().describe("Brief 12: the typed detail for a bounce, stop or referral. Its type must match the classification."),
|
|
42
42
|
meeting: z.object({ at: z.string().datetime(), source: z.enum(['sent_invite', 'received_invite']) }).strict().optional().describe("A booked meeting: when, and whether the user's calendar sent the invite or the prospect did. Records meeting_booked."),
|
|
43
|
+
intent: z.object({ quote: z.string().meta({ minLength: 1, maxLength: 300 }).describe('The words in the reply that support the classification or meeting. Copy them; case and spacing do not matter.'), reason: z.string().meta({ minLength: 1, maxLength: 200 }).describe('One plain sentence: why. A person reads it, so no IDs or error codes.'), confidence: z.enum(['high', 'medium', 'low']).optional().describe('How sure you are. Default high.') }).strict().optional().describe("The words behind your reading. With it, the label is checked like intent_record checks it (the quote is in the reply, the reason is plain) and the quote and reason are stored on the event. Requires a classification or a meeting."),
|
|
43
44
|
meetingState: z.enum(['requested', 'accepted', 'declined']).optional().describe("Where a meeting that is not booked yet stands after this reply: requested (a time was asked for), accepted (they agreed, no time yet) or declined. Records the matching meeting event once. Ignored once a meeting is booked.") }).strict().describe("Explicit inbound classification. needs_handling/not_reply require null outcome and responseEvidenceId. handled requires an outcome; response_recorded requires matching outbound evidence, closed_without_response an explicit closure reason, and suppressed active DNC or Reject."),
|
|
44
45
|
method: 'POST', scoped: true, path: detail,
|
|
45
46
|
body: ({ workspaceId, prospectId, evidenceId, ...rest }) => rest,
|
package/cli/audienti.mjs
CHANGED
|
@@ -81,6 +81,7 @@ Discovery: prospects list|get ID|timeline ID [--limit N]|observation create|cita
|
|
|
81
81
|
usage breakdown [--workspace ID] [--since month|7d|30d|90d] [--by tool|provider|model|caller|day] [--format json|csv]
|
|
82
82
|
workspace usage summary|callers|calls [--after ID] [--caller user:ID|agent:ID|key:ID] [--status all|succeeded|failed|pending]|call ID [--workspace ID] (paid tool use, last 30 days, and single calls)
|
|
83
83
|
intent list [--workspace ID] (the closed set of message labels: name, direction, event, channels, typed detail; output is JSON)
|
|
84
|
+
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)
|
|
84
85
|
audit list [--workspace ID] [--since 7d] [--actor agent|person] [--category NAME] [--format json|csv]
|
|
85
86
|
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%)
|
|
86
87
|
Qualification: campaigns qualification submit --file INPUT.json
|
|
@@ -114,6 +115,7 @@ Start local MCP stdio server: mcp
|
|
|
114
115
|
|
|
115
116
|
// Example inputs for the commands that save prospects, so `--help` shows a working shape.
|
|
116
117
|
const examples = {
|
|
118
|
+
'intent record': [{ label: 'stop', quote: 'please stop writing to me', reason: 'They ask us to stop.', result: { type: 'reply.stop', scope: 'person' } }],
|
|
117
119
|
'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' } },
|
|
118
120
|
'prospects citation create': { requestId: 'UUID', source: { kind: 'prospect', id: 'PERSON_PROSPECT_ID' }, target: { kind: 'prospect', id: 'COMPANY_PROSPECT_ID' }, edgeType: 'works_at', roles: [{ roleId: 'role-1', title: 'VP Sales', current: true, observedAt: '2026-01-01T00:00:00Z' }] },
|
|
119
121
|
'prospects evidence create': { requestId: 'UUID', subject: { kind: 'person', prospectId: 'PROSPECT_ID' }, campaignId: 'CAMPAIGN_ID', campaignRevision: 1, kind: 'interpretation', data: { conclusion: 'Leads a 40-person sales team', reason: 'Team page lists her as VP Sales' }, sourceUrl: 'https://example.com/team', observedAt: '2026-01-01T00:00:00Z', skill: { id: 'signal-prospect-research', version: '1' } },
|
|
@@ -130,8 +132,8 @@ function commandHelp() {
|
|
|
130
132
|
shown = lines.filter(line => line.includes(prefix));
|
|
131
133
|
}
|
|
132
134
|
console.log(shown.length ? shown.map(line => line.trim()).join('\n') : usage);
|
|
133
|
-
const example = Object.entries(examples).find(([command]) => words.join(' ').startsWith(command));
|
|
134
|
-
if (example) console.log(`\nExample --file INPUT.json for ${example[0]}:\n${JSON.stringify(example[1], null, 1)}\
|
|
135
|
+
const example = Object.entries(examples).find(([command]) => words.join(' ').startsWith(command) || (words.length > 0 && command.startsWith(words.join(' '))));
|
|
136
|
+
if (example) console.log(`\nExample --file INPUT.json for ${example[0]}:\n${JSON.stringify(example[1], null, 1)}\n${example[0].startsWith('intent') ? 'Rules: the "Label a message with its words" section of the getting-started replies reference.' : 'Steps and rules: the "Save prospects and contacts" section of llms.txt.'}`);
|
|
135
137
|
}
|
|
136
138
|
|
|
137
139
|
async function main() {
|
|
@@ -1,8 +1,35 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
const bounce = z.object({ type: z.literal('email.bounce'), address: z.string().meta({ minLength: 3, maxLength: 320 }).describe('The address that bounced.'), kind: z.enum(['hard', 'soft']).describe('hard: the address does not exist or refuses mail. soft: a full mailbox or a temporary failure.'), evidence: z.string().meta({ minLength: 1, maxLength: 2000 }).describe('The bounce message, its status code and diagnostic.') }).strict();
|
|
3
|
+
const own = z.string().meta({ minLength: 1, maxLength: 500 }).optional().describe("Optional. The label's own quote is used when this is left out.");
|
|
4
|
+
const resultInput = z.discriminatedUnion('type', [
|
|
5
|
+
bounce,
|
|
6
|
+
z.object({ type: z.literal('reply.stop'), scope: z.enum(['person', 'company']).describe('company only when the person asked to stop for their whole company.'), quote: own }).strict(),
|
|
7
|
+
z.object({ type: z.literal('reply.referral'), name: z.string().meta({ minLength: 1, maxLength: 200 }).describe('The person named.'), title: z.string().meta({ maxLength: 200 }).optional(), email: z.string().meta({ maxLength: 320 }).optional(), linkedinUrl: z.string().meta({ maxLength: 500 }).optional(), quote: own }).strict(),
|
|
8
|
+
]);
|
|
9
|
+
const uuid = z.string().uuid();
|
|
10
|
+
const label = z.object({
|
|
11
|
+
label: z.string().describe('A name from intent_labels_list that fits the message direction and channel.'),
|
|
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
|
+
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 and does not change what the label does yet.'),
|
|
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
|
+
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
|
+
returnsOn: z.string().datetime().optional().describe('Only for out_of_office: the day the person is back (UTC, ending in Z).'),
|
|
18
|
+
invite: z.literal(true).optional().describe('The person sent a calendar invite that is not processed yet.'),
|
|
19
|
+
}).strict();
|
|
2
20
|
export const intentOperations = {
|
|
3
21
|
intent_labels_list: {
|
|
4
|
-
schema: z.object({ workspaceId:
|
|
22
|
+
schema: z.object({ workspaceId: uuid.optional() }).strict(),
|
|
5
23
|
method: 'GET', path: a => `/workspaces/${a.workspaceId}/intent-labels`, scoped: true,
|
|
6
|
-
description: 'List the closed set of message labels: each name with its direction (inbound or outbound), the event it starts, the channels it fits, the typed detail it needs, whether it is locked or must stand alone, and whether it is only planned. Read it to learn which names exist
|
|
24
|
+
description: 'List the closed set of message labels: each name with its direction (inbound or outbound), the event it starts, the channels it fits, the typed detail it needs, whether it is locked or must stand alone, and whether it is only planned. Read it to learn which names exist, then record one with intent_record.',
|
|
25
|
+
},
|
|
26
|
+
intent_record: {
|
|
27
|
+
schema: z.object({
|
|
28
|
+
workspaceId: uuid.optional(), prospectId: uuid, evidenceId: uuid, requestId: uuid,
|
|
29
|
+
participationId: uuid.optional().describe('Record on this one campaign participation only. Leave out to record on every running campaign of the person.'),
|
|
30
|
+
labels: z.array(label).min(1),
|
|
31
|
+
}).strict(),
|
|
32
|
+
method: 'POST', path: a => `/workspaces/${a.workspaceId}/intents`, scoped: true, body: ({ workspaceId, ...rest }) => rest,
|
|
33
|
+
description: 'Record what a message means: one to three labels, each with the quote that supports it and a plain reason. The message must already be stored as conversation evidence with its text. Every label is checked first and nothing is written if one fails: the name is known and fits the direction and channel, the quote is in the message, the reason is plain words, the labels fit together (stop, bounce and out_of_office stand alone), and the typed detail a stop, bounce, referral or booked meeting needs is present. A refusal names the field, the allowed values and the next step. The same requestId with the same body returns the first answer. The matching events land on the person\'s running campaigns, so the playbook rules act on them. Replies set records the same labels when you give it an intent block.',
|
|
7
34
|
},
|
|
8
35
|
};
|
package/cli/intent.mjs
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
1
2
|
import { callWorkspace, configuration } from './workspaces.mjs';
|
|
3
|
+
import { readJsonFile } from './json-file.mjs';
|
|
4
|
+
|
|
5
|
+
const RECORD_FLAGS = ['workspace', 'profile', 'person', 'evidence', 'label', 'quote', 'reason', 'confidence', 'request-id', 'participation', 'file'];
|
|
6
|
+
const USAGE = 'Use intent list [--workspace ID], or intent record --person ID --evidence ID (--label NAME --quote TEXT --reason TEXT [--confidence high|medium|low] | --file LABELS.json) [--request-id UUID] [--participation ID] [--workspace ID].';
|
|
2
7
|
|
|
3
8
|
export async function intentCli(args) {
|
|
4
9
|
const flags = {}, positions = [];
|
|
@@ -7,9 +12,31 @@ export async function intentCli(args) {
|
|
|
7
12
|
if (arg === '--json') continue;
|
|
8
13
|
if (!arg.startsWith('--')) { positions.push(arg); continue; }
|
|
9
14
|
const key = arg.slice(2);
|
|
10
|
-
if (!
|
|
15
|
+
if (!RECORD_FLAGS.includes(key) || Object.hasOwn(flags, key) || i + 1 >= args.length) throw Error(`Invalid flag --${key}. ${USAGE}`);
|
|
11
16
|
flags[key] = args[++i];
|
|
12
17
|
}
|
|
13
|
-
|
|
14
|
-
|
|
18
|
+
const command = positions[1];
|
|
19
|
+
if (positions.length !== 2 || positions[0] !== 'intent' || !['list', 'record'].includes(command)) throw Error(USAGE);
|
|
20
|
+
if (command === 'list') {
|
|
21
|
+
if (Object.keys(flags).some(key => !['workspace', 'profile'].includes(key))) throw Error('Option does not apply to intent list.');
|
|
22
|
+
console.log(JSON.stringify(await callWorkspace('intent_labels_list', { workspaceId: flags.workspace }, configuration(flags.profile))));
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (!flags.person || !flags.evidence) throw Error(`--person ID and --evidence ID are required. ${USAGE}`);
|
|
26
|
+
const single = ['label', 'quote', 'reason', 'confidence'].filter(key => flags[key] !== undefined);
|
|
27
|
+
if (flags.file && single.length) throw Error('Give the labels in --file, or one label with --label, --quote and --reason. Not both.');
|
|
28
|
+
let labels, requestId = flags['request-id'];
|
|
29
|
+
if (flags.file) {
|
|
30
|
+
const body = readJsonFile(flags.file, 65536, 'Labels file');
|
|
31
|
+
if (Array.isArray(body)) labels = body;
|
|
32
|
+
else if (body && typeof body === 'object' && Array.isArray(body.labels) && Object.keys(body).every(key => ['labels', 'requestId'].includes(key))) { labels = body.labels; requestId ??= body.requestId; }
|
|
33
|
+
else throw Error('The labels file is a JSON array of labels, or { "labels": [...] }.');
|
|
34
|
+
} else {
|
|
35
|
+
if (!flags.label || !flags.quote || !flags.reason) throw Error(`--label, --quote and --reason are required without --file. ${USAGE}`);
|
|
36
|
+
labels = [{ label: flags.label, quote: flags.quote, reason: flags.reason, ...(flags.confidence ? { confidence: flags.confidence } : {}) }];
|
|
37
|
+
}
|
|
38
|
+
console.log(JSON.stringify(await callWorkspace('intent_record', {
|
|
39
|
+
workspaceId: flags.workspace, prospectId: flags.person, evidenceId: flags.evidence, requestId: requestId ?? randomUUID(),
|
|
40
|
+
...(flags.participation ? { participationId: flags.participation } : {}), labels,
|
|
41
|
+
}, configuration(flags.profile))));
|
|
15
42
|
}
|
|
@@ -9,11 +9,12 @@ const eventRule=z.object({key:z.string().uuid(),when:z.string().min(1).max(60).d
|
|
|
9
9
|
const stepRoutes=z.object({accept:routeTarget.optional(),reply:routeTarget.optional(),meeting:routeTarget.optional(),timeout:routeTarget.optional()}).strict().describe("Where an event that reaches the person after this step ran sends them. A route wins over the stage's jump list; an event with no route follows the stage. Going to an earlier stage needs rules.allowBackward.");
|
|
10
10
|
const playbookStepSchema=z.object({key:z.string().uuid(),channel:z.enum(['LinkedIn','Email','Custom']),action:label,condition:z.enum(['Always','Only if viable','Only if connected']).optional().describe('Left out: Only if connected for a LinkedIn message (it can only reach a connection), Always for everything else. Only if connected skips the step for a person who was asked to connect and has not accepted. Only if viable needs viability.'),viability:text.optional(),instructions:text.optional().describe('What the agent should write or do. Left out, one coaching sentence for the kind of action.'),wait:z.object({until:z.enum(['accept','reply','meeting','timeout','event']),days:z.number().int().min(1).max(60),event:label.optional().describe('Custom steps only, with until event: the name of the event a person records to end the wait, for example Gift delivered.'),followUps:z.number().int().min(0).max(10).optional().describe('Meeting waits only: how many follow-ups your agent writes, one after each wait, before the meeting is marked No answer. Default 3.')}).strict().nullable().optional().describe('How long the step waits and for what. Left out on a new step that sends: a reply wait of 3 days (a connection request waits 7 days for the accept). null means no wait. A step the playbook already has keeps what it has.'),waitDays:z.number().int().min(1).max(60).optional().describe('Older form of wait: the same as wait {until: timeout, days}.'),side:z.boolean().optional(),onTimeout:z.enum(['next','off_ramp']).optional(),humanize:z.boolean().optional().describe('Email only. Rewrite the email text before approval so it reads as written by a person. On by default; set false to send the text as written.'),actionId:z.string().max(80).optional().describe('An action id from the outreach rulebook, for example linkedin.message.send.'),routes:stepRoutes.optional(),eventRules:z.array(eventRule).max(10).optional()}).strict();
|
|
11
11
|
const completion=z.discriminatedUnion('kind',[z.object({kind:z.literal('next')}).strict(),z.object({kind:z.literal('stay')}).strict(),z.object({kind:z.literal('off_ramp'),offRampKey:z.string().uuid()}).strict()]);
|
|
12
|
-
const stageFields=identity.extend({
|
|
12
|
+
const stageFields=identity.extend({owner:z.enum(['Agent','Assignee']).optional(),goalDays:z.number().int().min(0).max(365).optional().describe('Days a person should take in this stage. Left out: the waits of its steps plus 2 days, at least 3.'),enabled:z.boolean().optional().describe('false skips the stage: people are never moved into it. The first stage and the goal stage cannot be skipped.'),steps:z.array(playbookStepSchema).max(20).optional(),completion:completion.optional(),on:z.array(z.enum(['accept','reply','meeting'])).max(3).optional().describe('The events that jump a person forward to this stage: accept, reply, meeting (a booked meeting). A person who has one of these events moves to the furthest later stage that lists it, and the stages passed over are recorded as skipped. Leave it out for a stage people reach when the stage before it finishes, or when a person moves them. Not for the first stage.'),eventRules:z.array(eventRule).max(10).optional()});
|
|
13
13
|
// Settings the product no longer asks for (#457). An older request may still carry them: they are accepted, ignored and left out of the new version.
|
|
14
|
-
// autoMove: conflict 20, approval lives on the account. flagOverdue, emailWindow, nurtureDays: no code read them. optional (stage): use enabled.
|
|
14
|
+
// autoMove: conflict 20, approval lives on the account. flagOverdue, emailWindow, nurtureDays: no code read them. optional (stage): use enabled. movesInWhen (words) and 'manual' in on: the stage's events say when a person enters it.
|
|
15
15
|
const dropKeys=keys=>value=>{if(!value||typeof value!=='object'||Array.isArray(value)||!keys.some(key=>key in value))return value;return Object.fromEntries(Object.entries(value).filter(([key])=>!keys.includes(key)));};
|
|
16
|
-
const
|
|
16
|
+
const dropRetiredStage=value=>{const rest=dropKeys(['optional','movesInWhen'])(value);if(!rest||typeof rest!=='object'||Array.isArray(rest)||!Array.isArray(rest.on))return rest;return {...rest,on:rest.on.filter(event=>event!=='manual')};};
|
|
17
|
+
const stage=z.preprocess(dropRetiredStage,stageFields);
|
|
17
18
|
const playbookRulesSchema=z.preprocess(dropKeys(['autoMove','flagOverdue','emailWindow','nurtureDays']),z.object({allowBackward:z.boolean().optional().describe('Let a move go to an earlier stage, for example when a meeting is cancelled. Default false.')}).strict());
|
|
18
19
|
const playbookFieldsSchema=z.object({name:label,stages:z.array(stage).min(2).max(30).optional().describe('The stages, in order. Leave it out for the default skeleton: Ready, Outreach underway (no steps; people who finish it leave as Unresponsive), In conversation and Meeting booked as the goal. Then add steps with a later save.'),goalKey:z.string().uuid().optional().describe('The key of the goal stage, after the first stage. Leave it out with stages to use the last stage; it is ignored when you leave stages out.'),offRamps:z.array(identity.extend({reason:z.enum(['rejected','not_fit','nurture','unresponsive','other','taken_over','bounced','skipped']).optional().describe('How Analytics counts people who leave here. Left out, it is guessed from the name (Not interested is rejected, Not a fit is not_fit, Unresponsive is unresponsive), else other.')})).max(30).optional().describe('Where people leave the playbook. Left out, the three standard off-ramps: Unresponsive, Not interested and Not a fit.'),voice:text.optional().describe('One sentence of how the agent writes. Left out, a plain, short default.'),rules:playbookRulesSchema.optional(),eventRules:z.array(eventRule).max(30).optional()}).strict();
|
|
19
20
|
const fields=playbookFieldsSchema;
|
package/cli/workspaces.mjs
CHANGED
|
@@ -164,6 +164,8 @@ export function localGuidance(name, issues, args) {
|
|
|
164
164
|
}
|
|
165
165
|
// The next step an error gives an agent (#483): one sentence, the path of the wrong value and the values that would have worked.
|
|
166
166
|
// Checked here too, so a response never puts the token or control text into the agent's context.
|
|
167
|
+
// #519: a refused label already says in plain words what is wrong; the code and the next step travel beside it.
|
|
168
|
+
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']);
|
|
167
169
|
export function errorGuidance(error, token) {
|
|
168
170
|
const clean = text => typeof text === 'string' && !/[\u0000-\u001f\u007f]/.test(text) && !(token && text.includes(token));
|
|
169
171
|
return {
|
|
@@ -196,9 +198,9 @@ export async function callWorkspace(name, args, config) {
|
|
|
196
198
|
if(!response.ok) {
|
|
197
199
|
const code = typeof result?.error?.code==='string' && /^[a-z_]+$/.test(result.error.code) ? result.error.code : 'request_failed';
|
|
198
200
|
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;
|
|
199
|
-
const detail = ((response.status===400 && (code==='invalid_fields' || code==='validation_error' || code==='secret_rejected')) || (response.status===404 && code==='not_found') || (response.status===422 && code==='playbook_cannot_work') || (response.status===409 && (code==='revision_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() : '';
|
|
201
|
+
const detail = (INTENT_REFUSALS.has(code) || (response.status===400 && (code==='invalid_fields' || code==='validation_error' || code==='secret_rejected')) || (response.status===404 && code==='not_found') || (response.status===422 && code==='playbook_cannot_work') || (response.status===409 && (code==='revision_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() : '';
|
|
200
202
|
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;
|
|
201
|
-
const error = new WorkspaceRequestError(response.status, code, `Workspace request failed: ${code} (HTTP ${response.status}).${detail ? ` ${detail}` : ''}${retryAt ? ` Retry after ${retryAt}.` : ''}`, retryAt, blockers, requestTraceId, errorGuidance(result?.error, config.token));
|
|
203
|
+
const error = new WorkspaceRequestError(response.status, code, name==='intent_record' && (INTENT_REFUSALS.has(code) || code==='invalid_fields') && detail ? detail : `Workspace request failed: ${code} (HTTP ${response.status}).${detail ? ` ${detail}` : ''}${retryAt ? ` Retry after ${retryAt}.` : ''}`, retryAt, blockers, requestTraceId, errorGuidance(result?.error, config.token));
|
|
202
204
|
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}}:{})}));
|
|
203
205
|
if(code==='revision_conflict'&&Number.isInteger(result?.error?.currentRevision)&&result.error.currentRevision>=0)error.currentRevision=result.error.currentRevision;
|
|
204
206
|
// A playbook that is in use names the campaigns that use it (#459).
|
package/package.json
CHANGED
|
@@ -75,6 +75,47 @@ when you pass `returnsOn`. Nothing is sent meanwhile and the person does not mov
|
|
|
75
75
|
settle: ask who the right person is. When the reply names the right person, classify it as a
|
|
76
76
|
`referral` with its typed `result`.
|
|
77
77
|
|
|
78
|
+
## Label a message with its words
|
|
79
|
+
|
|
80
|
+
A label says what a message means. Read the names with `audienti intent list`. A label goes on a message
|
|
81
|
+
that is stored as evidence. Get its `EVIDENCE` ID from `audienti prospects conversation append` (when you
|
|
82
|
+
store the message yourself) or `audienti prospects conversation list PERSON` (for one already stored); intake
|
|
83
|
+
returns it too. Record one to three labels, each with the `quote` that supports it (at most 300 characters,
|
|
84
|
+
copied from the message) and a plain `reason` (at most 200 characters, no IDs or error codes):
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
audienti intent record --person PERSON --evidence EVIDENCE \
|
|
88
|
+
--label interested --quote "we are interested in a demo" --reason "They ask for a demo next week."
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Flags carry one label. Use `--file LABELS.json` for several labels or a typed detail (the `result` of a
|
|
92
|
+
stop, bounce or referral; `meeting` for `meeting_booked`; `returnsOn` for `out_of_office`). A stop that
|
|
93
|
+
asks for everything to end looks like this:
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
[{"label": "stop", "quote": "please stop writing to me", "reason": "They ask us to stop.",
|
|
97
|
+
"result": {"type": "reply.stop", "scope": "person"}}]
|
|
98
|
+
```
|
|
99
|
+
|
|
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; it is stored only when you give it, and it
|
|
102
|
+
does not change what the label does yet. A message keeps the labels it has: a second request that repeats
|
|
103
|
+
one is refused with `already_recorded`, and one that would give the message a second reading is refused.
|
|
104
|
+
|
|
105
|
+
Audienti checks every label and writes nothing if one fails. A refusal names the `field`, the `allowed`
|
|
106
|
+
values and the `next` step. The checks:
|
|
107
|
+
|
|
108
|
+
- The message is stored with its text. A message stored without a body cannot carry a label: submit the text as evidence.
|
|
109
|
+
- The name is on the list, and fits the message direction and channel (`bounce` is email only).
|
|
110
|
+
- The quote is in the message (subject and body), ignoring case and spacing.
|
|
111
|
+
- The reason is plain words a person can read.
|
|
112
|
+
- The labels fit together: `stop`, `bounce` and `out_of_office` stand alone; a message has one reading of the
|
|
113
|
+
reply and at most one meeting label.
|
|
114
|
+
- The typed detail is present: a `stop`, `bounce` or `referral` needs its `result`; `meeting_booked` needs `meeting`.
|
|
115
|
+
|
|
116
|
+
`replies set` records the same labels. Add `"intent": {"quote": "...", "reason": "..."}` next to `classification`
|
|
117
|
+
or `meetingState` and it runs the same checks and stores the words on the event. Without `intent` it works as before.
|
|
118
|
+
|
|
78
119
|
## Stop, bounce and referral
|
|
79
120
|
|
|
80
121
|
These three need no decision from the user. Classify the reply, report the typed
|
|
@@ -47,7 +47,7 @@ The other items are warnings; read them after each save and clear them:
|
|
|
47
47
|
| Item `id` | Meaning and default fix |
|
|
48
48
|
| --- | --- |
|
|
49
49
|
| `stage_never_ends` | A stage with steps and `completion` `stay`. Use `off_ramp` with the Unresponsive off-ramp. |
|
|
50
|
-
| `stage_by_hand_only` | Nothing opens a stage but a person or agent. Give it an `on` event or finish the stage before it with `next`. |
|
|
50
|
+
| `stage_by_hand_only` | Nothing opens a stage but a person or agent. Give it an `on` event ("Jump forward to here when") or finish the stage before it with `next`. The first stage after Ready is not flagged. |
|
|
51
51
|
| `send_without_wait` | A send step with no `wait`, followed by another send on the same channel. Add a wait. |
|
|
52
52
|
| `goal_shorter_than_waits` | `goalDays` is below the waits added up. Use the waits plus 2 days. |
|
|
53
53
|
| `send_in_person_stage` | A send step in the goal stage or an `Assignee` stage writes to someone who already answered. Remove it. |
|
|
@@ -61,8 +61,7 @@ The other items are warnings; read them after each save and clear them:
|
|
|
61
61
|
| --- | --- |
|
|
62
62
|
| `key` | A UUID, unique in the playbook. Keep it across revisions. |
|
|
63
63
|
| `name` | Unique in the playbook. |
|
|
64
|
-
| `
|
|
65
|
-
| `on` | The events that open this stage: `accept`, `reply`, `meeting` or `manual`. Defaults to `["manual"]`. An event moves the prospect to the furthest later stage that opens on it; the stages passed over are recorded as skipped, never as done. |
|
|
64
|
+
| `on` | The events that jump a prospect forward to this stage: `accept` (they accept your LinkedIn connection request, not a meeting invite), `reply` or `meeting` (a booked meeting). Do not list `accept` or `reply` on the goal stage unless a reply or an accept should count as a win. An event moves the prospect to the furthest later stage that lists it; the stages passed over are recorded as skipped, never as done. Leave it out for a stage people reach when the stage before it finishes, or when a person moves them. Do not set it on the first stage. People and the pipeline read "Starts when they reply" from it. The older `movesInWhen` text and the value `manual` are accepted and ignored. |
|
|
66
65
|
| `owner` | `Agent` or `Assignee` (the person assigned to the prospect). |
|
|
67
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. |
|
|
68
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. |
|
|
@@ -131,7 +130,7 @@ Use the same UUID wherever the same placeholder letter appears.
|
|
|
131
130
|
{ "key": "<uuid>", "channel": "LinkedIn", "action": "Send message", "condition": "Only if connected", "instructions": "Lead with their signal and offer something useful.", "wait": { "until": "reply", "days": 5 } },
|
|
132
131
|
{ "key": "<uuid>", "channel": "Email", "action": "Email 1", "condition": "Always", "instructions": "Three sentences: signal, offer, one short question.", "wait": { "until": "reply", "days": 4 }, "onTimeout": "off_ramp" }
|
|
133
132
|
], "completion": { "kind": "off_ramp", "offRampKey": "<R>" } },
|
|
134
|
-
{ "key": "<D>", "name": "In conversation", "
|
|
133
|
+
{ "key": "<D>", "name": "In conversation", "on": ["reply"], "owner": "Assignee", "goalDays": 3, "steps": [
|
|
135
134
|
{ "key": "<uuid>", "channel": "Email", "action": "Draft a reply", "condition": "Always", "instructions": "Answer their question first. Propose a time only if they asked.", "wait": { "until": "reply", "days": 2 } }
|
|
136
135
|
], "completion": { "kind": "stay" } },
|
|
137
136
|
{ "key": "<E>", "name": "Meeting requested", "owner": "Assignee", "goalDays": 3, "steps": [], "completion": { "kind": "next" } },
|
package/source-manifest.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"repository": "https://github.com/omalab/exora",
|
|
3
|
-
"sourceCommit": "
|
|
3
|
+
"sourceCommit": "3f1aed65bd8abf5e19c78316d93d979b268f7b7b",
|
|
4
4
|
"files": {
|
|
5
5
|
"LICENSE": "220f297cfcd5c38eb64e6b4b4a77cab4ebc7f4686d4d99c6260a475d02ca11e5",
|
|
6
6
|
"README.md": "7ff32cc8ac89dd54b64d7b39c0b41b1f37e154b7ea10ad5a7bceefc3df77d87f",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"cli/account-intake-operations.mjs": "b43d764c628beabafad753d711ddb2af2c64276e4a6506ed0b854839091ea5db",
|
|
9
9
|
"cli/account-intake.mjs": "aa77f2f96fd030f8830d6de278932186aea7c82cfda086a5a6cf569202feebc8",
|
|
10
10
|
"cli/account-replies.mjs": "699e34e5c736ba316df8f164b4c50f82a7e4e2d5e51303e6b13944c3f2c854ab",
|
|
11
|
-
"cli/account-reply-operations.mjs": "
|
|
11
|
+
"cli/account-reply-operations.mjs": "21f1441e708d9de8af83ec8f7444a5ca5d686fab90be669b904dbe1454eb7a13",
|
|
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": "5bd178c033a5dfde80f50db18f9e81c13d2b7427de3bbff6ba421a73d08ae08c",
|
|
20
20
|
"cli/audit-operations.mjs": "afaf87d592a1e5f6bff2a8649a6a64a8f471950b7d99e98995d9205aa5910398",
|
|
21
21
|
"cli/audit.mjs": "a7becaed67c58b063e668b5e66844b6dc15c11a63b428faa171b542ad922c001",
|
|
22
22
|
"cli/budget-operations.mjs": "d74916551ef393e29734c6e13cf9021251e0ed126eeaa529e18cf2332146e1e0",
|
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
"cli/guardrails.mjs": "e8b9c0a19956a9eab84475d3c56a4b6c77baf7ed9a55e8686d660ef1ad562aa8",
|
|
37
37
|
"cli/hand-task-operations.mjs": "473473fe95324c8c8733e037feab5ac468c07b0658fabd739cfd7d400a7faa83",
|
|
38
38
|
"cli/hand-tasks.mjs": "2c895cf6b30c594a7e27cc4ec4b7eddeb39f941cd195b9255c6e0d6b4a56371f",
|
|
39
|
-
"cli/intent-operations.mjs": "
|
|
40
|
-
"cli/intent.mjs": "
|
|
39
|
+
"cli/intent-operations.mjs": "104e883319b4e9c2fb43de373fcf9d5df537871995a0b81b4ee86fa206ba6707",
|
|
40
|
+
"cli/intent.mjs": "db0ccb85977fca26f865970dacd0538e81b070d7c0ec078f38358f33c458a96a",
|
|
41
41
|
"cli/json-file.mjs": "96431b18215834c2531f5901e2526875c634dd2e85a9465fcffc937fbec52150",
|
|
42
42
|
"cli/kv-operations.mjs": "505d79d0d535d3441883f8a37817c30c73367216a35dddb07181f0f0e799df79",
|
|
43
43
|
"cli/kv.mjs": "05783610001dd002af07a4bb8f26e64d74aa4981bbe38a2100552b810cb3bc72",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"cli/outcomes.mjs": "765ccb112a90fc4d228b2a41e5e19c5e1f3e2fe14bc1d8128b0090fc95cdbf3f",
|
|
52
52
|
"cli/owner-account-client.mjs": "e7912f7c67571676cb94bcc9424b7d4a3bf9ef1461c9a5d6c31a2651e546a9ea",
|
|
53
53
|
"cli/playbook-help.mjs": "56a5b6ba4c94022931f9c751c954133239404048aba99d1ff28cd290eac22f41",
|
|
54
|
-
"cli/playbook-operations.mjs": "
|
|
54
|
+
"cli/playbook-operations.mjs": "dcf4df1d0e9d9b9ed62f98c646405e070d3ec727201e2c0e0275df30c4e832a5",
|
|
55
55
|
"cli/profiles.mjs": "45929b224ffc916b6dbd7a94e06ac09a1ff7feec430633b10d160c1608fc2d32",
|
|
56
56
|
"cli/progress.mjs": "aeca47f4e59be306dac4592cf2e4209a30e322fc13e4e103e64e09faa370f5a8",
|
|
57
57
|
"cli/prospecting-settings-operations.mjs": "37b881a867c19d9e5f2d02316038284ea0cf09de4f37b6dee2370a1faedf245e",
|
|
@@ -83,8 +83,8 @@
|
|
|
83
83
|
"cli/work-feedback.mjs": "f3b77ecfd7ea1aba9c9f03c9d894137547e9c5575d9daa359ed94505ad025852",
|
|
84
84
|
"cli/work-operations.mjs": "8b3a84ca15e8e4ad79ef92ced4c5adaffd5129f630e3bfc2c74a844a90867600",
|
|
85
85
|
"cli/work.mjs": "751107ecc3de5a6be9db4a068cddc41f3ad8e77c87e59686eaf522862caf9e9f",
|
|
86
|
-
"cli/workspaces.mjs": "
|
|
87
|
-
"package.json": "
|
|
86
|
+
"cli/workspaces.mjs": "5694825a763008bf0dcfa4e17bfe79a1d593207099f500bfa21184f899e83971",
|
|
87
|
+
"package.json": "5d97a043a8fd4d3fdc6f2c3ba4890e048ae50102fc2ca6cb47fe8acf1be29073",
|
|
88
88
|
"skills/agent-rooms/SKILL.md": "08f917ff85fee1d23499a4499056ce845b39ce6727029b104819dad41ee87cc2",
|
|
89
89
|
"skills/agent-rooms/references/commands.md": "ef5afa5543ca9da967a147f638774647998ea1c1a2a821959f3bf87d54c292bd",
|
|
90
90
|
"skills/audienti/SKILL.md": "8f2124065bfc3a005b44e194e7dcce9bbf14062ab332e7db66d441eeccb518ed",
|
|
@@ -100,9 +100,9 @@
|
|
|
100
100
|
"skills/getting-started/references/capabilities.md": "982e3583a47ba4ac832efae2f6af3a4d8c0c18cace159c76b2a4ad1a8d8e910e",
|
|
101
101
|
"skills/getting-started/references/mailbox-check.md": "113574bb5a573125620dcde4e65ebf44164f7b0727cc936fb41234f5d57bba73",
|
|
102
102
|
"skills/getting-started/references/outcome-checks.md": "094d09d231b38d4935aa9eb8ac089e46ba8e698489852fa97040e6b5eac77a9b",
|
|
103
|
-
"skills/getting-started/references/replies.md": "
|
|
103
|
+
"skills/getting-started/references/replies.md": "d085b7353f13d70a1b0333d851fe573fd2c99e6bb57923cf8717982d77a0504b",
|
|
104
104
|
"skills/getting-started/references/reply-checks.md": "85fe692ad6b7bc73e204de313b27d225be3da1a0af71e8e7bb9945ae511a0dc3",
|
|
105
105
|
"skills/playbook-design/SKILL.md": "86d77069a73d5c21c43623d2faa84a3957cfb1f30f7fae45f239b32e249f892a",
|
|
106
|
-
"skills/playbook-design/references/fields.md": "
|
|
106
|
+
"skills/playbook-design/references/fields.md": "7d4a22451c3b77bb21a76d211d25a44b84a1bc46cc6524472ed121e77d2866aa"
|
|
107
107
|
}
|
|
108
108
|
}
|