@sync.so/mcp-server 0.1.3 → 0.2.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 +261 -86
- package/deploy/generation-status/app.html.gz +0 -0
- package/deploy/generation-status/release.json +6 -0
- package/dist/abort-signals.d.ts +6 -0
- package/dist/abort-signals.d.ts.map +1 -0
- package/dist/abort-signals.js +23 -0
- package/dist/account-features.d.ts +7 -0
- package/dist/account-features.d.ts.map +1 -0
- package/dist/account-features.js +48 -0
- package/dist/app-upload-relay.d.ts +13 -0
- package/dist/app-upload-relay.d.ts.map +1 -0
- package/dist/app-upload-relay.js +136 -0
- package/dist/asset-thumbnail.d.ts +3 -0
- package/dist/asset-thumbnail.d.ts.map +1 -0
- package/dist/asset-thumbnail.js +2 -0
- package/dist/auth/async-context.d.ts +5 -0
- package/dist/auth/async-context.d.ts.map +1 -0
- package/dist/auth/async-context.js +14 -0
- package/dist/auth/bearer-auth.d.ts +16 -0
- package/dist/auth/bearer-auth.d.ts.map +1 -0
- package/dist/auth/bearer-auth.js +55 -0
- package/dist/auth/client-credentials.d.ts +13 -0
- package/dist/auth/client-credentials.d.ts.map +1 -0
- package/dist/auth/client-credentials.js +67 -0
- package/dist/auth/device-auth.d.ts.map +1 -1
- package/dist/auth/device-auth.js +3 -2
- package/dist/auth/fixture-cleanup.d.ts +4 -0
- package/dist/auth/fixture-cleanup.d.ts.map +1 -0
- package/dist/auth/fixture-cleanup.js +9 -0
- package/dist/auth/index.d.ts +2 -0
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +2 -0
- package/dist/auth/oauth-diagnostics.d.ts +11 -0
- package/dist/auth/oauth-diagnostics.d.ts.map +1 -0
- package/dist/auth/oauth-diagnostics.js +78 -0
- package/dist/auth/oauth-provider.d.ts +16 -0
- package/dist/auth/oauth-provider.d.ts.map +1 -0
- package/dist/auth/oauth-provider.js +107 -0
- package/dist/auth/resource-metadata.d.ts +2 -0
- package/dist/auth/resource-metadata.d.ts.map +1 -0
- package/dist/auth/resource-metadata.js +3 -0
- package/dist/auth/token-verification.d.ts +11 -0
- package/dist/auth/token-verification.d.ts.map +1 -0
- package/dist/auth/token-verification.js +99 -0
- package/dist/auth/verification-cache.d.ts +8 -0
- package/dist/auth/verification-cache.d.ts.map +1 -0
- package/dist/auth/verification-cache.js +111 -0
- package/dist/chatgpt-analytics.d.ts +22 -0
- package/dist/chatgpt-analytics.d.ts.map +1 -0
- package/dist/chatgpt-analytics.js +172 -0
- package/dist/chatgpt-app.d.ts +15 -0
- package/dist/chatgpt-app.d.ts.map +1 -0
- package/dist/chatgpt-app.js +133 -0
- package/dist/chatgpt-release.d.ts +12 -0
- package/dist/chatgpt-release.d.ts.map +1 -0
- package/dist/chatgpt-release.js +30 -0
- package/dist/cli.js +30 -16
- package/dist/client-profile.d.ts +10 -0
- package/dist/client-profile.d.ts.map +1 -0
- package/dist/client-profile.js +45 -0
- package/dist/config.d.ts +17 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +52 -0
- package/dist/generation-status-html.d.ts +2 -0
- package/dist/generation-status-html.d.ts.map +1 -0
- package/dist/generation-status-html.js +26 -0
- package/dist/generation-status.d.ts +6 -0
- package/dist/generation-status.d.ts.map +1 -0
- package/dist/generation-status.js +37 -0
- package/dist/http-client.d.ts +45 -1
- package/dist/http-client.d.ts.map +1 -1
- package/dist/http-client.js +171 -34
- package/dist/http-server.d.ts +53 -0
- package/dist/http-server.d.ts.map +1 -0
- package/dist/http-server.js +753 -0
- package/dist/http-session.d.ts +18 -0
- package/dist/http-session.d.ts.map +1 -0
- package/dist/http-session.js +189 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/oauth-response.d.ts +3 -0
- package/dist/oauth-response.d.ts.map +1 -0
- package/dist/oauth-response.js +30 -0
- package/dist/openapi/index.d.ts +2 -2
- package/dist/openapi/index.d.ts.map +1 -1
- package/dist/openapi/index.js +1 -1
- package/dist/openapi/parser.d.ts +2 -0
- package/dist/openapi/parser.d.ts.map +1 -1
- package/dist/openapi/parser.js +41 -1
- package/dist/openapi/types.d.ts +3 -0
- package/dist/openapi/types.d.ts.map +1 -1
- package/dist/organization-selection.d.ts +61 -0
- package/dist/organization-selection.d.ts.map +1 -0
- package/dist/organization-selection.js +142 -0
- package/dist/rate-limits.d.ts +13 -0
- package/dist/rate-limits.d.ts.map +1 -0
- package/dist/rate-limits.js +67 -0
- package/dist/revision.d.ts +6 -0
- package/dist/revision.d.ts.map +1 -0
- package/dist/revision.js +17 -0
- package/dist/runtime-diagnostics.d.ts +32 -0
- package/dist/runtime-diagnostics.d.ts.map +1 -0
- package/dist/runtime-diagnostics.js +58 -0
- package/dist/server.d.ts +23 -2
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +893 -32
- package/dist/session-registry.d.ts +54 -0
- package/dist/session-registry.d.ts.map +1 -0
- package/dist/session-registry.js +202 -0
- package/dist/session-telemetry.d.ts +13 -0
- package/dist/session-telemetry.d.ts.map +1 -0
- package/dist/session-telemetry.js +87 -0
- package/dist/spend-confirmation.d.ts +21 -0
- package/dist/spend-confirmation.d.ts.map +1 -0
- package/dist/spend-confirmation.js +105 -0
- package/dist/tool-analytics-context.d.ts +11 -0
- package/dist/tool-analytics-context.d.ts.map +1 -0
- package/dist/tool-analytics-context.js +8 -0
- package/dist/tool-names.d.ts +35 -0
- package/dist/tool-names.d.ts.map +1 -0
- package/dist/tool-names.js +133 -0
- package/dist/tools/account-context.d.ts +5 -0
- package/dist/tools/account-context.d.ts.map +1 -0
- package/dist/tools/account-context.js +131 -0
- package/dist/tools/account-management.d.ts +4 -0
- package/dist/tools/account-management.d.ts.map +1 -0
- package/dist/tools/account-management.js +111 -0
- package/dist/tools/app-tools.d.ts +31 -0
- package/dist/tools/app-tools.d.ts.map +1 -0
- package/dist/tools/app-tools.js +350 -0
- package/dist/tools/asset-analysis.d.ts +4 -0
- package/dist/tools/asset-analysis.d.ts.map +1 -0
- package/dist/tools/asset-analysis.js +172 -0
- package/dist/tools/asset-creators.d.ts +4 -0
- package/dist/tools/asset-creators.d.ts.map +1 -0
- package/dist/tools/asset-creators.js +38 -0
- package/dist/tools/audio-trim.d.ts +6 -0
- package/dist/tools/audio-trim.d.ts.map +1 -0
- package/dist/tools/audio-trim.js +79 -0
- package/dist/tools/batch-results.d.ts +16 -0
- package/dist/tools/batch-results.d.ts.map +1 -0
- package/dist/tools/batch-results.js +103 -0
- package/dist/tools/batches.d.ts +28 -0
- package/dist/tools/batches.d.ts.map +1 -0
- package/dist/tools/batches.js +293 -0
- package/dist/tools/dialogue.d.ts +6 -0
- package/dist/tools/dialogue.d.ts.map +1 -0
- package/dist/tools/dialogue.js +309 -0
- package/dist/tools/estimate-duration.d.ts +35 -0
- package/dist/tools/estimate-duration.d.ts.map +1 -0
- package/dist/tools/estimate-duration.js +66 -0
- package/dist/tools/feedback.d.ts +14 -0
- package/dist/tools/feedback.d.ts.map +1 -0
- package/dist/tools/feedback.js +72 -0
- package/dist/tools/generated-adapters.d.ts +25 -0
- package/dist/tools/generated-adapters.d.ts.map +1 -0
- package/dist/tools/generated-adapters.js +143 -0
- package/dist/tools/generation-context.d.ts +14 -0
- package/dist/tools/generation-context.d.ts.map +1 -0
- package/dist/tools/generation-context.js +39 -0
- package/dist/tools/generation-continuation.d.ts +4 -0
- package/dist/tools/generation-continuation.d.ts.map +1 -0
- package/dist/tools/generation-continuation.js +124 -0
- package/dist/tools/generation-costs.d.ts +6 -0
- package/dist/tools/generation-costs.d.ts.map +1 -0
- package/dist/tools/generation-costs.js +67 -0
- package/dist/tools/generation-output.d.ts +3 -0
- package/dist/tools/generation-output.d.ts.map +1 -0
- package/dist/tools/generation-output.js +39 -0
- package/dist/tools/generator.d.ts +35 -2
- package/dist/tools/generator.d.ts.map +1 -1
- package/dist/tools/generator.js +127 -9
- package/dist/tools/job-status.d.ts +35 -0
- package/dist/tools/job-status.d.ts.map +1 -0
- package/dist/tools/job-status.js +92 -0
- package/dist/tools/job-wait.d.ts +41 -0
- package/dist/tools/job-wait.d.ts.map +1 -0
- package/dist/tools/job-wait.js +84 -0
- package/dist/tools/jobs.d.ts +3 -0
- package/dist/tools/jobs.d.ts.map +1 -0
- package/dist/tools/jobs.js +210 -0
- package/dist/tools/media-upload.d.ts +27 -0
- package/dist/tools/media-upload.d.ts.map +1 -0
- package/dist/tools/media-upload.js +247 -0
- package/dist/tools/model-options.d.ts +36 -0
- package/dist/tools/model-options.d.ts.map +1 -0
- package/dist/tools/model-options.js +48 -0
- package/dist/tools/organization-choice.d.ts +9 -0
- package/dist/tools/organization-choice.d.ts.map +1 -0
- package/dist/tools/organization-choice.js +136 -0
- package/dist/tools/organizations.d.ts +56 -0
- package/dist/tools/organizations.d.ts.map +1 -0
- package/dist/tools/organizations.js +235 -0
- package/dist/tools/output-schemas.d.ts +54 -0
- package/dist/tools/output-schemas.d.ts.map +1 -0
- package/dist/tools/output-schemas.js +58 -0
- package/dist/tools/overrides.d.ts +3 -0
- package/dist/tools/overrides.d.ts.map +1 -1
- package/dist/tools/overrides.js +113 -15
- package/dist/tools/playback-metadata.d.ts +4 -0
- package/dist/tools/playback-metadata.d.ts.map +1 -0
- package/dist/tools/playback-metadata.js +51 -0
- package/dist/tools/project-destination.d.ts +13 -0
- package/dist/tools/project-destination.d.ts.map +1 -0
- package/dist/tools/project-destination.js +45 -0
- package/dist/tools/project-history.d.ts +6 -0
- package/dist/tools/project-history.d.ts.map +1 -0
- package/dist/tools/project-history.js +45 -0
- package/dist/tools/project-intent.d.ts +8 -0
- package/dist/tools/project-intent.d.ts.map +1 -0
- package/dist/tools/project-intent.js +87 -0
- package/dist/tools/running-submissions.d.ts +7 -0
- package/dist/tools/running-submissions.d.ts.map +1 -0
- package/dist/tools/running-submissions.js +91 -0
- package/dist/tools/speech.d.ts +6 -0
- package/dist/tools/speech.d.ts.map +1 -0
- package/dist/tools/speech.js +116 -0
- package/dist/tools/studio-draft.d.ts +7 -0
- package/dist/tools/studio-draft.d.ts.map +1 -0
- package/dist/tools/studio-draft.js +195 -0
- package/dist/tools/studio-view-contract.d.ts +184 -0
- package/dist/tools/studio-view-contract.d.ts.map +1 -0
- package/dist/tools/studio-view-contract.js +67 -0
- package/dist/tools/studio-view.d.ts +6 -0
- package/dist/tools/studio-view.d.ts.map +1 -0
- package/dist/tools/studio-view.js +126 -0
- package/dist/tools/timeline-lipsync.d.ts +316 -0
- package/dist/tools/timeline-lipsync.d.ts.map +1 -0
- package/dist/tools/timeline-lipsync.js +304 -0
- package/dist/tools/translation-preview.d.ts +8 -0
- package/dist/tools/translation-preview.d.ts.map +1 -0
- package/dist/tools/translation-preview.js +146 -0
- package/dist/tools/translation.d.ts +6 -0
- package/dist/tools/translation.d.ts.map +1 -0
- package/dist/tools/translation.js +131 -0
- package/dist/tools/upload-widget.d.ts +13 -0
- package/dist/tools/upload-widget.d.ts.map +1 -0
- package/dist/tools/upload-widget.js +788 -0
- package/dist/toolsets.d.ts +18 -0
- package/dist/toolsets.d.ts.map +1 -0
- package/dist/toolsets.js +157 -0
- package/dist/update-check.d.ts +38 -0
- package/dist/update-check.d.ts.map +1 -0
- package/dist/update-check.js +150 -0
- package/dist/upload-runtime.d.ts +45 -0
- package/dist/upload-runtime.d.ts.map +1 -0
- package/dist/upload-runtime.js +156 -0
- package/dist/upload-source.d.ts +4 -0
- package/dist/upload-source.d.ts.map +1 -0
- package/dist/upload-source.js +46 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +3 -0
- package/package.json +79 -33
- package/dist/openapi/parser.test.d.ts +0 -2
- package/dist/openapi/parser.test.d.ts.map +0 -1
- package/dist/openapi/parser.test.js +0 -188
- package/dist/tools/generator.test.d.ts +0 -2
- package/dist/tools/generator.test.d.ts.map +0 -1
- package/dist/tools/generator.test.js +0 -178
package/dist/tools/generator.js
CHANGED
|
@@ -1,27 +1,129 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { currentToolName } from '../tool-names.js';
|
|
3
|
+
import { estimateDurationInputs, resolveEstimateDuration } from './estimate-duration.js';
|
|
2
4
|
import { getOverride } from './overrides.js';
|
|
5
|
+
const voiceCloneSourceSchema = z
|
|
6
|
+
.object({
|
|
7
|
+
url: z
|
|
8
|
+
.url()
|
|
9
|
+
.optional()
|
|
10
|
+
.describe('Sync-hosted audio/video sample URL. Supply url or assetId, not both.'),
|
|
11
|
+
sampleUrls: z
|
|
12
|
+
.array(z.url())
|
|
13
|
+
.min(1)
|
|
14
|
+
.max(10)
|
|
15
|
+
.optional()
|
|
16
|
+
.describe('Sync-hosted samples for one reusable voice; use instead of url or assetId.'),
|
|
17
|
+
assetId: z
|
|
18
|
+
.uuid()
|
|
19
|
+
.optional()
|
|
20
|
+
.describe('Accessible audio/video sample asset ID. Supply assetId or url, not both.'),
|
|
21
|
+
})
|
|
22
|
+
.refine((input) => [input.url, input.assetId, input.sampleUrls].filter((value) => value !== undefined).length ===
|
|
23
|
+
1, {
|
|
24
|
+
message: 'Provide exactly one of url, assetId or sampleUrls for voice samples.',
|
|
25
|
+
});
|
|
3
26
|
export function generateTools(operations, httpClient) {
|
|
4
27
|
return operations.map((op) => generateTool(op, httpClient));
|
|
5
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Behaviour hints required by the ChatGPT and Claude app/connector directories
|
|
31
|
+
* (missing/incorrect annotations are a top review-rejection cause). Derived
|
|
32
|
+
* from the HTTP verb: reads are read-only; writes modify external Sync state,
|
|
33
|
+
* so they are destructive and open-world.
|
|
34
|
+
*/
|
|
35
|
+
export function deriveAnnotations(method, toolName) {
|
|
36
|
+
if (method === 'get' ||
|
|
37
|
+
['generations_estimate-cost', 'generations_estimate'].includes(toolName ?? '')) {
|
|
38
|
+
return { readOnlyHint: true, destructiveHint: false, openWorldHint: false };
|
|
39
|
+
}
|
|
40
|
+
if (method === 'delete') {
|
|
41
|
+
return { readOnlyHint: false, destructiveHint: true, openWorldHint: true };
|
|
42
|
+
}
|
|
43
|
+
// No idempotency contract for generation or other create/update operations.
|
|
44
|
+
return { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true };
|
|
45
|
+
}
|
|
6
46
|
function generateTool(operation, httpClient) {
|
|
7
47
|
const name = operationIdToToolName(operation.operationId);
|
|
8
48
|
const override = getOverride(name);
|
|
9
49
|
const description = override?.description ?? truncate(operation.summary, 200);
|
|
10
50
|
const inputSchema = buildInputSchema(operation);
|
|
51
|
+
const admission = admissionKey(operation);
|
|
52
|
+
if (admission)
|
|
53
|
+
inputSchema.idempotencyKey = admission.schema;
|
|
54
|
+
const estimatesCost = ['generations_estimate-cost', 'generations_estimate'].includes(name);
|
|
55
|
+
if (estimatesCost) {
|
|
56
|
+
if (inputSchema.duration) {
|
|
57
|
+
inputSchema.duration = inputSchema.duration
|
|
58
|
+
.optional()
|
|
59
|
+
.describe('Billable output seconds, or source seconds for translate-and-dub. For replacement speech, supply script or measured audioDuration instead. Never use the source video length for a shorter replacement script.');
|
|
60
|
+
}
|
|
61
|
+
Object.assign(inputSchema, estimateDurationInputs);
|
|
62
|
+
}
|
|
63
|
+
if (name === 'voices_clone') {
|
|
64
|
+
// Flattening the API's allOf/oneOf alternatives makes both sources required.
|
|
65
|
+
// Keep the descriptor flat, then enforce the exclusive choice before writing.
|
|
66
|
+
Object.assign(inputSchema, voiceCloneSourceSchema.shape);
|
|
67
|
+
}
|
|
11
68
|
return {
|
|
12
69
|
name,
|
|
70
|
+
title: override?.title ?? titleForToolName(name),
|
|
13
71
|
description,
|
|
14
72
|
inputSchema,
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
73
|
+
outputSchema: override?.outputSchema,
|
|
74
|
+
annotations: deriveAnnotations(operation.method, name),
|
|
75
|
+
handler: async (args, context) => {
|
|
76
|
+
if (name === 'voices_clone')
|
|
77
|
+
voiceCloneSourceSchema.parse(args);
|
|
78
|
+
const prepared = prepareAdmission(args, admission);
|
|
79
|
+
const estimate = estimatesCost ? resolveEstimateDuration(prepared.args) : undefined;
|
|
80
|
+
const requestArgs = estimate?.args ?? prepared.args;
|
|
81
|
+
if (estimatesCost && inputSchema.duration && requestArgs.duration === undefined)
|
|
82
|
+
throw new Error('Supply duration, script or audioDuration for a cost estimate.');
|
|
83
|
+
const path = buildPath(operation.path, requestArgs);
|
|
84
|
+
const query = buildQuery(operation.parameters, requestArgs);
|
|
85
|
+
const body = buildBody(operation, requestArgs);
|
|
86
|
+
const result = await httpClient.request(operation.method, path, {
|
|
87
|
+
query,
|
|
88
|
+
body,
|
|
89
|
+
...prepared.headers,
|
|
90
|
+
signal: context?.signal,
|
|
91
|
+
});
|
|
92
|
+
if (!estimate?.durationEstimate)
|
|
93
|
+
return result;
|
|
94
|
+
return {
|
|
95
|
+
...z.record(z.string(), z.unknown()).parse(result),
|
|
96
|
+
durationEstimate: estimate.durationEstimate,
|
|
97
|
+
};
|
|
20
98
|
},
|
|
21
99
|
};
|
|
22
100
|
}
|
|
23
|
-
|
|
24
|
-
|
|
101
|
+
/** Generated stdio writes must carry API admission keys as headers, not JSON fields. */
|
|
102
|
+
function admissionKey(operation) {
|
|
103
|
+
const parameter = operation.parameters.find((entry) => entry.in === 'header' && entry.name.toLowerCase() === 'idempotency-key');
|
|
104
|
+
if (!parameter)
|
|
105
|
+
return undefined;
|
|
106
|
+
const key = parameter.schema.format === 'uuid' ? z.uuid() : z.string().regex(/^[A-Za-z0-9._~-]{1,128}$/);
|
|
107
|
+
return {
|
|
108
|
+
name: parameter.name,
|
|
109
|
+
schema: (parameter.required ? key : key.optional()).describe('Persist before creation. Reuse this same key and identical inputs only to reconcile uncertain admission; do not create a new key for a retry.'),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function prepareAdmission(args, admission) {
|
|
113
|
+
if (!admission)
|
|
114
|
+
return { args, headers: {} };
|
|
115
|
+
const { idempotencyKey, ...fields } = args;
|
|
116
|
+
const key = admission.schema.parse(idempotencyKey);
|
|
117
|
+
return {
|
|
118
|
+
args: fields,
|
|
119
|
+
headers: key === undefined ? {} : { headers: { [admission.name]: key } },
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function titleForToolName(name) {
|
|
123
|
+
return name.replace(/[-_]/g, ' ').replace(/\b\w/g, (char) => char.toUpperCase());
|
|
124
|
+
}
|
|
125
|
+
/** "AssetsController_getAll" → "assets_get-all", the name before tool-names.ts renames it. */
|
|
126
|
+
export function derivedToolName(operationId) {
|
|
25
127
|
const parts = operationId.split('_');
|
|
26
128
|
if (parts.length < 2)
|
|
27
129
|
return toKebab(operationId);
|
|
@@ -29,6 +131,10 @@ export function operationIdToToolName(operationId) {
|
|
|
29
131
|
const method = toKebab(parts.slice(1).join('_'));
|
|
30
132
|
return `${controller}_${method}`;
|
|
31
133
|
}
|
|
134
|
+
/** "AssetsController_getAll" → "assets_list". */
|
|
135
|
+
export function operationIdToToolName(operationId) {
|
|
136
|
+
return currentToolName(derivedToolName(operationId));
|
|
137
|
+
}
|
|
32
138
|
function toKebab(str) {
|
|
33
139
|
return str
|
|
34
140
|
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
@@ -124,7 +230,7 @@ function buildPath(pathTemplate, args) {
|
|
|
124
230
|
if (value === undefined) {
|
|
125
231
|
throw new Error(`Missing required path parameter: ${paramName}`);
|
|
126
232
|
}
|
|
127
|
-
return encodeURIComponent(
|
|
233
|
+
return encodeURIComponent(parameterString(value));
|
|
128
234
|
});
|
|
129
235
|
}
|
|
130
236
|
function buildQuery(parameters, args) {
|
|
@@ -135,11 +241,23 @@ function buildQuery(parameters, args) {
|
|
|
135
241
|
for (const param of queryParams) {
|
|
136
242
|
const value = args[param.name];
|
|
137
243
|
if (value !== undefined && value !== null) {
|
|
138
|
-
query[param.name] =
|
|
244
|
+
query[param.name] = parameterString(value);
|
|
139
245
|
}
|
|
140
246
|
}
|
|
141
247
|
return Object.keys(query).length > 0 ? query : undefined;
|
|
142
248
|
}
|
|
249
|
+
// Path and query values are JSON scalars or arrays such as `types`; keep String()'s
|
|
250
|
+
// comma-joined array form and serialize plain objects instead of `[object Object]`.
|
|
251
|
+
function parameterString(value) {
|
|
252
|
+
if (typeof value === 'string')
|
|
253
|
+
return value;
|
|
254
|
+
if (typeof value === 'number' ||
|
|
255
|
+
typeof value === 'boolean' ||
|
|
256
|
+
typeof value === 'bigint' ||
|
|
257
|
+
Array.isArray(value))
|
|
258
|
+
return String(value);
|
|
259
|
+
return JSON.stringify(value);
|
|
260
|
+
}
|
|
143
261
|
function buildBody(operation, args) {
|
|
144
262
|
if (!operation.requestBody)
|
|
145
263
|
return undefined;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { ApiRequestError } from '../http-client.js';
|
|
2
|
+
/**
|
|
3
|
+
* One status vocabulary for every Sync job, so an agent waits on all of them the same way.
|
|
4
|
+
* Each kind's own read tool keeps returning its API status; jobs_get and jobs_wait add the
|
|
5
|
+
* unified one. docs/jobs.md has the full table.
|
|
6
|
+
*/
|
|
7
|
+
export declare const JOB_STATUSES: readonly ['PENDING', 'PROCESSING', 'COMPLETED', 'FAILED', 'REJECTED', 'CANCELED'];
|
|
8
|
+
export type JobStatus = (typeof JOB_STATUSES)[number];
|
|
9
|
+
export declare const JOB_KINDS: readonly ['generation', 'transcription', 'speaker_analysis', 'translation_preview', 'import', 'batch'];
|
|
10
|
+
export type JobKind = (typeof JOB_KINDS)[number];
|
|
11
|
+
export declare function isTerminal(status: JobStatus): boolean;
|
|
12
|
+
/** The API's own status string on a job payload, if it has one. */
|
|
13
|
+
export declare function rawStatus(payload: unknown): string | undefined;
|
|
14
|
+
/**
|
|
15
|
+
* Maps a job payload's `status` to the unified status. A payload without a status counts
|
|
16
|
+
* as COMPLETED, as the per-kind waiters always did, so a wait never spins on a shape it
|
|
17
|
+
* cannot read; an unknown status counts as PROCESSING, so the wait keeps going until the
|
|
18
|
+
* budget runs out instead of reporting a result that may not exist.
|
|
19
|
+
*/
|
|
20
|
+
export declare function jobStatus(payload: unknown): JobStatus;
|
|
21
|
+
/**
|
|
22
|
+
* Whether a wait should stop on this payload. A translation preview with recoveryRequired
|
|
23
|
+
* stays in its status, but polling will not resolve it: provider acceptance is uncertain
|
|
24
|
+
* and only Sync can recover it.
|
|
25
|
+
*/
|
|
26
|
+
export declare function jobSettled(payload: unknown): boolean;
|
|
27
|
+
/** True for the 409 a social link import answers while it is still downloading. */
|
|
28
|
+
export declare function isImportInProgress(error: unknown): error is ApiRequestError;
|
|
29
|
+
/**
|
|
30
|
+
* The unified status for an error from a social link import, or undefined when the error
|
|
31
|
+
* did not come from the import (auth, the project-intent guard, the network) and should
|
|
32
|
+
* stay a tool error.
|
|
33
|
+
*/
|
|
34
|
+
export declare function importErrorStatus(error: unknown): JobStatus | undefined;
|
|
35
|
+
//# sourceMappingURL=job-status.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"job-status.d.ts","sourceRoot":"","sources":["../../src/tools/job-status.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEpD;;;;GAIG;AACH,eAAO,MAAM,YAAY,YACvB,SAAS,EACT,YAAY,EACZ,WAAW,EACX,QAAQ,EACR,UAAU,EACV,UAAU,CACF,CAAC;AACX,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC;AAEtD,eAAO,MAAM,SAAS,YACpB,YAAY,EACZ,eAAe,EACf,kBAAkB,EAClB,qBAAqB,EACrB,QAAQ,EACR,OAAO,CACC,CAAC;AACX,MAAM,MAAM,OAAO,GAAG,CAAC,OAAO,SAAS,CAAC,CAAC,MAAM,CAAC,CAAC;AAIjD,wBAAgB,UAAU,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAErD;AAgBD,mEAAmE;AACnE,wBAAgB,SAAS,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAI9D;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,OAAO,GAAG,SAAS,CAMrD;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAKpD;AAED,mFAAmF;AACnF,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,eAAe,CAI3E;AAWD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,SAAS,GAAG,SAAS,CAKvE"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { ApiRequestError } from '../http-client.js';
|
|
2
|
+
/**
|
|
3
|
+
* One status vocabulary for every Sync job, so an agent waits on all of them the same way.
|
|
4
|
+
* Each kind's own read tool keeps returning its API status; jobs_get and jobs_wait add the
|
|
5
|
+
* unified one. docs/jobs.md has the full table.
|
|
6
|
+
*/
|
|
7
|
+
export const JOB_STATUSES = [
|
|
8
|
+
'PENDING',
|
|
9
|
+
'PROCESSING',
|
|
10
|
+
'COMPLETED',
|
|
11
|
+
'FAILED',
|
|
12
|
+
'REJECTED',
|
|
13
|
+
'CANCELED',
|
|
14
|
+
];
|
|
15
|
+
export const JOB_KINDS = [
|
|
16
|
+
'generation',
|
|
17
|
+
'transcription',
|
|
18
|
+
'speaker_analysis',
|
|
19
|
+
'translation_preview',
|
|
20
|
+
'import',
|
|
21
|
+
'batch',
|
|
22
|
+
];
|
|
23
|
+
const TERMINAL = new Set(['COMPLETED', 'FAILED', 'REJECTED', 'CANCELED']);
|
|
24
|
+
export function isTerminal(status) {
|
|
25
|
+
return TERMINAL.has(status);
|
|
26
|
+
}
|
|
27
|
+
// API statuses that are not already a unified name. Case is ignored.
|
|
28
|
+
const ALIASES = {
|
|
29
|
+
QUEUED: 'PENDING',
|
|
30
|
+
DOWNLOADING: 'PROCESSING',
|
|
31
|
+
// Translation previews attach their output after the provider finishes.
|
|
32
|
+
COMPLETING: 'PROCESSING',
|
|
33
|
+
// A batch has created its generations, or is writing its result file.
|
|
34
|
+
JOBS_CREATED: 'PROCESSING',
|
|
35
|
+
FINISHING: 'PROCESSING',
|
|
36
|
+
CANCELLED: 'CANCELED',
|
|
37
|
+
// Speaker analysis that cannot run for this source (no faces, too long, audio only).
|
|
38
|
+
UNAVAILABLE: 'FAILED',
|
|
39
|
+
};
|
|
40
|
+
/** The API's own status string on a job payload, if it has one. */
|
|
41
|
+
export function rawStatus(payload) {
|
|
42
|
+
const status = payload && typeof payload === 'object' ? payload.status : undefined;
|
|
43
|
+
return typeof status === 'string' ? status : undefined;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Maps a job payload's `status` to the unified status. A payload without a status counts
|
|
47
|
+
* as COMPLETED, as the per-kind waiters always did, so a wait never spins on a shape it
|
|
48
|
+
* cannot read; an unknown status counts as PROCESSING, so the wait keeps going until the
|
|
49
|
+
* budget runs out instead of reporting a result that may not exist.
|
|
50
|
+
*/
|
|
51
|
+
export function jobStatus(payload) {
|
|
52
|
+
const raw = rawStatus(payload);
|
|
53
|
+
if (raw === undefined)
|
|
54
|
+
return 'COMPLETED';
|
|
55
|
+
const status = raw.trim().toUpperCase();
|
|
56
|
+
if (JOB_STATUSES.includes(status))
|
|
57
|
+
return status;
|
|
58
|
+
return ALIASES[status] ?? 'PROCESSING';
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Whether a wait should stop on this payload. A translation preview with recoveryRequired
|
|
62
|
+
* stays in its status, but polling will not resolve it: provider acceptance is uncertain
|
|
63
|
+
* and only Sync can recover it.
|
|
64
|
+
*/
|
|
65
|
+
export function jobSettled(payload) {
|
|
66
|
+
return (isTerminal(jobStatus(payload)) ||
|
|
67
|
+
payload?.recoveryRequired === true);
|
|
68
|
+
}
|
|
69
|
+
/** True for the 409 a social link import answers while it is still downloading. */
|
|
70
|
+
export function isImportInProgress(error) {
|
|
71
|
+
return (error instanceof ApiRequestError && error.status === 409 && error.code === 'import_in_progress');
|
|
72
|
+
}
|
|
73
|
+
// Import error codes from the API's ImportErrorCodes. Unlisted import_* codes are refusals.
|
|
74
|
+
const IMPORT_ERROR_STATUS = {
|
|
75
|
+
import_failed: 'FAILED',
|
|
76
|
+
import_download_failed: 'FAILED',
|
|
77
|
+
import_upload_failed: 'FAILED',
|
|
78
|
+
import_processing_failed: 'FAILED',
|
|
79
|
+
import_cancelled_by_user: 'CANCELED',
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* The unified status for an error from a social link import, or undefined when the error
|
|
83
|
+
* did not come from the import (auth, the project-intent guard, the network) and should
|
|
84
|
+
* stay a tool error.
|
|
85
|
+
*/
|
|
86
|
+
export function importErrorStatus(error) {
|
|
87
|
+
if (!(error instanceof ApiRequestError) || !error.code?.startsWith('import_'))
|
|
88
|
+
return undefined;
|
|
89
|
+
if (error.status === 409 && error.code === 'import_in_progress')
|
|
90
|
+
return error.importDetails?.importStatus === 'queued' ? 'PENDING' : 'PROCESSING';
|
|
91
|
+
return IMPORT_ERROR_STATUS[error.code] ?? 'REJECTED';
|
|
92
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { ToolRequestContext } from './generator.js';
|
|
3
|
+
/**
|
|
4
|
+
* Server-side waiting for the job tools. Agents otherwise write the same poll loop for
|
|
5
|
+
* every job type, and agent gateways pause servers that answer identical calls with
|
|
6
|
+
* identical "still running" results. One call with wait: true holds for up to
|
|
7
|
+
* WAIT_BUDGET_MS, sends progress notifications when the caller asked for them, and returns
|
|
8
|
+
* the latest state either way, so a still-running job just means calling again.
|
|
9
|
+
*/
|
|
10
|
+
export declare const WAIT_BUDGET_MS = 50000;
|
|
11
|
+
export declare const waitInput: z.ZodOptional<z.ZodBoolean>;
|
|
12
|
+
type Notify = (notification: {
|
|
13
|
+
method: 'notifications/progress';
|
|
14
|
+
params: {
|
|
15
|
+
progressToken: string | number;
|
|
16
|
+
progress: number;
|
|
17
|
+
message?: string;
|
|
18
|
+
};
|
|
19
|
+
}) => Promise<void>;
|
|
20
|
+
export declare function progressSender(token: unknown, notify: Notify): (progress: number, message?: string) => Promise<void>;
|
|
21
|
+
export declare function sleep(ms: number, signal?: AbortSignal): Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* Read until done(state) or the budget runs out; returns the last state read.
|
|
24
|
+
* `describe` turns a state into the progress message; `delayMs` lets a pending
|
|
25
|
+
* state ask for a longer pause (an import's retryAfterMs).
|
|
26
|
+
*
|
|
27
|
+
* The budget bounds the whole call: no pause runs past it, and every read after
|
|
28
|
+
* the first gets a signal that aborts at the deadline, in which case the last
|
|
29
|
+
* state is returned. Only the caller's own cancellation is rethrown.
|
|
30
|
+
*/
|
|
31
|
+
export declare function pollUntil<T>(read: (signal?: AbortSignal) => Promise<T>, done: (state: T) => boolean, context: ToolRequestContext | undefined, options?: {
|
|
32
|
+
describe?: (state: T) => string;
|
|
33
|
+
delayMs?: (state: T) => number | undefined;
|
|
34
|
+
budgetMs?: number;
|
|
35
|
+
now?: () => number;
|
|
36
|
+
}): Promise<T>;
|
|
37
|
+
export declare const TIMED_OUT: unique symbol;
|
|
38
|
+
/** Run `task` with a signal that also aborts after `ms`; a timeout yields TIMED_OUT. */
|
|
39
|
+
export declare function boundedBy<T>(ms: number, caller: AbortSignal | undefined, task: (signal: AbortSignal) => Promise<T>): Promise<T | typeof TIMED_OUT>;
|
|
40
|
+
export {};
|
|
41
|
+
//# sourceMappingURL=job-wait.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"job-wait.d.ts","sourceRoot":"","sources":["../../src/tools/job-wait.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAEzD;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,QAAS,CAAC;AAIrC,eAAO,MAAM,SAAS,6BAKnB,CAAC;AAEJ,KAAK,MAAM,GAAG,CAAC,YAAY,EAAE;IAC3B,MAAM,EAAE,wBAAwB,CAAC;IACjC,MAAM,EAAE;QAAE,aAAa,EAAE,MAAM,GAAG,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAChF,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAEpB,wBAAgB,cAAc,CAC5B,KAAK,EAAE,OAAO,EACd,MAAM,EAAE,MAAM,GACb,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAYvD;AAED,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAarE;AAED;;;;;;;;GAQG;AACH,wBAAsB,SAAS,CAAC,CAAC,EAC/B,IAAI,EAAE,CAAC,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,CAAC,CAAC,EAC1C,IAAI,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,OAAO,EAC3B,OAAO,EAAE,kBAAkB,GAAG,SAAS,EACvC,OAAO,GAAE;IACP,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,MAAM,CAAC;IAChC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,MAAM,GAAG,SAAS,CAAC;IAC3C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACf,GACL,OAAO,CAAC,CAAC,CAAC,CAgBZ;AAED,eAAO,MAAM,SAAS,eAAsB,CAAC;AAE7C,wFAAwF;AACxF,wBAAsB,SAAS,CAAC,CAAC,EAC/B,EAAE,EAAE,MAAM,EACV,MAAM,EAAE,WAAW,GAAG,SAAS,EAC/B,IAAI,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,CAAC,CAAC,GACxC,OAAO,CAAC,CAAC,GAAG,OAAO,SAAS,CAAC,CAQ/B"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Server-side waiting for the job tools. Agents otherwise write the same poll loop for
|
|
4
|
+
* every job type, and agent gateways pause servers that answer identical calls with
|
|
5
|
+
* identical "still running" results. One call with wait: true holds for up to
|
|
6
|
+
* WAIT_BUDGET_MS, sends progress notifications when the caller asked for them, and returns
|
|
7
|
+
* the latest state either way, so a still-running job just means calling again.
|
|
8
|
+
*/
|
|
9
|
+
export const WAIT_BUDGET_MS = 50_000;
|
|
10
|
+
// Back off 1, 2, 4 then every 5 seconds, so quick jobs return quickly.
|
|
11
|
+
const INTERVALS_MS = [1_000, 2_000, 4_000, 5_000];
|
|
12
|
+
export const waitInput = z
|
|
13
|
+
.boolean()
|
|
14
|
+
.optional()
|
|
15
|
+
.describe('Wait on the server until the job finishes or about 50 seconds pass, then return its latest state. If it is still running, call again with wait: true instead of sleeping.');
|
|
16
|
+
export function progressSender(token, notify) {
|
|
17
|
+
if (typeof token !== 'string' && typeof token !== 'number')
|
|
18
|
+
return async () => { };
|
|
19
|
+
let last = -1;
|
|
20
|
+
return async (progress, message) => {
|
|
21
|
+
// The spec requires progress to increase with every notification.
|
|
22
|
+
const value = Math.max(progress, last + 1e-6);
|
|
23
|
+
last = value;
|
|
24
|
+
await notify({
|
|
25
|
+
method: 'notifications/progress',
|
|
26
|
+
params: { progressToken: token, progress: value, ...(message ? { message } : {}) },
|
|
27
|
+
}).catch(() => { });
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export function sleep(ms, signal) {
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
if (signal?.aborted)
|
|
33
|
+
return reject(signal.reason);
|
|
34
|
+
const timer = setTimeout(() => {
|
|
35
|
+
signal?.removeEventListener('abort', onAbort);
|
|
36
|
+
resolve();
|
|
37
|
+
}, ms);
|
|
38
|
+
const onAbort = () => {
|
|
39
|
+
clearTimeout(timer);
|
|
40
|
+
reject(signal?.reason);
|
|
41
|
+
};
|
|
42
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Read until done(state) or the budget runs out; returns the last state read.
|
|
47
|
+
* `describe` turns a state into the progress message; `delayMs` lets a pending
|
|
48
|
+
* state ask for a longer pause (an import's retryAfterMs).
|
|
49
|
+
*
|
|
50
|
+
* The budget bounds the whole call: no pause runs past it, and every read after
|
|
51
|
+
* the first gets a signal that aborts at the deadline, in which case the last
|
|
52
|
+
* state is returned. Only the caller's own cancellation is rethrown.
|
|
53
|
+
*/
|
|
54
|
+
export async function pollUntil(read, done, context, options = {}) {
|
|
55
|
+
const now = options.now ?? Date.now;
|
|
56
|
+
const deadline = now() + (options.budgetMs ?? WAIT_BUDGET_MS);
|
|
57
|
+
let state = await read(context?.signal);
|
|
58
|
+
for (let polls = 1; !done(state); polls++) {
|
|
59
|
+
await context?.progress?.(polls, options.describe?.(state));
|
|
60
|
+
const pause = options.delayMs?.(state) ?? INTERVALS_MS[Math.min(polls, INTERVALS_MS.length) - 1];
|
|
61
|
+
// Sleeping into the deadline only to return the same state helps nobody.
|
|
62
|
+
if (deadline - now() <= pause)
|
|
63
|
+
return state;
|
|
64
|
+
await sleep(pause, context?.signal);
|
|
65
|
+
const next = await boundedBy(deadline - now(), context?.signal, read);
|
|
66
|
+
if (next === TIMED_OUT)
|
|
67
|
+
return state;
|
|
68
|
+
state = next;
|
|
69
|
+
}
|
|
70
|
+
return state;
|
|
71
|
+
}
|
|
72
|
+
export const TIMED_OUT = Symbol('timed out');
|
|
73
|
+
/** Run `task` with a signal that also aborts after `ms`; a timeout yields TIMED_OUT. */
|
|
74
|
+
export async function boundedBy(ms, caller, task) {
|
|
75
|
+
const timeout = AbortSignal.timeout(Math.max(ms, 0));
|
|
76
|
+
try {
|
|
77
|
+
return await task(caller ? AbortSignal.any([caller, timeout]) : timeout);
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
if (timeout.aborted && !caller?.aborted)
|
|
81
|
+
return TIMED_OUT;
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jobs.d.ts","sourceRoot":"","sources":["../../src/tools/jobs.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,iBAAiB,EAAsB,MAAM,gBAAgB,CAAC;AA8K5E,wBAAgB,cAAc,CAAC,KAAK,EAAE,iBAAiB,EAAE,GAAG,iBAAiB,EAAE,CA+F9E"}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { batchProgress } from './batches.js';
|
|
3
|
+
import { attemptImport, importWaitOptions } from './generated-adapters.js';
|
|
4
|
+
import { importErrorStatus, isTerminal, JOB_KINDS, JOB_STATUSES, jobStatus, } from './job-status.js';
|
|
5
|
+
import { pollUntil } from './job-wait.js';
|
|
6
|
+
function isRecord(value) {
|
|
7
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
8
|
+
}
|
|
9
|
+
/** What the kind's own tool would show: its output schema's fields, when it has one. */
|
|
10
|
+
function project(tool, payload) {
|
|
11
|
+
if (!isRecord(payload))
|
|
12
|
+
return { value: payload };
|
|
13
|
+
if (!tool.outputSchema)
|
|
14
|
+
return payload;
|
|
15
|
+
return Object.fromEntries(Object.keys(tool.outputSchema)
|
|
16
|
+
.filter((key) => key in payload)
|
|
17
|
+
.map((key) => [key, payload[key]]));
|
|
18
|
+
}
|
|
19
|
+
function percent(value) {
|
|
20
|
+
return typeof value === 'number' && Number.isFinite(value)
|
|
21
|
+
? Math.min(Math.max(value, 0), 100)
|
|
22
|
+
: undefined;
|
|
23
|
+
}
|
|
24
|
+
const readJob = (args) => async (tool, input, context, signal) => {
|
|
25
|
+
const payload = await tool.handler(args(input), { ...context, signal });
|
|
26
|
+
return { status: jobStatus(payload), result: project(tool, payload) };
|
|
27
|
+
};
|
|
28
|
+
const KINDS = {
|
|
29
|
+
generation: {
|
|
30
|
+
tool: 'generations_get',
|
|
31
|
+
label: 'generation',
|
|
32
|
+
id: 'the generation id a create tool returned (lipsync_create, dubbing_create, lipsync_create-timeline, dialogue-videos_create)',
|
|
33
|
+
read: async (tool, input, context, signal) => {
|
|
34
|
+
// include=progress adds the worker's percent; the API's own long-poll would drop it.
|
|
35
|
+
const progressQuery = 'include' in tool.inputSchema ? { include: 'progress' } : {};
|
|
36
|
+
const payload = await tool.handler({ id: input.id, ...progressQuery }, { ...context, signal });
|
|
37
|
+
const raw = isRecord(payload) ? payload : {};
|
|
38
|
+
const progress = isRecord(raw.progress) ? raw.progress.percent : raw.progress_percent;
|
|
39
|
+
const estimate = isRecord(raw.generationEstimate) ? raw.generationEstimate : {};
|
|
40
|
+
const status = jobStatus(payload);
|
|
41
|
+
return {
|
|
42
|
+
status,
|
|
43
|
+
...(percent(progress) === undefined || isTerminal(status)
|
|
44
|
+
? {}
|
|
45
|
+
: { progress: percent(progress) }),
|
|
46
|
+
...(typeof estimate.estimatedFinishAt === 'string' && !isTerminal(status)
|
|
47
|
+
? { estimatedFinishAt: estimate.estimatedFinishAt }
|
|
48
|
+
: {}),
|
|
49
|
+
result: project(tool, payload),
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
transcription: {
|
|
54
|
+
tool: 'transcriptions_get',
|
|
55
|
+
label: 'transcription',
|
|
56
|
+
id: 'the transcription id (transcriptions_create or dialogue-transcriptions_create)',
|
|
57
|
+
read: readJob((input) => ({ id: input.id })),
|
|
58
|
+
},
|
|
59
|
+
speaker_analysis: {
|
|
60
|
+
tool: 'transcriptions_get-speaker-analysis',
|
|
61
|
+
label: 'speaker analysis',
|
|
62
|
+
id: 'the transcription id',
|
|
63
|
+
read: readJob((input) => ({ id: input.id })),
|
|
64
|
+
},
|
|
65
|
+
translation_preview: {
|
|
66
|
+
tool: 'translation-previews_get',
|
|
67
|
+
label: 'translation',
|
|
68
|
+
id: 'the translation-previews_create id, with its projectId',
|
|
69
|
+
read: readJob((input) => {
|
|
70
|
+
if (!input.projectId)
|
|
71
|
+
throw new Error('Pass the projectId of the translation preview.');
|
|
72
|
+
return { projectId: input.projectId, id: input.id };
|
|
73
|
+
}),
|
|
74
|
+
},
|
|
75
|
+
import: {
|
|
76
|
+
tool: 'assets_create',
|
|
77
|
+
label: 'import',
|
|
78
|
+
id: 'the social video link passed to assets_create, with the same projectId (and projectIntent, if that call needed it)',
|
|
79
|
+
read: async (tool, input, context, signal) => {
|
|
80
|
+
// Reading an import repeats the identical request; the API reuses the running import.
|
|
81
|
+
const args = {
|
|
82
|
+
url: input.id,
|
|
83
|
+
type: 'VIDEO',
|
|
84
|
+
...(input.projectId === undefined ? {} : { projectId: input.projectId }),
|
|
85
|
+
...(input.projectIntent === undefined ? {} : { projectIntent: input.projectIntent }),
|
|
86
|
+
};
|
|
87
|
+
try {
|
|
88
|
+
const attempt = await attemptImport(tool.handler, args, context, signal);
|
|
89
|
+
if ('asset' in attempt)
|
|
90
|
+
return { status: 'COMPLETED', result: project(tool, attempt.asset) };
|
|
91
|
+
const { pending } = attempt;
|
|
92
|
+
const progress = percent(pending.importDetails?.progress);
|
|
93
|
+
return {
|
|
94
|
+
status: importErrorStatus(pending) ?? 'PROCESSING',
|
|
95
|
+
...(progress === undefined ? {} : { progress }),
|
|
96
|
+
retryAfterMs: importWaitOptions.delayMs(attempt),
|
|
97
|
+
result: { code: pending.code, message: pending.message, ...pending.importDetails },
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
const status = importErrorStatus(error);
|
|
102
|
+
if (!status)
|
|
103
|
+
throw error;
|
|
104
|
+
const { message, status: httpStatus, code } = error;
|
|
105
|
+
return { status, result: { error: { message, status: httpStatus, code } } };
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
batch: {
|
|
110
|
+
tool: 'batches_get',
|
|
111
|
+
label: 'batch',
|
|
112
|
+
id: 'the batch id batches_create returned',
|
|
113
|
+
read: async (tool, input, context, signal) => {
|
|
114
|
+
const payload = await tool.handler({ id: input.id }, { ...context, signal });
|
|
115
|
+
const status = jobStatus(payload);
|
|
116
|
+
const progress = isRecord(payload) ? batchProgress(payload) : undefined;
|
|
117
|
+
return {
|
|
118
|
+
status,
|
|
119
|
+
...(progress === undefined || isTerminal(status) ? {} : { progress }),
|
|
120
|
+
result: project(tool, payload),
|
|
121
|
+
};
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
const STATUS_HELP = 'status is one of PENDING, PROCESSING (still running: call jobs_wait again with the same input; never create the job again), COMPLETED, FAILED, REJECTED (the API refused it, such as an import limit or a moderation rejection) or CANCELED. result is what the kind’s own read tool returns, with its original status; a COMPLETED import’s result is the saved asset, whose id is the asset id. A finished batch’s result.results lists each item’s generation status and output. A translation_preview whose result.recoveryRequired is true has uncertain provider acceptance: do not create another job.';
|
|
126
|
+
export function createJobTools(tools) {
|
|
127
|
+
const available = JOB_KINDS.flatMap((kind) => {
|
|
128
|
+
const tool = tools.find((candidate) => candidate.name === KINDS[kind].tool);
|
|
129
|
+
return tool && tool.resultFormat !== 'mcp' ? [{ kind, tool, spec: KINDS[kind] }] : [];
|
|
130
|
+
});
|
|
131
|
+
if (available.length === 0)
|
|
132
|
+
return [];
|
|
133
|
+
const kinds = available.map((entry) => entry.kind);
|
|
134
|
+
const input = z
|
|
135
|
+
.object({
|
|
136
|
+
kind: z.enum(kinds).describe('Which kind of job id is.'),
|
|
137
|
+
id: z.string().trim().min(1).describe('The job to read; see the tool description per kind.'),
|
|
138
|
+
projectId: z
|
|
139
|
+
.string()
|
|
140
|
+
.optional()
|
|
141
|
+
.describe('Required for translation_preview; for import, the project passed to assets_create.'),
|
|
142
|
+
projectIntent: z
|
|
143
|
+
.literal('explicit')
|
|
144
|
+
.optional()
|
|
145
|
+
.describe('import only: pass it when the assets_create call for this link needed it.'),
|
|
146
|
+
})
|
|
147
|
+
.strict();
|
|
148
|
+
const output = {
|
|
149
|
+
kind: z.enum(kinds),
|
|
150
|
+
id: z.string(),
|
|
151
|
+
status: z.enum(JOB_STATUSES).describe('The unified status; see the tool description.'),
|
|
152
|
+
progress: z
|
|
153
|
+
.number()
|
|
154
|
+
.min(0)
|
|
155
|
+
.max(100)
|
|
156
|
+
.optional()
|
|
157
|
+
.describe('Percent complete while running, when the API reports it.'),
|
|
158
|
+
estimatedFinishAt: z
|
|
159
|
+
.string()
|
|
160
|
+
.optional()
|
|
161
|
+
.describe('Server estimate (ISO 8601) of when a running job finishes, when the API has one.'),
|
|
162
|
+
retryAfterMs: z
|
|
163
|
+
.number()
|
|
164
|
+
.optional()
|
|
165
|
+
.describe('How long to wait before reading again, when the API asks for a pause.'),
|
|
166
|
+
result: z
|
|
167
|
+
.record(z.string(), z.unknown())
|
|
168
|
+
.describe('The job as the kind’s own read tool returns it.'),
|
|
169
|
+
};
|
|
170
|
+
const ids = available.map(({ kind, spec }) => `${kind}: ${spec.id}`).join('; ');
|
|
171
|
+
const read = async (args, context, signal = context?.signal) => {
|
|
172
|
+
const job = input.parse(args);
|
|
173
|
+
const entry = available.find((candidate) => candidate.kind === job.kind);
|
|
174
|
+
const reading = await entry.spec.read(entry.tool, job, context, signal);
|
|
175
|
+
return { kind: job.kind, id: job.id, ...reading };
|
|
176
|
+
};
|
|
177
|
+
// An import can start the download it reads, so neither tool is read-only.
|
|
178
|
+
const annotations = {
|
|
179
|
+
readOnlyHint: false,
|
|
180
|
+
destructiveHint: false,
|
|
181
|
+
idempotentHint: true,
|
|
182
|
+
openWorldHint: kinds.includes('import'),
|
|
183
|
+
};
|
|
184
|
+
return [
|
|
185
|
+
{
|
|
186
|
+
name: 'jobs_get',
|
|
187
|
+
title: 'Read any Sync job',
|
|
188
|
+
description: `Read the current state of any Sync job once, by kind and id (${ids}). Returns {kind, id, status, progress?, estimatedFinishAt?, retryAfterMs?, result}. ${STATUS_HELP} To wait for a running job, use jobs_wait instead of calling this in a loop.`,
|
|
189
|
+
inputSchema: input.shape,
|
|
190
|
+
outputSchema: output,
|
|
191
|
+
annotations,
|
|
192
|
+
handler: (args, context) => read(args, context),
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
name: 'jobs_wait',
|
|
196
|
+
title: 'Wait for any Sync job',
|
|
197
|
+
description: `Wait on the server for any Sync job to finish, by kind and id (${ids}). Holds for up to about 50 seconds, sending progress notifications when the client asks for them, then returns the latest {kind, id, status, progress?, estimatedFinishAt?, retryAfterMs?, result}. ${STATUS_HELP}`,
|
|
198
|
+
inputSchema: input.shape,
|
|
199
|
+
outputSchema: output,
|
|
200
|
+
annotations,
|
|
201
|
+
handler: async (args, context) => {
|
|
202
|
+
const label = KINDS[input.parse(args).kind].label;
|
|
203
|
+
return pollUntil((signal) => read(args, context, signal), (job) => isTerminal(job.status) || job.result.recoveryRequired === true, context, {
|
|
204
|
+
describe: (job) => `${label} ${job.status.toLowerCase()}${job.progress === undefined ? '' : ` ${Math.round(job.progress)}%`}`,
|
|
205
|
+
delayMs: (job) => job.retryAfterMs,
|
|
206
|
+
});
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
];
|
|
210
|
+
}
|