@opengeni/sdk 0.46.0 → 0.52.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/README.md +94 -2
- package/dist/artifact-client.d.ts +20 -2
- package/dist/artifacts.d.ts +4 -0
- package/dist/artifacts.js +11 -0
- package/dist/artifacts.js.map +1 -0
- package/dist/chunk-FHI4DFIG.js +128 -0
- package/dist/chunk-FHI4DFIG.js.map +1 -0
- package/dist/chunk-FRJFNDIR.js +218 -0
- package/dist/chunk-FRJFNDIR.js.map +1 -0
- package/dist/{chunk-QKV5OCLJ.js → chunk-ILQZR5N6.js} +1380 -595
- package/dist/chunk-ILQZR5N6.js.map +1 -0
- package/dist/chunk-MZWTWOX6.js +661 -0
- package/dist/chunk-MZWTWOX6.js.map +1 -0
- package/dist/chunk-VWE2QIVO.js +1162 -0
- package/dist/chunk-VWE2QIVO.js.map +1 -0
- package/dist/chunk-VYCTL62C.js +230 -0
- package/dist/chunk-VYCTL62C.js.map +1 -0
- package/dist/client.d.ts +176 -20
- package/dist/codex-realtime-controller.d.ts +6 -6
- package/dist/codex-realtime-lifecycle.d.ts +1 -1
- package/dist/codex-realtime-v3.d.ts +3 -3
- package/dist/codex-realtime.d.ts +1 -1
- package/dist/company-profile.d.ts +100 -0
- package/dist/core.d.ts +5 -5
- package/dist/core.js +9 -4
- package/dist/desktop.d.ts +1 -1
- package/dist/editable-artifact-resources.d.ts +96 -0
- package/dist/editable-artifacts/browser-session.d.ts +35 -0
- package/dist/editable-artifacts/controller.d.ts +60 -0
- package/dist/editable-artifacts/errors.d.ts +23 -0
- package/dist/editable-artifacts/http-live-transport.d.ts +46 -0
- package/dist/editable-artifacts/index.d.ts +19 -0
- package/dist/editable-artifacts/pool.d.ts +15 -0
- package/dist/editable-artifacts/session.d.ts +96 -0
- package/dist/editable-artifacts/storage.d.ts +72 -0
- package/dist/editable-artifacts/types.d.ts +416 -0
- package/dist/editable-artifacts/worker/browser-client.d.ts +80 -0
- package/dist/editable-artifacts/worker/browser-entry.d.ts +6 -0
- package/dist/editable-artifacts/worker/kernel-adapter.d.ts +45 -0
- package/dist/editable-artifacts/worker/rpc-protocol.d.ts +92 -0
- package/dist/editable-artifacts/worker/runtime.d.ts +66 -0
- package/dist/editable-artifacts/worker/wire-codec.d.ts +52 -0
- package/dist/editable-artifacts-worker.d.ts +8 -0
- package/dist/editable-artifacts-worker.js +1708 -0
- package/dist/editable-artifacts-worker.js.map +1 -0
- package/dist/editable-artifacts.d.ts +6 -0
- package/dist/editable-artifacts.js +5954 -0
- package/dist/editable-artifacts.js.map +1 -0
- package/dist/gateway-realtime-transport.d.ts +1 -1
- package/dist/index.d.ts +41 -35
- package/dist/index.js +104 -68
- package/dist/index.js.map +1 -1
- package/dist/interaction.d.ts +885 -0
- package/dist/interaction.js +55 -0
- package/dist/interaction.js.map +1 -0
- package/dist/memory-slack-client.d.ts +13 -0
- package/dist/memory-slack-delivery.d.ts +99 -0
- package/dist/memory-slack.d.ts +4 -0
- package/dist/memory-slack.js +44 -0
- package/dist/memory-slack.js.map +1 -0
- package/dist/proxy.d.ts +3 -3
- package/dist/realtime.d.ts +12 -12
- package/dist/retained-artifacts.d.ts +12 -0
- package/dist/stream.d.ts +16 -1
- package/dist/terminal.d.ts +1 -1
- package/dist/transcription.d.ts +2 -2
- package/dist/types.d.ts +1203 -23
- package/dist/workspace-control-stream.d.ts +2 -2
- package/dist/workspace-state.d.ts +1 -1
- package/package.json +29 -3
- package/src/artifact-client.ts +129 -0
- package/src/artifacts.ts +17 -0
- package/src/client.ts +1801 -206
- package/src/company-profile.ts +92 -0
- package/src/editable-artifact-resources.ts +119 -0
- package/src/editable-artifacts/browser-session.ts +176 -0
- package/src/editable-artifacts/controller.ts +2632 -0
- package/src/editable-artifacts/errors.ts +52 -0
- package/src/editable-artifacts/http-live-transport.ts +1449 -0
- package/src/editable-artifacts/index.ts +175 -0
- package/src/editable-artifacts/pool.ts +81 -0
- package/src/editable-artifacts/session.ts +778 -0
- package/src/editable-artifacts/storage.ts +1718 -0
- package/src/editable-artifacts/types.ts +499 -0
- package/src/editable-artifacts/worker/browser-client.ts +1167 -0
- package/src/editable-artifacts/worker/browser-entry.ts +57 -0
- package/src/editable-artifacts/worker/kernel-adapter.ts +1039 -0
- package/src/editable-artifacts/worker/rpc-protocol.ts +622 -0
- package/src/editable-artifacts/worker/runtime.ts +1204 -0
- package/src/editable-artifacts/worker/wire-codec.ts +852 -0
- package/src/editable-artifacts-worker.ts +8 -0
- package/src/editable-artifacts.ts +6 -0
- package/src/index.ts +194 -0
- package/src/interaction.ts +1830 -0
- package/src/memory-slack-client.ts +71 -0
- package/src/memory-slack-delivery.ts +128 -0
- package/src/memory-slack.ts +19 -0
- package/src/retained-artifacts.ts +326 -0
- package/src/stream.ts +102 -4
- package/src/transcription.ts +2 -2
- package/src/types.ts +1430 -33
- package/src/workspace-control-stream.ts +15 -5
- package/dist/chunk-QKV5OCLJ.js.map +0 -1
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { OpenGeniClient as OpenGeniCoreClient } from "./client";
|
|
2
|
+
import type {
|
|
3
|
+
MemorySlackPublication,
|
|
4
|
+
MemorySlackPublicationActionRequest,
|
|
5
|
+
MemorySlackPublicationConfiguration,
|
|
6
|
+
MemorySlackPublicationConfigurationResponse,
|
|
7
|
+
MemorySlackPublicationHistoryResponse,
|
|
8
|
+
SlackPublicationChannelListResponse,
|
|
9
|
+
UpdateMemorySlackPublicationConfigurationRequest,
|
|
10
|
+
} from "./memory-slack-delivery";
|
|
11
|
+
|
|
12
|
+
export type OpenGeniMemorySlackTransport = Pick<OpenGeniCoreClient, "requestJson">;
|
|
13
|
+
|
|
14
|
+
/** Optional Memory-to-Slack API surface, isolated from the eager core client graph. */
|
|
15
|
+
export class OpenGeniMemorySlackClient {
|
|
16
|
+
constructor(private readonly client: OpenGeniMemorySlackTransport) {}
|
|
17
|
+
|
|
18
|
+
async getMemorySlackPublicationConfiguration(
|
|
19
|
+
workspaceId: string,
|
|
20
|
+
): Promise<MemorySlackPublicationConfigurationResponse> {
|
|
21
|
+
return await this.client.requestJson<MemorySlackPublicationConfigurationResponse>(
|
|
22
|
+
"GET",
|
|
23
|
+
`/v1/workspaces/${workspaceId}/memory-slack-publications/configuration`,
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async updateMemorySlackPublicationConfiguration(
|
|
28
|
+
workspaceId: string,
|
|
29
|
+
request: UpdateMemorySlackPublicationConfigurationRequest,
|
|
30
|
+
): Promise<MemorySlackPublicationConfiguration> {
|
|
31
|
+
return await this.client.requestJson<MemorySlackPublicationConfiguration>(
|
|
32
|
+
"PUT",
|
|
33
|
+
`/v1/workspaces/${workspaceId}/memory-slack-publications/configuration`,
|
|
34
|
+
request,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async listMemorySlackPublicationChannels(
|
|
39
|
+
workspaceId: string,
|
|
40
|
+
connectionId: string,
|
|
41
|
+
cursor?: string,
|
|
42
|
+
): Promise<SlackPublicationChannelListResponse> {
|
|
43
|
+
const query = new URLSearchParams({ connectionId });
|
|
44
|
+
if (cursor) query.set("cursor", cursor);
|
|
45
|
+
return await this.client.requestJson<SlackPublicationChannelListResponse>(
|
|
46
|
+
"GET",
|
|
47
|
+
`/v1/workspaces/${workspaceId}/memory-slack-publications/channels?${query}`,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async listMemorySlackPublications(
|
|
52
|
+
workspaceId: string,
|
|
53
|
+
): Promise<MemorySlackPublicationHistoryResponse> {
|
|
54
|
+
return await this.client.requestJson<MemorySlackPublicationHistoryResponse>(
|
|
55
|
+
"GET",
|
|
56
|
+
`/v1/workspaces/${workspaceId}/memory-slack-publications`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async actOnMemorySlackPublication(
|
|
61
|
+
workspaceId: string,
|
|
62
|
+
publicationId: string,
|
|
63
|
+
request: MemorySlackPublicationActionRequest,
|
|
64
|
+
): Promise<MemorySlackPublication> {
|
|
65
|
+
return await this.client.requestJson<MemorySlackPublication>(
|
|
66
|
+
"POST",
|
|
67
|
+
`/v1/workspaces/${workspaceId}/memory-slack-publications/${encodeURIComponent(publicationId)}/action`,
|
|
68
|
+
request,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
export type MemorySlackImportance = "major" | "normal" | "minor";
|
|
2
|
+
export type MemorySlackRequestedMode = "auto" | "review" | "never";
|
|
3
|
+
|
|
4
|
+
export type MemorySlackPublicationDistribution = {
|
|
5
|
+
importance: MemorySlackImportance;
|
|
6
|
+
audience: "workspace";
|
|
7
|
+
slackMode: MemorySlackRequestedMode;
|
|
8
|
+
shareSummary: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type MemorySlackPublicationConfiguration = {
|
|
12
|
+
id: string;
|
|
13
|
+
workspaceId: string;
|
|
14
|
+
revision: number;
|
|
15
|
+
enabled: boolean;
|
|
16
|
+
connectionId: string | null;
|
|
17
|
+
slackTeamId: string | null;
|
|
18
|
+
slackChannelId: string | null;
|
|
19
|
+
slackChannelName: string | null;
|
|
20
|
+
autoImportances: MemorySlackImportance[];
|
|
21
|
+
reviewImportances: MemorySlackImportance[];
|
|
22
|
+
createdBySubjectId: string;
|
|
23
|
+
createdAt: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type UpdateMemorySlackPublicationConfigurationRequest = {
|
|
27
|
+
expectedRevision: number;
|
|
28
|
+
enabled: boolean;
|
|
29
|
+
connectionId: string | null;
|
|
30
|
+
slackChannelId: string | null;
|
|
31
|
+
slackChannelName: string | null;
|
|
32
|
+
autoImportances: MemorySlackImportance[];
|
|
33
|
+
reviewImportances: MemorySlackImportance[];
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type MemorySlackPublicationConfigurationResponse = {
|
|
37
|
+
current: MemorySlackPublicationConfiguration | null;
|
|
38
|
+
history: MemorySlackPublicationConfiguration[];
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type MemorySlackPublicationState =
|
|
42
|
+
| "review_pending"
|
|
43
|
+
| "queued"
|
|
44
|
+
| "delivering"
|
|
45
|
+
| "retry_wait"
|
|
46
|
+
| "delivered"
|
|
47
|
+
| "rejected"
|
|
48
|
+
| "failed"
|
|
49
|
+
| "cancelled";
|
|
50
|
+
|
|
51
|
+
export type MemorySlackPublicationReceiptKind =
|
|
52
|
+
| "enqueued"
|
|
53
|
+
| "review_approved"
|
|
54
|
+
| "review_rejected"
|
|
55
|
+
| "delivery_claimed"
|
|
56
|
+
| "retry_scheduled"
|
|
57
|
+
| "delivered"
|
|
58
|
+
| "failed"
|
|
59
|
+
| "cancelled"
|
|
60
|
+
| "manual_retry";
|
|
61
|
+
|
|
62
|
+
export type MemorySlackPublicationReceipt = {
|
|
63
|
+
id: string;
|
|
64
|
+
publicationId: string;
|
|
65
|
+
sequence: number;
|
|
66
|
+
kind: MemorySlackPublicationReceiptKind;
|
|
67
|
+
state: MemorySlackPublicationState;
|
|
68
|
+
attemptNumber: number;
|
|
69
|
+
actorKind: "human" | "agent" | "service";
|
|
70
|
+
actorSubjectId: string;
|
|
71
|
+
operationId: string;
|
|
72
|
+
errorCode: string | null;
|
|
73
|
+
retryAt: string | null;
|
|
74
|
+
slackChannelId: string | null;
|
|
75
|
+
slackMessageTimestamp: string | null;
|
|
76
|
+
createdAt: string;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export type MemorySlackPublication = {
|
|
80
|
+
id: string;
|
|
81
|
+
workspaceId: string;
|
|
82
|
+
configurationRevision: number;
|
|
83
|
+
connectionId: string;
|
|
84
|
+
slackTeamId: string;
|
|
85
|
+
slackChannelId: string;
|
|
86
|
+
sourceType: "workspace_memory" | "durable_learning";
|
|
87
|
+
sourceId: string;
|
|
88
|
+
sourceVersion: string | null;
|
|
89
|
+
importance: MemorySlackImportance;
|
|
90
|
+
deliveryMode: "auto" | "review";
|
|
91
|
+
state: MemorySlackPublicationState;
|
|
92
|
+
summary: string;
|
|
93
|
+
sourceLabel: string;
|
|
94
|
+
authoritativePath: string | null;
|
|
95
|
+
initiatorKind: "human" | "agent" | "service";
|
|
96
|
+
initiatorSubjectId: string;
|
|
97
|
+
initiatingHumanSubjectId: string | null;
|
|
98
|
+
attemptCount: number;
|
|
99
|
+
retryAt: string | null;
|
|
100
|
+
lastErrorCode: string | null;
|
|
101
|
+
slackMessageTimestamp: string | null;
|
|
102
|
+
deliveredAt: string | null;
|
|
103
|
+
terminalAt: string | null;
|
|
104
|
+
createdAt: string;
|
|
105
|
+
updatedAt: string;
|
|
106
|
+
receipts: MemorySlackPublicationReceipt[];
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export type MemorySlackPublicationHistoryResponse = {
|
|
110
|
+
publications: MemorySlackPublication[];
|
|
111
|
+
nextCursor: string | null;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export type MemorySlackPublicationActionRequest = {
|
|
115
|
+
action: "approve" | "reject" | "retry";
|
|
116
|
+
expectedState: MemorySlackPublicationState;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export type SlackPublicationChannel = {
|
|
120
|
+
id: string;
|
|
121
|
+
name: string | null;
|
|
122
|
+
isPrivate: boolean;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export type SlackPublicationChannelListResponse = {
|
|
126
|
+
channels: SlackPublicationChannel[];
|
|
127
|
+
nextCursor: string | null;
|
|
128
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Optional Memory-to-Slack API client; import this entry only where the capability is used. */
|
|
2
|
+
export { OpenGeniMemorySlackClient } from "./memory-slack-client";
|
|
3
|
+
export type { OpenGeniMemorySlackTransport } from "./memory-slack-client";
|
|
4
|
+
export type {
|
|
5
|
+
MemorySlackImportance,
|
|
6
|
+
MemorySlackPublication,
|
|
7
|
+
MemorySlackPublicationActionRequest,
|
|
8
|
+
MemorySlackPublicationConfiguration,
|
|
9
|
+
MemorySlackPublicationConfigurationResponse,
|
|
10
|
+
MemorySlackPublicationDistribution,
|
|
11
|
+
MemorySlackPublicationHistoryResponse,
|
|
12
|
+
MemorySlackPublicationReceipt,
|
|
13
|
+
MemorySlackPublicationReceiptKind,
|
|
14
|
+
MemorySlackPublicationState,
|
|
15
|
+
MemorySlackRequestedMode,
|
|
16
|
+
SlackPublicationChannel,
|
|
17
|
+
SlackPublicationChannelListResponse,
|
|
18
|
+
UpdateMemorySlackPublicationConfigurationRequest,
|
|
19
|
+
} from "./memory-slack-delivery";
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
GeneratedImageReceipt,
|
|
3
|
+
GeneratedVideoReceipt,
|
|
4
|
+
MediaGenerationResult,
|
|
5
|
+
RetainedArtifactReference,
|
|
6
|
+
} from "./types";
|
|
7
|
+
|
|
8
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
9
|
+
const SHA256 = /^[0-9a-f]{64}$/;
|
|
10
|
+
const ISO_DATETIME_WITH_OFFSET =
|
|
11
|
+
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
|
|
12
|
+
const WORKSPACE_PATH = /^\/v1\/workspaces\/([0-9a-f-]+)\/artifacts\/([0-9a-f-]+)\/content$/;
|
|
13
|
+
const MAX_BYTES = 64 * 1024 * 1024;
|
|
14
|
+
const MAX_VIDEO_BYTES = 512 * 1024 * 1024;
|
|
15
|
+
const MAX_DIMENSION = 16_384;
|
|
16
|
+
const MAX_PIXELS = 67_108_864;
|
|
17
|
+
const MAX_RANGE_BYTES = 1024 * 1024;
|
|
18
|
+
|
|
19
|
+
/** Zero-dependency validation for the SDK's permanent generated-image wire receipt. */
|
|
20
|
+
export function parseRetainedGeneratedImageReference(
|
|
21
|
+
value: unknown,
|
|
22
|
+
expectedWorkspaceId?: string,
|
|
23
|
+
): RetainedArtifactReference | null {
|
|
24
|
+
if (!recordWithKeys(value, GENERATED_IMAGE_REFERENCE_KEYS)) return null;
|
|
25
|
+
const artifact = value as Record<string, unknown>;
|
|
26
|
+
if (
|
|
27
|
+
artifact.available !== true ||
|
|
28
|
+
artifact.kind !== "generated_image" ||
|
|
29
|
+
typeof artifact.artifactId !== "string" ||
|
|
30
|
+
!UUID.test(artifact.artifactId) ||
|
|
31
|
+
!["image/png", "image/jpeg", "image/webp"].includes(String(artifact.contentType)) ||
|
|
32
|
+
!safeIntegerBetween(artifact.originalBytes, 1, MAX_BYTES) ||
|
|
33
|
+
typeof artifact.sha256 !== "string" ||
|
|
34
|
+
!SHA256.test(artifact.sha256) ||
|
|
35
|
+
typeof artifact.retainedAt !== "string" ||
|
|
36
|
+
artifact.retainedAt.length > 64 ||
|
|
37
|
+
!ISO_DATETIME_WITH_OFFSET.test(artifact.retainedAt) ||
|
|
38
|
+
!Number.isFinite(Date.parse(artifact.retainedAt)) ||
|
|
39
|
+
!recordWithKeys(artifact.dimensions, DIMENSION_KEYS) ||
|
|
40
|
+
!recordWithKeys(artifact.retention, RETENTION_KEYS) ||
|
|
41
|
+
!recordWithKeys(artifact.retrieval, RETRIEVAL_KEYS)
|
|
42
|
+
) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
const dimensions = artifact.dimensions as Record<string, unknown>;
|
|
46
|
+
if (
|
|
47
|
+
!safeIntegerBetween(dimensions.width, 1, MAX_DIMENSION) ||
|
|
48
|
+
!safeIntegerBetween(dimensions.height, 1, MAX_DIMENSION) ||
|
|
49
|
+
Number(dimensions.width) * Number(dimensions.height) > MAX_PIXELS
|
|
50
|
+
) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
const retention = artifact.retention as Record<string, unknown>;
|
|
54
|
+
if (retention.policy !== "workspace_file" || retention.expiresAt !== null) return null;
|
|
55
|
+
const retrieval = artifact.retrieval as Record<string, unknown>;
|
|
56
|
+
if (
|
|
57
|
+
retrieval.method !== "GET" ||
|
|
58
|
+
retrieval.acceptRanges !== "bytes" ||
|
|
59
|
+
retrieval.maxRangeBytes !== MAX_RANGE_BYTES ||
|
|
60
|
+
typeof retrieval.path !== "string" ||
|
|
61
|
+
retrieval.path.length > 256
|
|
62
|
+
) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
const match = WORKSPACE_PATH.exec(retrieval.path);
|
|
66
|
+
if (
|
|
67
|
+
!match ||
|
|
68
|
+
!UUID.test(match[1] ?? "") ||
|
|
69
|
+
match[2] !== artifact.artifactId ||
|
|
70
|
+
(expectedWorkspaceId !== undefined && match[1] !== expectedWorkspaceId)
|
|
71
|
+
) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
return value as RetainedArtifactReference;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function generatedImageSandboxPathMatches(
|
|
78
|
+
artifact: RetainedArtifactReference,
|
|
79
|
+
sandboxPath: unknown,
|
|
80
|
+
): sandboxPath is string {
|
|
81
|
+
if (typeof sandboxPath !== "string") return false;
|
|
82
|
+
const extension =
|
|
83
|
+
artifact.contentType === "image/png"
|
|
84
|
+
? "png"
|
|
85
|
+
: artifact.contentType === "image/jpeg"
|
|
86
|
+
? "jpg"
|
|
87
|
+
: artifact.contentType === "image/webp"
|
|
88
|
+
? "webp"
|
|
89
|
+
: null;
|
|
90
|
+
return (
|
|
91
|
+
extension !== null &&
|
|
92
|
+
sandboxPath ===
|
|
93
|
+
`/workspace/generated-images/generated-image-${artifact.artifactId}.${extension}`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Parse the complete closed generated-image tool result. */
|
|
98
|
+
export function parseGeneratedImageReceipt(
|
|
99
|
+
value: unknown,
|
|
100
|
+
expectedWorkspaceId?: string,
|
|
101
|
+
): GeneratedImageReceipt | null {
|
|
102
|
+
if (typeof value === "string" && value.length <= 4_096) {
|
|
103
|
+
try {
|
|
104
|
+
return parseGeneratedImageReceipt(JSON.parse(value), expectedWorkspaceId);
|
|
105
|
+
} catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (!recordWithKeys(value, GENERATED_IMAGE_RECEIPT_KEYS)) return null;
|
|
110
|
+
if (value.type !== "generated_image") return null;
|
|
111
|
+
const artifact = parseRetainedGeneratedImageReference(value.artifact, expectedWorkspaceId);
|
|
112
|
+
if (!artifact || !generatedImageSandboxPathMatches(artifact, value.sandboxPath)) return null;
|
|
113
|
+
return { type: "generated_image", artifact, sandboxPath: value.sandboxPath };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Zero-dependency validation for a permanent generated-video reference. */
|
|
117
|
+
export function parseRetainedGeneratedVideoReference(
|
|
118
|
+
value: unknown,
|
|
119
|
+
expectedWorkspaceId?: string,
|
|
120
|
+
): RetainedArtifactReference | null {
|
|
121
|
+
if (!recordWithKeys(value, GENERATED_IMAGE_REFERENCE_KEYS)) return null;
|
|
122
|
+
const artifact = value as Record<string, unknown>;
|
|
123
|
+
if (
|
|
124
|
+
artifact.available !== true ||
|
|
125
|
+
artifact.kind !== "generated_video" ||
|
|
126
|
+
typeof artifact.artifactId !== "string" ||
|
|
127
|
+
!UUID.test(artifact.artifactId) ||
|
|
128
|
+
artifact.contentType !== "video/mp4" ||
|
|
129
|
+
!safeIntegerBetween(artifact.originalBytes, 1, MAX_VIDEO_BYTES) ||
|
|
130
|
+
typeof artifact.sha256 !== "string" ||
|
|
131
|
+
!SHA256.test(artifact.sha256) ||
|
|
132
|
+
typeof artifact.retainedAt !== "string" ||
|
|
133
|
+
artifact.retainedAt.length > 64 ||
|
|
134
|
+
!ISO_DATETIME_WITH_OFFSET.test(artifact.retainedAt) ||
|
|
135
|
+
!Number.isFinite(Date.parse(artifact.retainedAt)) ||
|
|
136
|
+
!recordWithKeys(artifact.dimensions, DIMENSION_KEYS) ||
|
|
137
|
+
!recordWithKeys(artifact.retention, RETENTION_KEYS) ||
|
|
138
|
+
!recordWithKeys(artifact.retrieval, RETRIEVAL_KEYS)
|
|
139
|
+
) {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
const dimensions = artifact.dimensions as Record<string, unknown>;
|
|
143
|
+
if (
|
|
144
|
+
!safeIntegerBetween(dimensions.width, 1, 8_192) ||
|
|
145
|
+
!safeIntegerBetween(dimensions.height, 1, 8_192)
|
|
146
|
+
) {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
const retention = artifact.retention as Record<string, unknown>;
|
|
150
|
+
if (retention.policy !== "workspace_file" || retention.expiresAt !== null) return null;
|
|
151
|
+
const retrieval = artifact.retrieval as Record<string, unknown>;
|
|
152
|
+
if (
|
|
153
|
+
retrieval.method !== "GET" ||
|
|
154
|
+
retrieval.acceptRanges !== "bytes" ||
|
|
155
|
+
retrieval.maxRangeBytes !== MAX_RANGE_BYTES ||
|
|
156
|
+
typeof retrieval.path !== "string" ||
|
|
157
|
+
retrieval.path.length > 256
|
|
158
|
+
) {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
const match = WORKSPACE_PATH.exec(retrieval.path);
|
|
162
|
+
if (
|
|
163
|
+
!match ||
|
|
164
|
+
!UUID.test(match[1] ?? "") ||
|
|
165
|
+
match[2] !== artifact.artifactId ||
|
|
166
|
+
(expectedWorkspaceId !== undefined && match[1] !== expectedWorkspaceId)
|
|
167
|
+
) {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
return value as RetainedArtifactReference;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Parse the compact successful video result placed in model/timeline history. */
|
|
174
|
+
export function parseGeneratedVideoReceipt(
|
|
175
|
+
value: unknown,
|
|
176
|
+
expectedWorkspaceId?: string,
|
|
177
|
+
): GeneratedVideoReceipt | null {
|
|
178
|
+
if (typeof value === "string" && value.length <= 8_192) {
|
|
179
|
+
try {
|
|
180
|
+
return parseGeneratedVideoReceipt(JSON.parse(value), expectedWorkspaceId);
|
|
181
|
+
} catch {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (!recordWithKeys(value, GENERATED_VIDEO_RECEIPT_KEYS)) return null;
|
|
186
|
+
if (
|
|
187
|
+
value.type !== "generated_video" ||
|
|
188
|
+
value.schemaVersion !== 1 ||
|
|
189
|
+
typeof value.operationId !== "string" ||
|
|
190
|
+
!UUID.test(value.operationId) ||
|
|
191
|
+
!recordWithKeys(value.video, GENERATED_VIDEO_FACT_KEYS)
|
|
192
|
+
) {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
const artifact = parseRetainedGeneratedVideoReference(value.artifact, expectedWorkspaceId);
|
|
196
|
+
const video = value.video as Record<string, unknown>;
|
|
197
|
+
if (
|
|
198
|
+
!artifact ||
|
|
199
|
+
typeof value.sandboxPath !== "string" ||
|
|
200
|
+
value.sandboxPath !==
|
|
201
|
+
`/workspace/generated-videos/generated-video-${artifact.artifactId}.mp4` ||
|
|
202
|
+
typeof video.durationSeconds !== "number" ||
|
|
203
|
+
!Number.isFinite(video.durationSeconds) ||
|
|
204
|
+
video.durationSeconds <= 0 ||
|
|
205
|
+
video.durationSeconds > 120 ||
|
|
206
|
+
video.width !== artifact.dimensions?.width ||
|
|
207
|
+
video.height !== artifact.dimensions?.height ||
|
|
208
|
+
typeof video.fps !== "number" ||
|
|
209
|
+
!Number.isFinite(video.fps) ||
|
|
210
|
+
video.fps <= 0 ||
|
|
211
|
+
video.fps > 120 ||
|
|
212
|
+
typeof video.hasAudio !== "boolean" ||
|
|
213
|
+
video.videoCodec !== "h264" ||
|
|
214
|
+
(video.audioCodec !== null && video.audioCodec !== "aac")
|
|
215
|
+
) {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
return value as GeneratedVideoReceipt;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Parse one closed terminal envelope; failures never fabricate an artifact. */
|
|
222
|
+
export function parseMediaGenerationResult(
|
|
223
|
+
value: unknown,
|
|
224
|
+
expectedWorkspaceId?: string,
|
|
225
|
+
): MediaGenerationResult | null {
|
|
226
|
+
if (typeof value === "string" && value.length <= 12_000) {
|
|
227
|
+
try {
|
|
228
|
+
return parseMediaGenerationResult(JSON.parse(value), expectedWorkspaceId);
|
|
229
|
+
} catch {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
234
|
+
const result = value as Record<string, unknown>;
|
|
235
|
+
if (
|
|
236
|
+
result.type !== "media_generation_result" ||
|
|
237
|
+
result.schemaVersion !== 1 ||
|
|
238
|
+
typeof result.operationId !== "string" ||
|
|
239
|
+
!UUID.test(result.operationId)
|
|
240
|
+
) {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
if (result.status === "ready") {
|
|
244
|
+
if (!recordWithKeys(result, MEDIA_RESULT_READY_KEYS)) return null;
|
|
245
|
+
const receipt = parseGeneratedVideoReceipt(result.receipt, expectedWorkspaceId);
|
|
246
|
+
if (!receipt || receipt.operationId !== result.operationId) return null;
|
|
247
|
+
return value as MediaGenerationResult;
|
|
248
|
+
}
|
|
249
|
+
if (
|
|
250
|
+
!["provider_failed", "retention_failed", "cancelled_before_submit", "outcome_unknown"].includes(
|
|
251
|
+
String(result.status),
|
|
252
|
+
) ||
|
|
253
|
+
!recordWithKeys(result, MEDIA_RESULT_FAILURE_KEYS) ||
|
|
254
|
+
typeof result.boundedPublicReason !== "string" ||
|
|
255
|
+
result.boundedPublicReason.length < 1 ||
|
|
256
|
+
result.boundedPublicReason.length > 1_000
|
|
257
|
+
) {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
return value as MediaGenerationResult;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const GENERATED_IMAGE_REFERENCE_KEYS = new Set([
|
|
264
|
+
"available",
|
|
265
|
+
"artifactId",
|
|
266
|
+
"kind",
|
|
267
|
+
"contentType",
|
|
268
|
+
"originalBytes",
|
|
269
|
+
"sha256",
|
|
270
|
+
"retainedAt",
|
|
271
|
+
"dimensions",
|
|
272
|
+
"retention",
|
|
273
|
+
"retrieval",
|
|
274
|
+
]);
|
|
275
|
+
const DIMENSION_KEYS = new Set(["width", "height"]);
|
|
276
|
+
const RETENTION_KEYS = new Set(["policy", "expiresAt"]);
|
|
277
|
+
const RETRIEVAL_KEYS = new Set(["method", "path", "acceptRanges", "maxRangeBytes"]);
|
|
278
|
+
const GENERATED_IMAGE_RECEIPT_KEYS = new Set(["type", "artifact", "sandboxPath"]);
|
|
279
|
+
const GENERATED_VIDEO_RECEIPT_KEYS = new Set([
|
|
280
|
+
"type",
|
|
281
|
+
"schemaVersion",
|
|
282
|
+
"operationId",
|
|
283
|
+
"artifact",
|
|
284
|
+
"video",
|
|
285
|
+
"sandboxPath",
|
|
286
|
+
]);
|
|
287
|
+
const GENERATED_VIDEO_FACT_KEYS = new Set([
|
|
288
|
+
"durationSeconds",
|
|
289
|
+
"width",
|
|
290
|
+
"height",
|
|
291
|
+
"fps",
|
|
292
|
+
"hasAudio",
|
|
293
|
+
"videoCodec",
|
|
294
|
+
"audioCodec",
|
|
295
|
+
]);
|
|
296
|
+
const MEDIA_RESULT_READY_KEYS = new Set([
|
|
297
|
+
"type",
|
|
298
|
+
"schemaVersion",
|
|
299
|
+
"status",
|
|
300
|
+
"operationId",
|
|
301
|
+
"receipt",
|
|
302
|
+
]);
|
|
303
|
+
const MEDIA_RESULT_FAILURE_KEYS = new Set([
|
|
304
|
+
"type",
|
|
305
|
+
"schemaVersion",
|
|
306
|
+
"status",
|
|
307
|
+
"operationId",
|
|
308
|
+
"boundedPublicReason",
|
|
309
|
+
]);
|
|
310
|
+
|
|
311
|
+
function recordWithKeys(
|
|
312
|
+
value: unknown,
|
|
313
|
+
allowed: ReadonlySet<string>,
|
|
314
|
+
): value is Record<string, unknown> {
|
|
315
|
+
return (
|
|
316
|
+
value !== null &&
|
|
317
|
+
typeof value === "object" &&
|
|
318
|
+
!Array.isArray(value) &&
|
|
319
|
+
Object.keys(value).length === allowed.size &&
|
|
320
|
+
Object.keys(value).every((key) => allowed.has(key))
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function safeIntegerBetween(value: unknown, min: number, max: number): boolean {
|
|
325
|
+
return Number.isSafeInteger(value) && Number(value) >= min && Number(value) <= max;
|
|
326
|
+
}
|
package/src/stream.ts
CHANGED
|
@@ -29,13 +29,25 @@ export type StreamSessionEventsOptions = {
|
|
|
29
29
|
reconnectDelayMs?: number;
|
|
30
30
|
/** Backoff ceiling. Defaults to 10s. */
|
|
31
31
|
maxReconnectDelayMs?: number;
|
|
32
|
+
/** Random backoff spread as a fraction of the delay. Defaults to 0.2. */
|
|
33
|
+
reconnectJitterRatio?: number;
|
|
32
34
|
/**
|
|
33
35
|
* Give up after this many consecutive failed reconnect attempts (i.e. N
|
|
34
36
|
* reconnects = N+1 total open-stream calls). Defaults to unlimited.
|
|
35
37
|
*/
|
|
36
38
|
maxReconnectAttempts?: number;
|
|
39
|
+
/**
|
|
40
|
+
* Notify the consumer as soon as the SSE response body is open. This callback
|
|
41
|
+
* is deliberately synchronous and non-blocking: use it to start projection
|
|
42
|
+
* reconciliation without delaying consumption of live event bytes.
|
|
43
|
+
*/
|
|
44
|
+
onOpen?: (() => void) | undefined;
|
|
37
45
|
/** Await authoritative client reconciliation before exposing `live`. */
|
|
38
46
|
beforeLive?: (() => void | Promise<void>) | undefined;
|
|
47
|
+
/** Maximum reconciliation time before reconnecting. Defaults to 15s. */
|
|
48
|
+
beforeLiveTimeoutMs?: number;
|
|
49
|
+
/** Maximum time without any SSE bytes before reconnecting. Defaults to 45s. */
|
|
50
|
+
heartbeatTimeoutMs?: number;
|
|
39
51
|
onStateChange?: (state: StreamConnectionState) => void;
|
|
40
52
|
};
|
|
41
53
|
|
|
@@ -62,6 +74,9 @@ export async function* streamSessionEvents(
|
|
|
62
74
|
const reconnect = options.reconnect ?? true;
|
|
63
75
|
const baseDelayMs = options.reconnectDelayMs ?? 500;
|
|
64
76
|
const maxDelayMs = options.maxReconnectDelayMs ?? 10_000;
|
|
77
|
+
const jitterRatio = options.reconnectJitterRatio ?? 0.2;
|
|
78
|
+
const beforeLiveTimeoutMs = options.beforeLiveTimeoutMs ?? 15_000;
|
|
79
|
+
const heartbeatTimeoutMs = options.heartbeatTimeoutMs ?? 45_000;
|
|
65
80
|
const maxAttempts = options.maxReconnectAttempts ?? Number.POSITIVE_INFINITY;
|
|
66
81
|
let cursor = options.after ?? 0;
|
|
67
82
|
let failedAttempts = 0;
|
|
@@ -77,9 +92,16 @@ export async function* streamSessionEvents(
|
|
|
77
92
|
everConnected = true;
|
|
78
93
|
failedAttempts = 0;
|
|
79
94
|
delayMs = baseDelayMs;
|
|
80
|
-
|
|
95
|
+
try {
|
|
96
|
+
options.onOpen?.();
|
|
97
|
+
} catch {
|
|
98
|
+
// A notification observer cannot own or interrupt the event transport.
|
|
99
|
+
}
|
|
100
|
+
await runBeforeLive(options.beforeLive, beforeLiveTimeoutMs, signal);
|
|
81
101
|
options.onStateChange?.("live");
|
|
82
|
-
for await (const message of parseSseStream(
|
|
102
|
+
for await (const message of parseSseStream(
|
|
103
|
+
withStreamInactivityTimeout(body, heartbeatTimeoutMs, signal),
|
|
104
|
+
)) {
|
|
83
105
|
// Re-check after every yield resumption: an abort from the consumer
|
|
84
106
|
// must not let already-buffered events keep flowing.
|
|
85
107
|
if (signal?.aborted) {
|
|
@@ -108,7 +130,7 @@ export async function* streamSessionEvents(
|
|
|
108
130
|
// progress (servers legitimately cycle long SSE connections); pace
|
|
109
131
|
// empty closes so a misbehaving server is not hammered in a hot loop.
|
|
110
132
|
if (cursor === cursorAtOpen) {
|
|
111
|
-
await sleep(baseDelayMs, signal);
|
|
133
|
+
await sleep(jitteredDelay(baseDelayMs, jitterRatio), signal);
|
|
112
134
|
}
|
|
113
135
|
continue;
|
|
114
136
|
} catch (error) {
|
|
@@ -125,11 +147,87 @@ export async function* streamSessionEvents(
|
|
|
125
147
|
);
|
|
126
148
|
}
|
|
127
149
|
}
|
|
128
|
-
await sleep(delayMs, signal);
|
|
150
|
+
await sleep(jitteredDelay(delayMs, jitterRatio), signal);
|
|
129
151
|
delayMs = Math.min(Math.max(delayMs * 2, baseDelayMs), maxDelayMs);
|
|
130
152
|
}
|
|
131
153
|
}
|
|
132
154
|
|
|
155
|
+
export async function runBeforeLive(
|
|
156
|
+
beforeLive: (() => void | Promise<void>) | undefined,
|
|
157
|
+
timeoutMs: number,
|
|
158
|
+
signal: AbortSignal | undefined,
|
|
159
|
+
): Promise<void> {
|
|
160
|
+
if (!beforeLive) return;
|
|
161
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
162
|
+
let abortListener: (() => void) | undefined;
|
|
163
|
+
const timeout = new Promise<never>((_resolve, reject) => {
|
|
164
|
+
timer = setTimeout(
|
|
165
|
+
() => reject(new TypeError(`stream reconciliation timed out after ${timeoutMs}ms`)),
|
|
166
|
+
timeoutMs,
|
|
167
|
+
);
|
|
168
|
+
if (signal) {
|
|
169
|
+
abortListener = () => reject(new DOMException("Aborted", "AbortError"));
|
|
170
|
+
signal.addEventListener("abort", abortListener, { once: true });
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
try {
|
|
174
|
+
await Promise.race([Promise.resolve().then(beforeLive), timeout]);
|
|
175
|
+
} finally {
|
|
176
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
177
|
+
if (signal && abortListener) signal.removeEventListener("abort", abortListener);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function jitteredDelay(delayMs: number, ratio: number): number {
|
|
182
|
+
if (delayMs <= 0 || ratio <= 0) return delayMs;
|
|
183
|
+
const boundedRatio = Math.min(ratio, 1);
|
|
184
|
+
const spread = delayMs * boundedRatio;
|
|
185
|
+
return Math.max(0, delayMs - spread + Math.random() * spread * 2);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function withStreamInactivityTimeout(
|
|
189
|
+
stream: ReadableStream<Uint8Array>,
|
|
190
|
+
timeoutMs: number,
|
|
191
|
+
signal: AbortSignal | undefined,
|
|
192
|
+
): ReadableStream<Uint8Array> {
|
|
193
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) {
|
|
194
|
+
throw new RangeError("stream heartbeat timeout must be a positive safe integer");
|
|
195
|
+
}
|
|
196
|
+
const reader = stream.getReader();
|
|
197
|
+
return new ReadableStream<Uint8Array>({
|
|
198
|
+
pull: async (controller) => {
|
|
199
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
200
|
+
let abortListener: (() => void) | undefined;
|
|
201
|
+
try {
|
|
202
|
+
const result = await Promise.race([
|
|
203
|
+
reader.read(),
|
|
204
|
+
new Promise<never>((_resolve, reject) => {
|
|
205
|
+
timer = setTimeout(
|
|
206
|
+
() => reject(new TypeError(`event stream heartbeat timed out after ${timeoutMs}ms`)),
|
|
207
|
+
timeoutMs,
|
|
208
|
+
);
|
|
209
|
+
if (signal) {
|
|
210
|
+
abortListener = () => reject(new DOMException("Aborted", "AbortError"));
|
|
211
|
+
signal.addEventListener("abort", abortListener, { once: true });
|
|
212
|
+
}
|
|
213
|
+
}),
|
|
214
|
+
]);
|
|
215
|
+
if (result.done) controller.close();
|
|
216
|
+
else controller.enqueue(result.value);
|
|
217
|
+
} catch (error) {
|
|
218
|
+
void reader.cancel(error).catch(() => {});
|
|
219
|
+
controller.error(error);
|
|
220
|
+
} finally {
|
|
221
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
222
|
+
if (signal && abortListener) signal.removeEventListener("abort", abortListener);
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
cancel: async (reason) => {
|
|
226
|
+
await reader.cancel(reason);
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
133
231
|
/**
|
|
134
232
|
* Yield the durable events with `fromExclusive < sequence <= toInclusive`,
|
|
135
233
|
* in order. Sequences are contiguous, so every one of them must exist in the
|