@pushary/server 1.3.3 → 1.4.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/README.md +54 -0
- package/dist/adapters.cjs +452 -0
- package/dist/adapters.cjs.map +1 -0
- package/dist/adapters.d.cts +158 -0
- package/dist/adapters.d.ts +158 -0
- package/dist/adapters.js +156 -0
- package/dist/adapters.js.map +1 -0
- package/dist/chunk-22FUZ6F5.js +274 -0
- package/dist/chunk-22FUZ6F5.js.map +1 -0
- package/dist/index.d.cts +2 -412
- package/dist/index.d.ts +2 -412
- package/dist/index.js +9 -265
- package/dist/index.js.map +1 -1
- package/dist/util-Ihfh4f82.d.cts +419 -0
- package/dist/util-Ihfh4f82.d.ts +419 -0
- package/package.json +6 -1
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { D as DecisionType, I as AskResult, P as PusharyServer, G as EnrollResult } from './util-Ihfh4f82.cjs';
|
|
2
|
+
export { a0 as SIGNATURE_HEADER, a1 as deterministicKey, a2 as isApproved, $ as parseDecisionCallback, _ as verifyWebhookSignature } from './util-Ihfh4f82.cjs';
|
|
3
|
+
|
|
4
|
+
/** Config every adapter accepts, whatever framework it binds. */
|
|
5
|
+
interface PusharyAdapterConfig {
|
|
6
|
+
/** Pushary API key (pk_xxx.sk_xxx). Defaults to `process.env.PUSHARY_API_KEY`. */
|
|
7
|
+
readonly apiKey?: string;
|
|
8
|
+
/** Shown on the approval so the human knows which agent is asking. */
|
|
9
|
+
readonly agentName?: string;
|
|
10
|
+
/** How long a blocking ask waits before returning (default 55s, serverless-safe). */
|
|
11
|
+
readonly timeoutMs?: number;
|
|
12
|
+
/** Override the API base URL (tests / self-host). */
|
|
13
|
+
readonly baseUrl?: string;
|
|
14
|
+
}
|
|
15
|
+
/** One ask, as an adapter hands it to the kernel. */
|
|
16
|
+
interface AskHumanInput {
|
|
17
|
+
readonly question: string;
|
|
18
|
+
/** Your own stable id for the end-user who decides. */
|
|
19
|
+
readonly externalId: string;
|
|
20
|
+
/** Stable node/step/tool name, used to build a re-run-safe idempotency key. */
|
|
21
|
+
readonly node?: string;
|
|
22
|
+
readonly type?: DecisionType;
|
|
23
|
+
readonly options?: readonly string[];
|
|
24
|
+
readonly context?: string;
|
|
25
|
+
/** Present on the durable path; Pushary POSTs the answer here. */
|
|
26
|
+
readonly callbackUrl?: string;
|
|
27
|
+
}
|
|
28
|
+
/** A decision opened durably, before the framework parks its run. */
|
|
29
|
+
interface CreatedDecision {
|
|
30
|
+
readonly decisionId: string;
|
|
31
|
+
readonly correlationId: string;
|
|
32
|
+
readonly status: string;
|
|
33
|
+
readonly reachable?: boolean;
|
|
34
|
+
readonly reachableChannels?: number;
|
|
35
|
+
readonly deviceCount?: number;
|
|
36
|
+
}
|
|
37
|
+
/** A verified, parsed answer callback, keyed by `correlationId`. */
|
|
38
|
+
interface PusharyCallback {
|
|
39
|
+
readonly correlationId: string;
|
|
40
|
+
readonly answer: string;
|
|
41
|
+
readonly value: string;
|
|
42
|
+
readonly approved: boolean;
|
|
43
|
+
readonly context?: string;
|
|
44
|
+
readonly answeredAt: string;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Re-run-safe idempotency key. A node, step, or tool call that replays on resume
|
|
48
|
+
* lands on the same decision instead of paging the human twice.
|
|
49
|
+
*/
|
|
50
|
+
declare const idempotencyKeyFor: (input: AskHumanInput) => string;
|
|
51
|
+
/** Fail-closed yes/no check for a confirm answer. */
|
|
52
|
+
declare const isAffirmative: (answer: string | null | undefined) => boolean;
|
|
53
|
+
/** Turn a decision outcome into an unambiguous instruction for the model. */
|
|
54
|
+
declare const describeAnswer: (type: DecisionType, result: AskResult) => string;
|
|
55
|
+
/**
|
|
56
|
+
* Verify a callback signature and parse it, or return null. The `answer` is what
|
|
57
|
+
* you feed back into the framework's resume seam.
|
|
58
|
+
*/
|
|
59
|
+
declare const resolvePusharyCallback: (rawBody: string, signature: string | null | undefined, secret: string) => PusharyCallback | null;
|
|
60
|
+
/**
|
|
61
|
+
* Connection-level options for a request-time approval gate. Everything that
|
|
62
|
+
* varies per tool call lives on {@link ApprovalAsk} instead, so a gate holds one
|
|
63
|
+
* client and no per-call state.
|
|
64
|
+
*/
|
|
65
|
+
interface PusharyGateConfig {
|
|
66
|
+
/** Pushary API key. Defaults to `process.env.PUSHARY_API_KEY`. */
|
|
67
|
+
readonly apiKey?: string;
|
|
68
|
+
/** Shown on the approval so the human knows which agent is asking. */
|
|
69
|
+
readonly agentName?: string;
|
|
70
|
+
/** How long the decision stays answerable. */
|
|
71
|
+
readonly expiresInSeconds?: number;
|
|
72
|
+
/** How long to block waiting for an answer before failing closed. */
|
|
73
|
+
readonly timeoutMs?: number;
|
|
74
|
+
/**
|
|
75
|
+
* Refuse to open a decision nobody can receive, so an end-user with no connected
|
|
76
|
+
* device is denied at request time instead of silently expiring.
|
|
77
|
+
*/
|
|
78
|
+
readonly requireReachable?: boolean;
|
|
79
|
+
/** Override the API base URL (tests / self-host). */
|
|
80
|
+
readonly baseUrl?: string;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* One gated tool call, resolved down to what Pushary needs. The adapter owns the
|
|
84
|
+
* mapping from its framework's context, so per-call resolvers stay typed against
|
|
85
|
+
* the framework's own shapes rather than a lowest common denominator.
|
|
86
|
+
*/
|
|
87
|
+
interface ApprovalAsk {
|
|
88
|
+
/** The tool the model wants to run. */
|
|
89
|
+
readonly toolName: string;
|
|
90
|
+
/** Unique per tool call. Keys the decision together with `sessionId`. */
|
|
91
|
+
readonly callId: string;
|
|
92
|
+
/** Stable per run, session, or thread. */
|
|
93
|
+
readonly sessionId: string;
|
|
94
|
+
/** What the human is shown. */
|
|
95
|
+
readonly question: string;
|
|
96
|
+
/** The enrolled end-user who decides. */
|
|
97
|
+
readonly externalId: string;
|
|
98
|
+
}
|
|
99
|
+
/** What a gate answers. Denials always carry a reason the model can read. */
|
|
100
|
+
type ApprovalDecision = {
|
|
101
|
+
readonly approved: true;
|
|
102
|
+
} | {
|
|
103
|
+
readonly approved: false;
|
|
104
|
+
readonly reason: string;
|
|
105
|
+
};
|
|
106
|
+
/** A request-time gate: ask a person, fail closed. */
|
|
107
|
+
type ApprovalGate = (ask: ApprovalAsk) => Promise<ApprovalDecision>;
|
|
108
|
+
declare const DENIED_UNANSWERED = "No answer from the approver, so this was denied. Do not retry the same action.";
|
|
109
|
+
declare const DENIED_REFUSED = "The approver denied this action.";
|
|
110
|
+
/**
|
|
111
|
+
* The default question: the tool name plus a truncated view of its input, so the
|
|
112
|
+
* human sees what they are approving without a wall of JSON on a lock screen.
|
|
113
|
+
*/
|
|
114
|
+
declare const renderApprovalQuestion: (toolName: string, toolInput: unknown) => string;
|
|
115
|
+
/**
|
|
116
|
+
* The calls an adapter makes, bound to one framework's name so a missing key or a
|
|
117
|
+
* missing end-user says which helpers to configure. Build one per adapter module.
|
|
118
|
+
*/
|
|
119
|
+
interface AdapterKernel {
|
|
120
|
+
/** The underlying client, for anything the kernel does not cover. */
|
|
121
|
+
readonly client: (config: PusharyAdapterConfig) => PusharyServer;
|
|
122
|
+
/**
|
|
123
|
+
* Blocking ask: create a decision and poll durably until the human answers or
|
|
124
|
+
* the deadline passes. Idempotency keyed by externalId + node + question.
|
|
125
|
+
*/
|
|
126
|
+
readonly askExternalUser: (config: PusharyAdapterConfig, input: AskHumanInput) => Promise<AskResult>;
|
|
127
|
+
/**
|
|
128
|
+
* Durable create: open a decision with a `callbackUrl` and return at once, for
|
|
129
|
+
* a framework that parks its run and resumes on the webhook.
|
|
130
|
+
*/
|
|
131
|
+
readonly createDurableDecision: (config: PusharyAdapterConfig, input: AskHumanInput) => Promise<CreatedDecision>;
|
|
132
|
+
/** Connect one end-user's phone (keyless). Show them the returned link. */
|
|
133
|
+
readonly connect: (config: PusharyAdapterConfig, externalId: string) => Promise<EnrollResult>;
|
|
134
|
+
/**
|
|
135
|
+
* Build a request-time approval gate. The client is created here, so a missing
|
|
136
|
+
* key throws where the gate is defined rather than on the first tool call.
|
|
137
|
+
*/
|
|
138
|
+
readonly createGate: (config: PusharyGateConfig) => ApprovalGate;
|
|
139
|
+
/**
|
|
140
|
+
* The end-user to ask, or a clear error naming these helpers. Adapters call this
|
|
141
|
+
* after applying their own resolver and the framework's own default.
|
|
142
|
+
*/
|
|
143
|
+
readonly requireExternalId: (externalId: string | undefined) => string;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Build the kernel for one adapter.
|
|
147
|
+
*
|
|
148
|
+
* `helpersLabel` names the adapter in the missing-key error, so the message points
|
|
149
|
+
* at the helpers the caller is actually using ("the LangGraph helpers").
|
|
150
|
+
*
|
|
151
|
+
* ```ts
|
|
152
|
+
* const kernel = createAdapterKernel('the Acme helpers')
|
|
153
|
+
* export const askHuman = kernel.askExternalUser
|
|
154
|
+
* ```
|
|
155
|
+
*/
|
|
156
|
+
declare const createAdapterKernel: (helpersLabel: string) => AdapterKernel;
|
|
157
|
+
|
|
158
|
+
export { type AdapterKernel, type ApprovalAsk, type ApprovalDecision, type ApprovalGate, type AskHumanInput, AskResult, type CreatedDecision, DENIED_REFUSED, DENIED_UNANSWERED, DecisionType, EnrollResult, type PusharyAdapterConfig, type PusharyCallback, type PusharyGateConfig, PusharyServer, createAdapterKernel, describeAnswer, idempotencyKeyFor, isAffirmative, renderApprovalQuestion, resolvePusharyCallback };
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { D as DecisionType, I as AskResult, P as PusharyServer, G as EnrollResult } from './util-Ihfh4f82.js';
|
|
2
|
+
export { a0 as SIGNATURE_HEADER, a1 as deterministicKey, a2 as isApproved, $ as parseDecisionCallback, _ as verifyWebhookSignature } from './util-Ihfh4f82.js';
|
|
3
|
+
|
|
4
|
+
/** Config every adapter accepts, whatever framework it binds. */
|
|
5
|
+
interface PusharyAdapterConfig {
|
|
6
|
+
/** Pushary API key (pk_xxx.sk_xxx). Defaults to `process.env.PUSHARY_API_KEY`. */
|
|
7
|
+
readonly apiKey?: string;
|
|
8
|
+
/** Shown on the approval so the human knows which agent is asking. */
|
|
9
|
+
readonly agentName?: string;
|
|
10
|
+
/** How long a blocking ask waits before returning (default 55s, serverless-safe). */
|
|
11
|
+
readonly timeoutMs?: number;
|
|
12
|
+
/** Override the API base URL (tests / self-host). */
|
|
13
|
+
readonly baseUrl?: string;
|
|
14
|
+
}
|
|
15
|
+
/** One ask, as an adapter hands it to the kernel. */
|
|
16
|
+
interface AskHumanInput {
|
|
17
|
+
readonly question: string;
|
|
18
|
+
/** Your own stable id for the end-user who decides. */
|
|
19
|
+
readonly externalId: string;
|
|
20
|
+
/** Stable node/step/tool name, used to build a re-run-safe idempotency key. */
|
|
21
|
+
readonly node?: string;
|
|
22
|
+
readonly type?: DecisionType;
|
|
23
|
+
readonly options?: readonly string[];
|
|
24
|
+
readonly context?: string;
|
|
25
|
+
/** Present on the durable path; Pushary POSTs the answer here. */
|
|
26
|
+
readonly callbackUrl?: string;
|
|
27
|
+
}
|
|
28
|
+
/** A decision opened durably, before the framework parks its run. */
|
|
29
|
+
interface CreatedDecision {
|
|
30
|
+
readonly decisionId: string;
|
|
31
|
+
readonly correlationId: string;
|
|
32
|
+
readonly status: string;
|
|
33
|
+
readonly reachable?: boolean;
|
|
34
|
+
readonly reachableChannels?: number;
|
|
35
|
+
readonly deviceCount?: number;
|
|
36
|
+
}
|
|
37
|
+
/** A verified, parsed answer callback, keyed by `correlationId`. */
|
|
38
|
+
interface PusharyCallback {
|
|
39
|
+
readonly correlationId: string;
|
|
40
|
+
readonly answer: string;
|
|
41
|
+
readonly value: string;
|
|
42
|
+
readonly approved: boolean;
|
|
43
|
+
readonly context?: string;
|
|
44
|
+
readonly answeredAt: string;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Re-run-safe idempotency key. A node, step, or tool call that replays on resume
|
|
48
|
+
* lands on the same decision instead of paging the human twice.
|
|
49
|
+
*/
|
|
50
|
+
declare const idempotencyKeyFor: (input: AskHumanInput) => string;
|
|
51
|
+
/** Fail-closed yes/no check for a confirm answer. */
|
|
52
|
+
declare const isAffirmative: (answer: string | null | undefined) => boolean;
|
|
53
|
+
/** Turn a decision outcome into an unambiguous instruction for the model. */
|
|
54
|
+
declare const describeAnswer: (type: DecisionType, result: AskResult) => string;
|
|
55
|
+
/**
|
|
56
|
+
* Verify a callback signature and parse it, or return null. The `answer` is what
|
|
57
|
+
* you feed back into the framework's resume seam.
|
|
58
|
+
*/
|
|
59
|
+
declare const resolvePusharyCallback: (rawBody: string, signature: string | null | undefined, secret: string) => PusharyCallback | null;
|
|
60
|
+
/**
|
|
61
|
+
* Connection-level options for a request-time approval gate. Everything that
|
|
62
|
+
* varies per tool call lives on {@link ApprovalAsk} instead, so a gate holds one
|
|
63
|
+
* client and no per-call state.
|
|
64
|
+
*/
|
|
65
|
+
interface PusharyGateConfig {
|
|
66
|
+
/** Pushary API key. Defaults to `process.env.PUSHARY_API_KEY`. */
|
|
67
|
+
readonly apiKey?: string;
|
|
68
|
+
/** Shown on the approval so the human knows which agent is asking. */
|
|
69
|
+
readonly agentName?: string;
|
|
70
|
+
/** How long the decision stays answerable. */
|
|
71
|
+
readonly expiresInSeconds?: number;
|
|
72
|
+
/** How long to block waiting for an answer before failing closed. */
|
|
73
|
+
readonly timeoutMs?: number;
|
|
74
|
+
/**
|
|
75
|
+
* Refuse to open a decision nobody can receive, so an end-user with no connected
|
|
76
|
+
* device is denied at request time instead of silently expiring.
|
|
77
|
+
*/
|
|
78
|
+
readonly requireReachable?: boolean;
|
|
79
|
+
/** Override the API base URL (tests / self-host). */
|
|
80
|
+
readonly baseUrl?: string;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* One gated tool call, resolved down to what Pushary needs. The adapter owns the
|
|
84
|
+
* mapping from its framework's context, so per-call resolvers stay typed against
|
|
85
|
+
* the framework's own shapes rather than a lowest common denominator.
|
|
86
|
+
*/
|
|
87
|
+
interface ApprovalAsk {
|
|
88
|
+
/** The tool the model wants to run. */
|
|
89
|
+
readonly toolName: string;
|
|
90
|
+
/** Unique per tool call. Keys the decision together with `sessionId`. */
|
|
91
|
+
readonly callId: string;
|
|
92
|
+
/** Stable per run, session, or thread. */
|
|
93
|
+
readonly sessionId: string;
|
|
94
|
+
/** What the human is shown. */
|
|
95
|
+
readonly question: string;
|
|
96
|
+
/** The enrolled end-user who decides. */
|
|
97
|
+
readonly externalId: string;
|
|
98
|
+
}
|
|
99
|
+
/** What a gate answers. Denials always carry a reason the model can read. */
|
|
100
|
+
type ApprovalDecision = {
|
|
101
|
+
readonly approved: true;
|
|
102
|
+
} | {
|
|
103
|
+
readonly approved: false;
|
|
104
|
+
readonly reason: string;
|
|
105
|
+
};
|
|
106
|
+
/** A request-time gate: ask a person, fail closed. */
|
|
107
|
+
type ApprovalGate = (ask: ApprovalAsk) => Promise<ApprovalDecision>;
|
|
108
|
+
declare const DENIED_UNANSWERED = "No answer from the approver, so this was denied. Do not retry the same action.";
|
|
109
|
+
declare const DENIED_REFUSED = "The approver denied this action.";
|
|
110
|
+
/**
|
|
111
|
+
* The default question: the tool name plus a truncated view of its input, so the
|
|
112
|
+
* human sees what they are approving without a wall of JSON on a lock screen.
|
|
113
|
+
*/
|
|
114
|
+
declare const renderApprovalQuestion: (toolName: string, toolInput: unknown) => string;
|
|
115
|
+
/**
|
|
116
|
+
* The calls an adapter makes, bound to one framework's name so a missing key or a
|
|
117
|
+
* missing end-user says which helpers to configure. Build one per adapter module.
|
|
118
|
+
*/
|
|
119
|
+
interface AdapterKernel {
|
|
120
|
+
/** The underlying client, for anything the kernel does not cover. */
|
|
121
|
+
readonly client: (config: PusharyAdapterConfig) => PusharyServer;
|
|
122
|
+
/**
|
|
123
|
+
* Blocking ask: create a decision and poll durably until the human answers or
|
|
124
|
+
* the deadline passes. Idempotency keyed by externalId + node + question.
|
|
125
|
+
*/
|
|
126
|
+
readonly askExternalUser: (config: PusharyAdapterConfig, input: AskHumanInput) => Promise<AskResult>;
|
|
127
|
+
/**
|
|
128
|
+
* Durable create: open a decision with a `callbackUrl` and return at once, for
|
|
129
|
+
* a framework that parks its run and resumes on the webhook.
|
|
130
|
+
*/
|
|
131
|
+
readonly createDurableDecision: (config: PusharyAdapterConfig, input: AskHumanInput) => Promise<CreatedDecision>;
|
|
132
|
+
/** Connect one end-user's phone (keyless). Show them the returned link. */
|
|
133
|
+
readonly connect: (config: PusharyAdapterConfig, externalId: string) => Promise<EnrollResult>;
|
|
134
|
+
/**
|
|
135
|
+
* Build a request-time approval gate. The client is created here, so a missing
|
|
136
|
+
* key throws where the gate is defined rather than on the first tool call.
|
|
137
|
+
*/
|
|
138
|
+
readonly createGate: (config: PusharyGateConfig) => ApprovalGate;
|
|
139
|
+
/**
|
|
140
|
+
* The end-user to ask, or a clear error naming these helpers. Adapters call this
|
|
141
|
+
* after applying their own resolver and the framework's own default.
|
|
142
|
+
*/
|
|
143
|
+
readonly requireExternalId: (externalId: string | undefined) => string;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Build the kernel for one adapter.
|
|
147
|
+
*
|
|
148
|
+
* `helpersLabel` names the adapter in the missing-key error, so the message points
|
|
149
|
+
* at the helpers the caller is actually using ("the LangGraph helpers").
|
|
150
|
+
*
|
|
151
|
+
* ```ts
|
|
152
|
+
* const kernel = createAdapterKernel('the Acme helpers')
|
|
153
|
+
* export const askHuman = kernel.askExternalUser
|
|
154
|
+
* ```
|
|
155
|
+
*/
|
|
156
|
+
declare const createAdapterKernel: (helpersLabel: string) => AdapterKernel;
|
|
157
|
+
|
|
158
|
+
export { type AdapterKernel, type ApprovalAsk, type ApprovalDecision, type ApprovalGate, type AskHumanInput, AskResult, type CreatedDecision, DENIED_REFUSED, DENIED_UNANSWERED, DecisionType, EnrollResult, type PusharyAdapterConfig, type PusharyCallback, type PusharyGateConfig, PusharyServer, createAdapterKernel, describeAnswer, idempotencyKeyFor, isAffirmative, renderApprovalQuestion, resolvePusharyCallback };
|
package/dist/adapters.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SIGNATURE_HEADER,
|
|
3
|
+
createPusharyServer,
|
|
4
|
+
deterministicKey,
|
|
5
|
+
isApproved,
|
|
6
|
+
parseDecisionCallback,
|
|
7
|
+
verifyWebhookSignature
|
|
8
|
+
} from "./chunk-22FUZ6F5.js";
|
|
9
|
+
|
|
10
|
+
// src/adapters.ts
|
|
11
|
+
var DEFAULT_NODE = "ask-human";
|
|
12
|
+
var idempotencyKeyFor = (input) => deterministicKey([input.externalId, input.node ?? DEFAULT_NODE, input.question]);
|
|
13
|
+
var isAffirmative = (answer) => isApproved({ status: "answered", type: "confirm", value: answer ?? null });
|
|
14
|
+
var describeAnswer = (type, result) => {
|
|
15
|
+
if (!result.answered) {
|
|
16
|
+
return `No answer (status: ${result.status}). Treat this as NOT approved and do not proceed.`;
|
|
17
|
+
}
|
|
18
|
+
if (type === "confirm") {
|
|
19
|
+
return result.approved ? "The human approved. You may proceed." : "The human declined. Do not proceed.";
|
|
20
|
+
}
|
|
21
|
+
return `The human answered: ${result.value ?? ""}`;
|
|
22
|
+
};
|
|
23
|
+
var resolvePusharyCallback = (rawBody, signature, secret) => {
|
|
24
|
+
if (!verifyWebhookSignature(rawBody, signature, secret)) return null;
|
|
25
|
+
const callback = parseDecisionCallback(rawBody);
|
|
26
|
+
if (!callback) return null;
|
|
27
|
+
return {
|
|
28
|
+
correlationId: callback.correlationId,
|
|
29
|
+
answer: callback.answer,
|
|
30
|
+
value: callback.value,
|
|
31
|
+
approved: isAffirmative(callback.answer),
|
|
32
|
+
context: callback.context,
|
|
33
|
+
answeredAt: callback.answeredAt
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
var MAX_INPUT_CHARS = 300;
|
|
37
|
+
var DENIED_UNANSWERED = "No answer from the approver, so this was denied. Do not retry the same action.";
|
|
38
|
+
var DENIED_REFUSED = "The approver denied this action.";
|
|
39
|
+
var renderApprovalQuestion = (toolName, toolInput) => {
|
|
40
|
+
if (toolInput === void 0 || toolInput === null) return `Approve ${toolName}?`;
|
|
41
|
+
const summary = summarise(toolInput);
|
|
42
|
+
return summary === void 0 ? `Approve ${toolName}?` : `Approve ${toolName}? ${summary}`;
|
|
43
|
+
};
|
|
44
|
+
var summarise = (toolInput) => {
|
|
45
|
+
let rendered;
|
|
46
|
+
if (typeof toolInput === "string") {
|
|
47
|
+
rendered = toolInput;
|
|
48
|
+
} else {
|
|
49
|
+
try {
|
|
50
|
+
rendered = JSON.stringify(toolInput);
|
|
51
|
+
} catch {
|
|
52
|
+
return void 0;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (rendered === void 0) return void 0;
|
|
56
|
+
return rendered.length > MAX_INPUT_CHARS ? `${rendered.slice(0, MAX_INPUT_CHARS)}...` : rendered;
|
|
57
|
+
};
|
|
58
|
+
var createAdapterKernel = (helpersLabel) => {
|
|
59
|
+
const requireApiKey = (apiKey) => {
|
|
60
|
+
const resolved = apiKey ?? process.env.PUSHARY_API_KEY;
|
|
61
|
+
if (!resolved) {
|
|
62
|
+
throw new Error(`Pushary: set PUSHARY_API_KEY or pass { apiKey } to ${helpersLabel}.`);
|
|
63
|
+
}
|
|
64
|
+
return resolved;
|
|
65
|
+
};
|
|
66
|
+
const client = (config) => createPusharyServer({
|
|
67
|
+
apiKey: requireApiKey(config.apiKey),
|
|
68
|
+
...config.baseUrl ? { baseUrl: config.baseUrl } : {}
|
|
69
|
+
});
|
|
70
|
+
const requireExternalId = (externalId) => {
|
|
71
|
+
if (!externalId) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`Pushary: no end-user to ask. Pass { externalId } to ${helpersLabel}, or run user-scoped auth so the framework supplies one.`
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
return externalId;
|
|
77
|
+
};
|
|
78
|
+
const createGate = (config) => {
|
|
79
|
+
const gateClient = createPusharyServer({
|
|
80
|
+
apiKey: requireApiKey(config.apiKey),
|
|
81
|
+
...config.baseUrl ? { baseUrl: config.baseUrl } : {}
|
|
82
|
+
});
|
|
83
|
+
return async (ask) => {
|
|
84
|
+
const result = await gateClient.decisions.ask({
|
|
85
|
+
question: ask.question,
|
|
86
|
+
type: "confirm",
|
|
87
|
+
externalId: ask.externalId,
|
|
88
|
+
...config.agentName ? { agentName: config.agentName } : {},
|
|
89
|
+
...config.expiresInSeconds ? { expiresInSeconds: config.expiresInSeconds } : {},
|
|
90
|
+
...config.timeoutMs !== void 0 ? { timeoutMs: config.timeoutMs } : {},
|
|
91
|
+
...config.requireReachable ? { requireReachable: config.requireReachable } : {},
|
|
92
|
+
// Stable across replays, so a re-run of this call resolves to the same
|
|
93
|
+
// decision instead of asking twice.
|
|
94
|
+
idempotencyKey: deterministicKey([ask.sessionId, ask.callId, ask.toolName])
|
|
95
|
+
});
|
|
96
|
+
if (result.approved) return { approved: true };
|
|
97
|
+
return { approved: false, reason: result.answered ? DENIED_REFUSED : DENIED_UNANSWERED };
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
const askExternalUser = (config, input) => client(config).decisions.ask({
|
|
101
|
+
question: input.question,
|
|
102
|
+
type: input.type,
|
|
103
|
+
options: input.options,
|
|
104
|
+
externalId: input.externalId,
|
|
105
|
+
context: input.context,
|
|
106
|
+
agentName: config.agentName,
|
|
107
|
+
timeoutMs: config.timeoutMs,
|
|
108
|
+
idempotencyKey: idempotencyKeyFor(input)
|
|
109
|
+
});
|
|
110
|
+
const createDurableDecision = async (config, input) => {
|
|
111
|
+
const created = await client(config).decisions.create({
|
|
112
|
+
question: input.question,
|
|
113
|
+
type: input.type,
|
|
114
|
+
options: input.options,
|
|
115
|
+
externalId: input.externalId,
|
|
116
|
+
context: input.context,
|
|
117
|
+
callbackUrl: input.callbackUrl,
|
|
118
|
+
agentName: config.agentName,
|
|
119
|
+
idempotencyKey: idempotencyKeyFor(input),
|
|
120
|
+
wait: false
|
|
121
|
+
});
|
|
122
|
+
return {
|
|
123
|
+
decisionId: created.decisionId,
|
|
124
|
+
correlationId: created.decisionId,
|
|
125
|
+
status: created.status,
|
|
126
|
+
reachable: created.reachable,
|
|
127
|
+
reachableChannels: created.reachableChannels,
|
|
128
|
+
deviceCount: created.deviceCount
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
const connect = (config, externalId) => client(config).enroll(externalId);
|
|
132
|
+
return Object.freeze({
|
|
133
|
+
client,
|
|
134
|
+
askExternalUser,
|
|
135
|
+
createDurableDecision,
|
|
136
|
+
connect,
|
|
137
|
+
createGate,
|
|
138
|
+
requireExternalId
|
|
139
|
+
});
|
|
140
|
+
};
|
|
141
|
+
export {
|
|
142
|
+
DENIED_REFUSED,
|
|
143
|
+
DENIED_UNANSWERED,
|
|
144
|
+
SIGNATURE_HEADER,
|
|
145
|
+
createAdapterKernel,
|
|
146
|
+
describeAnswer,
|
|
147
|
+
deterministicKey,
|
|
148
|
+
idempotencyKeyFor,
|
|
149
|
+
isAffirmative,
|
|
150
|
+
isApproved,
|
|
151
|
+
parseDecisionCallback,
|
|
152
|
+
renderApprovalQuestion,
|
|
153
|
+
resolvePusharyCallback,
|
|
154
|
+
verifyWebhookSignature
|
|
155
|
+
};
|
|
156
|
+
//# sourceMappingURL=adapters.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/adapters.ts"],"sourcesContent":["// The shared kernel behind every Pushary framework adapter. Published as\n// `@pushary/server/adapters` so a new adapter is a thin binding — map the\n// framework's context onto these shapes, map the result back — instead of another\n// copy of the same ~150 lines. Nothing here imports a framework.\n\nimport { createPusharyServer, type PusharyServer } from './client'\nimport { deterministicKey, isApproved } from './util'\nimport { parseDecisionCallback, verifyWebhookSignature } from './webhook'\nimport type { AskResult, DecisionType, EnrollResult } from './types'\n\n/** Config every adapter accepts, whatever framework it binds. */\nexport interface PusharyAdapterConfig {\n /** Pushary API key (pk_xxx.sk_xxx). Defaults to `process.env.PUSHARY_API_KEY`. */\n readonly apiKey?: string\n /** Shown on the approval so the human knows which agent is asking. */\n readonly agentName?: string\n /** How long a blocking ask waits before returning (default 55s, serverless-safe). */\n readonly timeoutMs?: number\n /** Override the API base URL (tests / self-host). */\n readonly baseUrl?: string\n}\n\n/** One ask, as an adapter hands it to the kernel. */\nexport interface AskHumanInput {\n readonly question: string\n /** Your own stable id for the end-user who decides. */\n readonly externalId: string\n /** Stable node/step/tool name, used to build a re-run-safe idempotency key. */\n readonly node?: string\n readonly type?: DecisionType\n readonly options?: readonly string[]\n readonly context?: string\n /** Present on the durable path; Pushary POSTs the answer here. */\n readonly callbackUrl?: string\n}\n\n/** A decision opened durably, before the framework parks its run. */\nexport interface CreatedDecision {\n readonly decisionId: string\n readonly correlationId: string\n readonly status: string\n readonly reachable?: boolean\n readonly reachableChannels?: number\n readonly deviceCount?: number\n}\n\n/** A verified, parsed answer callback, keyed by `correlationId`. */\nexport interface PusharyCallback {\n readonly correlationId: string\n readonly answer: string\n readonly value: string\n readonly approved: boolean\n readonly context?: string\n readonly answeredAt: string\n}\n\nconst DEFAULT_NODE = 'ask-human'\n\n/**\n * Re-run-safe idempotency key. A node, step, or tool call that replays on resume\n * lands on the same decision instead of paging the human twice.\n */\nexport const idempotencyKeyFor = (input: AskHumanInput): string =>\n deterministicKey([input.externalId, input.node ?? DEFAULT_NODE, input.question])\n\n/** Fail-closed yes/no check for a confirm answer. */\nexport const isAffirmative = (answer: string | null | undefined): boolean =>\n isApproved({ status: 'answered', type: 'confirm', value: answer ?? null })\n\n/** Turn a decision outcome into an unambiguous instruction for the model. */\nexport const describeAnswer = (type: DecisionType, result: AskResult): string => {\n if (!result.answered) {\n return `No answer (status: ${result.status}). Treat this as NOT approved and do not proceed.`\n }\n if (type === 'confirm') {\n return result.approved\n ? 'The human approved. You may proceed.'\n : 'The human declined. Do not proceed.'\n }\n return `The human answered: ${result.value ?? ''}`\n}\n\n/**\n * Verify a callback signature and parse it, or return null. The `answer` is what\n * you feed back into the framework's resume seam.\n */\nexport const resolvePusharyCallback = (\n rawBody: string,\n signature: string | null | undefined,\n secret: string,\n): PusharyCallback | null => {\n if (!verifyWebhookSignature(rawBody, signature, secret)) return null\n const callback = parseDecisionCallback(rawBody)\n if (!callback) return null\n return {\n correlationId: callback.correlationId,\n answer: callback.answer,\n value: callback.value,\n approved: isAffirmative(callback.answer),\n context: callback.context,\n answeredAt: callback.answeredAt,\n }\n}\n\n/**\n * Connection-level options for a request-time approval gate. Everything that\n * varies per tool call lives on {@link ApprovalAsk} instead, so a gate holds one\n * client and no per-call state.\n */\nexport interface PusharyGateConfig {\n /** Pushary API key. Defaults to `process.env.PUSHARY_API_KEY`. */\n readonly apiKey?: string\n /** Shown on the approval so the human knows which agent is asking. */\n readonly agentName?: string\n /** How long the decision stays answerable. */\n readonly expiresInSeconds?: number\n /** How long to block waiting for an answer before failing closed. */\n readonly timeoutMs?: number\n /**\n * Refuse to open a decision nobody can receive, so an end-user with no connected\n * device is denied at request time instead of silently expiring.\n */\n readonly requireReachable?: boolean\n /** Override the API base URL (tests / self-host). */\n readonly baseUrl?: string\n}\n\n/**\n * One gated tool call, resolved down to what Pushary needs. The adapter owns the\n * mapping from its framework's context, so per-call resolvers stay typed against\n * the framework's own shapes rather than a lowest common denominator.\n */\nexport interface ApprovalAsk {\n /** The tool the model wants to run. */\n readonly toolName: string\n /** Unique per tool call. Keys the decision together with `sessionId`. */\n readonly callId: string\n /** Stable per run, session, or thread. */\n readonly sessionId: string\n /** What the human is shown. */\n readonly question: string\n /** The enrolled end-user who decides. */\n readonly externalId: string\n}\n\n/** What a gate answers. Denials always carry a reason the model can read. */\nexport type ApprovalDecision =\n | { readonly approved: true }\n | { readonly approved: false; readonly reason: string }\n\n/** A request-time gate: ask a person, fail closed. */\nexport type ApprovalGate = (ask: ApprovalAsk) => Promise<ApprovalDecision>\n\nconst MAX_INPUT_CHARS = 300\n\nexport const DENIED_UNANSWERED =\n 'No answer from the approver, so this was denied. Do not retry the same action.'\nexport const DENIED_REFUSED = 'The approver denied this action.'\n\n/**\n * The default question: the tool name plus a truncated view of its input, so the\n * human sees what they are approving without a wall of JSON on a lock screen.\n */\nexport const renderApprovalQuestion = (toolName: string, toolInput: unknown): string => {\n if (toolInput === undefined || toolInput === null) return `Approve ${toolName}?`\n const summary = summarise(toolInput)\n return summary === undefined ? `Approve ${toolName}?` : `Approve ${toolName}? ${summary}`\n}\n\nconst summarise = (toolInput: unknown): string | undefined => {\n let rendered: string | undefined\n if (typeof toolInput === 'string') {\n rendered = toolInput\n } else {\n try {\n // Returns undefined for a function or a symbol, and throws on a cycle. Neither\n // is a reason to fail the tool call: a gate that cannot render its input still\n // has to ask, so fall back to the bare tool name.\n rendered = JSON.stringify(toolInput)\n } catch {\n return undefined\n }\n }\n if (rendered === undefined) return undefined\n return rendered.length > MAX_INPUT_CHARS ? `${rendered.slice(0, MAX_INPUT_CHARS)}...` : rendered\n}\n\n/**\n * The calls an adapter makes, bound to one framework's name so a missing key or a\n * missing end-user says which helpers to configure. Build one per adapter module.\n */\nexport interface AdapterKernel {\n /** The underlying client, for anything the kernel does not cover. */\n readonly client: (config: PusharyAdapterConfig) => PusharyServer\n /**\n * Blocking ask: create a decision and poll durably until the human answers or\n * the deadline passes. Idempotency keyed by externalId + node + question.\n */\n readonly askExternalUser: (\n config: PusharyAdapterConfig,\n input: AskHumanInput,\n ) => Promise<AskResult>\n /**\n * Durable create: open a decision with a `callbackUrl` and return at once, for\n * a framework that parks its run and resumes on the webhook.\n */\n readonly createDurableDecision: (\n config: PusharyAdapterConfig,\n input: AskHumanInput,\n ) => Promise<CreatedDecision>\n /** Connect one end-user's phone (keyless). Show them the returned link. */\n readonly connect: (config: PusharyAdapterConfig, externalId: string) => Promise<EnrollResult>\n /**\n * Build a request-time approval gate. The client is created here, so a missing\n * key throws where the gate is defined rather than on the first tool call.\n */\n readonly createGate: (config: PusharyGateConfig) => ApprovalGate\n /**\n * The end-user to ask, or a clear error naming these helpers. Adapters call this\n * after applying their own resolver and the framework's own default.\n */\n readonly requireExternalId: (externalId: string | undefined) => string\n}\n\n/**\n * Build the kernel for one adapter.\n *\n * `helpersLabel` names the adapter in the missing-key error, so the message points\n * at the helpers the caller is actually using (\"the LangGraph helpers\").\n *\n * ```ts\n * const kernel = createAdapterKernel('the Acme helpers')\n * export const askHuman = kernel.askExternalUser\n * ```\n */\nexport const createAdapterKernel = (helpersLabel: string): AdapterKernel => {\n const requireApiKey = (apiKey: string | undefined): string => {\n const resolved = apiKey ?? process.env.PUSHARY_API_KEY\n if (!resolved) {\n throw new Error(`Pushary: set PUSHARY_API_KEY or pass { apiKey } to ${helpersLabel}.`)\n }\n return resolved\n }\n\n const client = (config: PusharyAdapterConfig): PusharyServer =>\n createPusharyServer({\n apiKey: requireApiKey(config.apiKey),\n ...(config.baseUrl ? { baseUrl: config.baseUrl } : {}),\n })\n\n const requireExternalId = (externalId: string | undefined): string => {\n if (!externalId) {\n throw new Error(\n `Pushary: no end-user to ask. Pass { externalId } to ${helpersLabel}, or run ` +\n 'user-scoped auth so the framework supplies one.',\n )\n }\n return externalId\n }\n\n const createGate = (config: PusharyGateConfig): ApprovalGate => {\n const gateClient = createPusharyServer({\n apiKey: requireApiKey(config.apiKey),\n ...(config.baseUrl ? { baseUrl: config.baseUrl } : {}),\n })\n\n return async (ask: ApprovalAsk): Promise<ApprovalDecision> => {\n const result = await gateClient.decisions.ask({\n question: ask.question,\n type: 'confirm',\n externalId: ask.externalId,\n ...(config.agentName ? { agentName: config.agentName } : {}),\n ...(config.expiresInSeconds ? { expiresInSeconds: config.expiresInSeconds } : {}),\n ...(config.timeoutMs !== undefined ? { timeoutMs: config.timeoutMs } : {}),\n ...(config.requireReachable ? { requireReachable: config.requireReachable } : {}),\n // Stable across replays, so a re-run of this call resolves to the same\n // decision instead of asking twice.\n idempotencyKey: deterministicKey([ask.sessionId, ask.callId, ask.toolName]),\n })\n\n if (result.approved) return { approved: true }\n return { approved: false, reason: result.answered ? DENIED_REFUSED : DENIED_UNANSWERED }\n }\n }\n\n const askExternalUser = (\n config: PusharyAdapterConfig,\n input: AskHumanInput,\n ): Promise<AskResult> =>\n client(config).decisions.ask({\n question: input.question,\n type: input.type,\n options: input.options,\n externalId: input.externalId,\n context: input.context,\n agentName: config.agentName,\n timeoutMs: config.timeoutMs,\n idempotencyKey: idempotencyKeyFor(input),\n })\n\n const createDurableDecision = async (\n config: PusharyAdapterConfig,\n input: AskHumanInput,\n ): Promise<CreatedDecision> => {\n const created = await client(config).decisions.create({\n question: input.question,\n type: input.type,\n options: input.options,\n externalId: input.externalId,\n context: input.context,\n callbackUrl: input.callbackUrl,\n agentName: config.agentName,\n idempotencyKey: idempotencyKeyFor(input),\n wait: false,\n })\n return {\n decisionId: created.decisionId,\n correlationId: created.decisionId,\n status: created.status,\n reachable: created.reachable,\n reachableChannels: created.reachableChannels,\n deviceCount: created.deviceCount,\n }\n }\n\n const connect = (config: PusharyAdapterConfig, externalId: string): Promise<EnrollResult> =>\n client(config).enroll(externalId)\n\n return Object.freeze({\n client,\n askExternalUser,\n createDurableDecision,\n connect,\n createGate,\n requireExternalId,\n })\n}\n\nexport { SIGNATURE_HEADER, verifyWebhookSignature, parseDecisionCallback } from './webhook'\nexport { deterministicKey, isApproved } from './util'\nexport type { AskResult, DecisionType, EnrollResult, PusharyServer }\n"],"mappings":"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","names":[]}
|