@trustloopguard/sdk 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.d.ts +98 -0
- package/dist/client.js +198 -0
- package/dist/errors.d.ts +46 -0
- package/dist/errors.js +187 -0
- package/dist/generated/AgentAuthority.d.ts +4 -0
- package/dist/generated/AgentAuthority.js +2 -0
- package/dist/generated/AgentListResponse.d.ts +4 -0
- package/dist/generated/AgentListResponse.js +1 -0
- package/dist/generated/AgentProfile.d.ts +29 -0
- package/dist/generated/AgentProfile.js +1 -0
- package/dist/generated/AgentScope.d.ts +4 -0
- package/dist/generated/AgentScope.js +2 -0
- package/dist/generated/AgentTone.d.ts +4 -0
- package/dist/generated/AgentTone.js +2 -0
- package/dist/generated/ApiError.d.ts +25 -0
- package/dist/generated/ApiError.js +1 -0
- package/dist/generated/ApiErrorCode.d.ts +6 -0
- package/dist/generated/ApiErrorCode.js +2 -0
- package/dist/generated/ApiKeyBatchRevokeRequest.d.ts +3 -0
- package/dist/generated/ApiKeyBatchRevokeRequest.js +2 -0
- package/dist/generated/ApiKeyBatchRevokeResponse.d.ts +4 -0
- package/dist/generated/ApiKeyBatchRevokeResponse.js +1 -0
- package/dist/generated/ApiKeyListResponse.d.ts +4 -0
- package/dist/generated/ApiKeyListResponse.js +1 -0
- package/dist/generated/AuthRequest.d.ts +15 -0
- package/dist/generated/AuthRequest.js +2 -0
- package/dist/generated/AuthResponse.d.ts +16 -0
- package/dist/generated/AuthResponse.js +2 -0
- package/dist/generated/ChangePasswordRequest.d.ts +15 -0
- package/dist/generated/ChangePasswordRequest.js +2 -0
- package/dist/generated/Channel.d.ts +8 -0
- package/dist/generated/Channel.js +2 -0
- package/dist/generated/CheckRequest.d.ts +21 -0
- package/dist/generated/CheckRequest.js +1 -0
- package/dist/generated/CreateApiKeyRequest.d.ts +3 -0
- package/dist/generated/CreateApiKeyRequest.js +2 -0
- package/dist/generated/CreateApiKeyResponse.d.ts +8 -0
- package/dist/generated/CreateApiKeyResponse.js +1 -0
- package/dist/generated/CreateInviteRequest.d.ts +8 -0
- package/dist/generated/CreateInviteRequest.js +1 -0
- package/dist/generated/CreateInviteResponse.d.ts +18 -0
- package/dist/generated/CreateInviteResponse.js +1 -0
- package/dist/generated/CreateKnowledgeSourceRequest.d.ts +9 -0
- package/dist/generated/CreateKnowledgeSourceRequest.js +1 -0
- package/dist/generated/CreateRunEventRequest.d.ts +13 -0
- package/dist/generated/CreateRunEventRequest.js +1 -0
- package/dist/generated/CreateRunRequest.d.ts +9 -0
- package/dist/generated/CreateRunRequest.js +1 -0
- package/dist/generated/CreateWorkspaceRequest.d.ts +7 -0
- package/dist/generated/CreateWorkspaceRequest.js +2 -0
- package/dist/generated/DashboardApiKey.d.ts +15 -0
- package/dist/generated/DashboardApiKey.js +2 -0
- package/dist/generated/DashboardKnowledgeSourceKind.d.ts +1 -0
- package/dist/generated/DashboardKnowledgeSourceKind.js +2 -0
- package/dist/generated/Decision.d.ts +17 -0
- package/dist/generated/Decision.js +1 -0
- package/dist/generated/GuardrailGenerateResponse.d.ts +11 -0
- package/dist/generated/GuardrailGenerateResponse.js +1 -0
- package/dist/generated/GuardrailListResponse.d.ts +7 -0
- package/dist/generated/GuardrailListResponse.js +1 -0
- package/dist/generated/InviteListResponse.d.ts +4 -0
- package/dist/generated/InviteListResponse.js +1 -0
- package/dist/generated/InviteStatus.d.ts +4 -0
- package/dist/generated/InviteStatus.js +2 -0
- package/dist/generated/KnowledgeFileInput.d.ts +5 -0
- package/dist/generated/KnowledgeFileInput.js +2 -0
- package/dist/generated/KnowledgeFileMetadata.d.ts +6 -0
- package/dist/generated/KnowledgeFileMetadata.js +2 -0
- package/dist/generated/KnowledgeSource.d.ts +7 -0
- package/dist/generated/KnowledgeSource.js +1 -0
- package/dist/generated/KnowledgeSourceDocument.d.ts +22 -0
- package/dist/generated/KnowledgeSourceDocument.js +1 -0
- package/dist/generated/KnowledgeSourceFileResponse.d.ts +6 -0
- package/dist/generated/KnowledgeSourceFileResponse.js +2 -0
- package/dist/generated/KnowledgeSourceKind.d.ts +1 -0
- package/dist/generated/KnowledgeSourceKind.js +2 -0
- package/dist/generated/KnowledgeSourceListResponse.d.ts +4 -0
- package/dist/generated/KnowledgeSourceListResponse.js +1 -0
- package/dist/generated/KnowledgeSourceStatus.d.ts +1 -0
- package/dist/generated/KnowledgeSourceStatus.js +2 -0
- package/dist/generated/MemberListResponse.d.ts +4 -0
- package/dist/generated/MemberListResponse.js +1 -0
- package/dist/generated/MyWorkspace.d.ts +12 -0
- package/dist/generated/MyWorkspace.js +1 -0
- package/dist/generated/MyWorkspacesResponse.d.ts +4 -0
- package/dist/generated/MyWorkspacesResponse.js +1 -0
- package/dist/generated/PolicyAction.d.ts +4 -0
- package/dist/generated/PolicyAction.js +2 -0
- package/dist/generated/PolicyBatchSetEnabledRequest.d.ts +4 -0
- package/dist/generated/PolicyBatchSetEnabledRequest.js +2 -0
- package/dist/generated/PolicyBatchSetEnabledResponse.d.ts +4 -0
- package/dist/generated/PolicyBatchSetEnabledResponse.js +1 -0
- package/dist/generated/PolicyDocument.d.ts +8 -0
- package/dist/generated/PolicyDocument.js +1 -0
- package/dist/generated/PolicyDraft.d.ts +17 -0
- package/dist/generated/PolicyDraft.js +1 -0
- package/dist/generated/PolicyDraftRequest.d.ts +6 -0
- package/dist/generated/PolicyDraftRequest.js +2 -0
- package/dist/generated/PolicyDraftResponse.d.ts +4 -0
- package/dist/generated/PolicyDraftResponse.js +1 -0
- package/dist/generated/PolicyListResponse.d.ts +4 -0
- package/dist/generated/PolicyListResponse.js +1 -0
- package/dist/generated/PolicyMatchType.d.ts +5 -0
- package/dist/generated/PolicyMatchType.js +2 -0
- package/dist/generated/PolicySetEnabledRequest.d.ts +3 -0
- package/dist/generated/PolicySetEnabledRequest.js +2 -0
- package/dist/generated/PolicySummary.d.ts +9 -0
- package/dist/generated/PolicySummary.js +1 -0
- package/dist/generated/PolicyValidateResponse.d.ts +6 -0
- package/dist/generated/PolicyValidateResponse.js +1 -0
- package/dist/generated/PolicyValidationIssue.d.ts +4 -0
- package/dist/generated/PolicyValidationIssue.js +2 -0
- package/dist/generated/RunDetail.d.ts +8 -0
- package/dist/generated/RunDetail.js +1 -0
- package/dist/generated/RunEventKind.d.ts +1 -0
- package/dist/generated/RunEventKind.js +2 -0
- package/dist/generated/RunEventListResponse.d.ts +4 -0
- package/dist/generated/RunEventListResponse.js +1 -0
- package/dist/generated/RunEventSummary.d.ts +20 -0
- package/dist/generated/RunEventSummary.js +1 -0
- package/dist/generated/RunKind.d.ts +1 -0
- package/dist/generated/RunKind.js +2 -0
- package/dist/generated/RunListResponse.d.ts +4 -0
- package/dist/generated/RunListResponse.js +1 -0
- package/dist/generated/RunStatus.d.ts +1 -0
- package/dist/generated/RunStatus.js +2 -0
- package/dist/generated/RunSummary.d.ts +32 -0
- package/dist/generated/RunSummary.js +1 -0
- package/dist/generated/Severity.d.ts +1 -0
- package/dist/generated/Severity.js +2 -0
- package/dist/generated/Tier.d.ts +4 -0
- package/dist/generated/Tier.js +2 -0
- package/dist/generated/TierResult.d.ts +12 -0
- package/dist/generated/TierResult.js +1 -0
- package/dist/generated/TierStatus.d.ts +4 -0
- package/dist/generated/TierStatus.js +2 -0
- package/dist/generated/TraceListResponse.d.ts +4 -0
- package/dist/generated/TraceListResponse.js +1 -0
- package/dist/generated/TraceSummary.d.ts +13 -0
- package/dist/generated/TraceSummary.js +2 -0
- package/dist/generated/TriggeredPolicy.d.ts +6 -0
- package/dist/generated/TriggeredPolicy.js +1 -0
- package/dist/generated/UpdateRunRequest.d.ts +10 -0
- package/dist/generated/UpdateRunRequest.js +1 -0
- package/dist/generated/Verdict.d.ts +4 -0
- package/dist/generated/Verdict.js +2 -0
- package/dist/generated/WorkspaceInvite.d.ts +20 -0
- package/dist/generated/WorkspaceInvite.js +1 -0
- package/dist/generated/WorkspaceMember.d.ts +13 -0
- package/dist/generated/WorkspaceMember.js +1 -0
- package/dist/generated/WorkspaceRole.d.ts +5 -0
- package/dist/generated/WorkspaceRole.js +2 -0
- package/dist/generated/WorkspaceSettings.d.ts +11 -0
- package/dist/generated/WorkspaceSettings.js +2 -0
- package/dist/guard.d.ts +193 -0
- package/dist/guard.js +211 -0
- package/dist/index.d.ts +64 -0
- package/dist/index.js +64 -0
- package/dist/retry.d.ts +18 -0
- package/dist/retry.js +48 -0
- package/package.json +39 -0
- package/src/client.ts +453 -0
- package/src/errors.ts +202 -0
- package/src/generated/AgentAuthority.ts +3 -0
- package/src/generated/AgentListResponse.ts +4 -0
- package/src/generated/AgentProfile.ts +23 -0
- package/src/generated/AgentScope.ts +3 -0
- package/src/generated/AgentTone.ts +3 -0
- package/src/generated/ApiError.ts +24 -0
- package/src/generated/ApiErrorCode.ts +8 -0
- package/src/generated/ApiKeyBatchRevokeRequest.ts +3 -0
- package/src/generated/ApiKeyBatchRevokeResponse.ts +4 -0
- package/src/generated/ApiKeyListResponse.ts +4 -0
- package/src/generated/AuthRequest.ts +16 -0
- package/src/generated/AuthResponse.ts +15 -0
- package/src/generated/ChangePasswordRequest.ts +15 -0
- package/src/generated/Channel.ts +10 -0
- package/src/generated/CheckRequest.ts +11 -0
- package/src/generated/CreateApiKeyRequest.ts +3 -0
- package/src/generated/CreateApiKeyResponse.ts +8 -0
- package/src/generated/CreateInviteRequest.ts +7 -0
- package/src/generated/CreateInviteResponse.ts +14 -0
- package/src/generated/CreateKnowledgeSourceRequest.ts +5 -0
- package/src/generated/CreateRunEventRequest.ts +8 -0
- package/src/generated/CreateRunRequest.ts +5 -0
- package/src/generated/CreateWorkspaceRequest.ts +7 -0
- package/src/generated/DashboardApiKey.ts +11 -0
- package/src/generated/DashboardKnowledgeSourceKind.ts +3 -0
- package/src/generated/Decision.ts +12 -0
- package/src/generated/GuardrailGenerateResponse.ts +11 -0
- package/src/generated/GuardrailListResponse.ts +7 -0
- package/src/generated/InviteListResponse.ts +4 -0
- package/src/generated/InviteStatus.ts +6 -0
- package/src/generated/KnowledgeFileInput.ts +3 -0
- package/src/generated/KnowledgeFileMetadata.ts +3 -0
- package/src/generated/KnowledgeSource.ts +4 -0
- package/src/generated/KnowledgeSourceDocument.ts +17 -0
- package/src/generated/KnowledgeSourceFileResponse.ts +3 -0
- package/src/generated/KnowledgeSourceKind.ts +3 -0
- package/src/generated/KnowledgeSourceListResponse.ts +4 -0
- package/src/generated/KnowledgeSourceStatus.ts +3 -0
- package/src/generated/MemberListResponse.ts +4 -0
- package/src/generated/MyWorkspace.ts +8 -0
- package/src/generated/MyWorkspacesResponse.ts +4 -0
- package/src/generated/PolicyAction.ts +6 -0
- package/src/generated/PolicyBatchSetEnabledRequest.ts +3 -0
- package/src/generated/PolicyBatchSetEnabledResponse.ts +4 -0
- package/src/generated/PolicyDocument.ts +4 -0
- package/src/generated/PolicyDraft.ts +11 -0
- package/src/generated/PolicyDraftRequest.ts +6 -0
- package/src/generated/PolicyDraftResponse.ts +4 -0
- package/src/generated/PolicyListResponse.ts +4 -0
- package/src/generated/PolicyMatchType.ts +7 -0
- package/src/generated/PolicySetEnabledRequest.ts +3 -0
- package/src/generated/PolicySummary.ts +4 -0
- package/src/generated/PolicyValidateResponse.ts +4 -0
- package/src/generated/PolicyValidationIssue.ts +3 -0
- package/src/generated/README.md +1 -0
- package/src/generated/RunDetail.ts +6 -0
- package/src/generated/RunEventKind.ts +3 -0
- package/src/generated/RunEventListResponse.ts +4 -0
- package/src/generated/RunEventSummary.ts +12 -0
- package/src/generated/RunKind.ts +3 -0
- package/src/generated/RunListResponse.ts +4 -0
- package/src/generated/RunStatus.ts +3 -0
- package/src/generated/RunSummary.ts +21 -0
- package/src/generated/Severity.ts +3 -0
- package/src/generated/Tier.ts +6 -0
- package/src/generated/TierResult.ts +9 -0
- package/src/generated/TierStatus.ts +6 -0
- package/src/generated/TraceListResponse.ts +4 -0
- package/src/generated/TraceSummary.ts +7 -0
- package/src/generated/TriggeredPolicy.ts +4 -0
- package/src/generated/UpdateRunRequest.ts +9 -0
- package/src/generated/Verdict.ts +6 -0
- package/src/generated/WorkspaceInvite.ts +14 -0
- package/src/generated/WorkspaceMember.ts +11 -0
- package/src/generated/WorkspaceRole.ts +7 -0
- package/src/generated/WorkspaceSettings.ts +7 -0
- package/src/guard.ts +492 -0
- package/src/index.ts +97 -0
- package/src/retry.ts +67 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
|
2
|
+
import type { WorkspaceRole } from "./WorkspaceRole";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A user who currently has access to a workspace.
|
|
6
|
+
*/
|
|
7
|
+
export type WorkspaceMember = { user_id: string, username: string, role: WorkspaceRole,
|
|
8
|
+
/**
|
|
9
|
+
* RFC3339 timestamp.
|
|
10
|
+
*/
|
|
11
|
+
joined_at: string, };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Permission level a user holds inside a workspace. Mirrors the
|
|
5
|
+
* `workspace_role` Postgres enum (migration 6).
|
|
6
|
+
*/
|
|
7
|
+
export type WorkspaceRole = "owner" | "admin" | "editor" | "viewer";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
|
2
|
+
|
|
3
|
+
export type WorkspaceSettings = { default_action: string, escalation_webhook_url: string | null, telemetry_enabled: boolean, retention_days: string, config: Record<string, unknown>,
|
|
4
|
+
/**
|
|
5
|
+
* RFC 3339 timestamp.
|
|
6
|
+
*/
|
|
7
|
+
updated_at: string | null, };
|
package/src/guard.ts
ADDED
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
// `guard()` — output-boundary helper.
|
|
2
|
+
//
|
|
3
|
+
// Most integrations should create one guardrail at startup and call it before
|
|
4
|
+
// delivering an agent draft:
|
|
5
|
+
//
|
|
6
|
+
// const guardrail = guard({ agentId: 'acme-support-v3' });
|
|
7
|
+
// const reply = await guardrail({ input: userMessage, draft: agentDraft });
|
|
8
|
+
// await sendToCustomer(reply);
|
|
9
|
+
//
|
|
10
|
+
// The lower-level form remains available for custom client ownership:
|
|
11
|
+
//
|
|
12
|
+
// const reply = await guard({
|
|
13
|
+
// client,
|
|
14
|
+
// input,
|
|
15
|
+
// draft,
|
|
16
|
+
// context,
|
|
17
|
+
// agentId: 'acme-support-v3',
|
|
18
|
+
// onBlock: () => cannedSafeReply,
|
|
19
|
+
// onEscalate: () => { humanQueue.push(...); return holdMessage; },
|
|
20
|
+
// });
|
|
21
|
+
// await sendToCustomer(reply);
|
|
22
|
+
//
|
|
23
|
+
// The handler stays the source of truth on what to do per-verdict.
|
|
24
|
+
// `guard` just makes the dispatch ergonomic and applies a fail-open
|
|
25
|
+
// default for transport errors so an outage on our side doesn't take
|
|
26
|
+
// down the agent.
|
|
27
|
+
//
|
|
28
|
+
// Factory guards support three presets:
|
|
29
|
+
// strict -> treat rewrite verdicts as blocked output
|
|
30
|
+
// rewrite -> use safeOutput, block when no safeOutput exists
|
|
31
|
+
// rewrite_or_regenerate -> use safeOutput, otherwise regenerate and check again
|
|
32
|
+
|
|
33
|
+
import { Client, type ClientOptions } from './client';
|
|
34
|
+
import type { Channel } from './generated/Channel';
|
|
35
|
+
import type { CheckRequest } from './generated/CheckRequest';
|
|
36
|
+
import type { CreateRunEventRequest } from './generated/CreateRunEventRequest';
|
|
37
|
+
import type { Decision } from './generated/Decision';
|
|
38
|
+
import { SdkError } from './errors';
|
|
39
|
+
|
|
40
|
+
const DEFAULT_BLOCK_MESSAGE = "I can't help with that request.";
|
|
41
|
+
const DEFAULT_ESCALATE_MESSAGE = 'A human teammate should review this before we continue.';
|
|
42
|
+
|
|
43
|
+
type DecisionHandler = string | ((decision: Decision) => string | Promise<string>);
|
|
44
|
+
type ErrorHandler = string | ((err: SdkError, draft: string) => string | Promise<string>);
|
|
45
|
+
export const GuardMode = {
|
|
46
|
+
Strict: 'strict',
|
|
47
|
+
Rewrite: 'rewrite',
|
|
48
|
+
RewriteOrRegenerate: 'rewrite_or_regenerate',
|
|
49
|
+
} as const;
|
|
50
|
+
export type GuardMode = (typeof GuardMode)[keyof typeof GuardMode];
|
|
51
|
+
|
|
52
|
+
export interface RegenerateFeedback {
|
|
53
|
+
/** What the user said. */
|
|
54
|
+
input: string;
|
|
55
|
+
|
|
56
|
+
/** The draft that failed the guard check. */
|
|
57
|
+
draft: string;
|
|
58
|
+
|
|
59
|
+
/** Full TrustLoopGuard decision for the failed draft. */
|
|
60
|
+
decision: Decision;
|
|
61
|
+
|
|
62
|
+
/** Human-readable reason returned by TrustLoopGuard. */
|
|
63
|
+
reason: string;
|
|
64
|
+
|
|
65
|
+
/** Safe output returned by TrustLoopGuard, when available. */
|
|
66
|
+
safeOutput: string | null;
|
|
67
|
+
|
|
68
|
+
/** 1-based regeneration attempt number. */
|
|
69
|
+
attempt: number;
|
|
70
|
+
|
|
71
|
+
/** Maximum allowed regeneration attempts. */
|
|
72
|
+
maxAttempts: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
type RegenerateHandler = (feedback: RegenerateFeedback) => string | Promise<string>;
|
|
76
|
+
|
|
77
|
+
export interface GuardCallbacks {
|
|
78
|
+
/**
|
|
79
|
+
* Called when the verdict is `allow`. Default: return the original
|
|
80
|
+
* draft unchanged. Override only if you want to log the allow path
|
|
81
|
+
* or strip a draft suffix etc.
|
|
82
|
+
*/
|
|
83
|
+
onAllow?: (draft: string, decision: Decision) => string | Promise<string>;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Called when the verdict is `rewrite`. Default: return
|
|
87
|
+
* `decision.safe_output ?? draft`. Override to post-process or
|
|
88
|
+
* substitute your own canned rewrite.
|
|
89
|
+
*/
|
|
90
|
+
onRevise?: (
|
|
91
|
+
revised: string | null,
|
|
92
|
+
draft: string,
|
|
93
|
+
decision: Decision,
|
|
94
|
+
) => string | Promise<string>;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Called when the verdict is `block`. **Required** — there is no
|
|
98
|
+
* sensible automatic answer. Return the canned safe message your
|
|
99
|
+
* brand wants the customer to see (or throw to abort the send).
|
|
100
|
+
*/
|
|
101
|
+
onBlock: (decision: Decision) => string | Promise<string>;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Called when the verdict is `escalate`. **Required** — typically
|
|
105
|
+
* pushes onto a human-review queue and returns a holding message.
|
|
106
|
+
*/
|
|
107
|
+
onEscalate: (decision: Decision) => string | Promise<string>;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Called when the SDK transport itself fails (network down, server
|
|
111
|
+
* 5xx, decode error, retries exhausted). Default: **fail-open** —
|
|
112
|
+
* return the original draft. Pass an explicit handler if you'd
|
|
113
|
+
* rather fail-closed (e.g. `() => cannedSafeReply`).
|
|
114
|
+
*/
|
|
115
|
+
onError?: (err: SdkError, draft: string) => string | Promise<string>;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface GuardOptions extends GuardCallbacks {
|
|
119
|
+
client: Client;
|
|
120
|
+
|
|
121
|
+
/** What the user said. */
|
|
122
|
+
input: string;
|
|
123
|
+
|
|
124
|
+
/** What the agent wants to send. The string returned by `guard` is
|
|
125
|
+
* what the caller should actually deliver. */
|
|
126
|
+
draft: string;
|
|
127
|
+
|
|
128
|
+
/** Conversation channel — drives latency budget on the server. */
|
|
129
|
+
channel?: Channel;
|
|
130
|
+
|
|
131
|
+
/** Required: the registered agent profile id. */
|
|
132
|
+
agentId: string;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Optional structured context — typically `{ docs: [...] }` for
|
|
136
|
+
* grounding the LLM judges. Anything JSON-serialisable.
|
|
137
|
+
*/
|
|
138
|
+
context?: Record<string, unknown>;
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Optional override for `domain` (defaults to the server's
|
|
142
|
+
* `customer_support` dispatcher).
|
|
143
|
+
*/
|
|
144
|
+
domain?: string;
|
|
145
|
+
|
|
146
|
+
/** Optional caller-supplied trace id — overrides the server-assigned one. */
|
|
147
|
+
traceId?: string;
|
|
148
|
+
|
|
149
|
+
/** Optional run id used to group this check in the dashboard. */
|
|
150
|
+
runId?: string;
|
|
151
|
+
|
|
152
|
+
/** Optional existing run event id to attach to this check. Requires runId. */
|
|
153
|
+
runEventId?: string;
|
|
154
|
+
|
|
155
|
+
/** Optional inline run event to create and attach to this check. Requires runId. */
|
|
156
|
+
runEvent?: CreateRunEventRequest;
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Logger hook. If provided, gets one structured event per `guard`
|
|
160
|
+
* invocation: { trace_id, verdict, branch, latency_ms }. Useful for
|
|
161
|
+
* surfacing which branch fired without forcing a specific logger
|
|
162
|
+
* dependency.
|
|
163
|
+
*/
|
|
164
|
+
log?: (event: GuardLogEvent) => void;
|
|
165
|
+
|
|
166
|
+
/** Optional cancellation. Forwarded to the underlying check call. */
|
|
167
|
+
signal?: AbortSignal;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export interface GuardFactoryOptions {
|
|
171
|
+
/** Required: the registered agent profile id. */
|
|
172
|
+
agentId: string;
|
|
173
|
+
|
|
174
|
+
/** Existing client. Pass this when you want to own transport lifecycle/config. */
|
|
175
|
+
client?: Client;
|
|
176
|
+
|
|
177
|
+
/** TrustLoopGuard server URL. Defaults to env or localhost. */
|
|
178
|
+
baseUrl?: string;
|
|
179
|
+
|
|
180
|
+
/** Bearer token. Defaults to env when available. */
|
|
181
|
+
apiKey?: string;
|
|
182
|
+
|
|
183
|
+
/** Retry policy forwarded when the factory owns the Client. */
|
|
184
|
+
retry?: ClientOptions['retry'];
|
|
185
|
+
|
|
186
|
+
/** Fetch implementation forwarded when the factory owns the Client. */
|
|
187
|
+
fetchImpl?: ClientOptions['fetchImpl'];
|
|
188
|
+
|
|
189
|
+
/** Retry logger forwarded when the factory owns the Client. */
|
|
190
|
+
onRetry?: ClientOptions['onRetry'];
|
|
191
|
+
|
|
192
|
+
/** Conversation channel — drives latency budget on the server. */
|
|
193
|
+
channel?: Channel;
|
|
194
|
+
|
|
195
|
+
/** Optional override for `domain`. */
|
|
196
|
+
domain?: string;
|
|
197
|
+
|
|
198
|
+
/** Structured context merged into every call. */
|
|
199
|
+
context?: Record<string, unknown>;
|
|
200
|
+
|
|
201
|
+
/** Default block branch. Omit for the SDK safe message. */
|
|
202
|
+
onBlock?: DecisionHandler;
|
|
203
|
+
|
|
204
|
+
/** Default escalation branch. Omit for the SDK safe message. */
|
|
205
|
+
onEscalate?: DecisionHandler;
|
|
206
|
+
|
|
207
|
+
/** Transport failure branch. Omit for fail-open. */
|
|
208
|
+
onError?: ErrorHandler;
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Output mode:
|
|
212
|
+
* - strict: treat rewrite verdicts as blocked output
|
|
213
|
+
* - rewrite: use safeOutput, block when no safeOutput exists
|
|
214
|
+
* - rewrite_or_regenerate: use safeOutput, otherwise ask the model to try again
|
|
215
|
+
*/
|
|
216
|
+
mode?: GuardMode;
|
|
217
|
+
|
|
218
|
+
/** Called by rewrite_or_regenerate when TrustLoopGuard has no safeOutput. */
|
|
219
|
+
regenerate?: RegenerateHandler;
|
|
220
|
+
|
|
221
|
+
/** Hard cap for model regeneration loops. Defaults to 1. */
|
|
222
|
+
maxRegenerations?: number;
|
|
223
|
+
|
|
224
|
+
/** Return the default block message on transport errors when no onError is set. */
|
|
225
|
+
failClosed?: boolean;
|
|
226
|
+
|
|
227
|
+
/** Logger hook for every guard invocation. */
|
|
228
|
+
log?: (event: GuardLogEvent) => void;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export interface GuardCallOptions {
|
|
232
|
+
/** What the user said. */
|
|
233
|
+
input: string;
|
|
234
|
+
|
|
235
|
+
/** What the agent wants to send. */
|
|
236
|
+
draft: string;
|
|
237
|
+
|
|
238
|
+
channel?: Channel;
|
|
239
|
+
domain?: string;
|
|
240
|
+
context?: Record<string, unknown>;
|
|
241
|
+
traceId?: string;
|
|
242
|
+
runId?: string;
|
|
243
|
+
runEventId?: string;
|
|
244
|
+
runEvent?: CreateRunEventRequest;
|
|
245
|
+
onBlock?: DecisionHandler;
|
|
246
|
+
onEscalate?: DecisionHandler;
|
|
247
|
+
onError?: ErrorHandler;
|
|
248
|
+
mode?: GuardMode;
|
|
249
|
+
regenerate?: RegenerateHandler;
|
|
250
|
+
maxRegenerations?: number;
|
|
251
|
+
log?: (event: GuardLogEvent) => void;
|
|
252
|
+
signal?: AbortSignal;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export interface OutputGuard {
|
|
256
|
+
(opts: GuardCallOptions): Promise<string>;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export interface GuardLogEvent {
|
|
260
|
+
trace_id: string;
|
|
261
|
+
verdict: Decision['verdict'];
|
|
262
|
+
/** Which callback we ended up calling. */
|
|
263
|
+
branch: 'allow' | 'revise' | 'block' | 'escalate' | 'error';
|
|
264
|
+
latency_ms: number;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Run the SDK's check + dispatch the appropriate callback. Returns the
|
|
269
|
+
* string the caller should actually send to the customer.
|
|
270
|
+
*
|
|
271
|
+
* Verdicts map 1:1 to callbacks:
|
|
272
|
+
*
|
|
273
|
+
* allow → onAllow (default: return draft as-is)
|
|
274
|
+
* rewrite → onRevise (default: return decision.safe_output ?? draft)
|
|
275
|
+
* block → onBlock (required)
|
|
276
|
+
* escalate → onEscalate (required)
|
|
277
|
+
*
|
|
278
|
+
* Transport / decode / retry-exhausted errors go to `onError`. Default
|
|
279
|
+
* is **fail-open** — return the original draft. Pass an explicit
|
|
280
|
+
* `onError` if your domain prefers fail-closed.
|
|
281
|
+
*/
|
|
282
|
+
export function guard(opts: GuardFactoryOptions): OutputGuard;
|
|
283
|
+
export function guard(opts: GuardOptions): Promise<string>;
|
|
284
|
+
export function guard(opts: GuardFactoryOptions | GuardOptions): OutputGuard | Promise<string> {
|
|
285
|
+
if ('input' in opts && 'draft' in opts) {
|
|
286
|
+
return guardOnce(opts as GuardOptions);
|
|
287
|
+
}
|
|
288
|
+
return createOutputGuard(opts);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async function guardOnce(opts: GuardOptions): Promise<string> {
|
|
292
|
+
const start = performance.now();
|
|
293
|
+
const req: CheckRequest = {
|
|
294
|
+
agent_id: opts.agentId,
|
|
295
|
+
channel: opts.channel ?? 'chat',
|
|
296
|
+
input: opts.input,
|
|
297
|
+
proposed_output: opts.draft,
|
|
298
|
+
domain: opts.domain ?? null,
|
|
299
|
+
policies: [],
|
|
300
|
+
// Server's `context` is `Record<string, unknown> | null`; null
|
|
301
|
+
// matches the wire shape when no context is supplied.
|
|
302
|
+
context: (opts.context ?? null) as unknown as Record<string, unknown>,
|
|
303
|
+
trace_id: opts.traceId ?? null,
|
|
304
|
+
};
|
|
305
|
+
addDefined(req, 'run_id', opts.runId);
|
|
306
|
+
addDefined(req, 'run_event_id', opts.runEventId);
|
|
307
|
+
addDefined(req, 'run_event', opts.runEvent);
|
|
308
|
+
|
|
309
|
+
let decision: Decision;
|
|
310
|
+
try {
|
|
311
|
+
decision = await opts.client.check(req, opts.signal);
|
|
312
|
+
} catch (e) {
|
|
313
|
+
if (!(e instanceof SdkError)) throw e;
|
|
314
|
+
const fallback = opts.onError ? await opts.onError(e, opts.draft) : opts.draft; // fail-open default
|
|
315
|
+
opts.log?.({
|
|
316
|
+
trace_id: opts.traceId ?? '',
|
|
317
|
+
// Wire shape doesn't have an "error" verdict; we synthesise the
|
|
318
|
+
// log line for observability without lying about the wire.
|
|
319
|
+
verdict: 'allow',
|
|
320
|
+
branch: 'error',
|
|
321
|
+
latency_ms: Math.round(performance.now() - start),
|
|
322
|
+
});
|
|
323
|
+
return fallback;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const result = await dispatch(opts, decision);
|
|
327
|
+
opts.log?.({
|
|
328
|
+
trace_id: decision.trace_id,
|
|
329
|
+
verdict: decision.verdict,
|
|
330
|
+
branch: branchFor(decision.verdict),
|
|
331
|
+
latency_ms: Math.round(performance.now() - start),
|
|
332
|
+
});
|
|
333
|
+
return result;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function createOutputGuard(opts: GuardFactoryOptions): OutputGuard {
|
|
337
|
+
const client = opts.client ?? new Client(clientOptions(opts));
|
|
338
|
+
|
|
339
|
+
return async (call: GuardCallOptions) => {
|
|
340
|
+
const mode = call.mode ?? opts.mode ?? 'rewrite';
|
|
341
|
+
const regenerate = call.regenerate ?? opts.regenerate;
|
|
342
|
+
const maxRegenerations = call.maxRegenerations ?? opts.maxRegenerations ?? 1;
|
|
343
|
+
const onBlock = decisionHandler(call.onBlock ?? opts.onBlock, DEFAULT_BLOCK_MESSAGE);
|
|
344
|
+
const onEscalate = decisionHandler(
|
|
345
|
+
call.onEscalate ?? opts.onEscalate,
|
|
346
|
+
DEFAULT_ESCALATE_MESSAGE,
|
|
347
|
+
);
|
|
348
|
+
const onError = errorHandler(
|
|
349
|
+
call.onError ?? opts.onError,
|
|
350
|
+
opts.failClosed === true ? DEFAULT_BLOCK_MESSAGE : undefined,
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
const runAttempt = async (
|
|
354
|
+
currentDraft: string,
|
|
355
|
+
completedRegenerations: number,
|
|
356
|
+
): Promise<string> => {
|
|
357
|
+
const onRevise = async (
|
|
358
|
+
revised: string | null,
|
|
359
|
+
checkedDraft: string,
|
|
360
|
+
decision: Decision,
|
|
361
|
+
): Promise<string> => {
|
|
362
|
+
if (mode === 'strict') return await onBlock(decision);
|
|
363
|
+
if (revised !== null) return revised;
|
|
364
|
+
if (
|
|
365
|
+
mode !== 'rewrite_or_regenerate' ||
|
|
366
|
+
regenerate === undefined ||
|
|
367
|
+
completedRegenerations >= maxRegenerations
|
|
368
|
+
) {
|
|
369
|
+
return await onBlock(decision);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const nextAttempt = completedRegenerations + 1;
|
|
373
|
+
const nextDraft = await regenerate({
|
|
374
|
+
input: call.input,
|
|
375
|
+
draft: checkedDraft,
|
|
376
|
+
decision,
|
|
377
|
+
reason: decision.reason,
|
|
378
|
+
safeOutput: decision.safe_output ?? null,
|
|
379
|
+
attempt: nextAttempt,
|
|
380
|
+
maxAttempts: maxRegenerations,
|
|
381
|
+
});
|
|
382
|
+
return await runAttempt(nextDraft, nextAttempt);
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
const guardOpts: GuardOptions = {
|
|
386
|
+
client,
|
|
387
|
+
agentId: opts.agentId,
|
|
388
|
+
input: call.input,
|
|
389
|
+
draft: currentDraft,
|
|
390
|
+
context: { ...(opts.context ?? {}), ...(call.context ?? {}) },
|
|
391
|
+
onBlock,
|
|
392
|
+
onEscalate,
|
|
393
|
+
onRevise,
|
|
394
|
+
};
|
|
395
|
+
addDefined(guardOpts, 'channel', call.channel ?? opts.channel);
|
|
396
|
+
addDefined(guardOpts, 'domain', call.domain ?? opts.domain);
|
|
397
|
+
addDefined(guardOpts, 'traceId', call.traceId);
|
|
398
|
+
addDefined(guardOpts, 'runId', call.runId);
|
|
399
|
+
addDefined(guardOpts, 'runEventId', call.runEventId);
|
|
400
|
+
addDefined(guardOpts, 'runEvent', call.runEvent);
|
|
401
|
+
addDefined(guardOpts, 'onError', onError);
|
|
402
|
+
addDefined(guardOpts, 'log', call.log ?? opts.log);
|
|
403
|
+
addDefined(guardOpts, 'signal', call.signal);
|
|
404
|
+
|
|
405
|
+
return await guardOnce(guardOpts);
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
return await runAttempt(call.draft, 0);
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
async function dispatch(opts: GuardOptions, decision: Decision): Promise<string> {
|
|
413
|
+
switch (decision.verdict) {
|
|
414
|
+
case 'allow':
|
|
415
|
+
return opts.onAllow ? await opts.onAllow(opts.draft, decision) : opts.draft;
|
|
416
|
+
case 'rewrite':
|
|
417
|
+
return opts.onRevise
|
|
418
|
+
? await opts.onRevise(decision.safe_output ?? null, opts.draft, decision)
|
|
419
|
+
: (decision.safe_output ?? opts.draft);
|
|
420
|
+
case 'block':
|
|
421
|
+
return await opts.onBlock(decision);
|
|
422
|
+
case 'escalate':
|
|
423
|
+
return await opts.onEscalate(decision);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function branchFor(v: Decision['verdict']): GuardLogEvent['branch'] {
|
|
428
|
+
if (v === 'rewrite') return 'revise';
|
|
429
|
+
return v;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function decisionHandler(
|
|
433
|
+
handler: DecisionHandler | undefined,
|
|
434
|
+
defaultMessage: string,
|
|
435
|
+
): (decision: Decision) => string | Promise<string> {
|
|
436
|
+
return async (decision) => {
|
|
437
|
+
if (handler === undefined) return defaultMessage;
|
|
438
|
+
if (typeof handler === 'string') return handler;
|
|
439
|
+
return await handler(decision);
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function errorHandler(
|
|
444
|
+
handler: ErrorHandler | undefined,
|
|
445
|
+
defaultMessage: string | undefined,
|
|
446
|
+
): ((err: SdkError, draft: string) => string | Promise<string>) | undefined {
|
|
447
|
+
if (handler === undefined && defaultMessage === undefined) return undefined;
|
|
448
|
+
return async (err, draft) => {
|
|
449
|
+
if (handler === undefined) return defaultMessage ?? draft;
|
|
450
|
+
if (typeof handler === 'string') return handler;
|
|
451
|
+
return await handler(err, draft);
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function env(...names: string[]): string | undefined {
|
|
456
|
+
const proc = globalThis as typeof globalThis & {
|
|
457
|
+
process?: { env?: Record<string, string | undefined> };
|
|
458
|
+
};
|
|
459
|
+
for (const name of names) {
|
|
460
|
+
const value = proc.process?.env?.[name];
|
|
461
|
+
if (value !== undefined && value.length > 0) return value;
|
|
462
|
+
}
|
|
463
|
+
return undefined;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function clientOptions(opts: GuardFactoryOptions): ClientOptions {
|
|
467
|
+
const clientOpts: ClientOptions = {
|
|
468
|
+
baseUrl:
|
|
469
|
+
opts.baseUrl ??
|
|
470
|
+
env('TL_SERVER_URL', 'TRUSTLOOPGUARD_URL', 'TRUSTLOOP_URL') ??
|
|
471
|
+
'http://127.0.0.1:8080',
|
|
472
|
+
};
|
|
473
|
+
addDefined(
|
|
474
|
+
clientOpts,
|
|
475
|
+
'apiKey',
|
|
476
|
+
opts.apiKey ?? env('TL_API_KEY', 'TRUSTLOOPGUARD_API_KEY', 'TRUSTLOOP_API_KEY'),
|
|
477
|
+
);
|
|
478
|
+
addDefined(clientOpts, 'retry', opts.retry);
|
|
479
|
+
addDefined(clientOpts, 'fetchImpl', opts.fetchImpl);
|
|
480
|
+
addDefined(clientOpts, 'onRetry', opts.onRetry);
|
|
481
|
+
return clientOpts;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function addDefined<T extends object, K extends keyof T>(
|
|
485
|
+
target: T,
|
|
486
|
+
key: K,
|
|
487
|
+
value: T[K] | undefined,
|
|
488
|
+
): void {
|
|
489
|
+
if (value !== undefined) {
|
|
490
|
+
target[key] = value;
|
|
491
|
+
}
|
|
492
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// Public surface of the TrustLoopGuard TypeScript SDK.
|
|
2
|
+
// Type definitions are generated from Rust by `cargo run -p tl-codegen`.
|
|
3
|
+
// See README.md in src/generated for regen instructions.
|
|
4
|
+
|
|
5
|
+
export * from './generated/CheckRequest';
|
|
6
|
+
export * from './generated/Decision';
|
|
7
|
+
export * from './generated/Verdict';
|
|
8
|
+
export * from './generated/Channel';
|
|
9
|
+
export * from './generated/Severity';
|
|
10
|
+
export * from './generated/TriggeredPolicy';
|
|
11
|
+
export * from './generated/AgentAuthority';
|
|
12
|
+
export * from './generated/AgentListResponse';
|
|
13
|
+
export * from './generated/AgentProfile';
|
|
14
|
+
export * from './generated/AgentScope';
|
|
15
|
+
export * from './generated/AgentTone';
|
|
16
|
+
export * from './generated/KnowledgeSource';
|
|
17
|
+
export * from './generated/CreateKnowledgeSourceRequest';
|
|
18
|
+
export * from './generated/DashboardKnowledgeSourceKind';
|
|
19
|
+
export * from './generated/KnowledgeFileInput';
|
|
20
|
+
export * from './generated/KnowledgeFileMetadata';
|
|
21
|
+
export * from './generated/KnowledgeSourceDocument';
|
|
22
|
+
export * from './generated/KnowledgeSourceFileResponse';
|
|
23
|
+
export * from './generated/KnowledgeSourceListResponse';
|
|
24
|
+
export * from './generated/KnowledgeSourceStatus';
|
|
25
|
+
export * from './generated/ApiError';
|
|
26
|
+
export * from './generated/ApiErrorCode';
|
|
27
|
+
export * from './generated/PolicyDocument';
|
|
28
|
+
export * from './generated/PolicyBatchSetEnabledRequest';
|
|
29
|
+
export * from './generated/PolicyBatchSetEnabledResponse';
|
|
30
|
+
export * from './generated/PolicyListResponse';
|
|
31
|
+
export * from './generated/PolicySetEnabledRequest';
|
|
32
|
+
export * from './generated/PolicySummary';
|
|
33
|
+
export * from './generated/PolicyValidateResponse';
|
|
34
|
+
export * from './generated/PolicyValidationIssue';
|
|
35
|
+
export * from './generated/PolicyAction';
|
|
36
|
+
export * from './generated/PolicyMatchType';
|
|
37
|
+
export * from './generated/PolicyDraft';
|
|
38
|
+
export * from './generated/PolicyDraftRequest';
|
|
39
|
+
export * from './generated/PolicyDraftResponse';
|
|
40
|
+
export * from './generated/GuardrailGenerateResponse';
|
|
41
|
+
export * from './generated/GuardrailListResponse';
|
|
42
|
+
export * from './generated/ApiKeyBatchRevokeRequest';
|
|
43
|
+
export * from './generated/ApiKeyBatchRevokeResponse';
|
|
44
|
+
export * from './generated/ApiKeyListResponse';
|
|
45
|
+
export * from './generated/CreateApiKeyRequest';
|
|
46
|
+
export * from './generated/CreateApiKeyResponse';
|
|
47
|
+
export * from './generated/DashboardApiKey';
|
|
48
|
+
export * from './generated/WorkspaceSettings';
|
|
49
|
+
export * from './generated/TraceListResponse';
|
|
50
|
+
export * from './generated/TraceSummary';
|
|
51
|
+
export * from './generated/CreateRunEventRequest';
|
|
52
|
+
export * from './generated/CreateRunRequest';
|
|
53
|
+
export * from './generated/UpdateRunRequest';
|
|
54
|
+
export * from './generated/RunDetail';
|
|
55
|
+
export * from './generated/RunEventKind';
|
|
56
|
+
export * from './generated/RunEventListResponse';
|
|
57
|
+
export * from './generated/RunEventSummary';
|
|
58
|
+
export * from './generated/RunKind';
|
|
59
|
+
export * from './generated/RunListResponse';
|
|
60
|
+
export * from './generated/RunStatus';
|
|
61
|
+
export * from './generated/RunSummary';
|
|
62
|
+
|
|
63
|
+
export { Client } from './client';
|
|
64
|
+
export type { ClientOptions } from './client';
|
|
65
|
+
|
|
66
|
+
export { GuardMode, guard } from './guard';
|
|
67
|
+
export type {
|
|
68
|
+
GuardCallbacks,
|
|
69
|
+
GuardOptions,
|
|
70
|
+
GuardFactoryOptions,
|
|
71
|
+
GuardCallOptions,
|
|
72
|
+
GuardLogEvent,
|
|
73
|
+
OutputGuard,
|
|
74
|
+
RegenerateFeedback,
|
|
75
|
+
} from './guard';
|
|
76
|
+
|
|
77
|
+
export { DEFAULT_RETRY, nextDelay } from './retry';
|
|
78
|
+
export type { RetryConfig } from './retry';
|
|
79
|
+
|
|
80
|
+
export {
|
|
81
|
+
SdkError,
|
|
82
|
+
Invalid,
|
|
83
|
+
Unauthorized,
|
|
84
|
+
Forbidden,
|
|
85
|
+
NotFound,
|
|
86
|
+
Gone,
|
|
87
|
+
Unprocessable,
|
|
88
|
+
RateLimited,
|
|
89
|
+
Internal,
|
|
90
|
+
Unavailable,
|
|
91
|
+
Transport,
|
|
92
|
+
Decode,
|
|
93
|
+
codeFromHttpStatus,
|
|
94
|
+
synthesizeApiError,
|
|
95
|
+
fromResponse,
|
|
96
|
+
parseRetryAfter,
|
|
97
|
+
} from './errors';
|
package/src/retry.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Retry policy for the TrustLoopGuard TypeScript SDK.
|
|
2
|
+
//
|
|
3
|
+
// Mirrors `tl-sdk-rust`'s `RetryConfig` exactly. Same defaults
|
|
4
|
+
// (4 attempts, 30s budget, 200ms base, 8s cap), same `nextDelay`
|
|
5
|
+
// contract: given the attempt number, elapsed time, the error that just
|
|
6
|
+
// occurred, and a jitter fraction, return the delay before the next
|
|
7
|
+
// attempt (in seconds) or `undefined` to stop.
|
|
8
|
+
//
|
|
9
|
+
// The function is pure so it can be unit-tested without spinning up an
|
|
10
|
+
// HTTP server. Production callers feed `Math.random()`; tests pin the
|
|
11
|
+
// value for determinism.
|
|
12
|
+
|
|
13
|
+
import { RateLimited, SdkError } from './errors';
|
|
14
|
+
|
|
15
|
+
export interface RetryConfig {
|
|
16
|
+
/** Total attempts including the initial one. `1` = no retry. */
|
|
17
|
+
maxAttempts: number;
|
|
18
|
+
/** Hard cap on total wall time including sleeps, in seconds. */
|
|
19
|
+
totalBudgetS: number;
|
|
20
|
+
/** Base for exponential backoff: delay = base * 2^(attempt-1). */
|
|
21
|
+
baseDelayS: number;
|
|
22
|
+
/** Cap on a single retry delay; prevents runaway exponential. */
|
|
23
|
+
maxDelayS: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const DEFAULT_RETRY: Readonly<RetryConfig> = Object.freeze({
|
|
27
|
+
maxAttempts: 4,
|
|
28
|
+
totalBudgetS: 30.0,
|
|
29
|
+
baseDelayS: 0.2,
|
|
30
|
+
maxDelayS: 8.0,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Compute the delay before the next attempt, or `undefined` to stop.
|
|
35
|
+
* Pure mirror of `tl_sdk_rust::RetryConfig::next_delay` and
|
|
36
|
+
* `trustloopguard.retry.RetryConfig.next_delay`.
|
|
37
|
+
*/
|
|
38
|
+
export function nextDelay(
|
|
39
|
+
cfg: RetryConfig,
|
|
40
|
+
attempt: number,
|
|
41
|
+
elapsedS: number,
|
|
42
|
+
err: SdkError,
|
|
43
|
+
jitterFraction: number,
|
|
44
|
+
): number | undefined {
|
|
45
|
+
if (!err.isRetriable()) return undefined;
|
|
46
|
+
if (attempt >= cfg.maxAttempts) return undefined;
|
|
47
|
+
if (elapsedS >= cfg.totalBudgetS) return undefined;
|
|
48
|
+
|
|
49
|
+
const exp = cfg.baseDelayS * 2 ** (attempt - 1);
|
|
50
|
+
const capped = Math.min(exp, cfg.maxDelayS);
|
|
51
|
+
|
|
52
|
+
// ±25% jitter: jitterFraction in [0,1] maps to multiplier [0.75, 1.25].
|
|
53
|
+
const frac = Math.max(0, Math.min(1, jitterFraction));
|
|
54
|
+
const multiplier = 0.75 + frac * 0.5;
|
|
55
|
+
const jittered = capped * multiplier;
|
|
56
|
+
|
|
57
|
+
let delay: number;
|
|
58
|
+
if (err instanceof RateLimited && err.retryAfter !== undefined) {
|
|
59
|
+
delay = Math.max(err.retryAfter, jittered);
|
|
60
|
+
} else {
|
|
61
|
+
delay = jittered;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const remaining = cfg.totalBudgetS - elapsedS;
|
|
65
|
+
if (remaining <= 0) return undefined;
|
|
66
|
+
return Math.min(delay, remaining);
|
|
67
|
+
}
|