@greatwait/ai-sdk-tool 0.0.0-stage → 0.3.3
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 +38 -2
- package/dist/index.d.ts +360 -0
- package/dist/index.js +277 -0
- package/package.json +46 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 FlowXO LLC
|
|
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
CHANGED
|
@@ -1,3 +1,39 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @greatwait/ai-sdk-tool
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
One server-side Vercel AI SDK tool for scheduling a callback to an **existing GreatWait endpoint**. It does not create endpoints or check callback delivery. `status: "accepted"` means GreatWait accepted a schedule; it does not mean delivery completed.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npm install @greatwait/ai-sdk-tool ai
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Use Node 22.13 or newer. Start with the live [JavaScript quickstart](https://docs.greatwait.dev/guides/javascript) for GreatWait endpoint and credential setup. A dedicated AI SDK scheduling guide is staged at `docs/site/drafts/ai-sdk.mdx` and will be served at `/guides/ai-sdk` after the package is available on npm and the docs release checks pass.
|
|
10
|
+
|
|
11
|
+
From the repository root, `npm run check --workspace @greatwait/ai-sdk-tool` runs TypeScript, the ESM/declaration build, offline tests, and a model-mock example. The release path separately packs the compiled package and checks a clean external install.
|
|
12
|
+
|
|
13
|
+
The example uses `MockLanguageModelV4`, a fake fetch, and an **in-memory fixture only**. It is not a durable intent store and contacts no model provider or GreatWait service.
|
|
14
|
+
|
|
15
|
+
## Server-side interface
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
const persistedIntent =
|
|
19
|
+
await applicationStore.getOrCreateSchedulingIntent(userActionId);
|
|
20
|
+
// The store must atomically persist this record before tool execution. It contains:
|
|
21
|
+
// idempotencyKey, firstAttemptAtMs, originalInput, externalIdentifier.
|
|
22
|
+
const scheduling = createGreatwaitScheduleTool({
|
|
23
|
+
apiKey: process.env.GREATWAIT_API_KEY!,
|
|
24
|
+
endpointId: process.env.GREATWAIT_ENDPOINT_ID!,
|
|
25
|
+
persistedIntent,
|
|
26
|
+
});
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The caller owns a durable, unique logical intent. Its key is 16–255 printable ASCII characters. Its `firstAttemptAtMs` and original input must be written durably before the first possible POST and retained after GreatWait's 24-hour receipt window. `externalIdentifier` is a unique non-secret host ID, at most 256 UTF-8 bytes, included in the schedule for read-based recovery. The tool snapshots these values at construction, so later mutation of the supplied object cannot change the request or replay window. The model can only repeat the persisted `delaySeconds` and short `data` message; changed arguments return `uncertain` without a POST because the original create may have succeeded. A same-body replay is allowed only **before** the 24-hour boundary. At or after expiry, the tool sends nothing and directs authoritative read-based recovery. Process-local `inFlight` sharing prevents concurrent calls on one tool instance from sending twice; a joining caller can stop waiting on its own abort signal while the shared POST continues. Cross-process safety depends on the caller's durable intent and GreatWait's idempotency receipt.
|
|
30
|
+
|
|
31
|
+
The model-facing input is `delaySeconds` (integer 1–158,000,000) and a JSON object with `message` (1–2,000 characters) and optional `reference` (up to 128 characters). The server fixes the credential, endpoint, API origin, key, and original body. Request bytes are capped at 64 KiB. GreatWait may apply a lower organization payload allowance and refuse the request.
|
|
32
|
+
|
|
33
|
+
One tool execution makes at most one POST with `redirect: "manual"`. A single deadline covers both fetch and response parsing (10 seconds by default, server configurable up to 30 seconds), and response bodies are capped at 32 KiB. A caller abort before sending returns `not_sent`; timeout or cancellation after send returns `uncertain`. `refused` is a non-ambiguous refusal of the current request; `idempotency_key_reused` can still mean an earlier create succeeded. Save accepted IDs durably, then use `schedules.read` and occurrence/attempt reads for delivery status. If uncertain, read by a known schedule ID; otherwise query `schedules.list` with `external_identifier` and trust absence only when the page reports `freshness.kind: "authoritative"`. A projected page cannot prove absence. Do not create a new key automatically.
|
|
34
|
+
|
|
35
|
+
## Why this adapter
|
|
36
|
+
|
|
37
|
+
The existing `@greatwait/api-client` is the general client, with generated operations and contract parsing. Its `schedules_create` path automatically retries replay-safe failures, and its `FetchRequestInit` does not expose redirect mode. This candidate uses a minimal direct fetch to the already published `POST /v1/schedules` to enforce one POST and redirect refusal. It adds no backend or broad client.
|
|
38
|
+
|
|
39
|
+
The guide source is staged at `docs/site/drafts/ai-sdk.mdx` until npm publication makes its install and imports true for readers. The implementation and tests require no live model provider or GreatWait credential.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
declare const inputSchema: z.ZodObject<{
|
|
3
|
+
delaySeconds: z.ZodNumber;
|
|
4
|
+
data: z.ZodObject<{
|
|
5
|
+
message: z.ZodString;
|
|
6
|
+
reference: z.ZodOptional<z.ZodString>;
|
|
7
|
+
}, z.core.$strict>;
|
|
8
|
+
}, z.core.$strict>;
|
|
9
|
+
export type ScheduleInput = z.infer<typeof inputSchema>;
|
|
10
|
+
export type ScheduleResult = {
|
|
11
|
+
status: "accepted";
|
|
12
|
+
scheduleId: string;
|
|
13
|
+
occurrenceId: string;
|
|
14
|
+
replayed: boolean;
|
|
15
|
+
note: string;
|
|
16
|
+
} | {
|
|
17
|
+
status: "refused";
|
|
18
|
+
httpStatus: number;
|
|
19
|
+
code: string;
|
|
20
|
+
note: string;
|
|
21
|
+
} | {
|
|
22
|
+
status: "not_sent";
|
|
23
|
+
note: string;
|
|
24
|
+
} | {
|
|
25
|
+
status: "uncertain";
|
|
26
|
+
note: string;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* The application must durably persist this record before exposing the tool.
|
|
30
|
+
* Keep it after GreatWait's receipt expires. `originalInput` is frozen into the
|
|
31
|
+
* request body at tool construction; a later model call cannot change it.
|
|
32
|
+
*/
|
|
33
|
+
export interface PersistedScheduleIntent {
|
|
34
|
+
readonly idempotencyKey: string;
|
|
35
|
+
readonly firstAttemptAtMs: number;
|
|
36
|
+
readonly originalInput: ScheduleInput;
|
|
37
|
+
/** Unique, non-secret host identifier for read-based recovery. */
|
|
38
|
+
readonly externalIdentifier: string;
|
|
39
|
+
}
|
|
40
|
+
export interface GreatwaitScheduleToolOptions {
|
|
41
|
+
/** Server-held credential with schedules:write. Never put it in model input. */
|
|
42
|
+
readonly apiKey: string;
|
|
43
|
+
/** Existing destination in this credential's project and environment. */
|
|
44
|
+
readonly endpointId: string;
|
|
45
|
+
readonly persistedIntent: PersistedScheduleIntent;
|
|
46
|
+
/** Server-only bound; defaults to 10 seconds and cannot exceed 30 seconds. */
|
|
47
|
+
readonly requestTimeoutMs?: number;
|
|
48
|
+
/** Test seams. Neither is exposed to the model. */
|
|
49
|
+
readonly fetchImpl?: typeof fetch;
|
|
50
|
+
readonly now?: () => number;
|
|
51
|
+
}
|
|
52
|
+
/** One execution makes at most one POST; no automatic retry follows uncertainty. */
|
|
53
|
+
export declare function createGreatwaitScheduleTool(options: GreatwaitScheduleToolOptions): ({
|
|
54
|
+
deferLoading?: boolean;
|
|
55
|
+
title?: string;
|
|
56
|
+
providerOptions?: import("@ai-sdk/provider-utils").ProviderOptions;
|
|
57
|
+
metadata?: import("@ai-sdk/provider").JSONObject;
|
|
58
|
+
inputSchema: import("ai").FlexibleSchema<{
|
|
59
|
+
delaySeconds: number;
|
|
60
|
+
data: {
|
|
61
|
+
message: string;
|
|
62
|
+
reference?: string | undefined;
|
|
63
|
+
};
|
|
64
|
+
}>;
|
|
65
|
+
contextSchema?: import("ai").FlexibleSchema<import("@ai-sdk/provider-utils").Context> | undefined;
|
|
66
|
+
needsApproval?: boolean | import("@ai-sdk/provider-utils").ToolNeedsApprovalFunction<{
|
|
67
|
+
delaySeconds: number;
|
|
68
|
+
data: {
|
|
69
|
+
message: string;
|
|
70
|
+
reference?: string | undefined;
|
|
71
|
+
};
|
|
72
|
+
}, NoInfer<import("@ai-sdk/provider-utils").Context>> | undefined;
|
|
73
|
+
onInputStart?: ((options: import("ai").ToolExecutionOptions<NoInfer<import("@ai-sdk/provider-utils").Context>>) => void | PromiseLike<void>) | undefined;
|
|
74
|
+
onInputDelta?: ((options: {
|
|
75
|
+
inputTextDelta: string;
|
|
76
|
+
} & import("ai").ToolExecutionOptions<NoInfer<import("@ai-sdk/provider-utils").Context>>) => void | PromiseLike<void>) | undefined;
|
|
77
|
+
onInputAvailable?: ((options: {
|
|
78
|
+
input: {
|
|
79
|
+
delaySeconds: number;
|
|
80
|
+
data: {
|
|
81
|
+
message: string;
|
|
82
|
+
reference?: string | undefined;
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
} & import("ai").ToolExecutionOptions<NoInfer<import("@ai-sdk/provider-utils").Context>>) => void | PromiseLike<void>) | undefined;
|
|
86
|
+
toModelOutput?: ((options: {
|
|
87
|
+
toolCallId: string;
|
|
88
|
+
input: {
|
|
89
|
+
delaySeconds: number;
|
|
90
|
+
data: {
|
|
91
|
+
message: string;
|
|
92
|
+
reference?: string | undefined;
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
output: NoInfer<ScheduleResult>;
|
|
96
|
+
}) => import("@ai-sdk/provider-utils").ToolResultOutput | PromiseLike<import("@ai-sdk/provider-utils").ToolResultOutput>) | undefined;
|
|
97
|
+
} & {
|
|
98
|
+
outputSchema?: import("ai").FlexibleSchema<ScheduleResult> | undefined;
|
|
99
|
+
execute: import("ai").ToolExecuteFunction<{
|
|
100
|
+
delaySeconds: number;
|
|
101
|
+
data: {
|
|
102
|
+
message: string;
|
|
103
|
+
reference?: string | undefined;
|
|
104
|
+
};
|
|
105
|
+
}, ScheduleResult, NoInfer<import("@ai-sdk/provider-utils").Context>>;
|
|
106
|
+
} & {
|
|
107
|
+
description?: string | ((options: {
|
|
108
|
+
context: NoInfer<import("@ai-sdk/provider-utils").Context>;
|
|
109
|
+
experimental_sandbox?: import("ai").Experimental_SandboxSession;
|
|
110
|
+
}) => string) | undefined;
|
|
111
|
+
strict?: boolean;
|
|
112
|
+
inputExamples?: {
|
|
113
|
+
input: NoInfer<{
|
|
114
|
+
delaySeconds: number;
|
|
115
|
+
data: {
|
|
116
|
+
message: string;
|
|
117
|
+
reference?: string | undefined;
|
|
118
|
+
};
|
|
119
|
+
}>;
|
|
120
|
+
}[] | undefined;
|
|
121
|
+
id?: never;
|
|
122
|
+
isProviderExecuted?: never;
|
|
123
|
+
args?: never;
|
|
124
|
+
supportsDeferredResults?: never;
|
|
125
|
+
} & {
|
|
126
|
+
type?: undefined | "function";
|
|
127
|
+
} & {
|
|
128
|
+
execute: import("ai").ToolExecuteFunction<{
|
|
129
|
+
delaySeconds: number;
|
|
130
|
+
data: {
|
|
131
|
+
message: string;
|
|
132
|
+
reference?: string | undefined;
|
|
133
|
+
};
|
|
134
|
+
}, ScheduleResult, NoInfer<import("@ai-sdk/provider-utils").Context>>;
|
|
135
|
+
}) | ({
|
|
136
|
+
deferLoading?: boolean;
|
|
137
|
+
title?: string;
|
|
138
|
+
providerOptions?: import("@ai-sdk/provider-utils").ProviderOptions;
|
|
139
|
+
metadata?: import("@ai-sdk/provider").JSONObject;
|
|
140
|
+
inputSchema: import("ai").FlexibleSchema<{
|
|
141
|
+
delaySeconds: number;
|
|
142
|
+
data: {
|
|
143
|
+
message: string;
|
|
144
|
+
reference?: string | undefined;
|
|
145
|
+
};
|
|
146
|
+
}>;
|
|
147
|
+
contextSchema?: import("ai").FlexibleSchema<import("@ai-sdk/provider-utils").Context> | undefined;
|
|
148
|
+
needsApproval?: boolean | import("@ai-sdk/provider-utils").ToolNeedsApprovalFunction<{
|
|
149
|
+
delaySeconds: number;
|
|
150
|
+
data: {
|
|
151
|
+
message: string;
|
|
152
|
+
reference?: string | undefined;
|
|
153
|
+
};
|
|
154
|
+
}, NoInfer<import("@ai-sdk/provider-utils").Context>> | undefined;
|
|
155
|
+
onInputStart?: ((options: import("ai").ToolExecutionOptions<NoInfer<import("@ai-sdk/provider-utils").Context>>) => void | PromiseLike<void>) | undefined;
|
|
156
|
+
onInputDelta?: ((options: {
|
|
157
|
+
inputTextDelta: string;
|
|
158
|
+
} & import("ai").ToolExecutionOptions<NoInfer<import("@ai-sdk/provider-utils").Context>>) => void | PromiseLike<void>) | undefined;
|
|
159
|
+
onInputAvailable?: ((options: {
|
|
160
|
+
input: {
|
|
161
|
+
delaySeconds: number;
|
|
162
|
+
data: {
|
|
163
|
+
message: string;
|
|
164
|
+
reference?: string | undefined;
|
|
165
|
+
};
|
|
166
|
+
};
|
|
167
|
+
} & import("ai").ToolExecutionOptions<NoInfer<import("@ai-sdk/provider-utils").Context>>) => void | PromiseLike<void>) | undefined;
|
|
168
|
+
toModelOutput?: ((options: {
|
|
169
|
+
toolCallId: string;
|
|
170
|
+
input: {
|
|
171
|
+
delaySeconds: number;
|
|
172
|
+
data: {
|
|
173
|
+
message: string;
|
|
174
|
+
reference?: string | undefined;
|
|
175
|
+
};
|
|
176
|
+
};
|
|
177
|
+
output: NoInfer<ScheduleResult>;
|
|
178
|
+
}) => import("@ai-sdk/provider-utils").ToolResultOutput | PromiseLike<import("@ai-sdk/provider-utils").ToolResultOutput>) | undefined;
|
|
179
|
+
} & {
|
|
180
|
+
outputSchema?: import("ai").FlexibleSchema<ScheduleResult> | undefined;
|
|
181
|
+
execute: import("ai").ToolExecuteFunction<{
|
|
182
|
+
delaySeconds: number;
|
|
183
|
+
data: {
|
|
184
|
+
message: string;
|
|
185
|
+
reference?: string | undefined;
|
|
186
|
+
};
|
|
187
|
+
}, ScheduleResult, NoInfer<import("@ai-sdk/provider-utils").Context>>;
|
|
188
|
+
} & {
|
|
189
|
+
description?: string | ((options: {
|
|
190
|
+
context: NoInfer<import("@ai-sdk/provider-utils").Context>;
|
|
191
|
+
experimental_sandbox?: import("ai").Experimental_SandboxSession;
|
|
192
|
+
}) => string) | undefined;
|
|
193
|
+
strict?: boolean;
|
|
194
|
+
inputExamples?: {
|
|
195
|
+
input: NoInfer<{
|
|
196
|
+
delaySeconds: number;
|
|
197
|
+
data: {
|
|
198
|
+
message: string;
|
|
199
|
+
reference?: string | undefined;
|
|
200
|
+
};
|
|
201
|
+
}>;
|
|
202
|
+
}[] | undefined;
|
|
203
|
+
id?: never;
|
|
204
|
+
isProviderExecuted?: never;
|
|
205
|
+
args?: never;
|
|
206
|
+
supportsDeferredResults?: never;
|
|
207
|
+
} & {
|
|
208
|
+
type: "dynamic";
|
|
209
|
+
} & {
|
|
210
|
+
execute: import("ai").ToolExecuteFunction<{
|
|
211
|
+
delaySeconds: number;
|
|
212
|
+
data: {
|
|
213
|
+
message: string;
|
|
214
|
+
reference?: string | undefined;
|
|
215
|
+
};
|
|
216
|
+
}, ScheduleResult, NoInfer<import("@ai-sdk/provider-utils").Context>>;
|
|
217
|
+
}) | ({
|
|
218
|
+
deferLoading?: boolean;
|
|
219
|
+
title?: string;
|
|
220
|
+
providerOptions?: import("@ai-sdk/provider-utils").ProviderOptions;
|
|
221
|
+
metadata?: import("@ai-sdk/provider").JSONObject;
|
|
222
|
+
inputSchema: import("ai").FlexibleSchema<{
|
|
223
|
+
delaySeconds: number;
|
|
224
|
+
data: {
|
|
225
|
+
message: string;
|
|
226
|
+
reference?: string | undefined;
|
|
227
|
+
};
|
|
228
|
+
}>;
|
|
229
|
+
contextSchema?: import("ai").FlexibleSchema<import("@ai-sdk/provider-utils").Context> | undefined;
|
|
230
|
+
needsApproval?: boolean | import("@ai-sdk/provider-utils").ToolNeedsApprovalFunction<{
|
|
231
|
+
delaySeconds: number;
|
|
232
|
+
data: {
|
|
233
|
+
message: string;
|
|
234
|
+
reference?: string | undefined;
|
|
235
|
+
};
|
|
236
|
+
}, NoInfer<import("@ai-sdk/provider-utils").Context>> | undefined;
|
|
237
|
+
onInputStart?: ((options: import("ai").ToolExecutionOptions<NoInfer<import("@ai-sdk/provider-utils").Context>>) => void | PromiseLike<void>) | undefined;
|
|
238
|
+
onInputDelta?: ((options: {
|
|
239
|
+
inputTextDelta: string;
|
|
240
|
+
} & import("ai").ToolExecutionOptions<NoInfer<import("@ai-sdk/provider-utils").Context>>) => void | PromiseLike<void>) | undefined;
|
|
241
|
+
onInputAvailable?: ((options: {
|
|
242
|
+
input: {
|
|
243
|
+
delaySeconds: number;
|
|
244
|
+
data: {
|
|
245
|
+
message: string;
|
|
246
|
+
reference?: string | undefined;
|
|
247
|
+
};
|
|
248
|
+
};
|
|
249
|
+
} & import("ai").ToolExecutionOptions<NoInfer<import("@ai-sdk/provider-utils").Context>>) => void | PromiseLike<void>) | undefined;
|
|
250
|
+
toModelOutput?: ((options: {
|
|
251
|
+
toolCallId: string;
|
|
252
|
+
input: {
|
|
253
|
+
delaySeconds: number;
|
|
254
|
+
data: {
|
|
255
|
+
message: string;
|
|
256
|
+
reference?: string | undefined;
|
|
257
|
+
};
|
|
258
|
+
};
|
|
259
|
+
output: NoInfer<ScheduleResult>;
|
|
260
|
+
}) => import("@ai-sdk/provider-utils").ToolResultOutput | PromiseLike<import("@ai-sdk/provider-utils").ToolResultOutput>) | undefined;
|
|
261
|
+
} & {
|
|
262
|
+
outputSchema?: import("ai").FlexibleSchema<ScheduleResult> | undefined;
|
|
263
|
+
execute: import("ai").ToolExecuteFunction<{
|
|
264
|
+
delaySeconds: number;
|
|
265
|
+
data: {
|
|
266
|
+
message: string;
|
|
267
|
+
reference?: string | undefined;
|
|
268
|
+
};
|
|
269
|
+
}, ScheduleResult, NoInfer<import("@ai-sdk/provider-utils").Context>>;
|
|
270
|
+
} & {
|
|
271
|
+
type: "provider";
|
|
272
|
+
id: `${string}.${string}`;
|
|
273
|
+
args: Record<string, unknown>;
|
|
274
|
+
description?: never;
|
|
275
|
+
strict?: never;
|
|
276
|
+
inputExamples?: never;
|
|
277
|
+
} & {
|
|
278
|
+
isProviderExecuted: false;
|
|
279
|
+
supportsDeferredResults?: never;
|
|
280
|
+
} & {
|
|
281
|
+
execute: import("ai").ToolExecuteFunction<{
|
|
282
|
+
delaySeconds: number;
|
|
283
|
+
data: {
|
|
284
|
+
message: string;
|
|
285
|
+
reference?: string | undefined;
|
|
286
|
+
};
|
|
287
|
+
}, ScheduleResult, NoInfer<import("@ai-sdk/provider-utils").Context>>;
|
|
288
|
+
}) | ({
|
|
289
|
+
deferLoading?: boolean;
|
|
290
|
+
title?: string;
|
|
291
|
+
providerOptions?: import("@ai-sdk/provider-utils").ProviderOptions;
|
|
292
|
+
metadata?: import("@ai-sdk/provider").JSONObject;
|
|
293
|
+
inputSchema: import("ai").FlexibleSchema<{
|
|
294
|
+
delaySeconds: number;
|
|
295
|
+
data: {
|
|
296
|
+
message: string;
|
|
297
|
+
reference?: string | undefined;
|
|
298
|
+
};
|
|
299
|
+
}>;
|
|
300
|
+
contextSchema?: import("ai").FlexibleSchema<import("@ai-sdk/provider-utils").Context> | undefined;
|
|
301
|
+
needsApproval?: boolean | import("@ai-sdk/provider-utils").ToolNeedsApprovalFunction<{
|
|
302
|
+
delaySeconds: number;
|
|
303
|
+
data: {
|
|
304
|
+
message: string;
|
|
305
|
+
reference?: string | undefined;
|
|
306
|
+
};
|
|
307
|
+
}, NoInfer<import("@ai-sdk/provider-utils").Context>> | undefined;
|
|
308
|
+
onInputStart?: ((options: import("ai").ToolExecutionOptions<NoInfer<import("@ai-sdk/provider-utils").Context>>) => void | PromiseLike<void>) | undefined;
|
|
309
|
+
onInputDelta?: ((options: {
|
|
310
|
+
inputTextDelta: string;
|
|
311
|
+
} & import("ai").ToolExecutionOptions<NoInfer<import("@ai-sdk/provider-utils").Context>>) => void | PromiseLike<void>) | undefined;
|
|
312
|
+
onInputAvailable?: ((options: {
|
|
313
|
+
input: {
|
|
314
|
+
delaySeconds: number;
|
|
315
|
+
data: {
|
|
316
|
+
message: string;
|
|
317
|
+
reference?: string | undefined;
|
|
318
|
+
};
|
|
319
|
+
};
|
|
320
|
+
} & import("ai").ToolExecutionOptions<NoInfer<import("@ai-sdk/provider-utils").Context>>) => void | PromiseLike<void>) | undefined;
|
|
321
|
+
toModelOutput?: ((options: {
|
|
322
|
+
toolCallId: string;
|
|
323
|
+
input: {
|
|
324
|
+
delaySeconds: number;
|
|
325
|
+
data: {
|
|
326
|
+
message: string;
|
|
327
|
+
reference?: string | undefined;
|
|
328
|
+
};
|
|
329
|
+
};
|
|
330
|
+
output: NoInfer<ScheduleResult>;
|
|
331
|
+
}) => import("@ai-sdk/provider-utils").ToolResultOutput | PromiseLike<import("@ai-sdk/provider-utils").ToolResultOutput>) | undefined;
|
|
332
|
+
} & {
|
|
333
|
+
outputSchema?: import("ai").FlexibleSchema<ScheduleResult> | undefined;
|
|
334
|
+
execute: import("ai").ToolExecuteFunction<{
|
|
335
|
+
delaySeconds: number;
|
|
336
|
+
data: {
|
|
337
|
+
message: string;
|
|
338
|
+
reference?: string | undefined;
|
|
339
|
+
};
|
|
340
|
+
}, ScheduleResult, NoInfer<import("@ai-sdk/provider-utils").Context>>;
|
|
341
|
+
} & {
|
|
342
|
+
type: "provider";
|
|
343
|
+
id: `${string}.${string}`;
|
|
344
|
+
args: Record<string, unknown>;
|
|
345
|
+
description?: never;
|
|
346
|
+
strict?: never;
|
|
347
|
+
inputExamples?: never;
|
|
348
|
+
} & {
|
|
349
|
+
isProviderExecuted: true;
|
|
350
|
+
supportsDeferredResults?: boolean;
|
|
351
|
+
} & {
|
|
352
|
+
execute: import("ai").ToolExecuteFunction<{
|
|
353
|
+
delaySeconds: number;
|
|
354
|
+
data: {
|
|
355
|
+
message: string;
|
|
356
|
+
reference?: string | undefined;
|
|
357
|
+
};
|
|
358
|
+
}, ScheduleResult, NoInfer<import("@ai-sdk/provider-utils").Context>>;
|
|
359
|
+
});
|
|
360
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { tool } from "ai";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
const API_URL = "https://api.greatwait.dev/v1/schedules";
|
|
4
|
+
const MAX_DELAY_SECONDS = 158_000_000;
|
|
5
|
+
const MAX_JSON_BYTES = 64 * 1024;
|
|
6
|
+
const MAX_RESPONSE_BYTES = 32 * 1024;
|
|
7
|
+
const RECEIPT_MS = 24 * 60 * 60 * 1000;
|
|
8
|
+
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
9
|
+
const idempotencyKeySchema = z
|
|
10
|
+
.string()
|
|
11
|
+
.min(16)
|
|
12
|
+
.max(255)
|
|
13
|
+
.regex(/^[\x20-\x7e]+$/u);
|
|
14
|
+
const inputSchema = z
|
|
15
|
+
.object({
|
|
16
|
+
delaySeconds: z.number().int().min(1).max(MAX_DELAY_SECONDS),
|
|
17
|
+
data: z
|
|
18
|
+
.object({
|
|
19
|
+
message: z.string().min(1).max(2_000),
|
|
20
|
+
reference: z.string().max(128).optional(),
|
|
21
|
+
})
|
|
22
|
+
.strict(),
|
|
23
|
+
})
|
|
24
|
+
.strict();
|
|
25
|
+
const publicCodes = new Set([
|
|
26
|
+
"authentication_required",
|
|
27
|
+
"credential_expired",
|
|
28
|
+
"credential_revoked",
|
|
29
|
+
"idempotency_key_reused",
|
|
30
|
+
"invalid_request",
|
|
31
|
+
"not_found",
|
|
32
|
+
"plan_limit_reached",
|
|
33
|
+
"project_archived",
|
|
34
|
+
"state_conflict",
|
|
35
|
+
"rate_limited",
|
|
36
|
+
"scope_missing",
|
|
37
|
+
"capability_not_configured",
|
|
38
|
+
"service_unavailable",
|
|
39
|
+
]);
|
|
40
|
+
function safeCode(value) {
|
|
41
|
+
return typeof value === "string" && publicCodes.has(value)
|
|
42
|
+
? value
|
|
43
|
+
: "request_refused";
|
|
44
|
+
}
|
|
45
|
+
function isId(value, prefix) {
|
|
46
|
+
return (typeof value === "string" &&
|
|
47
|
+
new RegExp(`^${prefix}[0-9abcdefghjkmnpqrstvwxyz]{26}$`, "u").test(value));
|
|
48
|
+
}
|
|
49
|
+
function makeBody(input, endpoint, externalIdentifier) {
|
|
50
|
+
return JSON.stringify({
|
|
51
|
+
delaySeconds: input.delaySeconds,
|
|
52
|
+
endpoint,
|
|
53
|
+
external_identifier: externalIdentifier,
|
|
54
|
+
data: {
|
|
55
|
+
message: input.data.message,
|
|
56
|
+
...(input.data.reference === undefined
|
|
57
|
+
? {}
|
|
58
|
+
: { reference: input.data.reference }),
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
const uncertain = (note) => ({
|
|
63
|
+
status: "uncertain",
|
|
64
|
+
note,
|
|
65
|
+
});
|
|
66
|
+
async function readJsonBounded(response, interrupted) {
|
|
67
|
+
if (!response.body)
|
|
68
|
+
throw new Error("Empty response body");
|
|
69
|
+
const reader = response.body.getReader();
|
|
70
|
+
const chunks = [];
|
|
71
|
+
let size = 0;
|
|
72
|
+
try {
|
|
73
|
+
while (true) {
|
|
74
|
+
const part = await Promise.race([reader.read(), interrupted]);
|
|
75
|
+
if (part.done)
|
|
76
|
+
break;
|
|
77
|
+
size += part.value.byteLength;
|
|
78
|
+
if (size > MAX_RESPONSE_BYTES)
|
|
79
|
+
throw new Error("Response body too large");
|
|
80
|
+
chunks.push(part.value);
|
|
81
|
+
}
|
|
82
|
+
const bytes = new Uint8Array(size);
|
|
83
|
+
let offset = 0;
|
|
84
|
+
for (const chunk of chunks) {
|
|
85
|
+
bytes.set(chunk, offset);
|
|
86
|
+
offset += chunk.byteLength;
|
|
87
|
+
}
|
|
88
|
+
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
// A timed-out mock can ignore cancellation. Do not wait for it or keep the tool open.
|
|
92
|
+
void reader.cancel().catch(() => { });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/** One execution makes at most one POST; no automatic retry follows uncertainty. */
|
|
96
|
+
export function createGreatwaitScheduleTool(options) {
|
|
97
|
+
if (typeof window !== "undefined")
|
|
98
|
+
throw new Error("GreatWait tool is server-only");
|
|
99
|
+
const apiKey = options.apiKey;
|
|
100
|
+
const endpointId = options.endpointId;
|
|
101
|
+
if (!apiKey || /[\r\n]/u.test(apiKey))
|
|
102
|
+
throw new Error("Invalid server credential");
|
|
103
|
+
if (!/^ep_[0-9abcdefghjkmnpqrstvwxyz]{26}$/u.test(endpointId))
|
|
104
|
+
throw new Error("Invalid configured endpoint ID");
|
|
105
|
+
// Snapshot every value used after construction; readonly TypeScript fields do
|
|
106
|
+
// not prevent a caller from mutating the supplied object at runtime.
|
|
107
|
+
const idempotencyKey = idempotencyKeySchema.parse(options.persistedIntent.idempotencyKey);
|
|
108
|
+
const firstAttemptAtMs = options.persistedIntent.firstAttemptAtMs;
|
|
109
|
+
const externalIdentifier = options.persistedIntent.externalIdentifier;
|
|
110
|
+
if (!Number.isSafeInteger(firstAttemptAtMs) || firstAttemptAtMs < 0)
|
|
111
|
+
throw new Error("Invalid persisted first-attempt time");
|
|
112
|
+
if (!externalIdentifier ||
|
|
113
|
+
Buffer.byteLength(externalIdentifier, "utf8") > 256 ||
|
|
114
|
+
/[\r\n]/u.test(externalIdentifier)) {
|
|
115
|
+
throw new Error("Invalid persisted external identifier");
|
|
116
|
+
}
|
|
117
|
+
const originalInput = inputSchema.parse(options.persistedIntent.originalInput);
|
|
118
|
+
const body = makeBody(originalInput, endpointId, externalIdentifier);
|
|
119
|
+
if (Buffer.byteLength(body, "utf8") > MAX_JSON_BYTES)
|
|
120
|
+
throw new Error("Persisted request exceeds the JSON limit");
|
|
121
|
+
const timeoutMs = options.requestTimeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
122
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 30_000)
|
|
123
|
+
throw new Error("Invalid server timeout");
|
|
124
|
+
const now = options.now ?? Date.now;
|
|
125
|
+
const send = options.fetchImpl ?? fetch;
|
|
126
|
+
let inFlight;
|
|
127
|
+
async function awaitShared(pending, callerSignal) {
|
|
128
|
+
if (!callerSignal)
|
|
129
|
+
return pending;
|
|
130
|
+
const stopped = () => uncertain("This call stopped waiting for a shared create. It may still be accepted; recover the persisted intent before any new create.");
|
|
131
|
+
if (callerSignal.aborted)
|
|
132
|
+
return stopped();
|
|
133
|
+
let onAbort = () => { };
|
|
134
|
+
const interrupted = new Promise((resolve) => {
|
|
135
|
+
onAbort = () => resolve(stopped());
|
|
136
|
+
callerSignal.addEventListener("abort", onAbort, { once: true });
|
|
137
|
+
if (callerSignal.aborted)
|
|
138
|
+
onAbort();
|
|
139
|
+
});
|
|
140
|
+
try {
|
|
141
|
+
return await Promise.race([pending, interrupted]);
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
144
|
+
callerSignal.removeEventListener("abort", onAbort);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
async function sendOnce(callerSignal) {
|
|
148
|
+
const controller = new AbortController();
|
|
149
|
+
const onCallerAbort = () => controller.abort();
|
|
150
|
+
callerSignal?.addEventListener("abort", onCallerAbort, { once: true });
|
|
151
|
+
if (callerSignal?.aborted)
|
|
152
|
+
controller.abort();
|
|
153
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
154
|
+
const interrupted = new Promise((_, reject) => {
|
|
155
|
+
if (controller.signal.aborted)
|
|
156
|
+
reject(new Error("interrupted"));
|
|
157
|
+
else
|
|
158
|
+
controller.signal.addEventListener("abort", () => reject(new Error("interrupted")), { once: true });
|
|
159
|
+
});
|
|
160
|
+
try {
|
|
161
|
+
if (controller.signal.aborted)
|
|
162
|
+
return {
|
|
163
|
+
status: "not_sent",
|
|
164
|
+
note: "The request was cancelled before sending.",
|
|
165
|
+
};
|
|
166
|
+
const response = await Promise.race([
|
|
167
|
+
send(API_URL, {
|
|
168
|
+
method: "POST",
|
|
169
|
+
redirect: "manual",
|
|
170
|
+
signal: controller.signal,
|
|
171
|
+
headers: {
|
|
172
|
+
Authorization: `Bearer ${apiKey}`,
|
|
173
|
+
"Content-Type": "application/json",
|
|
174
|
+
"Idempotency-Key": idempotencyKey,
|
|
175
|
+
},
|
|
176
|
+
body,
|
|
177
|
+
}),
|
|
178
|
+
interrupted,
|
|
179
|
+
]);
|
|
180
|
+
if (response.status >= 300 && response.status < 400) {
|
|
181
|
+
return uncertain("A redirect was refused. Recover using the persisted identifier before any new create.");
|
|
182
|
+
}
|
|
183
|
+
if (response.status >= 500 ||
|
|
184
|
+
response.status === 408 ||
|
|
185
|
+
response.status === 499 ||
|
|
186
|
+
response.status === 0) {
|
|
187
|
+
return uncertain("The server outcome is unclear. Recover using the persisted identifier before any new create.");
|
|
188
|
+
}
|
|
189
|
+
if (!response.ok) {
|
|
190
|
+
let code;
|
|
191
|
+
try {
|
|
192
|
+
code = (await readJsonBounded(response, interrupted)).code;
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
if (controller.signal.aborted)
|
|
196
|
+
return uncertain("The response was interrupted. Recover using the persisted identifier.");
|
|
197
|
+
}
|
|
198
|
+
const publicCode = safeCode(code);
|
|
199
|
+
return {
|
|
200
|
+
status: "refused",
|
|
201
|
+
httpStatus: response.status,
|
|
202
|
+
code: publicCode,
|
|
203
|
+
note: publicCode === "idempotency_key_reused"
|
|
204
|
+
? "This request was refused; an earlier create using this key may have succeeded. Recover the original intent."
|
|
205
|
+
: "GreatWait refused the create request. No delivery was confirmed.",
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
let value;
|
|
209
|
+
try {
|
|
210
|
+
value = await readJsonBounded(response, interrupted);
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
return uncertain("The success response was interrupted or malformed. Recover using the persisted identifier.");
|
|
214
|
+
}
|
|
215
|
+
if (typeof value !== "object" || value === null)
|
|
216
|
+
return uncertain("The success response was malformed. Recover using the persisted identifier.");
|
|
217
|
+
const record = value;
|
|
218
|
+
if (!isId(record.schedule?.id, "sch_") ||
|
|
219
|
+
!isId(record.occurrence?.id, "occ_") ||
|
|
220
|
+
record.occurrence?.schedule !== record.schedule.id ||
|
|
221
|
+
(record.schedule.endpoint !== undefined &&
|
|
222
|
+
record.schedule.endpoint !== endpointId) ||
|
|
223
|
+
(record.schedule.external_identifier !== undefined &&
|
|
224
|
+
record.schedule.external_identifier !== externalIdentifier)) {
|
|
225
|
+
return uncertain("The success response had inconsistent identifiers. Recover using the persisted identifier.");
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
status: "accepted",
|
|
229
|
+
scheduleId: record.schedule.id,
|
|
230
|
+
occurrenceId: record.occurrence.id,
|
|
231
|
+
replayed: response.headers.get("Idempotency-Replayed") === "true",
|
|
232
|
+
note: "GreatWait accepted the schedule. Callback delivery has not been confirmed.",
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
return uncertain("No reliable response was received. Recover using the persisted identifier before any new create.");
|
|
237
|
+
}
|
|
238
|
+
finally {
|
|
239
|
+
clearTimeout(timeout);
|
|
240
|
+
callerSignal?.removeEventListener("abort", onCallerAbort);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return tool({
|
|
244
|
+
description: "Schedule one future GreatWait callback to the server's configured endpoint. Acceptance means scheduled, not delivered.",
|
|
245
|
+
inputSchema,
|
|
246
|
+
execute: async (untrustedInput, executionOptions) => {
|
|
247
|
+
const input = inputSchema.parse(untrustedInput);
|
|
248
|
+
const proposedBody = makeBody(input, endpointId, externalIdentifier);
|
|
249
|
+
if (proposedBody !== body) {
|
|
250
|
+
return uncertain("The model changed the persisted request. Its original create may have succeeded; recover that intent before proposing another.");
|
|
251
|
+
}
|
|
252
|
+
if (executionOptions.abortSignal?.aborted) {
|
|
253
|
+
if (inFlight)
|
|
254
|
+
return uncertain("This call was cancelled while another call for the same intent was in flight. That shared create may still be accepted.");
|
|
255
|
+
return {
|
|
256
|
+
status: "not_sent",
|
|
257
|
+
note: "This tool call was cancelled before sending. An earlier call for the same intent may still exist.",
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
const ageMs = now() - firstAttemptAtMs;
|
|
261
|
+
if (!Number.isFinite(ageMs) || ageMs < 0 || ageMs >= RECEIPT_MS) {
|
|
262
|
+
return uncertain("The idempotency receipt window has expired or its clock is invalid. No POST was sent; use authoritative read-based recovery.");
|
|
263
|
+
}
|
|
264
|
+
if (inFlight)
|
|
265
|
+
return awaitShared(inFlight, executionOptions.abortSignal);
|
|
266
|
+
const pending = sendOnce(executionOptions.abortSignal);
|
|
267
|
+
inFlight = pending;
|
|
268
|
+
try {
|
|
269
|
+
return await pending;
|
|
270
|
+
}
|
|
271
|
+
finally {
|
|
272
|
+
if (inFlight === pending)
|
|
273
|
+
inFlight = undefined;
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,48 @@
|
|
|
1
1
|
{
|
|
2
|
+
"bugs": {
|
|
3
|
+
"url": "https://github.com/flowxo/greatwait/issues"
|
|
4
|
+
},
|
|
5
|
+
"dependencies": {
|
|
6
|
+
"ai": "7.0.146",
|
|
7
|
+
"zod": "4.4.3"
|
|
8
|
+
},
|
|
9
|
+
"description": "Server-side Vercel AI SDK tool for submitting a bounded GreatWait callback schedule to an existing endpoint.",
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=22.13.0"
|
|
12
|
+
},
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"import": "./dist/index.js",
|
|
17
|
+
"default": "./dist/index.js"
|
|
18
|
+
},
|
|
19
|
+
"./package.json": "./package.json"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"LICENSE",
|
|
23
|
+
"README.md",
|
|
24
|
+
"dist"
|
|
25
|
+
],
|
|
26
|
+
"homepage": "https://greatwait.dev",
|
|
27
|
+
"keywords": [
|
|
28
|
+
"greatwait",
|
|
29
|
+
"scheduling",
|
|
30
|
+
"ai-sdk",
|
|
31
|
+
"tools",
|
|
32
|
+
"webhooks"
|
|
33
|
+
],
|
|
34
|
+
"license": "MIT",
|
|
2
35
|
"name": "@greatwait/ai-sdk-tool",
|
|
3
|
-
"
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
}
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public",
|
|
38
|
+
"tag": "latest"
|
|
39
|
+
},
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "git+https://github.com/flowxo/greatwait.git",
|
|
43
|
+
"directory": "packages/ai-sdk-tool"
|
|
44
|
+
},
|
|
45
|
+
"sideEffects": false,
|
|
46
|
+
"type": "module",
|
|
47
|
+
"version": "0.3.3"
|
|
48
|
+
}
|