@guuey/hooks 0.25.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/LICENSE +21 -0
- package/README.md +48 -0
- package/dist/index.d.ts +384 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +296 -0
- package/package.json +55 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Loqu, Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# @guuey/hooks
|
|
2
|
+
|
|
3
|
+
The guuey **hook spec**: the lifecycle events the platform fires for a hosted
|
|
4
|
+
agent, the JSON envelope a hook receives, the result it returns, the two
|
|
5
|
+
handler kinds, and the `hooks` block of `guuey.json`.
|
|
6
|
+
|
|
7
|
+
A hook is **not** code inside the agent. The platform detects the moment (a
|
|
8
|
+
conversation went quiet, a visitor asked for a person) and dispatches it; a
|
|
9
|
+
hook handler is either an **agent run** inside the app's own session runtime
|
|
10
|
+
(`kind: 'agent'` — an instruction, a tool allowlist, the effects that must
|
|
11
|
+
happen) or a **direct tool call** (`kind: 'tool'` — one MCP tool, no model).
|
|
12
|
+
Prebuilt hooks (`use: 'email-reporter'`) and dev-defined ones share this spec.
|
|
13
|
+
|
|
14
|
+
```jsonc
|
|
15
|
+
// guuey.json → agent.hooks
|
|
16
|
+
"hooks": {
|
|
17
|
+
"session.ended": [ { "use": "email-reporter" } ],
|
|
18
|
+
"handoff.requested": [ { "use": "email-reporter" },
|
|
19
|
+
{ "kind": "tool", "server": "my-crm", "tool": "create_lead" } ],
|
|
20
|
+
"definitions": {
|
|
21
|
+
"nightly-triage": { "kind": "agent", "on": ["session.ended"],
|
|
22
|
+
"instruction": "…", "tools": ["my-crm.*"], "required": ["my-crm.create_lead"],
|
|
23
|
+
"model": "small", "maxTurns": 4, "timeoutMs": 60000 }
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## What this package holds
|
|
29
|
+
|
|
30
|
+
- `HOOK_EVENT_NAMES` / `HookEventName` — the event vocabulary; `HOOK_EVENTS`
|
|
31
|
+
— the per-event table (`class`, `blockable`, `firedInV1`, `allowsAppend`).
|
|
32
|
+
v1 fires `session.ended` and `handoff.requested`; the rest are reserved.
|
|
33
|
+
- `hookEventSchema` / `HookEvent` — the envelope the platform sends, a
|
|
34
|
+
discriminated union on `type`.
|
|
35
|
+
- `hookResultSchema` / `HookResult` / `HookEffect` — what a hook returns.
|
|
36
|
+
`note` is owner-visible only; `append` reaches the visitor and is allowed
|
|
37
|
+
only on live-session events.
|
|
38
|
+
- `hooksSectionSchema` / `HooksSection`, `hookDefinitionSchema`,
|
|
39
|
+
`hookHandlerRefSchema` — the `guuey.json` block, validated by
|
|
40
|
+
`@guuey/config`.
|
|
41
|
+
- `toWireToolName('server.tool')` → `mcp__server__tool` — the one function
|
|
42
|
+
both config names and the runtime's tool extractor call.
|
|
43
|
+
|
|
44
|
+
Everything is optional both ways across a rolling release: a `guuey.json`
|
|
45
|
+
without `hooks` behaves exactly as before.
|
|
46
|
+
|
|
47
|
+
Questions and help: the guuey community on Discord — https://guuey.com/discord.
|
|
48
|
+
Bugs and feature requests: https://github.com/withguuey/guuey-sdks/issues.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@guuey/hooks` — the guuey hook spec (guuey#1511 §8, guuey#1528).
|
|
3
|
+
*
|
|
4
|
+
* A hook is a lifecycle moment the PLATFORM detects and dispatches — never a
|
|
5
|
+
* function inside the agent. Two handler kinds share one contract:
|
|
6
|
+
*
|
|
7
|
+
* - `agent` — a one-shot run inside the app's own session runtime: an
|
|
8
|
+
* instruction, a tool allowlist, the effects that MUST happen. The model
|
|
9
|
+
* decides content; the Router enforces `required` after the fold.
|
|
10
|
+
* - `tool` — one MCP tool called directly by the dispatcher, no model.
|
|
11
|
+
*
|
|
12
|
+
* This package holds the vocabulary, the envelope in, the result out, the
|
|
13
|
+
* `guuey.json` block, and the per-event table. It has no runtime of its own:
|
|
14
|
+
* the dispatcher (backend) and the Router (`nocode-runtime`) import it to
|
|
15
|
+
* validate what they send and receive; `@guuey/config` composes
|
|
16
|
+
* {@link hooksSectionSchema} into the agent section of `guuey.json`.
|
|
17
|
+
*
|
|
18
|
+
* Rolling-release posture: every field is optional both ways. A config
|
|
19
|
+
* without `hooks` behaves exactly as before; a result without `effects`
|
|
20
|
+
* parses with `effects: []`.
|
|
21
|
+
*/
|
|
22
|
+
import { z } from 'zod';
|
|
23
|
+
/** Every event name the spec knows. v1 FIRES the first two; the rest are reserved. */
|
|
24
|
+
export declare const HOOK_EVENT_NAMES: readonly ["session.ended", "handoff.requested", "session.idled", "turn.completed", "session.started", "turn.start", "schedule.tick"];
|
|
25
|
+
export type HookEventName = (typeof HOOK_EVENT_NAMES)[number];
|
|
26
|
+
/** `notify` handlers observe after the fact; `decide` handlers sit on the turn path (reserved). */
|
|
27
|
+
export type HookEventClass = 'notify' | 'decide';
|
|
28
|
+
/** `session` events carry a `threadId`; `app` events (schedules) bind to the app alone. */
|
|
29
|
+
export type HookEventScope = 'session' | 'app';
|
|
30
|
+
export interface HookEventSpec {
|
|
31
|
+
readonly class: HookEventClass;
|
|
32
|
+
readonly scope: HookEventScope;
|
|
33
|
+
/** May a handler block or rewrite what the event describes? Declared, never inferred. */
|
|
34
|
+
readonly blockable: boolean;
|
|
35
|
+
/** Does the v1 platform fire this event? Reserved names parse but are refused in `hooks`. */
|
|
36
|
+
readonly firedInV1: boolean;
|
|
37
|
+
/** May a handler's `append` reach the visitor? Only while the session is live. */
|
|
38
|
+
readonly allowsAppend: boolean;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The per-event table (§8.1). Managed Agents' split is kept on purpose:
|
|
42
|
+
* `session.idled` = awaiting input at every turn end; `session.ended` =
|
|
43
|
+
* dormant/closed — one name never carries both.
|
|
44
|
+
*/
|
|
45
|
+
export declare const HOOK_EVENTS: Readonly<Record<HookEventName, HookEventSpec>>;
|
|
46
|
+
export declare const hookEventNameSchema: z.ZodEnum<{
|
|
47
|
+
"session.ended": "session.ended";
|
|
48
|
+
"handoff.requested": "handoff.requested";
|
|
49
|
+
"session.idled": "session.idled";
|
|
50
|
+
"turn.completed": "turn.completed";
|
|
51
|
+
"session.started": "session.started";
|
|
52
|
+
"turn.start": "turn.start";
|
|
53
|
+
"schedule.tick": "schedule.tick";
|
|
54
|
+
}>;
|
|
55
|
+
/** Prebuilt hooks a `{ use }` handler may name. The platform's registry is the authority; this list is the v1 catalog. */
|
|
56
|
+
export declare const PREBUILT_HOOKS: readonly ["email-reporter"];
|
|
57
|
+
export type PrebuiltHookName = (typeof PREBUILT_HOOKS)[number];
|
|
58
|
+
export type JsonPrimitive = string | number | boolean | null;
|
|
59
|
+
export type JsonValue = JsonPrimitive | JsonValue[] | {
|
|
60
|
+
[key: string]: JsonValue;
|
|
61
|
+
};
|
|
62
|
+
/** A JSON-shaped value — the hook's `output` and a reserved event's `data`. */
|
|
63
|
+
export declare const jsonValueSchema: z.ZodType<JsonValue>;
|
|
64
|
+
/** `"<server>.<tool>"` — the name a config uses for one tool. */
|
|
65
|
+
export declare const toolNameSchema: z.ZodString;
|
|
66
|
+
/** `"<server>.<tool>"` or `"<server>.*"` — an allowlist entry. */
|
|
67
|
+
export declare const toolPatternSchema: z.ZodString;
|
|
68
|
+
/**
|
|
69
|
+
* The wire name of a tool call as the runtime's fold sees it —
|
|
70
|
+
* `mcp__<server>__<tool>` (the reserved hand-off child already uses this
|
|
71
|
+
* grammar: `mcp__guuey-handoff__request_human`). ONE function for both sides:
|
|
72
|
+
* config names in, extractor names out.
|
|
73
|
+
*/
|
|
74
|
+
export declare function toWireToolName(name: string): string;
|
|
75
|
+
/** Does an allowlist pattern (`server.tool` | `server.*`) cover a tool name? */
|
|
76
|
+
export declare function toolPatternCovers(pattern: string, name: string): boolean;
|
|
77
|
+
/** A hook or schedule name: a lowercase slug (a-z, 0-9, -), 64 chars max. */
|
|
78
|
+
export declare const hookNameSchema: z.ZodString;
|
|
79
|
+
/** One tick of a declared schedule — app-scoped; the run has no conversation history and reads what it needs through tools. */
|
|
80
|
+
export declare const scheduleTickDataSchema: z.ZodObject<{
|
|
81
|
+
schedule: z.ZodString;
|
|
82
|
+
scheduledFor: z.ZodISODateTime;
|
|
83
|
+
}, z.core.$strict>;
|
|
84
|
+
/** What the rep recorded when the visitor asked for a person (the hand-off child's fields + #1510's summary). */
|
|
85
|
+
export declare const handoffRequestedDataSchema: z.ZodObject<{
|
|
86
|
+
question: z.ZodString;
|
|
87
|
+
contactEmail: z.ZodOptional<z.ZodString>;
|
|
88
|
+
contactName: z.ZodOptional<z.ZodString>;
|
|
89
|
+
summary: z.ZodOptional<z.ZodString>;
|
|
90
|
+
}, z.core.$strict>;
|
|
91
|
+
export declare const sessionEndedDataSchema: z.ZodObject<{
|
|
92
|
+
reason: z.ZodLiteral<"idle">;
|
|
93
|
+
idleMinutes: z.ZodNumber;
|
|
94
|
+
lastActivityAt: z.ZodISODateTime;
|
|
95
|
+
turns: z.ZodNumber;
|
|
96
|
+
handoff: z.ZodOptional<z.ZodObject<{
|
|
97
|
+
question: z.ZodString;
|
|
98
|
+
contactEmail: z.ZodOptional<z.ZodString>;
|
|
99
|
+
contactName: z.ZodOptional<z.ZodString>;
|
|
100
|
+
summary: z.ZodOptional<z.ZodString>;
|
|
101
|
+
}, z.core.$strict>>;
|
|
102
|
+
}, z.core.$strict>;
|
|
103
|
+
/**
|
|
104
|
+
* Reserved session-scoped events, as a LITERAL tuple so the envelope's
|
|
105
|
+
* discriminated union stays precise at the type level (a filtered array
|
|
106
|
+
* would type as `HookEventName[]` and swallow the fired members' narrowing).
|
|
107
|
+
* `index.test.ts` pins this list to the table.
|
|
108
|
+
*/
|
|
109
|
+
export declare const RESERVED_SESSION_EVENT_NAMES: readonly ["session.idled", "turn.completed", "session.started", "turn.start"];
|
|
110
|
+
export declare const hookEventSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
111
|
+
type: z.ZodLiteral<"session.ended">;
|
|
112
|
+
data: z.ZodObject<{
|
|
113
|
+
reason: z.ZodLiteral<"idle">;
|
|
114
|
+
idleMinutes: z.ZodNumber;
|
|
115
|
+
lastActivityAt: z.ZodISODateTime;
|
|
116
|
+
turns: z.ZodNumber;
|
|
117
|
+
handoff: z.ZodOptional<z.ZodObject<{
|
|
118
|
+
question: z.ZodString;
|
|
119
|
+
contactEmail: z.ZodOptional<z.ZodString>;
|
|
120
|
+
contactName: z.ZodOptional<z.ZodString>;
|
|
121
|
+
summary: z.ZodOptional<z.ZodString>;
|
|
122
|
+
}, z.core.$strict>>;
|
|
123
|
+
}, z.core.$strict>;
|
|
124
|
+
threadId: z.ZodString;
|
|
125
|
+
id: z.ZodString;
|
|
126
|
+
at: z.ZodISODateTime;
|
|
127
|
+
appId: z.ZodString;
|
|
128
|
+
hook: z.ZodObject<{
|
|
129
|
+
name: z.ZodString;
|
|
130
|
+
runId: z.ZodString;
|
|
131
|
+
}, z.core.$strict>;
|
|
132
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
133
|
+
type: z.ZodLiteral<"handoff.requested">;
|
|
134
|
+
data: z.ZodObject<{
|
|
135
|
+
question: z.ZodString;
|
|
136
|
+
contactEmail: z.ZodOptional<z.ZodString>;
|
|
137
|
+
contactName: z.ZodOptional<z.ZodString>;
|
|
138
|
+
summary: z.ZodOptional<z.ZodString>;
|
|
139
|
+
}, z.core.$strict>;
|
|
140
|
+
threadId: z.ZodString;
|
|
141
|
+
id: z.ZodString;
|
|
142
|
+
at: z.ZodISODateTime;
|
|
143
|
+
appId: z.ZodString;
|
|
144
|
+
hook: z.ZodObject<{
|
|
145
|
+
name: z.ZodString;
|
|
146
|
+
runId: z.ZodString;
|
|
147
|
+
}, z.core.$strict>;
|
|
148
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
149
|
+
type: z.ZodLiteral<"schedule.tick">;
|
|
150
|
+
data: z.ZodObject<{
|
|
151
|
+
schedule: z.ZodString;
|
|
152
|
+
scheduledFor: z.ZodISODateTime;
|
|
153
|
+
}, z.core.$strict>;
|
|
154
|
+
id: z.ZodString;
|
|
155
|
+
at: z.ZodISODateTime;
|
|
156
|
+
appId: z.ZodString;
|
|
157
|
+
hook: z.ZodObject<{
|
|
158
|
+
name: z.ZodString;
|
|
159
|
+
runId: z.ZodString;
|
|
160
|
+
}, z.core.$strict>;
|
|
161
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
162
|
+
type: z.ZodEnum<{
|
|
163
|
+
"session.idled": "session.idled";
|
|
164
|
+
"turn.completed": "turn.completed";
|
|
165
|
+
"session.started": "session.started";
|
|
166
|
+
"turn.start": "turn.start";
|
|
167
|
+
}>;
|
|
168
|
+
data: z.ZodType<JsonValue, unknown, z.core.$ZodTypeInternals<JsonValue, unknown>>;
|
|
169
|
+
threadId: z.ZodString;
|
|
170
|
+
id: z.ZodString;
|
|
171
|
+
at: z.ZodISODateTime;
|
|
172
|
+
appId: z.ZodString;
|
|
173
|
+
hook: z.ZodObject<{
|
|
174
|
+
name: z.ZodString;
|
|
175
|
+
runId: z.ZodString;
|
|
176
|
+
}, z.core.$strict>;
|
|
177
|
+
}, z.core.$strict>], "type">;
|
|
178
|
+
export type HookEvent = z.infer<typeof hookEventSchema>;
|
|
179
|
+
export type SessionEndedData = z.infer<typeof sessionEndedDataSchema>;
|
|
180
|
+
export type ScheduleTickData = z.infer<typeof scheduleTickDataSchema>;
|
|
181
|
+
export type HandoffRequestedData = z.infer<typeof handoffRequestedDataSchema>;
|
|
182
|
+
export declare const hookEffectSchema: z.ZodObject<{
|
|
183
|
+
tool: z.ZodString;
|
|
184
|
+
status: z.ZodEnum<{
|
|
185
|
+
called: "called";
|
|
186
|
+
enforced: "enforced";
|
|
187
|
+
failed: "failed";
|
|
188
|
+
}>;
|
|
189
|
+
}, z.core.$strict>;
|
|
190
|
+
export declare const hookResultSchema: z.ZodObject<{
|
|
191
|
+
status: z.ZodEnum<{
|
|
192
|
+
failed: "failed";
|
|
193
|
+
ok: "ok";
|
|
194
|
+
skipped: "skipped";
|
|
195
|
+
}>;
|
|
196
|
+
output: z.ZodOptional<z.ZodType<JsonValue, unknown, z.core.$ZodTypeInternals<JsonValue, unknown>>>;
|
|
197
|
+
effects: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
198
|
+
tool: z.ZodString;
|
|
199
|
+
status: z.ZodEnum<{
|
|
200
|
+
called: "called";
|
|
201
|
+
enforced: "enforced";
|
|
202
|
+
failed: "failed";
|
|
203
|
+
}>;
|
|
204
|
+
}, z.core.$strict>>>;
|
|
205
|
+
note: z.ZodOptional<z.ZodObject<{
|
|
206
|
+
text: z.ZodString;
|
|
207
|
+
}, z.core.$strict>>;
|
|
208
|
+
append: z.ZodOptional<z.ZodObject<{
|
|
209
|
+
text: z.ZodString;
|
|
210
|
+
}, z.core.$strict>>;
|
|
211
|
+
reason: z.ZodOptional<z.ZodString>;
|
|
212
|
+
}, z.core.$strict>;
|
|
213
|
+
export type HookResult = z.infer<typeof hookResultSchema>;
|
|
214
|
+
export type HookEffect = z.infer<typeof hookEffectSchema>;
|
|
215
|
+
/** A run's lifecycle as the dispatcher records it — the result statuses plus the two it decides alone. */
|
|
216
|
+
export type HookRunStatus = HookResult['status'] | 'deferred' | 'parked';
|
|
217
|
+
export declare const hookHandlerRefSchema: z.ZodUnion<readonly [z.ZodObject<{
|
|
218
|
+
use: z.ZodString;
|
|
219
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
220
|
+
kind: z.ZodLiteral<"tool">;
|
|
221
|
+
server: z.ZodString;
|
|
222
|
+
tool: z.ZodString;
|
|
223
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
224
|
+
kind: z.ZodLiteral<"agent">;
|
|
225
|
+
definition: z.ZodString;
|
|
226
|
+
}, z.core.$strict>]>;
|
|
227
|
+
export type HookHandlerRef = z.infer<typeof hookHandlerRefSchema>;
|
|
228
|
+
export declare const hookDefinitionSchema: z.ZodUnion<readonly [z.ZodObject<{
|
|
229
|
+
kind: z.ZodLiteral<"agent">;
|
|
230
|
+
on: z.ZodArray<z.ZodEnum<{
|
|
231
|
+
"session.ended": "session.ended";
|
|
232
|
+
"handoff.requested": "handoff.requested";
|
|
233
|
+
"session.idled": "session.idled";
|
|
234
|
+
"turn.completed": "turn.completed";
|
|
235
|
+
"session.started": "session.started";
|
|
236
|
+
"turn.start": "turn.start";
|
|
237
|
+
"schedule.tick": "schedule.tick";
|
|
238
|
+
}>>;
|
|
239
|
+
instruction: z.ZodString;
|
|
240
|
+
tools: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
241
|
+
required: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
242
|
+
model: z.ZodOptional<z.ZodEnum<{
|
|
243
|
+
default: "default";
|
|
244
|
+
small: "small";
|
|
245
|
+
}>>;
|
|
246
|
+
maxTurns: z.ZodOptional<z.ZodNumber>;
|
|
247
|
+
timeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
248
|
+
actAs: z.ZodOptional<z.ZodLiteral<"hook">>;
|
|
249
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
250
|
+
kind: z.ZodLiteral<"tool">;
|
|
251
|
+
on: z.ZodArray<z.ZodEnum<{
|
|
252
|
+
"session.ended": "session.ended";
|
|
253
|
+
"handoff.requested": "handoff.requested";
|
|
254
|
+
"session.idled": "session.idled";
|
|
255
|
+
"turn.completed": "turn.completed";
|
|
256
|
+
"session.started": "session.started";
|
|
257
|
+
"turn.start": "turn.start";
|
|
258
|
+
"schedule.tick": "schedule.tick";
|
|
259
|
+
}>>;
|
|
260
|
+
tool: z.ZodString;
|
|
261
|
+
}, z.core.$strict>]>;
|
|
262
|
+
export type HookDefinition = z.infer<typeof hookDefinitionSchema>;
|
|
263
|
+
export declare const hooksSectionSchema: z.ZodObject<{
|
|
264
|
+
'session.ended': z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodObject<{
|
|
265
|
+
use: z.ZodString;
|
|
266
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
267
|
+
kind: z.ZodLiteral<"tool">;
|
|
268
|
+
server: z.ZodString;
|
|
269
|
+
tool: z.ZodString;
|
|
270
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
271
|
+
kind: z.ZodLiteral<"agent">;
|
|
272
|
+
definition: z.ZodString;
|
|
273
|
+
}, z.core.$strict>]>>>;
|
|
274
|
+
'handoff.requested': z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodObject<{
|
|
275
|
+
use: z.ZodString;
|
|
276
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
277
|
+
kind: z.ZodLiteral<"tool">;
|
|
278
|
+
server: z.ZodString;
|
|
279
|
+
tool: z.ZodString;
|
|
280
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
281
|
+
kind: z.ZodLiteral<"agent">;
|
|
282
|
+
definition: z.ZodString;
|
|
283
|
+
}, z.core.$strict>]>>>;
|
|
284
|
+
'session.idled': z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodObject<{
|
|
285
|
+
use: z.ZodString;
|
|
286
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
287
|
+
kind: z.ZodLiteral<"tool">;
|
|
288
|
+
server: z.ZodString;
|
|
289
|
+
tool: z.ZodString;
|
|
290
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
291
|
+
kind: z.ZodLiteral<"agent">;
|
|
292
|
+
definition: z.ZodString;
|
|
293
|
+
}, z.core.$strict>]>>>;
|
|
294
|
+
'turn.completed': z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodObject<{
|
|
295
|
+
use: z.ZodString;
|
|
296
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
297
|
+
kind: z.ZodLiteral<"tool">;
|
|
298
|
+
server: z.ZodString;
|
|
299
|
+
tool: z.ZodString;
|
|
300
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
301
|
+
kind: z.ZodLiteral<"agent">;
|
|
302
|
+
definition: z.ZodString;
|
|
303
|
+
}, z.core.$strict>]>>>;
|
|
304
|
+
'session.started': z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodObject<{
|
|
305
|
+
use: z.ZodString;
|
|
306
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
307
|
+
kind: z.ZodLiteral<"tool">;
|
|
308
|
+
server: z.ZodString;
|
|
309
|
+
tool: z.ZodString;
|
|
310
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
311
|
+
kind: z.ZodLiteral<"agent">;
|
|
312
|
+
definition: z.ZodString;
|
|
313
|
+
}, z.core.$strict>]>>>;
|
|
314
|
+
'turn.start': z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodObject<{
|
|
315
|
+
use: z.ZodString;
|
|
316
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
317
|
+
kind: z.ZodLiteral<"tool">;
|
|
318
|
+
server: z.ZodString;
|
|
319
|
+
tool: z.ZodString;
|
|
320
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
321
|
+
kind: z.ZodLiteral<"agent">;
|
|
322
|
+
definition: z.ZodString;
|
|
323
|
+
}, z.core.$strict>]>>>;
|
|
324
|
+
'schedule.tick': z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodObject<{
|
|
325
|
+
use: z.ZodString;
|
|
326
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
327
|
+
kind: z.ZodLiteral<"tool">;
|
|
328
|
+
server: z.ZodString;
|
|
329
|
+
tool: z.ZodString;
|
|
330
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
331
|
+
kind: z.ZodLiteral<"agent">;
|
|
332
|
+
definition: z.ZodString;
|
|
333
|
+
}, z.core.$strict>]>>>;
|
|
334
|
+
definitions: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodObject<{
|
|
335
|
+
kind: z.ZodLiteral<"agent">;
|
|
336
|
+
on: z.ZodArray<z.ZodEnum<{
|
|
337
|
+
"session.ended": "session.ended";
|
|
338
|
+
"handoff.requested": "handoff.requested";
|
|
339
|
+
"session.idled": "session.idled";
|
|
340
|
+
"turn.completed": "turn.completed";
|
|
341
|
+
"session.started": "session.started";
|
|
342
|
+
"turn.start": "turn.start";
|
|
343
|
+
"schedule.tick": "schedule.tick";
|
|
344
|
+
}>>;
|
|
345
|
+
instruction: z.ZodString;
|
|
346
|
+
tools: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
347
|
+
required: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
348
|
+
model: z.ZodOptional<z.ZodEnum<{
|
|
349
|
+
default: "default";
|
|
350
|
+
small: "small";
|
|
351
|
+
}>>;
|
|
352
|
+
maxTurns: z.ZodOptional<z.ZodNumber>;
|
|
353
|
+
timeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
354
|
+
actAs: z.ZodOptional<z.ZodLiteral<"hook">>;
|
|
355
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
356
|
+
kind: z.ZodLiteral<"tool">;
|
|
357
|
+
on: z.ZodArray<z.ZodEnum<{
|
|
358
|
+
"session.ended": "session.ended";
|
|
359
|
+
"handoff.requested": "handoff.requested";
|
|
360
|
+
"session.idled": "session.idled";
|
|
361
|
+
"turn.completed": "turn.completed";
|
|
362
|
+
"session.started": "session.started";
|
|
363
|
+
"turn.start": "turn.start";
|
|
364
|
+
"schedule.tick": "schedule.tick";
|
|
365
|
+
}>>;
|
|
366
|
+
tool: z.ZodString;
|
|
367
|
+
}, z.core.$strict>]>>>;
|
|
368
|
+
schedules: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
369
|
+
cron: z.ZodString;
|
|
370
|
+
timezone: z.ZodOptional<z.ZodString>;
|
|
371
|
+
handlers: z.ZodArray<z.ZodUnion<readonly [z.ZodObject<{
|
|
372
|
+
use: z.ZodString;
|
|
373
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
374
|
+
kind: z.ZodLiteral<"tool">;
|
|
375
|
+
server: z.ZodString;
|
|
376
|
+
tool: z.ZodString;
|
|
377
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
378
|
+
kind: z.ZodLiteral<"agent">;
|
|
379
|
+
definition: z.ZodString;
|
|
380
|
+
}, z.core.$strict>]>>;
|
|
381
|
+
}, z.core.$strict>>>;
|
|
382
|
+
}, z.core.$strict>;
|
|
383
|
+
export type HooksSection = z.infer<typeof hooksSectionSchema>;
|
|
384
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@guuey/hooks` — the guuey hook spec (guuey#1511 §8, guuey#1528).
|
|
3
|
+
*
|
|
4
|
+
* A hook is a lifecycle moment the PLATFORM detects and dispatches — never a
|
|
5
|
+
* function inside the agent. Two handler kinds share one contract:
|
|
6
|
+
*
|
|
7
|
+
* - `agent` — a one-shot run inside the app's own session runtime: an
|
|
8
|
+
* instruction, a tool allowlist, the effects that MUST happen. The model
|
|
9
|
+
* decides content; the Router enforces `required` after the fold.
|
|
10
|
+
* - `tool` — one MCP tool called directly by the dispatcher, no model.
|
|
11
|
+
*
|
|
12
|
+
* This package holds the vocabulary, the envelope in, the result out, the
|
|
13
|
+
* `guuey.json` block, and the per-event table. It has no runtime of its own:
|
|
14
|
+
* the dispatcher (backend) and the Router (`nocode-runtime`) import it to
|
|
15
|
+
* validate what they send and receive; `@guuey/config` composes
|
|
16
|
+
* {@link hooksSectionSchema} into the agent section of `guuey.json`.
|
|
17
|
+
*
|
|
18
|
+
* Rolling-release posture: every field is optional both ways. A config
|
|
19
|
+
* without `hooks` behaves exactly as before; a result without `effects`
|
|
20
|
+
* parses with `effects: []`.
|
|
21
|
+
*/
|
|
22
|
+
import { z } from 'zod';
|
|
23
|
+
// ── Vocabulary ────────────────────────────────────────────────────────
|
|
24
|
+
/** Every event name the spec knows. v1 FIRES the first two; the rest are reserved. */
|
|
25
|
+
export const HOOK_EVENT_NAMES = [
|
|
26
|
+
'session.ended',
|
|
27
|
+
'handoff.requested',
|
|
28
|
+
'session.idled',
|
|
29
|
+
'turn.completed',
|
|
30
|
+
'session.started',
|
|
31
|
+
'turn.start',
|
|
32
|
+
'schedule.tick',
|
|
33
|
+
];
|
|
34
|
+
/**
|
|
35
|
+
* The per-event table (§8.1). Managed Agents' split is kept on purpose:
|
|
36
|
+
* `session.idled` = awaiting input at every turn end; `session.ended` =
|
|
37
|
+
* dormant/closed — one name never carries both.
|
|
38
|
+
*/
|
|
39
|
+
export const HOOK_EVENTS = {
|
|
40
|
+
'session.ended': { class: 'notify', scope: 'session', blockable: false, firedInV1: true, allowsAppend: false },
|
|
41
|
+
'handoff.requested': { class: 'notify', scope: 'session', blockable: false, firedInV1: true, allowsAppend: true },
|
|
42
|
+
'session.idled': { class: 'notify', scope: 'session', blockable: false, firedInV1: false, allowsAppend: true },
|
|
43
|
+
'turn.completed': { class: 'notify', scope: 'session', blockable: false, firedInV1: false, allowsAppend: true },
|
|
44
|
+
'session.started': { class: 'notify', scope: 'session', blockable: false, firedInV1: false, allowsAppend: true },
|
|
45
|
+
'turn.start': { class: 'decide', scope: 'session', blockable: true, firedInV1: false, allowsAppend: true },
|
|
46
|
+
/** A cron the app declares (`hooks.schedules`) — no thread, no session; the daily digest (#1507) rides it. */
|
|
47
|
+
'schedule.tick': { class: 'notify', scope: 'app', blockable: false, firedInV1: false, allowsAppend: false },
|
|
48
|
+
};
|
|
49
|
+
export const hookEventNameSchema = z.enum(HOOK_EVENT_NAMES);
|
|
50
|
+
/** Prebuilt hooks a `{ use }` handler may name. The platform's registry is the authority; this list is the v1 catalog. */
|
|
51
|
+
export const PREBUILT_HOOKS = ['email-reporter'];
|
|
52
|
+
/** A JSON-shaped value — the hook's `output` and a reserved event's `data`. */
|
|
53
|
+
export const jsonValueSchema = z.lazy(() => z.union([
|
|
54
|
+
z.string(),
|
|
55
|
+
z.number(),
|
|
56
|
+
z.boolean(),
|
|
57
|
+
z.null(),
|
|
58
|
+
z.array(jsonValueSchema),
|
|
59
|
+
z.record(z.string(), jsonValueSchema),
|
|
60
|
+
]));
|
|
61
|
+
// ── Tool names ────────────────────────────────────────────────────────
|
|
62
|
+
/** `guuey.json` mcpServers keys: the same grammar `@guuey/config` accepts (non-empty, no dots — the dot separates server from tool). */
|
|
63
|
+
const SERVER_KEY = /^[A-Za-z0-9_-]+$/;
|
|
64
|
+
const TOOL_NAME = /^[A-Za-z0-9_-]+$/;
|
|
65
|
+
/** `"<server>.<tool>"` — the name a config uses for one tool. */
|
|
66
|
+
export const toolNameSchema = z
|
|
67
|
+
.string()
|
|
68
|
+
.min(3)
|
|
69
|
+
.max(200)
|
|
70
|
+
.refine((v) => {
|
|
71
|
+
const i = v.indexOf('.');
|
|
72
|
+
return i > 0 && SERVER_KEY.test(v.slice(0, i)) && TOOL_NAME.test(v.slice(i + 1));
|
|
73
|
+
}, 'a tool is named "<server>.<tool>" — the mcpServers key, a dot, the tool name');
|
|
74
|
+
/** `"<server>.<tool>"` or `"<server>.*"` — an allowlist entry. */
|
|
75
|
+
export const toolPatternSchema = z
|
|
76
|
+
.string()
|
|
77
|
+
.min(3)
|
|
78
|
+
.max(200)
|
|
79
|
+
.refine((v) => {
|
|
80
|
+
const i = v.indexOf('.');
|
|
81
|
+
if (i <= 0 || !SERVER_KEY.test(v.slice(0, i)))
|
|
82
|
+
return false;
|
|
83
|
+
const tool = v.slice(i + 1);
|
|
84
|
+
return tool === '*' || TOOL_NAME.test(tool);
|
|
85
|
+
}, 'a tool pattern is "<server>.<tool>" or "<server>.*"');
|
|
86
|
+
/**
|
|
87
|
+
* The wire name of a tool call as the runtime's fold sees it —
|
|
88
|
+
* `mcp__<server>__<tool>` (the reserved hand-off child already uses this
|
|
89
|
+
* grammar: `mcp__guuey-handoff__request_human`). ONE function for both sides:
|
|
90
|
+
* config names in, extractor names out.
|
|
91
|
+
*/
|
|
92
|
+
export function toWireToolName(name) {
|
|
93
|
+
const parsed = toolNameSchema.safeParse(name);
|
|
94
|
+
if (!parsed.success)
|
|
95
|
+
throw new Error(`toWireToolName: not a "<server>.<tool>" name: ${name}`);
|
|
96
|
+
const i = name.indexOf('.');
|
|
97
|
+
return `mcp__${name.slice(0, i)}__${name.slice(i + 1)}`;
|
|
98
|
+
}
|
|
99
|
+
/** Does an allowlist pattern (`server.tool` | `server.*`) cover a tool name? */
|
|
100
|
+
export function toolPatternCovers(pattern, name) {
|
|
101
|
+
if (pattern === name)
|
|
102
|
+
return true;
|
|
103
|
+
const i = pattern.indexOf('.');
|
|
104
|
+
if (i <= 0 || pattern.slice(i + 1) !== '*')
|
|
105
|
+
return false;
|
|
106
|
+
return name.startsWith(pattern.slice(0, i + 1));
|
|
107
|
+
}
|
|
108
|
+
// ── Names ─────────────────────────────────────────────────────────────
|
|
109
|
+
const HOOK_NAME = /^[a-z][a-z0-9-]{0,63}$/;
|
|
110
|
+
/** A hook or schedule name: a lowercase slug (a-z, 0-9, -), 64 chars max. */
|
|
111
|
+
export const hookNameSchema = z.string().regex(HOOK_NAME, 'a hook name is a lowercase slug (a-z, 0-9, -), 64 chars max');
|
|
112
|
+
function hookNameSchemaRef() {
|
|
113
|
+
return hookNameSchema;
|
|
114
|
+
}
|
|
115
|
+
// ── The envelope in (§8.2) ────────────────────────────────────────────
|
|
116
|
+
const hookRefSchema = z.strictObject({
|
|
117
|
+
/** The hook the platform is dispatching (a prebuilt name or a `definitions` key). */
|
|
118
|
+
name: z.string().min(1).max(64),
|
|
119
|
+
/** The HookRun id — the idempotency key of this delivery. */
|
|
120
|
+
runId: z.string().min(1).max(128),
|
|
121
|
+
});
|
|
122
|
+
const baseEvent = {
|
|
123
|
+
/** The event id — dedupe on it; deliveries are at-least-once and unordered. */
|
|
124
|
+
id: z.string().min(1).max(128),
|
|
125
|
+
at: z.iso.datetime({ offset: true }),
|
|
126
|
+
appId: z.string().min(1).max(128),
|
|
127
|
+
hook: hookRefSchema,
|
|
128
|
+
};
|
|
129
|
+
/** Session-scoped events name their conversation; app-scoped events (schedules) have none. */
|
|
130
|
+
const sessionEvent = { ...baseEvent, threadId: z.string().min(1).max(128) };
|
|
131
|
+
/** One tick of a declared schedule — app-scoped; the run has no conversation history and reads what it needs through tools. */
|
|
132
|
+
export const scheduleTickDataSchema = z.strictObject({
|
|
133
|
+
/** The `hooks.schedules` key that fired. */
|
|
134
|
+
schedule: hookNameSchemaRef(),
|
|
135
|
+
/** The tick the platform intended (a cron may fire late; the intended instant is the idempotency key with the event id). */
|
|
136
|
+
scheduledFor: z.iso.datetime({ offset: true }),
|
|
137
|
+
});
|
|
138
|
+
/** What the rep recorded when the visitor asked for a person (the hand-off child's fields + #1510's summary). */
|
|
139
|
+
export const handoffRequestedDataSchema = z.strictObject({
|
|
140
|
+
question: z.string().min(1).max(2000),
|
|
141
|
+
contactEmail: z.string().max(320).optional(),
|
|
142
|
+
contactName: z.string().max(200).optional(),
|
|
143
|
+
summary: z.string().max(1200).optional(),
|
|
144
|
+
});
|
|
145
|
+
export const sessionEndedDataSchema = z.strictObject({
|
|
146
|
+
reason: z.literal('idle'),
|
|
147
|
+
idleMinutes: z.number().int().positive(),
|
|
148
|
+
lastActivityAt: z.iso.datetime({ offset: true }),
|
|
149
|
+
/** Turns in the conversation at the time it ended. */
|
|
150
|
+
turns: z.number().int().nonnegative(),
|
|
151
|
+
/** The hand-off recorded in this conversation, if one happened. */
|
|
152
|
+
handoff: handoffRequestedDataSchema.optional(),
|
|
153
|
+
});
|
|
154
|
+
/**
|
|
155
|
+
* Reserved session-scoped events, as a LITERAL tuple so the envelope's
|
|
156
|
+
* discriminated union stays precise at the type level (a filtered array
|
|
157
|
+
* would type as `HookEventName[]` and swallow the fired members' narrowing).
|
|
158
|
+
* `index.test.ts` pins this list to the table.
|
|
159
|
+
*/
|
|
160
|
+
export const RESERVED_SESSION_EVENT_NAMES = ['session.idled', 'turn.completed', 'session.started', 'turn.start'];
|
|
161
|
+
export const hookEventSchema = z.discriminatedUnion('type', [
|
|
162
|
+
z.strictObject({ ...sessionEvent, type: z.literal('session.ended'), data: sessionEndedDataSchema }),
|
|
163
|
+
z.strictObject({ ...sessionEvent, type: z.literal('handoff.requested'), data: handoffRequestedDataSchema }),
|
|
164
|
+
// App-scoped: a schedule tick carries no threadId.
|
|
165
|
+
z.strictObject({ ...baseEvent, type: z.literal('schedule.tick'), data: scheduleTickDataSchema }),
|
|
166
|
+
// Reserved session events carry an opaque payload until their slice defines one.
|
|
167
|
+
z.strictObject({ ...sessionEvent, type: z.enum(RESERVED_SESSION_EVENT_NAMES), data: jsonValueSchema }),
|
|
168
|
+
]);
|
|
169
|
+
// ── The result out (§8.2, §8.9) ───────────────────────────────────────
|
|
170
|
+
export const hookEffectSchema = z.strictObject({
|
|
171
|
+
/** `"<server>.<tool>"`. */
|
|
172
|
+
tool: toolNameSchema,
|
|
173
|
+
/** `called` by the model · `enforced` by the Router (a `required` tool the model skipped) · `failed`. */
|
|
174
|
+
status: z.enum(['called', 'enforced', 'failed']),
|
|
175
|
+
});
|
|
176
|
+
export const hookResultSchema = z.strictObject({
|
|
177
|
+
status: z.enum(['ok', 'skipped', 'failed']),
|
|
178
|
+
/** The hook's structured output (the definition's result schema shapes it). */
|
|
179
|
+
output: jsonValueSchema.optional(),
|
|
180
|
+
effects: z.array(hookEffectSchema).default([]),
|
|
181
|
+
/** Owner-visible only: a `kind:'event'` row in the console, never the widget, never the model's history. */
|
|
182
|
+
note: z.strictObject({ text: z.string().min(1).max(2000) }).optional(),
|
|
183
|
+
/** Visitor-visible text — honoured only on events whose `allowsAppend` is true. */
|
|
184
|
+
append: z.strictObject({ text: z.string().min(1).max(4000) }).optional(),
|
|
185
|
+
reason: z.string().max(500).optional(),
|
|
186
|
+
});
|
|
187
|
+
// ── The `guuey.json` block (§8.7) ─────────────────────────────────────
|
|
188
|
+
export const hookHandlerRefSchema = z.union([
|
|
189
|
+
/** A prebuilt hook, by name. */
|
|
190
|
+
z.strictObject({ use: hookNameSchema }),
|
|
191
|
+
/** One MCP tool, called directly with the envelope — no model. */
|
|
192
|
+
z.strictObject({ kind: z.literal('tool'), server: z.string().regex(SERVER_KEY), tool: z.string().regex(TOOL_NAME) }),
|
|
193
|
+
/** A dev-defined agent hook, by its `definitions` key. */
|
|
194
|
+
z.strictObject({ kind: z.literal('agent'), definition: hookNameSchema }),
|
|
195
|
+
]);
|
|
196
|
+
const agentDefinitionSchema = z
|
|
197
|
+
.strictObject({
|
|
198
|
+
kind: z.literal('agent'),
|
|
199
|
+
on: z.array(hookEventNameSchema).min(1),
|
|
200
|
+
/** The hook's system instruction — what to do with the conversation and the event. */
|
|
201
|
+
instruction: z.string().min(1).max(8000),
|
|
202
|
+
/** Tools the run may call (`server.tool` | `server.*`); absent = the app's declared servers. */
|
|
203
|
+
tools: z.array(toolPatternSchema).max(64).optional(),
|
|
204
|
+
/** Effects that MUST happen; the Router calls them with the structured output if the model did not. */
|
|
205
|
+
required: z.array(toolNameSchema).max(16).optional(),
|
|
206
|
+
model: z.enum(['small', 'default']).optional(),
|
|
207
|
+
maxTurns: z.number().int().min(1).max(16).optional(),
|
|
208
|
+
timeoutMs: z.number().int().min(1_000).max(300_000).optional(),
|
|
209
|
+
/** v1: the hook principal only. The field exists so the enum can widen without a shape change. */
|
|
210
|
+
actAs: z.literal('hook').optional(),
|
|
211
|
+
})
|
|
212
|
+
.superRefine((d, ctx) => {
|
|
213
|
+
if (!d.tools || !d.required)
|
|
214
|
+
return;
|
|
215
|
+
for (const [i, req] of d.required.entries()) {
|
|
216
|
+
if (!d.tools.some((p) => toolPatternCovers(p, req))) {
|
|
217
|
+
ctx.addIssue({
|
|
218
|
+
code: z.ZodIssueCode.custom,
|
|
219
|
+
path: ['required', i],
|
|
220
|
+
message: `required tool "${req}" is not covered by the hook's tools allowlist`,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
const toolDefinitionSchema = z.strictObject({
|
|
226
|
+
kind: z.literal('tool'),
|
|
227
|
+
on: z.array(hookEventNameSchema).min(1),
|
|
228
|
+
tool: toolNameSchema,
|
|
229
|
+
});
|
|
230
|
+
export const hookDefinitionSchema = z.union([agentDefinitionSchema, toolDefinitionSchema]);
|
|
231
|
+
const handlers = z.array(hookHandlerRefSchema).min(1).max(8);
|
|
232
|
+
export const hooksSectionSchema = z
|
|
233
|
+
.strictObject({
|
|
234
|
+
'session.ended': handlers.optional(),
|
|
235
|
+
'handoff.requested': handlers.optional(),
|
|
236
|
+
'session.idled': handlers.optional(),
|
|
237
|
+
'turn.completed': handlers.optional(),
|
|
238
|
+
'session.started': handlers.optional(),
|
|
239
|
+
'turn.start': handlers.optional(),
|
|
240
|
+
'schedule.tick': handlers.optional(),
|
|
241
|
+
definitions: z.record(hookNameSchema, hookDefinitionSchema).optional(),
|
|
242
|
+
/**
|
|
243
|
+
* App-scoped crons (reserved in v1 — parses, refused like any reserved
|
|
244
|
+
* event until the platform fires `schedule.tick`): `{ <name>: { cron,
|
|
245
|
+
* timezone?, handlers } }`. The daily digest (#1507) is the first.
|
|
246
|
+
*/
|
|
247
|
+
schedules: z
|
|
248
|
+
.record(hookNameSchema, z.strictObject({
|
|
249
|
+
/** Five-field cron, evaluated by the platform's scheduler. */
|
|
250
|
+
cron: z.string().min(9).max(64),
|
|
251
|
+
timezone: z.string().min(1).max(64).optional(),
|
|
252
|
+
handlers,
|
|
253
|
+
}))
|
|
254
|
+
.optional(),
|
|
255
|
+
})
|
|
256
|
+
.superRefine((section, ctx) => {
|
|
257
|
+
if (section.schedules && !HOOK_EVENTS['schedule.tick'].firedInV1) {
|
|
258
|
+
ctx.addIssue({
|
|
259
|
+
code: z.ZodIssueCode.custom,
|
|
260
|
+
path: ['schedules'],
|
|
261
|
+
message: 'hooks.schedules: reserved — the platform does not fire schedule.tick yet',
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
for (const event of HOOK_EVENT_NAMES) {
|
|
265
|
+
const refs = section[event];
|
|
266
|
+
if (!refs)
|
|
267
|
+
continue;
|
|
268
|
+
if (!HOOK_EVENTS[event].firedInV1) {
|
|
269
|
+
ctx.addIssue({
|
|
270
|
+
code: z.ZodIssueCode.custom,
|
|
271
|
+
path: [event],
|
|
272
|
+
message: `hooks.${event}: reserved — the platform does not fire it yet`,
|
|
273
|
+
});
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
for (const [i, ref] of refs.entries()) {
|
|
277
|
+
if (!('kind' in ref) || ref.kind !== 'agent')
|
|
278
|
+
continue;
|
|
279
|
+
const def = section.definitions?.[ref.definition];
|
|
280
|
+
if (!def) {
|
|
281
|
+
ctx.addIssue({
|
|
282
|
+
code: z.ZodIssueCode.custom,
|
|
283
|
+
path: [event, i, 'definition'],
|
|
284
|
+
message: `hooks.${event}[${i}]: no definition named "${ref.definition}"`,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
else if (!def.on.includes(event)) {
|
|
288
|
+
ctx.addIssue({
|
|
289
|
+
code: z.ZodIssueCode.custom,
|
|
290
|
+
path: [event, i, 'definition'],
|
|
291
|
+
message: `hooks.${event}[${i}]: definition "${ref.definition}" is not declared for ${event} (its "on" is ${def.on.join(', ')})`,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@guuey/hooks",
|
|
3
|
+
"version": "0.25.0",
|
|
4
|
+
"description": "The guuey hook spec — lifecycle events a hosted agent's platform fires (session.ended, handoff.requested), the JSON envelope in / result out, the two handler kinds (agent | tool), and the hooks block of guuey.json. Types + zod schemas + the per-event class table; no runtime of its own.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"import": "./dist/index.js",
|
|
17
|
+
"default": "./dist/index.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"peerDependencies": {
|
|
21
|
+
"zod": "^4.3.6"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/node": "^24.0.0",
|
|
25
|
+
"typescript": "^5.0.0",
|
|
26
|
+
"vitest": "^4.1.11",
|
|
27
|
+
"zod": "^4.3.6"
|
|
28
|
+
},
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"guuey",
|
|
34
|
+
"hooks",
|
|
35
|
+
"lifecycle",
|
|
36
|
+
"webhooks",
|
|
37
|
+
"agent"
|
|
38
|
+
],
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "git+https://github.com/withguuey/guuey-sdks.git",
|
|
42
|
+
"directory": "packages/hooks"
|
|
43
|
+
},
|
|
44
|
+
"bugs": {
|
|
45
|
+
"url": "https://github.com/withguuey/guuey-sdks/issues"
|
|
46
|
+
},
|
|
47
|
+
"homepage": "https://guuey.com",
|
|
48
|
+
"scripts": {
|
|
49
|
+
"build": "tsc -p tsconfig.build.json",
|
|
50
|
+
"dev": "tsc --watch",
|
|
51
|
+
"typecheck": "tsc --noEmit",
|
|
52
|
+
"test": "vitest run",
|
|
53
|
+
"test:watch": "vitest"
|
|
54
|
+
}
|
|
55
|
+
}
|