@wappy_ai/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/clock.d.ts +8 -0
- package/dist/clock.js +17 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +12 -0
- package/dist/interfaces.d.ts +92 -0
- package/dist/interfaces.js +1 -0
- package/dist/logger.d.ts +7 -0
- package/dist/logger.js +6 -0
- package/dist/registry.d.ts +26 -0
- package/dist/registry.js +40 -0
- package/dist/schemas.d.ts +285 -0
- package/dist/schemas.js +149 -0
- package/dist/semver.d.ts +1 -0
- package/dist/semver.js +34 -0
- package/dist/setup-manifest.d.ts +20 -0
- package/dist/setup-manifest.js +19 -0
- package/dist/state/drift.d.ts +9 -0
- package/dist/state/drift.js +13 -0
- package/dist/state/errors.d.ts +14 -0
- package/dist/state/errors.js +26 -0
- package/dist/state/index.d.ts +11 -0
- package/dist/state/index.js +11 -0
- package/dist/state/io.d.ts +11 -0
- package/dist/state/io.js +54 -0
- package/dist/state/load.d.ts +23 -0
- package/dist/state/load.js +23 -0
- package/dist/state/lock.d.ts +19 -0
- package/dist/state/lock.js +85 -0
- package/dist/state/manifest.d.ts +18 -0
- package/dist/state/manifest.js +25 -0
- package/dist/state/migrations.d.ts +11 -0
- package/dist/state/migrations.js +28 -0
- package/dist/state/reset.d.ts +15 -0
- package/dist/state/reset.js +19 -0
- package/dist/state/schema.d.ts +73 -0
- package/dist/state/schema.js +56 -0
- package/dist/state/step-runner.d.ts +24 -0
- package/dist/state/step-runner.js +42 -0
- package/dist/state/version-skew.d.ts +9 -0
- package/dist/state/version-skew.js +7 -0
- package/dist/tracer.d.ts +16 -0
- package/dist/tracer.js +13 -0
- package/package.json +44 -0
package/dist/clock.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Injectable time + scheduling port. Never call Date.now()/setTimeout directly outside an impl of this. */
|
|
2
|
+
export interface Clock {
|
|
3
|
+
now(): number;
|
|
4
|
+
setTimeout(fn: () => void, ms: number): number;
|
|
5
|
+
clearTimeout(handle: number): void;
|
|
6
|
+
sleep(ms: number): Promise<void>;
|
|
7
|
+
}
|
|
8
|
+
export declare const systemClock: Clock;
|
package/dist/clock.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const timers = new Map();
|
|
2
|
+
let nextHandle = 1;
|
|
3
|
+
export const systemClock = {
|
|
4
|
+
now: () => Date.now(),
|
|
5
|
+
setTimeout(fn, ms) {
|
|
6
|
+
const handle = nextHandle++;
|
|
7
|
+
timers.set(handle, setTimeout(fn, ms));
|
|
8
|
+
return handle;
|
|
9
|
+
},
|
|
10
|
+
clearTimeout(handle) {
|
|
11
|
+
const native = timers.get(handle);
|
|
12
|
+
if (native !== undefined)
|
|
13
|
+
clearTimeout(native);
|
|
14
|
+
timers.delete(handle);
|
|
15
|
+
},
|
|
16
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
17
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const packageName = "@wappy_ai/core";
|
|
2
|
+
/** Kept in sync with package.json "version" by hand; PluginRegistry checks against this. */
|
|
3
|
+
export declare const CORE_VERSION = "0.1.0";
|
|
4
|
+
export * from "./schemas.js";
|
|
5
|
+
export * from "./clock.js";
|
|
6
|
+
export * from "./logger.js";
|
|
7
|
+
export * from "./tracer.js";
|
|
8
|
+
export * from "./interfaces.js";
|
|
9
|
+
export * from "./semver.js";
|
|
10
|
+
export * from "./setup-manifest.js";
|
|
11
|
+
export * from "./registry.js";
|
|
12
|
+
export * from "./state/index.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const packageName = "@wappy_ai/core";
|
|
2
|
+
/** Kept in sync with package.json "version" by hand; PluginRegistry checks against this. */
|
|
3
|
+
export const CORE_VERSION = "0.1.0";
|
|
4
|
+
export * from "./schemas.js";
|
|
5
|
+
export * from "./clock.js";
|
|
6
|
+
export * from "./logger.js";
|
|
7
|
+
export * from "./tracer.js";
|
|
8
|
+
export * from "./interfaces.js";
|
|
9
|
+
export * from "./semver.js";
|
|
10
|
+
export * from "./setup-manifest.js";
|
|
11
|
+
export * from "./registry.js";
|
|
12
|
+
export * from "./state/index.js";
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { DeliveryResult, InboundMessage, RouterDecision, SessionProfile, SmartMessage, ToolResult, Turn } from "./schemas.js";
|
|
2
|
+
/** JSON Schema object, as produced by zod's toJSONSchema and consumed by Model tool-calling. */
|
|
3
|
+
export type JsonSchema = Record<string, unknown>;
|
|
4
|
+
/** A single callable tool (§3, §8). */
|
|
5
|
+
export interface Tool {
|
|
6
|
+
name: string;
|
|
7
|
+
description: string;
|
|
8
|
+
parameters: JsonSchema;
|
|
9
|
+
/** Never mutates external state; safe to auto-call without confirmation. */
|
|
10
|
+
readOnly: boolean;
|
|
11
|
+
/** Requires explicit user/operator confirmation before executing, regardless of readOnly. */
|
|
12
|
+
confirmBefore: boolean;
|
|
13
|
+
execute(args: unknown): Promise<ToolResult>;
|
|
14
|
+
}
|
|
15
|
+
/** Generates Tools from a source (OpenAPI spec, Shopify, custom code) (§3, §8). */
|
|
16
|
+
export interface ToolProvider {
|
|
17
|
+
name: string;
|
|
18
|
+
listTools(): Promise<Tool[]> | Tool[];
|
|
19
|
+
}
|
|
20
|
+
/** Prompt fragment + tools + optional memory schema for one capability (§17 glossary). */
|
|
21
|
+
export interface Skill {
|
|
22
|
+
name: string;
|
|
23
|
+
description: string;
|
|
24
|
+
promptFragment: string;
|
|
25
|
+
/** Names of tools (from registered ToolProviders) this skill may use. */
|
|
26
|
+
tools?: string[];
|
|
27
|
+
/** Opaque, skill-owned shape persisted alongside Memory; core never interprets it. */
|
|
28
|
+
memorySchema?: unknown;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* receive() returns an array: one webhook payload may batch several message
|
|
32
|
+
* events, or contain zero when it's a status/read-receipt-only notification
|
|
33
|
+
* (§6.2). send() takes an explicit recipient because a channel serves many
|
|
34
|
+
* contacts.
|
|
35
|
+
*/
|
|
36
|
+
export interface MessageChannel {
|
|
37
|
+
name: string;
|
|
38
|
+
receive(rawWebhook: unknown): Promise<InboundMessage[]> | InboundMessage[];
|
|
39
|
+
send(to: string, message: SmartMessage): Promise<DeliveryResult>;
|
|
40
|
+
}
|
|
41
|
+
export interface Memory {
|
|
42
|
+
load(contactId: string): Promise<Turn[]>;
|
|
43
|
+
append(turn: Turn): Promise<void>;
|
|
44
|
+
/** Semantic recall scoped to one contact's history/knowledge. Returns text snippets. */
|
|
45
|
+
recall(contactId: string, query: string): Promise<string[]>;
|
|
46
|
+
}
|
|
47
|
+
/** M13: one focused store for the session profile, same pattern as `@wappy_ai/whatsapp`'s
|
|
48
|
+
* `SeenStore`/`SessionWindowTracker` — not an overload of `Memory` (turn history is a separate
|
|
49
|
+
* concern from a structured, TTL-bound profile). */
|
|
50
|
+
export interface SessionProfileStore {
|
|
51
|
+
/** `undefined` when no profile exists for this contact, OR it exists but `now > expiresAt` — a
|
|
52
|
+
* caller never has to separately check expiry; an expired profile IS an absent one. */
|
|
53
|
+
get(contactId: string, now: number): Promise<SessionProfile | undefined>;
|
|
54
|
+
/** Upsert. Callers always pass a fresh `expiresAt`, so calling this after every successful turn
|
|
55
|
+
* is also how the TTL renews — there's no separate "touch" operation. */
|
|
56
|
+
set(profile: SessionProfile): Promise<void>;
|
|
57
|
+
}
|
|
58
|
+
export interface RouterInput {
|
|
59
|
+
message: InboundMessage;
|
|
60
|
+
history: Turn[];
|
|
61
|
+
availableSkills: string[];
|
|
62
|
+
availableTools: string[];
|
|
63
|
+
}
|
|
64
|
+
export interface Router {
|
|
65
|
+
route(input: RouterInput): Promise<RouterDecision>;
|
|
66
|
+
}
|
|
67
|
+
export interface ModelToolCall {
|
|
68
|
+
name: string;
|
|
69
|
+
args: unknown;
|
|
70
|
+
}
|
|
71
|
+
export interface ModelRequest {
|
|
72
|
+
prompt: string;
|
|
73
|
+
/** Sent as a system-role message when the underlying adapter supports it — kept separate from `prompt` so structured role separation survives even when the caller has already assembled everything into one deterministic prompt string (T6.2). */
|
|
74
|
+
system?: string;
|
|
75
|
+
history?: Turn[];
|
|
76
|
+
tools?: Tool[];
|
|
77
|
+
/** e.g. smartMessageJsonSchema, when the model must emit a SmartMessage (§6.3). */
|
|
78
|
+
responseSchema?: JsonSchema;
|
|
79
|
+
}
|
|
80
|
+
export interface ModelResult {
|
|
81
|
+
text?: string;
|
|
82
|
+
structured?: unknown;
|
|
83
|
+
toolCalls?: ModelToolCall[];
|
|
84
|
+
}
|
|
85
|
+
/** The model port — Vercel AI SDK or any provider sits behind this (§2.1). */
|
|
86
|
+
export interface Model {
|
|
87
|
+
generate(req: ModelRequest): Promise<ModelResult>;
|
|
88
|
+
}
|
|
89
|
+
/** Orchestrates a single inbound message -> reply, wiring Router/Memory/Tools/Channel/Model (§3). */
|
|
90
|
+
export interface Agent {
|
|
91
|
+
handle(message: InboundMessage): Promise<DeliveryResult>;
|
|
92
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/logger.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface Logger {
|
|
2
|
+
debug(msg: string, meta?: Record<string, unknown>): void;
|
|
3
|
+
info(msg: string, meta?: Record<string, unknown>): void;
|
|
4
|
+
warn(msg: string, meta?: Record<string, unknown>): void;
|
|
5
|
+
error(msg: string, meta?: Record<string, unknown>): void;
|
|
6
|
+
}
|
|
7
|
+
export declare const noopLogger: Logger;
|
package/dist/logger.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { SetupManifest } from "./setup-manifest.js";
|
|
2
|
+
export type PluginKind = "channel" | "memory" | "router" | "toolProvider" | "skill" | "model";
|
|
3
|
+
export interface Plugin<T = unknown> {
|
|
4
|
+
name: string;
|
|
5
|
+
kind: PluginKind;
|
|
6
|
+
/** semver range of @wappy_ai/core this plugin was built against. */
|
|
7
|
+
coreVersionRange: string;
|
|
8
|
+
instance: T;
|
|
9
|
+
setup?: SetupManifest;
|
|
10
|
+
}
|
|
11
|
+
/** Parts self-register here; core loads the enabled subset from config (§3). Deterministic (registration) order. */
|
|
12
|
+
export declare class PluginRegistry {
|
|
13
|
+
private readonly coreVersion;
|
|
14
|
+
private readonly plugins;
|
|
15
|
+
private readonly names;
|
|
16
|
+
constructor(coreVersion: string);
|
|
17
|
+
register(plugin: Plugin): void;
|
|
18
|
+
/** First-registered plugin of `kind` (optionally by `name`); undefined if none match — never throws on an unknown kind. */
|
|
19
|
+
get<T>(kind: PluginKind, name?: string): T | undefined;
|
|
20
|
+
list(kind?: PluginKind): Plugin[];
|
|
21
|
+
}
|
|
22
|
+
export interface RegistryConfig {
|
|
23
|
+
enabled: string[];
|
|
24
|
+
}
|
|
25
|
+
/** Resolves the configured enabled-plugin list in config order; throws on an unregistered name. */
|
|
26
|
+
export declare function resolveEnabledPlugins(registry: PluginRegistry, config: RegistryConfig): Plugin[];
|
package/dist/registry.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { satisfiesRange } from "./semver.js";
|
|
2
|
+
/** Parts self-register here; core loads the enabled subset from config (§3). Deterministic (registration) order. */
|
|
3
|
+
export class PluginRegistry {
|
|
4
|
+
coreVersion;
|
|
5
|
+
plugins = [];
|
|
6
|
+
names = new Set();
|
|
7
|
+
constructor(coreVersion) {
|
|
8
|
+
this.coreVersion = coreVersion;
|
|
9
|
+
}
|
|
10
|
+
register(plugin) {
|
|
11
|
+
if (this.names.has(plugin.name)) {
|
|
12
|
+
throw new Error(`PluginRegistry: duplicate plugin name "${plugin.name}"`);
|
|
13
|
+
}
|
|
14
|
+
if (!satisfiesRange(this.coreVersion, plugin.coreVersionRange)) {
|
|
15
|
+
throw new Error(`PluginRegistry: plugin "${plugin.name}" requires core ${plugin.coreVersionRange}, running ${this.coreVersion}`);
|
|
16
|
+
}
|
|
17
|
+
this.names.add(plugin.name);
|
|
18
|
+
this.plugins.push(plugin);
|
|
19
|
+
}
|
|
20
|
+
/** First-registered plugin of `kind` (optionally by `name`); undefined if none match — never throws on an unknown kind. */
|
|
21
|
+
get(kind, name) {
|
|
22
|
+
const found = name
|
|
23
|
+
? this.plugins.find((p) => p.kind === kind && p.name === name)
|
|
24
|
+
: this.plugins.find((p) => p.kind === kind);
|
|
25
|
+
return found?.instance;
|
|
26
|
+
}
|
|
27
|
+
list(kind) {
|
|
28
|
+
return kind ? this.plugins.filter((p) => p.kind === kind) : [...this.plugins];
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** Resolves the configured enabled-plugin list in config order; throws on an unregistered name. */
|
|
32
|
+
export function resolveEnabledPlugins(registry, config) {
|
|
33
|
+
const byName = new Map(registry.list().map((p) => [p.name, p]));
|
|
34
|
+
return config.enabled.map((name) => {
|
|
35
|
+
const plugin = byName.get(name);
|
|
36
|
+
if (!plugin)
|
|
37
|
+
throw new Error(`resolveEnabledPlugins: unknown plugin "${name}"`);
|
|
38
|
+
return plugin;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Runtime schemas for the core data shapes (§3, §6.1, §6.3, §9 of SPEC.md).
|
|
4
|
+
*
|
|
5
|
+
* Layering rule for WhatsApp send-time limits (§6.1): this schema enforces
|
|
6
|
+
* STRUCTURAL bounds only (button/row/section counts) because those can't be
|
|
7
|
+
* fixed by truncation without changing meaning. TEXT LENGTH limits (button
|
|
8
|
+
* title <=20, list row title <=24, row description <=72) are intentionally
|
|
9
|
+
* NOT enforced here — @wappy_ai/whatsapp (M4) truncates them at send time, so
|
|
10
|
+
* an over-length string is valid input at this layer and a renderer concern
|
|
11
|
+
* downstream. See docs/CONTRACTS.md.
|
|
12
|
+
*/
|
|
13
|
+
export declare const InboundMediaSchema: z.ZodObject<{
|
|
14
|
+
kind: z.ZodEnum<{
|
|
15
|
+
image: "image";
|
|
16
|
+
document: "document";
|
|
17
|
+
video: "video";
|
|
18
|
+
audio: "audio";
|
|
19
|
+
voice: "voice";
|
|
20
|
+
location: "location";
|
|
21
|
+
}>;
|
|
22
|
+
url: z.ZodOptional<z.ZodString>;
|
|
23
|
+
mimeType: z.ZodOptional<z.ZodString>;
|
|
24
|
+
caption: z.ZodOptional<z.ZodString>;
|
|
25
|
+
latitude: z.ZodOptional<z.ZodNumber>;
|
|
26
|
+
longitude: z.ZodOptional<z.ZodNumber>;
|
|
27
|
+
}, z.core.$strip>;
|
|
28
|
+
export type InboundMedia = z.infer<typeof InboundMediaSchema>;
|
|
29
|
+
export declare const InboundMessageSchema: z.ZodObject<{
|
|
30
|
+
id: z.ZodString;
|
|
31
|
+
contactId: z.ZodString;
|
|
32
|
+
channel: z.ZodString;
|
|
33
|
+
text: z.ZodOptional<z.ZodString>;
|
|
34
|
+
media: z.ZodOptional<z.ZodObject<{
|
|
35
|
+
kind: z.ZodEnum<{
|
|
36
|
+
image: "image";
|
|
37
|
+
document: "document";
|
|
38
|
+
video: "video";
|
|
39
|
+
audio: "audio";
|
|
40
|
+
voice: "voice";
|
|
41
|
+
location: "location";
|
|
42
|
+
}>;
|
|
43
|
+
url: z.ZodOptional<z.ZodString>;
|
|
44
|
+
mimeType: z.ZodOptional<z.ZodString>;
|
|
45
|
+
caption: z.ZodOptional<z.ZodString>;
|
|
46
|
+
latitude: z.ZodOptional<z.ZodNumber>;
|
|
47
|
+
longitude: z.ZodOptional<z.ZodNumber>;
|
|
48
|
+
}, z.core.$strip>>;
|
|
49
|
+
selectionId: z.ZodOptional<z.ZodString>;
|
|
50
|
+
timestamp: z.ZodNumber;
|
|
51
|
+
raw: z.ZodOptional<z.ZodUnknown>;
|
|
52
|
+
}, z.core.$strip>;
|
|
53
|
+
export type InboundMessage = z.infer<typeof InboundMessageSchema>;
|
|
54
|
+
export declare const SmartButtonSchema: z.ZodObject<{
|
|
55
|
+
id: z.ZodString;
|
|
56
|
+
title: z.ZodString;
|
|
57
|
+
}, z.core.$strip>;
|
|
58
|
+
export declare const SmartButtonsSchema: z.ZodArray<z.ZodObject<{
|
|
59
|
+
id: z.ZodString;
|
|
60
|
+
title: z.ZodString;
|
|
61
|
+
}, z.core.$strip>>;
|
|
62
|
+
export declare const SmartListRowSchema: z.ZodObject<{
|
|
63
|
+
id: z.ZodString;
|
|
64
|
+
title: z.ZodString;
|
|
65
|
+
description: z.ZodOptional<z.ZodString>;
|
|
66
|
+
}, z.core.$strip>;
|
|
67
|
+
export declare const SmartListSectionSchema: z.ZodObject<{
|
|
68
|
+
title: z.ZodOptional<z.ZodString>;
|
|
69
|
+
rows: z.ZodArray<z.ZodObject<{
|
|
70
|
+
id: z.ZodString;
|
|
71
|
+
title: z.ZodString;
|
|
72
|
+
description: z.ZodOptional<z.ZodString>;
|
|
73
|
+
}, z.core.$strip>>;
|
|
74
|
+
}, z.core.$strip>;
|
|
75
|
+
export declare const SmartListSchema: z.ZodObject<{
|
|
76
|
+
buttonText: z.ZodString;
|
|
77
|
+
sections: z.ZodArray<z.ZodObject<{
|
|
78
|
+
title: z.ZodOptional<z.ZodString>;
|
|
79
|
+
rows: z.ZodArray<z.ZodObject<{
|
|
80
|
+
id: z.ZodString;
|
|
81
|
+
title: z.ZodString;
|
|
82
|
+
description: z.ZodOptional<z.ZodString>;
|
|
83
|
+
}, z.core.$strip>>;
|
|
84
|
+
}, z.core.$strip>>;
|
|
85
|
+
}, z.core.$strip>;
|
|
86
|
+
export declare const SmartCtaSchema: z.ZodObject<{
|
|
87
|
+
text: z.ZodString;
|
|
88
|
+
url: z.ZodString;
|
|
89
|
+
}, z.core.$strip>;
|
|
90
|
+
/** An interactive message's header (§6.1/M12): image/video/document/text alongside body + action.
|
|
91
|
+
* Meta's real Cloud API supports this on button/list interactive messages — verified against
|
|
92
|
+
* their docs, not assumed — for a "preview, then text, then CTA/list" reply. Valid only alongside
|
|
93
|
+
* buttons/list/cta (enforced below); a header on a plain text/media-only send is meaningless. */
|
|
94
|
+
export declare const SmartHeaderSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
95
|
+
type: z.ZodLiteral<"text">;
|
|
96
|
+
text: z.ZodString;
|
|
97
|
+
subText: z.ZodOptional<z.ZodString>;
|
|
98
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
99
|
+
type: z.ZodLiteral<"image">;
|
|
100
|
+
url: z.ZodString;
|
|
101
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
102
|
+
type: z.ZodLiteral<"video">;
|
|
103
|
+
url: z.ZodString;
|
|
104
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
105
|
+
type: z.ZodLiteral<"document">;
|
|
106
|
+
url: z.ZodString;
|
|
107
|
+
filename: z.ZodOptional<z.ZodString>;
|
|
108
|
+
}, z.core.$strip>], "type">;
|
|
109
|
+
export declare const SmartMediaSchema: z.ZodObject<{
|
|
110
|
+
kind: z.ZodEnum<{
|
|
111
|
+
image: "image";
|
|
112
|
+
document: "document";
|
|
113
|
+
video: "video";
|
|
114
|
+
audio: "audio";
|
|
115
|
+
voice: "voice";
|
|
116
|
+
}>;
|
|
117
|
+
url: z.ZodString;
|
|
118
|
+
caption: z.ZodOptional<z.ZodString>;
|
|
119
|
+
mimeType: z.ZodOptional<z.ZodString>;
|
|
120
|
+
filename: z.ZodOptional<z.ZodString>;
|
|
121
|
+
}, z.core.$strip>;
|
|
122
|
+
export declare const SmartMessageSchema: z.ZodObject<{
|
|
123
|
+
text: z.ZodOptional<z.ZodString>;
|
|
124
|
+
buttons: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
125
|
+
id: z.ZodString;
|
|
126
|
+
title: z.ZodString;
|
|
127
|
+
}, z.core.$strip>>>;
|
|
128
|
+
list: z.ZodOptional<z.ZodObject<{
|
|
129
|
+
buttonText: z.ZodString;
|
|
130
|
+
sections: z.ZodArray<z.ZodObject<{
|
|
131
|
+
title: z.ZodOptional<z.ZodString>;
|
|
132
|
+
rows: z.ZodArray<z.ZodObject<{
|
|
133
|
+
id: z.ZodString;
|
|
134
|
+
title: z.ZodString;
|
|
135
|
+
description: z.ZodOptional<z.ZodString>;
|
|
136
|
+
}, z.core.$strip>>;
|
|
137
|
+
}, z.core.$strip>>;
|
|
138
|
+
}, z.core.$strip>>;
|
|
139
|
+
cta: z.ZodOptional<z.ZodObject<{
|
|
140
|
+
text: z.ZodString;
|
|
141
|
+
url: z.ZodString;
|
|
142
|
+
}, z.core.$strip>>;
|
|
143
|
+
media: z.ZodOptional<z.ZodObject<{
|
|
144
|
+
kind: z.ZodEnum<{
|
|
145
|
+
image: "image";
|
|
146
|
+
document: "document";
|
|
147
|
+
video: "video";
|
|
148
|
+
audio: "audio";
|
|
149
|
+
voice: "voice";
|
|
150
|
+
}>;
|
|
151
|
+
url: z.ZodString;
|
|
152
|
+
caption: z.ZodOptional<z.ZodString>;
|
|
153
|
+
mimeType: z.ZodOptional<z.ZodString>;
|
|
154
|
+
filename: z.ZodOptional<z.ZodString>;
|
|
155
|
+
}, z.core.$strip>>;
|
|
156
|
+
header: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
157
|
+
type: z.ZodLiteral<"text">;
|
|
158
|
+
text: z.ZodString;
|
|
159
|
+
subText: z.ZodOptional<z.ZodString>;
|
|
160
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
161
|
+
type: z.ZodLiteral<"image">;
|
|
162
|
+
url: z.ZodString;
|
|
163
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
164
|
+
type: z.ZodLiteral<"video">;
|
|
165
|
+
url: z.ZodString;
|
|
166
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
167
|
+
type: z.ZodLiteral<"document">;
|
|
168
|
+
url: z.ZodString;
|
|
169
|
+
filename: z.ZodOptional<z.ZodString>;
|
|
170
|
+
}, z.core.$strip>], "type">>;
|
|
171
|
+
quoteId: z.ZodOptional<z.ZodString>;
|
|
172
|
+
flow: z.ZodOptional<z.ZodUnknown>;
|
|
173
|
+
}, z.core.$strip>;
|
|
174
|
+
export type SmartMessage = z.infer<typeof SmartMessageSchema>;
|
|
175
|
+
/** JSON Schema for SmartMessage — this is what the model is asked to emit (§6.3). */
|
|
176
|
+
export declare const smartMessageJsonSchema: z.core.ZodStandardJSONSchemaPayload<z.ZodObject<{
|
|
177
|
+
text: z.ZodOptional<z.ZodString>;
|
|
178
|
+
buttons: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
179
|
+
id: z.ZodString;
|
|
180
|
+
title: z.ZodString;
|
|
181
|
+
}, z.core.$strip>>>;
|
|
182
|
+
list: z.ZodOptional<z.ZodObject<{
|
|
183
|
+
buttonText: z.ZodString;
|
|
184
|
+
sections: z.ZodArray<z.ZodObject<{
|
|
185
|
+
title: z.ZodOptional<z.ZodString>;
|
|
186
|
+
rows: z.ZodArray<z.ZodObject<{
|
|
187
|
+
id: z.ZodString;
|
|
188
|
+
title: z.ZodString;
|
|
189
|
+
description: z.ZodOptional<z.ZodString>;
|
|
190
|
+
}, z.core.$strip>>;
|
|
191
|
+
}, z.core.$strip>>;
|
|
192
|
+
}, z.core.$strip>>;
|
|
193
|
+
cta: z.ZodOptional<z.ZodObject<{
|
|
194
|
+
text: z.ZodString;
|
|
195
|
+
url: z.ZodString;
|
|
196
|
+
}, z.core.$strip>>;
|
|
197
|
+
media: z.ZodOptional<z.ZodObject<{
|
|
198
|
+
kind: z.ZodEnum<{
|
|
199
|
+
image: "image";
|
|
200
|
+
document: "document";
|
|
201
|
+
video: "video";
|
|
202
|
+
audio: "audio";
|
|
203
|
+
voice: "voice";
|
|
204
|
+
}>;
|
|
205
|
+
url: z.ZodString;
|
|
206
|
+
caption: z.ZodOptional<z.ZodString>;
|
|
207
|
+
mimeType: z.ZodOptional<z.ZodString>;
|
|
208
|
+
filename: z.ZodOptional<z.ZodString>;
|
|
209
|
+
}, z.core.$strip>>;
|
|
210
|
+
header: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
211
|
+
type: z.ZodLiteral<"text">;
|
|
212
|
+
text: z.ZodString;
|
|
213
|
+
subText: z.ZodOptional<z.ZodString>;
|
|
214
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
215
|
+
type: z.ZodLiteral<"image">;
|
|
216
|
+
url: z.ZodString;
|
|
217
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
218
|
+
type: z.ZodLiteral<"video">;
|
|
219
|
+
url: z.ZodString;
|
|
220
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
221
|
+
type: z.ZodLiteral<"document">;
|
|
222
|
+
url: z.ZodString;
|
|
223
|
+
filename: z.ZodOptional<z.ZodString>;
|
|
224
|
+
}, z.core.$strip>], "type">>;
|
|
225
|
+
quoteId: z.ZodOptional<z.ZodString>;
|
|
226
|
+
flow: z.ZodOptional<z.ZodUnknown>;
|
|
227
|
+
}, z.core.$strip>>;
|
|
228
|
+
export declare const DeliveryStatusSchema: z.ZodEnum<{
|
|
229
|
+
sent: "sent";
|
|
230
|
+
failed: "failed";
|
|
231
|
+
queued: "queued";
|
|
232
|
+
fellBack: "fellBack";
|
|
233
|
+
}>;
|
|
234
|
+
export declare const DeliveryResultSchema: z.ZodObject<{
|
|
235
|
+
status: z.ZodEnum<{
|
|
236
|
+
sent: "sent";
|
|
237
|
+
failed: "failed";
|
|
238
|
+
queued: "queued";
|
|
239
|
+
fellBack: "fellBack";
|
|
240
|
+
}>;
|
|
241
|
+
messageId: z.ZodOptional<z.ZodString>;
|
|
242
|
+
reason: z.ZodOptional<z.ZodString>;
|
|
243
|
+
}, z.core.$strip>;
|
|
244
|
+
export type DeliveryResult = z.infer<typeof DeliveryResultSchema>;
|
|
245
|
+
export declare const TurnSchema: z.ZodObject<{
|
|
246
|
+
id: z.ZodString;
|
|
247
|
+
contactId: z.ZodString;
|
|
248
|
+
role: z.ZodEnum<{
|
|
249
|
+
user: "user";
|
|
250
|
+
agent: "agent";
|
|
251
|
+
system: "system";
|
|
252
|
+
}>;
|
|
253
|
+
text: z.ZodOptional<z.ZodString>;
|
|
254
|
+
timestamp: z.ZodNumber;
|
|
255
|
+
meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
256
|
+
}, z.core.$strip>;
|
|
257
|
+
export type Turn = z.infer<typeof TurnSchema>;
|
|
258
|
+
export declare const RouterDecisionSchema: z.ZodObject<{
|
|
259
|
+
intent: z.ZodString;
|
|
260
|
+
skill: z.ZodOptional<z.ZodString>;
|
|
261
|
+
needsRAG: z.ZodBoolean;
|
|
262
|
+
needsTool: z.ZodBoolean;
|
|
263
|
+
escalate: z.ZodBoolean;
|
|
264
|
+
confidence: z.ZodNumber;
|
|
265
|
+
}, z.core.$strip>;
|
|
266
|
+
export type RouterDecision = z.infer<typeof RouterDecisionSchema>;
|
|
267
|
+
/** M13: a small, structured, per-contact profile — facts (name, location, ...), the fresh open
|
|
268
|
+
* thread right now (`currentState`, updated every turn, never compressed), and a coarser
|
|
269
|
+
* `summary` allowed to lag behind it. TTL-bound via `expiresAt`; session-scoped, not a permanent
|
|
270
|
+
* cross-session user profile (see docs/milestones/M13.md — that's explicitly deferred). */
|
|
271
|
+
export declare const SessionProfileSchema: z.ZodObject<{
|
|
272
|
+
contactId: z.ZodString;
|
|
273
|
+
facts: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
274
|
+
currentState: z.ZodOptional<z.ZodString>;
|
|
275
|
+
summary: z.ZodOptional<z.ZodString>;
|
|
276
|
+
expiresAt: z.ZodNumber;
|
|
277
|
+
}, z.core.$strip>;
|
|
278
|
+
export type SessionProfile = z.infer<typeof SessionProfileSchema>;
|
|
279
|
+
export declare const ToolResultSchema: z.ZodObject<{
|
|
280
|
+
toolName: z.ZodString;
|
|
281
|
+
ok: z.ZodBoolean;
|
|
282
|
+
data: z.ZodOptional<z.ZodUnknown>;
|
|
283
|
+
error: z.ZodOptional<z.ZodString>;
|
|
284
|
+
}, z.core.$strip>;
|
|
285
|
+
export type ToolResult = z.infer<typeof ToolResultSchema>;
|
package/dist/schemas.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Runtime schemas for the core data shapes (§3, §6.1, §6.3, §9 of SPEC.md).
|
|
4
|
+
*
|
|
5
|
+
* Layering rule for WhatsApp send-time limits (§6.1): this schema enforces
|
|
6
|
+
* STRUCTURAL bounds only (button/row/section counts) because those can't be
|
|
7
|
+
* fixed by truncation without changing meaning. TEXT LENGTH limits (button
|
|
8
|
+
* title <=20, list row title <=24, row description <=72) are intentionally
|
|
9
|
+
* NOT enforced here — @wappy_ai/whatsapp (M4) truncates them at send time, so
|
|
10
|
+
* an over-length string is valid input at this layer and a renderer concern
|
|
11
|
+
* downstream. See docs/CONTRACTS.md.
|
|
12
|
+
*/
|
|
13
|
+
export const InboundMediaSchema = z.object({
|
|
14
|
+
kind: z.enum(["image", "document", "video", "audio", "voice", "location"]),
|
|
15
|
+
url: z.string().optional(),
|
|
16
|
+
mimeType: z.string().optional(),
|
|
17
|
+
caption: z.string().optional(),
|
|
18
|
+
/** Only meaningful when kind === "location". */
|
|
19
|
+
latitude: z.number().optional(),
|
|
20
|
+
longitude: z.number().optional(),
|
|
21
|
+
});
|
|
22
|
+
export const InboundMessageSchema = z.object({
|
|
23
|
+
/** Channel-native message id, used for idempotency/dedupe (§6.2). */
|
|
24
|
+
id: z.string().min(1),
|
|
25
|
+
/** Opaque contact identifier, stable per sender on this channel. */
|
|
26
|
+
contactId: z.string().min(1),
|
|
27
|
+
/** Which MessageChannel produced this, e.g. "whatsapp". */
|
|
28
|
+
channel: z.string().min(1),
|
|
29
|
+
text: z.string().optional(),
|
|
30
|
+
media: InboundMediaSchema.optional(),
|
|
31
|
+
/**
|
|
32
|
+
* The stable id of whatever discrete option the user picked (a button/list-row id, a quick-reply
|
|
33
|
+
* payload, ...) — distinct from `text`, which carries the human-readable title and may be
|
|
34
|
+
* duplicated, localized, or renamed. Routing must key off this, never off `text`.
|
|
35
|
+
*/
|
|
36
|
+
selectionId: z.string().optional(),
|
|
37
|
+
/** Epoch milliseconds. */
|
|
38
|
+
timestamp: z.number(),
|
|
39
|
+
/** Channel-native payload, kept for debugging/replay; never parsed by core. */
|
|
40
|
+
raw: z.unknown().optional(),
|
|
41
|
+
});
|
|
42
|
+
export const SmartButtonSchema = z.object({
|
|
43
|
+
id: z.string().min(1),
|
|
44
|
+
title: z.string().min(1),
|
|
45
|
+
});
|
|
46
|
+
export const SmartButtonsSchema = z.array(SmartButtonSchema).min(1).max(3);
|
|
47
|
+
export const SmartListRowSchema = z.object({
|
|
48
|
+
id: z.string().min(1),
|
|
49
|
+
title: z.string().min(1),
|
|
50
|
+
description: z.string().optional(),
|
|
51
|
+
});
|
|
52
|
+
export const SmartListSectionSchema = z.object({
|
|
53
|
+
title: z.string().optional(),
|
|
54
|
+
rows: z.array(SmartListRowSchema).min(1),
|
|
55
|
+
});
|
|
56
|
+
export const SmartListSchema = z
|
|
57
|
+
.object({
|
|
58
|
+
buttonText: z.string().min(1),
|
|
59
|
+
sections: z.array(SmartListSectionSchema).min(1).max(10),
|
|
60
|
+
})
|
|
61
|
+
.refine((l) => l.sections.reduce((n, s) => n + s.rows.length, 0) <= 10, {
|
|
62
|
+
message: "list: total rows across all sections must be <= 10",
|
|
63
|
+
});
|
|
64
|
+
export const SmartCtaSchema = z.object({
|
|
65
|
+
text: z.string().min(1),
|
|
66
|
+
url: z.string().url(),
|
|
67
|
+
});
|
|
68
|
+
/** An interactive message's header (§6.1/M12): image/video/document/text alongside body + action.
|
|
69
|
+
* Meta's real Cloud API supports this on button/list interactive messages — verified against
|
|
70
|
+
* their docs, not assumed — for a "preview, then text, then CTA/list" reply. Valid only alongside
|
|
71
|
+
* buttons/list/cta (enforced below); a header on a plain text/media-only send is meaningless. */
|
|
72
|
+
export const SmartHeaderSchema = z.discriminatedUnion("type", [
|
|
73
|
+
z.object({ type: z.literal("text"), text: z.string().min(1), subText: z.string().optional() }),
|
|
74
|
+
z.object({ type: z.literal("image"), url: z.string().min(1) }),
|
|
75
|
+
z.object({ type: z.literal("video"), url: z.string().min(1) }),
|
|
76
|
+
z.object({ type: z.literal("document"), url: z.string().min(1), filename: z.string().optional() }),
|
|
77
|
+
]);
|
|
78
|
+
export const SmartMediaSchema = z.object({
|
|
79
|
+
kind: z.enum(["image", "document", "video", "audio", "voice"]),
|
|
80
|
+
url: z.string().min(1),
|
|
81
|
+
caption: z.string().optional(),
|
|
82
|
+
mimeType: z.string().optional(),
|
|
83
|
+
filename: z.string().optional(),
|
|
84
|
+
});
|
|
85
|
+
export const SmartMessageSchema = z
|
|
86
|
+
.object({
|
|
87
|
+
text: z.string().min(1).optional(),
|
|
88
|
+
buttons: SmartButtonsSchema.optional(),
|
|
89
|
+
list: SmartListSchema.optional(),
|
|
90
|
+
cta: SmartCtaSchema.optional(),
|
|
91
|
+
media: SmartMediaSchema.optional(),
|
|
92
|
+
header: SmartHeaderSchema.optional(),
|
|
93
|
+
/** id of an inbound message this reply quotes ("reply in context"). */
|
|
94
|
+
quoteId: z.string().optional(),
|
|
95
|
+
/** Reserved slot for WhatsApp Flows/forms (deferred; SPEC §6.1). Not validated in v0.1. */
|
|
96
|
+
flow: z.unknown().optional(),
|
|
97
|
+
})
|
|
98
|
+
.refine((m) => Boolean(m.text ?? m.buttons ?? m.list ?? m.cta ?? m.media), {
|
|
99
|
+
message: "SmartMessage: at least one of text/buttons/list/cta/media is required",
|
|
100
|
+
})
|
|
101
|
+
.refine((m) => !m.header || Boolean(m.buttons ?? m.list ?? m.cta), {
|
|
102
|
+
message: "SmartMessage: header is only valid alongside buttons/list/cta (Meta's own constraint)",
|
|
103
|
+
});
|
|
104
|
+
/** JSON Schema for SmartMessage — this is what the model is asked to emit (§6.3). */
|
|
105
|
+
export const smartMessageJsonSchema = z.toJSONSchema(SmartMessageSchema);
|
|
106
|
+
export const DeliveryStatusSchema = z.enum(["sent", "failed", "queued", "fellBack"]);
|
|
107
|
+
export const DeliveryResultSchema = z
|
|
108
|
+
.object({
|
|
109
|
+
status: DeliveryStatusSchema,
|
|
110
|
+
messageId: z.string().optional(),
|
|
111
|
+
/** Actionable, human-readable reason — never a raw stack trace (§10). */
|
|
112
|
+
reason: z.string().optional(),
|
|
113
|
+
})
|
|
114
|
+
.refine((d) => (d.status === "failed" || d.status === "fellBack" ? Boolean(d.reason) : true), {
|
|
115
|
+
message: "DeliveryResult: reason is required when status is failed or fellBack",
|
|
116
|
+
});
|
|
117
|
+
export const TurnSchema = z.object({
|
|
118
|
+
id: z.string().min(1),
|
|
119
|
+
contactId: z.string().min(1),
|
|
120
|
+
role: z.enum(["user", "agent", "system"]),
|
|
121
|
+
text: z.string().optional(),
|
|
122
|
+
timestamp: z.number(),
|
|
123
|
+
meta: z.record(z.string(), z.unknown()).optional(),
|
|
124
|
+
});
|
|
125
|
+
export const RouterDecisionSchema = z.object({
|
|
126
|
+
intent: z.string().min(1),
|
|
127
|
+
skill: z.string().optional(),
|
|
128
|
+
needsRAG: z.boolean(),
|
|
129
|
+
needsTool: z.boolean(),
|
|
130
|
+
escalate: z.boolean(),
|
|
131
|
+
confidence: z.number().min(0).max(1),
|
|
132
|
+
});
|
|
133
|
+
/** M13: a small, structured, per-contact profile — facts (name, location, ...), the fresh open
|
|
134
|
+
* thread right now (`currentState`, updated every turn, never compressed), and a coarser
|
|
135
|
+
* `summary` allowed to lag behind it. TTL-bound via `expiresAt`; session-scoped, not a permanent
|
|
136
|
+
* cross-session user profile (see docs/milestones/M13.md — that's explicitly deferred). */
|
|
137
|
+
export const SessionProfileSchema = z.object({
|
|
138
|
+
contactId: z.string().min(1),
|
|
139
|
+
facts: z.record(z.string(), z.string()),
|
|
140
|
+
currentState: z.string().optional(),
|
|
141
|
+
summary: z.string().optional(),
|
|
142
|
+
expiresAt: z.number(),
|
|
143
|
+
});
|
|
144
|
+
export const ToolResultSchema = z.object({
|
|
145
|
+
toolName: z.string().min(1),
|
|
146
|
+
ok: z.boolean(),
|
|
147
|
+
data: z.unknown().optional(),
|
|
148
|
+
error: z.string().optional(),
|
|
149
|
+
});
|
package/dist/semver.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function satisfiesRange(version: string, range: string): boolean;
|