@pinepaper.studio/mcp-server 1.6.18 → 1.6.20
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.de.md +1 -1
- package/README.es.md +1 -1
- package/README.fr.md +1 -1
- package/README.hi.md +1 -1
- package/README.ja.md +1 -1
- package/README.ko.md +1 -1
- package/README.md +179 -10
- package/README.pt-BR.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/agent/export-manager.js +2 -2
- package/dist/agent/session-manager.d.ts +8 -0
- package/dist/agent/session-manager.d.ts.map +1 -1
- package/dist/agent/session-manager.js +17 -2
- package/dist/agent/session-manager.js.map +1 -1
- package/dist/assets/asset-manager.d.ts +28 -1
- package/dist/assets/asset-manager.d.ts.map +1 -1
- package/dist/assets/asset-manager.js +65 -25
- package/dist/assets/asset-manager.js.map +1 -1
- package/dist/assets/index.d.ts +1 -3
- package/dist/assets/index.d.ts.map +1 -1
- package/dist/assets/index.js +1 -3
- package/dist/assets/index.js.map +1 -1
- package/dist/assets/repositories/iconify.js +1 -1
- package/dist/assets/repositories/iconify.js.map +1 -1
- package/dist/assets/repositories/openclipart.js +1 -1
- package/dist/assets/repositories/openclipart.js.map +1 -1
- package/dist/assets/repositories/svgrepo.js +2 -2
- package/dist/assets/repositories/svgrepo.js.map +1 -1
- package/dist/assets/types.d.ts +2 -2
- package/dist/assets/types.d.ts.map +1 -1
- package/dist/browser/puppeteer-controller.d.ts +25 -0
- package/dist/browser/puppeteer-controller.d.ts.map +1 -1
- package/dist/browser/puppeteer-controller.js +107 -1
- package/dist/browser/puppeteer-controller.js.map +1 -1
- package/dist/build-info.json +3 -3
- package/dist/cli.js +4 -2
- package/dist/cli.js.map +1 -1
- package/dist/design/design-systems.d.ts +2 -0
- package/dist/design/design-systems.d.ts.map +1 -1
- package/dist/design/design-systems.js +2 -0
- package/dist/design/design-systems.js.map +1 -1
- package/dist/execution/canvas-state.d.ts.map +1 -1
- package/dist/execution/canvas-state.js +19 -43
- package/dist/execution/canvas-state.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +27 -11
- package/dist/index.js.map +1 -1
- package/dist/ontology/design-graph.js +2 -2
- package/dist/ontology/design-graph.js.map +1 -1
- package/dist/ontology/upstream-vocabulary.d.ts +2 -2
- package/dist/ontology/upstream-vocabulary.js +2 -2
- package/dist/ontology/validator.js +1 -1
- package/dist/ontology/validator.js.map +1 -1
- package/dist/ontology/vocabulary.d.ts.map +1 -1
- package/dist/ontology/vocabulary.js +8 -0
- package/dist/ontology/vocabulary.js.map +1 -1
- package/dist/tools/choreograph.d.ts +64 -0
- package/dist/tools/choreograph.d.ts.map +1 -0
- package/dist/tools/choreograph.js +336 -0
- package/dist/tools/choreograph.js.map +1 -0
- package/dist/tools/compact-descriptions.d.ts.map +1 -1
- package/dist/tools/compact-descriptions.js +16 -15
- package/dist/tools/compact-descriptions.js.map +1 -1
- package/dist/tools/definitions.d.ts +10 -1
- package/dist/tools/definitions.d.ts.map +1 -1
- package/dist/tools/definitions.js +705 -150
- package/dist/tools/definitions.js.map +1 -1
- package/dist/tools/effect-check.d.ts +26 -0
- package/dist/tools/effect-check.d.ts.map +1 -0
- package/dist/tools/effect-check.js +129 -0
- package/dist/tools/effect-check.js.map +1 -0
- package/dist/tools/effect-class.d.ts +31 -0
- package/dist/tools/effect-class.d.ts.map +1 -0
- package/dist/tools/effect-class.js +170 -0
- package/dist/tools/effect-class.js.map +1 -0
- package/dist/tools/engine-lists.d.ts +20 -5
- package/dist/tools/engine-lists.d.ts.map +1 -1
- package/dist/tools/engine-lists.js +106 -4
- package/dist/tools/engine-lists.js.map +1 -1
- package/dist/tools/engine-methods.d.ts.map +1 -1
- package/dist/tools/engine-methods.js +22 -1
- package/dist/tools/engine-methods.js.map +1 -1
- package/dist/tools/engine-surface.d.ts +1 -1
- package/dist/tools/engine-surface.d.ts.map +1 -1
- package/dist/tools/engine-surface.js +232 -13
- package/dist/tools/engine-surface.js.map +1 -1
- package/dist/tools/handlers/camera.js +8 -8
- package/dist/tools/handlers/camera.js.map +1 -1
- package/dist/tools/handlers/export.d.ts.map +1 -1
- package/dist/tools/handlers/export.js +84 -5
- package/dist/tools/handlers/export.js.map +1 -1
- package/dist/tools/handlers/font.js +10 -10
- package/dist/tools/handlers/font.js.map +1 -1
- package/dist/tools/handlers/generate.d.ts +12 -12
- package/dist/tools/handlers/maps.d.ts.map +1 -1
- package/dist/tools/handlers/maps.js +23 -3
- package/dist/tools/handlers/maps.js.map +1 -1
- package/dist/tools/handlers.d.ts +44 -0
- package/dist/tools/handlers.d.ts.map +1 -1
- package/dist/tools/handlers.js +413 -39
- package/dist/tools/handlers.js.map +1 -1
- package/dist/tools/index.d.ts +2 -2
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +2 -2
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/minimal-descriptions.d.ts.map +1 -1
- package/dist/tools/minimal-descriptions.js +39 -26
- package/dist/tools/minimal-descriptions.js.map +1 -1
- package/dist/tools/shape-params.d.ts +2 -2
- package/dist/tools/shape-params.d.ts.map +1 -1
- package/dist/tools/shape-params.js +27 -2
- package/dist/tools/shape-params.js.map +1 -1
- package/dist/tools/stick-vocabulary.d.ts +1 -1
- package/dist/tools/stick-vocabulary.js +1 -1
- package/dist/tools/toolkits.d.ts.map +1 -1
- package/dist/tools/toolkits.js +11 -0
- package/dist/tools/toolkits.js.map +1 -1
- package/dist/types/code-generator.d.ts +91 -3
- package/dist/types/code-generator.d.ts.map +1 -1
- package/dist/types/code-generator.js +1544 -276
- package/dist/types/code-generator.js.map +1 -1
- package/dist/types/schemas.d.ts +3203 -907
- package/dist/types/schemas.d.ts.map +1 -1
- package/dist/types/schemas.js +393 -53
- package/dist/types/schemas.js.map +1 -1
- package/manifest.json +46 -2
- package/package.json +4 -3
package/dist/tools/handlers.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Supports browser execution via Puppeteer.
|
|
7
7
|
*/
|
|
8
8
|
import { codeGenerator, generateQueryMutationsCode, SHEET_RUBRIC } from '../types/code-generator.js';
|
|
9
|
-
import { CreateItemInputSchema, ModifyItemInputSchema, DeleteItemInputSchema, AddRelationInputSchema, RemoveRelationInputSchema, QueryRelationsInputSchema, AnimateItemInputSchema, ImportMotionCaptureInputSchema, KeyframeAnimateInputSchema, ExecuteGeneratorInputSchema, ApplyEffectInputSchema, GetItemsInputSchema, PlayTimelineInputSchema, SetBackgroundColorInputSchema, SetCanvasSizeInputSchema, ImportSVGInputSchema, ImportMermaidInputSchema, AddFilterInputSchema, CreateGlossySphereInputSchema, CreateDiagonalStripesInputSchema, BatchCreateInputSchema, BatchModifyInputSchema, CreateGridInputSchema, GeometryInputSchema, EquationPathInputSchema, EventInputSchema, ConstructionSequenceInputSchema, ValidateSceneInputSchema, CaptureFramesInputSchema, InstantiateOntologyInputSchema, LintSceneInputSchema, MediaInputSchema, TextStyleInputSchema, TextEffectInputSchema, DesignMediumInputSchema, ShatterImageInputSchema, ImportLayeredCharacterInputSchema, CharacterInputSchema, GameInputSchema, World3DInputSchema, BrandKitInputSchema, ComponentInputSchema, ArtboardInputSchema, CommentInputSchema, ProvenanceInputSchema, SceneDiffInputSchema, AudioBeatsInputSchema, TemplateParamsInputSchema, RenderBatchInputSchema, AccessibilityCheckInputSchema, QueryMutationsInputSchema, ComposeInputSchema, CropImageInputSchema, PathOpInputSchema, MotionInputSchema, SoundInputSchema, InterchangeInputSchema, StickInputSchema, DesignSystemInputSchema, StoryInputSchema, ChromaKeyInputSchema, RiggingInputSchema, GroupInputSchema, CameraDirectorInputSchema, DetectObjectsInputSchema, ExtractObjectInputSchema, ArrangeInputSchema, CreateSceneInputSchema, GetPerformanceMetricsInputSchema, DiagnosticReportInputSchema, SearchAssetsInputSchema, ImportAssetInputSchema, P5DrawInputSchema,
|
|
9
|
+
import { CreateItemInputSchema, ModifyItemInputSchema, DeleteItemInputSchema, AddRelationInputSchema, RemoveRelationInputSchema, QueryRelationsInputSchema, AnimateItemInputSchema, ImportMotionCaptureInputSchema, KeyframeAnimateInputSchema, ExecuteGeneratorInputSchema, ApplyEffectInputSchema, GetItemsInputSchema, PlayTimelineInputSchema, SetBackgroundColorInputSchema, SetCanvasSizeInputSchema, ImportSVGInputSchema, ImportMermaidInputSchema, AddFilterInputSchema, CreateGlossySphereInputSchema, CreateDiagonalStripesInputSchema, BatchCreateInputSchema, BatchModifyInputSchema, CreateGridInputSchema, GeometryInputSchema, EquationPathInputSchema, EventInputSchema, ConstructionSequenceInputSchema, ValidateSceneInputSchema, CaptureFramesInputSchema, InstantiateOntologyInputSchema, LintSceneInputSchema, MediaInputSchema, TextStyleInputSchema, TextEffectInputSchema, DesignMediumInputSchema, RelightInputSchema, ChoreographInputSchema, EmitterInputSchema, MorphSequenceInputSchema, OriginalCharacterInputSchema, LookInputSchema, RenderHookInputSchema, ShaderGraphInputSchema, ShatterImageInputSchema, ImportLayeredCharacterInputSchema, CharacterInputSchema, GameInputSchema, World3DInputSchema, BrandKitInputSchema, ComponentInputSchema, ArtboardInputSchema, CommentInputSchema, ProvenanceInputSchema, SceneDiffInputSchema, AudioBeatsInputSchema, TemplateParamsInputSchema, RenderBatchInputSchema, AccessibilityCheckInputSchema, QueryMutationsInputSchema, ComposeInputSchema, CropImageInputSchema, PathOpInputSchema, MotionInputSchema, SoundInputSchema, InterchangeInputSchema, StickInputSchema, DesignSystemInputSchema, StoryInputSchema, ChromaKeyInputSchema, RiggingInputSchema, GroupInputSchema, CameraDirectorInputSchema, DetectObjectsInputSchema, ExtractObjectInputSchema, ArrangeInputSchema, CreateSceneInputSchema, GetPerformanceMetricsInputSchema, DiagnosticReportInputSchema, SearchAssetsInputSchema, ImportAssetInputSchema, P5DrawInputSchema,
|
|
10
10
|
// Diagram schemas
|
|
11
11
|
CreateDiagramShapeInputSchema, ConnectInputSchema, ConnectPortsInputSchema, AddPortsInputSchema, AutoLayoutInputSchema, GetDiagramShapesInputSchema, UpdateConnectorInputSchema, RemoveConnectorInputSchema, DiagramModeInputSchema,
|
|
12
12
|
// Agent flow mode schemas
|
|
@@ -27,8 +27,11 @@ ImportImageInputSchema,
|
|
|
27
27
|
// Scene management schemas
|
|
28
28
|
ManageScenesInputSchema, ScenePlaybackInputSchema, SceneGraphInputSchema, SequenceInputSchema, StaggerInputSchema, FlipInputSchema,
|
|
29
29
|
// New consolidated tool schemas
|
|
30
|
-
SelectionInputSchema, TransformInputSchema, HistoryInputSchema, ImageFilterInputSchema, LassoInputSchema, CutoutStyleInputSchema, PrecompInputSchema, BackgroundInputSchema, QueryInputSchema, QueryCapabilitiesInputSchema, DeformInputSchema, SpriteSheetInputSchema, InteractionInputSchema, ExportWidgetInputSchema, ExportWidgetHtmlInputSchema, CreateChartInputSchema, MagicInputSchema, PhysicsInputSchema, MeasurementInputSchema, ErrorCodes, } from '../types/schemas.js';
|
|
30
|
+
SelectionInputSchema, TransformInputSchema, HistoryInputSchema, ImageFilterInputSchema, LassoInputSchema, CutoutStyleInputSchema, PrecompInputSchema, BackgroundInputSchema, QueryInputSchema, QueryCapabilitiesInputSchema, DeformInputSchema, SpriteSheetInputSchema, InteractionInputSchema, ExportWidgetInputSchema, ExportWidgetHtmlInputSchema, CreateChartInputSchema, MagicInputSchema, PhysicsInputSchema, MeasurementInputSchema, PlaceOnSurfaceInputSchema, StyledSceneInputSchema, ErrorCodes, } from '../types/schemas.js';
|
|
31
31
|
import { ZodError, z } from 'zod';
|
|
32
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
33
|
+
import { effectClass } from './effect-class.js';
|
|
34
|
+
import { EFFECT_BEFORE_JS, effectAfterJs, namedItemIds, idMayBeGone, NO_CHANGE_NOTE } from './effect-check.js';
|
|
32
35
|
import { writeFile, mkdir, appendFile, unlink, readFile, stat } from 'node:fs/promises';
|
|
33
36
|
import { tmpdir } from 'node:os';
|
|
34
37
|
import { join, extname } from 'node:path';
|
|
@@ -37,6 +40,7 @@ import { getBrowserController, resetBrowserController, } from '../browser/puppet
|
|
|
37
40
|
import { sameStudioTarget, validateStudioUrl } from '../browser/url-target.js';
|
|
38
41
|
import { getPerformanceTracker } from '../metrics/index.js';
|
|
39
42
|
import { formatErrorContext, captureCanvasState } from '../execution/index.js';
|
|
43
|
+
import { FILTER_PARAM_RANGES } from './engine-lists.js';
|
|
40
44
|
import { getSessionManager } from '../agent/session-manager.js';
|
|
41
45
|
import { vocabularyHintForPath, validateBatchVocabulary, detectBatchPropertyTypos } from '../ontology/hints.js';
|
|
42
46
|
import { cameraHandlers } from './handlers/camera.js';
|
|
@@ -560,10 +564,37 @@ export function getExecutionMode() {
|
|
|
560
564
|
// Default to 'puppeteer' for live browser execution
|
|
561
565
|
return 'puppeteer';
|
|
562
566
|
}
|
|
567
|
+
/** Where a generated script travels for a host to record, beside the agent-visible text. */
|
|
568
|
+
/** Relation types registered with register_custom_relation in this server session (gate D2). */
|
|
569
|
+
export const CUSTOM_RELATION_NAMES = new Set();
|
|
570
|
+
/**
|
|
571
|
+
* Tools whose browser work legitimately outlasts the governor's 10 s default.
|
|
572
|
+
* The first detection call downloads and initialises a model, which on a cold
|
|
573
|
+
* page took longer than 10 s and died as a timeout (gate C10, 1.6.19).
|
|
574
|
+
*/
|
|
575
|
+
const LONG_GOVERNOR_BUDGET_MS = {
|
|
576
|
+
pinepaper_detect_objects: 120_000,
|
|
577
|
+
pinepaper_extract_object: 120_000,
|
|
578
|
+
};
|
|
579
|
+
export const CODE_META_KEY = 'pinepaper.studio/code';
|
|
563
580
|
// =============================================================================
|
|
564
581
|
// RESULT HELPERS
|
|
565
582
|
// =============================================================================
|
|
566
|
-
function successResult(code, description) {
|
|
583
|
+
function successResult(code, description, deferred = false) {
|
|
584
|
+
if (deferred) {
|
|
585
|
+
return {
|
|
586
|
+
content: [{
|
|
587
|
+
type: 'text',
|
|
588
|
+
text: JSON.stringify({
|
|
589
|
+
success: true,
|
|
590
|
+
status: 'recorded',
|
|
591
|
+
step: description || 'canvas step',
|
|
592
|
+
note: 'Recorded on the scene. It runs in the studio when the scene renders or exports, not now; keep building, then render.',
|
|
593
|
+
}, null, 2),
|
|
594
|
+
}],
|
|
595
|
+
_meta: { [CODE_META_KEY]: code },
|
|
596
|
+
};
|
|
597
|
+
}
|
|
567
598
|
const content = [
|
|
568
599
|
{
|
|
569
600
|
type: 'text',
|
|
@@ -576,7 +607,7 @@ function successResult(code, description) {
|
|
|
576
607
|
text: `\n${description}`,
|
|
577
608
|
});
|
|
578
609
|
}
|
|
579
|
-
return { content };
|
|
610
|
+
return { content, _meta: { [CODE_META_KEY]: code } };
|
|
580
611
|
}
|
|
581
612
|
/**
|
|
582
613
|
* A plain data answer.
|
|
@@ -589,6 +620,29 @@ function successResult(code, description) {
|
|
|
589
620
|
function dataResult(value) {
|
|
590
621
|
return { content: [{ type: 'text', text: JSON.stringify(value, null, 2) }] };
|
|
591
622
|
}
|
|
623
|
+
/**
|
|
624
|
+
* Where a call's time went (O1, per-request trace). In the result's _meta, the
|
|
625
|
+
* spec's slot for metadata, under one namespaced key, so it adds nothing to
|
|
626
|
+
* the text a model reads. browserMs includes the screenshot when one was taken.
|
|
627
|
+
* index.ts adds toolMs, the whole call, for every tool.
|
|
628
|
+
*/
|
|
629
|
+
export const TIMING_META_KEY = 'pinepaper.studio/timing';
|
|
630
|
+
export const EFFECT_META_KEY = 'pinepaper.studio/effect';
|
|
631
|
+
/** The tool and action of the call in flight, for executeOrGenerate's effect check. */
|
|
632
|
+
const callContext = new AsyncLocalStorage();
|
|
633
|
+
let READ_ONLY_TOOLS = null;
|
|
634
|
+
async function readOnlyTools() {
|
|
635
|
+
if (!READ_ONLY_TOOLS) {
|
|
636
|
+
const { PINEPAPER_TOOLS } = await import('./definitions.js');
|
|
637
|
+
READ_ONLY_TOOLS = new Set(PINEPAPER_TOOLS.filter((t) => t.annotations?.readOnlyHint).map((t) => t.name));
|
|
638
|
+
}
|
|
639
|
+
return READ_ONLY_TOOLS;
|
|
640
|
+
}
|
|
641
|
+
export function withTiming(out, t) {
|
|
642
|
+
const meta = (out._meta ?? {});
|
|
643
|
+
const prev = (meta[TIMING_META_KEY] ?? {});
|
|
644
|
+
return { ...out, _meta: { ...meta, [TIMING_META_KEY]: { ...prev, ...t } } };
|
|
645
|
+
}
|
|
592
646
|
/**
|
|
593
647
|
* A call that only succeeded after the browser session was recovered says so,
|
|
594
648
|
* in the result: the controller has reported `recovered` / `canvasReset` for
|
|
@@ -601,7 +655,7 @@ function withSessionNote(out, r) {
|
|
|
601
655
|
const note = r.relaunched
|
|
602
656
|
? `The browser session had been lost; the browser was relaunched and this call ran on an EMPTY canvas${r.canvasSizeRestored ? ` (size restored to ${r.canvasSizeRestored.width}x${r.canvasSizeRestored.height})` : ' at the default size'}. Item ids from before are gone: rebuild the scene.`
|
|
603
657
|
: r.canvasReset
|
|
604
|
-
? 'The page had reloaded and the canvas came back EMPTY. Item ids from before are gone: rebuild the scene.'
|
|
658
|
+
? 'The studio page had reloaded or navigated away, and the canvas came back EMPTY. Item ids from before are gone: rebuild the scene.'
|
|
605
659
|
: 'The page connection was re-established; the scene survived.';
|
|
606
660
|
const first = out.content[0];
|
|
607
661
|
if (first && first.type === 'text') {
|
|
@@ -804,6 +858,80 @@ const REMOVE_ITEMS_CREATED_SINCE_MARK = `(function () {
|
|
|
804
858
|
* false when the page could not be marked (the rollback is then skipped and
|
|
805
859
|
* the failure note says nothing about the scene it cannot vouch for).
|
|
806
860
|
*/
|
|
861
|
+
/**
|
|
862
|
+
* The studio's job gate (FxTool D92). PinePaperAgent.startJob is the one place
|
|
863
|
+
* a stale studio reloads, and only when the job is about to clear the canvas:
|
|
864
|
+
* it resolves {reloading: true, fromBuild, toBuild} and the PAGE reloads, so
|
|
865
|
+
* nothing running in it survives — the wait has to happen here. Poll the new
|
|
866
|
+
* page through waitForReady, open the job again, then let start_job's own
|
|
867
|
+
* setup (clear, canvas size) run on the fresh page.
|
|
868
|
+
*/
|
|
869
|
+
export async function studioJobGate(input, options, wait = (ms) => new Promise((r) => setTimeout(r, ms))) {
|
|
870
|
+
const mode = options.executionMode ?? getExecutionMode();
|
|
871
|
+
if (mode === 'code' || !options.executeInBrowser || options.deferred)
|
|
872
|
+
return {};
|
|
873
|
+
const controller = options.browserController || getBrowserController();
|
|
874
|
+
if (!controller.connected) {
|
|
875
|
+
try {
|
|
876
|
+
await controller.connect();
|
|
877
|
+
}
|
|
878
|
+
catch {
|
|
879
|
+
return {};
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
const args = JSON.stringify({ ...(input.name ? { name: input.name } : {}), clearCanvas: input.clearCanvas !== false });
|
|
883
|
+
const START = `(async function () {
|
|
884
|
+
const A = typeof window !== 'undefined' ? window.PinePaperAgent : null;
|
|
885
|
+
if (!A || !A.startJob) return { legacy: true };
|
|
886
|
+
return await A.startJob(${args});
|
|
887
|
+
})();`;
|
|
888
|
+
const call = async (code) => {
|
|
889
|
+
try {
|
|
890
|
+
const r = await controller.executeCode(code, false, { bypassGovernor: true });
|
|
891
|
+
return r.success ? r.result : null;
|
|
892
|
+
}
|
|
893
|
+
catch {
|
|
894
|
+
return null;
|
|
895
|
+
}
|
|
896
|
+
};
|
|
897
|
+
const first = await call(START);
|
|
898
|
+
if (!first || first.legacy || (first.jobOpen === undefined && first.reloading === undefined))
|
|
899
|
+
return {};
|
|
900
|
+
const build = (j) => ({ buildId: j.buildId, liveBuildId: j.liveBuildId, stale: !!j.stale });
|
|
901
|
+
if (first.reloading) {
|
|
902
|
+
const READY = `(async function () {
|
|
903
|
+
const A = typeof window !== 'undefined' ? window.PinePaperAgent : null;
|
|
904
|
+
if (!A || !A.waitForReady) return { ready: false };
|
|
905
|
+
await A.waitForReady();
|
|
906
|
+
return { ready: true };
|
|
907
|
+
})();`;
|
|
908
|
+
let ready = false;
|
|
909
|
+
for (let i = 0; i < 60 && !ready; i++) { // ~30 s: a reload is ~100 ms plus a fresh boot
|
|
910
|
+
await wait(500);
|
|
911
|
+
const r = await call(READY);
|
|
912
|
+
ready = !!(r && r.ready);
|
|
913
|
+
}
|
|
914
|
+
if (!ready)
|
|
915
|
+
return { warning: `the studio was reloading from build ${first.fromBuild} to ${first.toBuild} and did not come back within 30 s; this job may run on the old page.`, build: build(first) };
|
|
916
|
+
const second = await call(START);
|
|
917
|
+
if (!second)
|
|
918
|
+
return { warning: 'the studio reloaded but did not open the job again.', build: build(first) };
|
|
919
|
+
return {
|
|
920
|
+
note: `Studio was on build ${first.fromBuild}, reloaded to ${first.toBuild} before this job.`,
|
|
921
|
+
...(second.stale ? { warning: `the studio is still on an old build after reloading (${second.buildId}; live ${second.liveBuildId})${second.reason ? ': ' + second.reason : ''}.` } : {}),
|
|
922
|
+
build: build(second),
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
if (first.stale) {
|
|
926
|
+
return {
|
|
927
|
+
warning: input.clearCanvas === false
|
|
928
|
+
? `studio is on old build ${first.buildId} (live: ${first.liveBuildId}); finish, then start a new job to update.`
|
|
929
|
+
: `studio is on old build ${first.buildId} (live: ${first.liveBuildId}) and was not reloaded${first.reason ? ': ' + first.reason : ''}.`,
|
|
930
|
+
build: build(first),
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
return { build: build(first) };
|
|
934
|
+
}
|
|
807
935
|
async function markItemsBeforeRun(controller) {
|
|
808
936
|
try {
|
|
809
937
|
const r = await controller.executeCode(MARK_ITEMS_BEFORE_RUN, false, { bypassGovernor: true });
|
|
@@ -894,6 +1022,40 @@ async function brandFromUrl(url, shots, options) {
|
|
|
894
1022
|
return errorResult(ErrorCodes.EXECUTION_ERROR, `could not read ${url}: ${e instanceof Error ? e.message : String(e)}`, {}, { toolName: 'pinepaper_brand_kit' });
|
|
895
1023
|
}
|
|
896
1024
|
}
|
|
1025
|
+
/**
|
|
1026
|
+
* If an executed result carries `data` (a data URL, or JSON/text), write it to
|
|
1027
|
+
* a file in the export dir and replace it with filePath + bytes.
|
|
1028
|
+
*/
|
|
1029
|
+
async function writeResultDataToFile(out, ext) {
|
|
1030
|
+
const first = out.content?.[0];
|
|
1031
|
+
if (out.isError || !first || first.type !== 'text')
|
|
1032
|
+
return out;
|
|
1033
|
+
let obj;
|
|
1034
|
+
try {
|
|
1035
|
+
obj = JSON.parse(first.text);
|
|
1036
|
+
}
|
|
1037
|
+
catch {
|
|
1038
|
+
return out;
|
|
1039
|
+
}
|
|
1040
|
+
const r = obj.result;
|
|
1041
|
+
if (!r || r.data === undefined || r.data === null)
|
|
1042
|
+
return out;
|
|
1043
|
+
let bytes;
|
|
1044
|
+
if (typeof r.data === 'string' && r.data.startsWith('data:'))
|
|
1045
|
+
bytes = Buffer.from(r.data.slice(r.data.indexOf(',') + 1), 'base64');
|
|
1046
|
+
else if (typeof r.data === 'string')
|
|
1047
|
+
bytes = Buffer.from(r.data, 'utf-8');
|
|
1048
|
+
else
|
|
1049
|
+
bytes = Buffer.from(JSON.stringify(r.data), 'utf-8');
|
|
1050
|
+
const dir = getExportDir();
|
|
1051
|
+
await mkdir(dir, { recursive: true });
|
|
1052
|
+
const filePath = join(dir, `pinepaper_${String(r.format ?? 'export').replace(/[^a-z0-9-]/gi, '')}_${Date.now()}.${ext}`);
|
|
1053
|
+
await writeFile(filePath, bytes);
|
|
1054
|
+
const { data: _d, ...rest } = r;
|
|
1055
|
+
void _d;
|
|
1056
|
+
obj.result = { ...rest, filePath, bytes: bytes.length };
|
|
1057
|
+
return { ...out, content: [{ type: 'text', text: JSON.stringify(obj, null, 2) }, ...out.content.slice(1)] };
|
|
1058
|
+
}
|
|
897
1059
|
/**
|
|
898
1060
|
* pinepaper_capture_frames with `sheet`: one tiled image of the film, saved
|
|
899
1061
|
* to a file (an image is never useful inline, see ALWAYS_SAVE_FORMATS), with
|
|
@@ -998,7 +1160,7 @@ The code is ready to use. Each subsequent tool call will also generate code you
|
|
|
998
1160
|
timestamp: Date.now(),
|
|
999
1161
|
success: true,
|
|
1000
1162
|
});
|
|
1001
|
-
return successResult(code, description);
|
|
1163
|
+
return successResult(code, description, options.deferred === true);
|
|
1002
1164
|
}
|
|
1003
1165
|
const controller = browserController || getBrowserController();
|
|
1004
1166
|
// Auto-connect in agent mode if not connected
|
|
@@ -1081,18 +1243,46 @@ ${code}
|
|
|
1081
1243
|
// emitter is not (its verdict already describes what it left behind).
|
|
1082
1244
|
const rollbackOnFailure = toolName === 'pinepaper_execute_custom_code'
|
|
1083
1245
|
&& await markItemsBeforeRun(controller);
|
|
1084
|
-
const
|
|
1246
|
+
const governorTimeoutMs = LONG_GOVERNOR_BUDGET_MS[toolName];
|
|
1247
|
+
// NO SILENT SUCCESS (effect-class.ts / effect-check.ts): fingerprint a
|
|
1248
|
+
// mutating call before and after it runs.
|
|
1249
|
+
const ctx = callContext.getStore();
|
|
1250
|
+
const action = ctx && ctx.toolName === toolName ? ctx.action : undefined;
|
|
1251
|
+
const effect = effectClass(toolName, action, (await readOnlyTools()).has(toolName));
|
|
1252
|
+
const checkEffect = effect === 'mutates' && !rollbackOnFailure;
|
|
1253
|
+
let effectCheckMs = 0;
|
|
1254
|
+
let effectArmed = false;
|
|
1255
|
+
if (checkEffect) {
|
|
1256
|
+
const t0 = Date.now();
|
|
1257
|
+
try {
|
|
1258
|
+
const r = await controller.executeCode(EFFECT_BEFORE_JS, false, { bypassGovernor: true });
|
|
1259
|
+
effectArmed = !!(r.success && r.result?.ok);
|
|
1260
|
+
}
|
|
1261
|
+
catch { /* unchecked this time */ }
|
|
1262
|
+
effectCheckMs += Date.now() - t0;
|
|
1263
|
+
}
|
|
1264
|
+
const result = await controller.executeCode(code, shouldTakeScreenshot, {
|
|
1265
|
+
...(stage ? { stage } : {}),
|
|
1266
|
+
...(governorTimeoutMs ? { governorTimeoutMs } : {}),
|
|
1267
|
+
});
|
|
1085
1268
|
const browserDuration = tracker.endTimer(`${timerId}_browser_execution`);
|
|
1269
|
+
// THE METRIC RECORDS THE CALL'S VERDICT, NOT THE TRANSPORT'S (gate H2,
|
|
1270
|
+
// 1.6.19). result.success only says the code RAN; an emitter that ran and
|
|
1271
|
+
// returned { success: false } is a failed call, and get_performance_metrics
|
|
1272
|
+
// reported "Success Rate: 100.0%" over exactly such a failure.
|
|
1273
|
+
const innerVerdict = result.success ? innerFailure(result.result) : null;
|
|
1086
1274
|
tracker.recordMetric({
|
|
1087
1275
|
toolName,
|
|
1088
1276
|
phase: 'browser_execution',
|
|
1089
1277
|
duration: browserDuration,
|
|
1090
1278
|
timestamp: Date.now(),
|
|
1091
|
-
success: result.success,
|
|
1092
|
-
error: result.error,
|
|
1279
|
+
success: result.success && !innerVerdict,
|
|
1280
|
+
error: result.error || innerVerdict || undefined,
|
|
1093
1281
|
});
|
|
1282
|
+
let screenshotMs;
|
|
1094
1283
|
if (shouldTakeScreenshot) {
|
|
1095
1284
|
const screenshotDuration = tracker.endTimer(`${timerId}_screenshot`);
|
|
1285
|
+
screenshotMs = screenshotDuration;
|
|
1096
1286
|
tracker.recordMetric({
|
|
1097
1287
|
toolName,
|
|
1098
1288
|
phase: 'screenshot',
|
|
@@ -1108,13 +1298,14 @@ ${code}
|
|
|
1108
1298
|
phase: 'total',
|
|
1109
1299
|
duration: totalDuration,
|
|
1110
1300
|
timestamp: Date.now(),
|
|
1111
|
-
success: result.success,
|
|
1112
|
-
error: result.error,
|
|
1301
|
+
success: result.success && !innerVerdict,
|
|
1302
|
+
error: result.error || innerVerdict || undefined,
|
|
1113
1303
|
});
|
|
1304
|
+
const timing = { browserMs: Math.round(browserDuration), ...(screenshotMs !== undefined ? { screenshotMs: Math.round(screenshotMs) } : {}) };
|
|
1114
1305
|
if (!result.success) {
|
|
1115
1306
|
// Capture canvas state for error context
|
|
1116
1307
|
const canvasState = await captureCanvasState(controller);
|
|
1117
|
-
return errorResult(ErrorCodes.EXECUTION_ERROR, result.error || 'Failed to execute code in browser',
|
|
1308
|
+
return withTiming(errorResult(ErrorCodes.EXECUTION_ERROR, result.error || 'Failed to execute code in browser',
|
|
1118
1309
|
// errorCode surfaces the governor's structured code (PP_ITEM_BUDGET,
|
|
1119
1310
|
// PP_LOOP_BUDGET, PP_TIMEOUT) so the model can correct on the next turn.
|
|
1120
1311
|
{
|
|
@@ -1123,7 +1314,7 @@ ${code}
|
|
|
1123
1314
|
}, {
|
|
1124
1315
|
toolName,
|
|
1125
1316
|
canvasState: canvasState || undefined,
|
|
1126
|
-
});
|
|
1317
|
+
}), timing);
|
|
1127
1318
|
}
|
|
1128
1319
|
// TWO VERDICTS. `result.success` says the CODE RAN. The emitted code has its
|
|
1129
1320
|
// own verdict, and almost every emitter in this repo returns
|
|
@@ -1137,15 +1328,57 @@ ${code}
|
|
|
1137
1328
|
// outer success and the agent kept building on a rig that did not exist.
|
|
1138
1329
|
// pinepaper_agent_export got this check in 1.6.9; it belongs HERE, where
|
|
1139
1330
|
// every tool inherits it.
|
|
1140
|
-
const inner =
|
|
1331
|
+
const inner = innerVerdict;
|
|
1141
1332
|
if (inner) {
|
|
1142
1333
|
const canvasState = await captureCanvasState(controller);
|
|
1143
|
-
|
|
1334
|
+
// THE ENGINE'S OWN REASON, when it gave one. An emitter's refusal is often
|
|
1335
|
+
// generic ("the studio refused the time_expression relation") while the
|
|
1336
|
+
// engine said exactly why in a warning the governor captured ("unknown
|
|
1337
|
+
// property 'opacty' — did you mean 'opacity'?"; D78). Production strips
|
|
1338
|
+
// console.warn, so this is the only place the model can read it.
|
|
1339
|
+
const said = (Array.isArray(result.report?.warnings) ? result.report.warnings : [])
|
|
1340
|
+
.filter((w) => typeof w === 'string' && w.length > 0 && !inner.includes(w))
|
|
1341
|
+
.slice(0, 3);
|
|
1342
|
+
return withTiming(errorResult(ErrorCodes.EXECUTION_ERROR, said.length ? `${inner} The studio said: ${said.join(' | ')}` : inner, {
|
|
1144
1343
|
code, result: result.result, governorReport: result.report,
|
|
1145
1344
|
...(rollbackOnFailure ? await customCodeRollback(controller, inner) : {}),
|
|
1146
|
-
}, { toolName, canvasState: canvasState || undefined });
|
|
1345
|
+
}, { toolName, canvasState: canvasState || undefined }), timing);
|
|
1346
|
+
}
|
|
1347
|
+
// Session item counts come from the governor's own count of what this call
|
|
1348
|
+
// created (gate H3: the session stats read 0 with items on the canvas).
|
|
1349
|
+
const created = result.report?.items?.created;
|
|
1350
|
+
if (typeof created === 'number' && created > 0) {
|
|
1351
|
+
try {
|
|
1352
|
+
getSessionManager().noteItemsCreated(created);
|
|
1353
|
+
}
|
|
1354
|
+
catch { /* no session */ }
|
|
1355
|
+
}
|
|
1356
|
+
let noChangeNote = null;
|
|
1357
|
+
let effectMeta = null;
|
|
1358
|
+
if (checkEffect && effectArmed) {
|
|
1359
|
+
const t0 = Date.now();
|
|
1360
|
+
const ids = idMayBeGone(toolName, action) ? [] : namedItemIds(result.result);
|
|
1361
|
+
const verdict = await controller.executeCode(effectAfterJs(ids), false, { bypassGovernor: true })
|
|
1362
|
+
.then((r) => (r.success ? r.result : null))
|
|
1363
|
+
.catch(() => null);
|
|
1364
|
+
effectCheckMs += Date.now() - t0;
|
|
1365
|
+
if (verdict && verdict.ok) {
|
|
1366
|
+
// A CONTRADICTION, not idempotence: the call names an item that is not there.
|
|
1367
|
+
if (verdict.missing && verdict.missing.length) {
|
|
1368
|
+
return withTiming(errorResult(ErrorCodes.EXECUTION_ERROR, `the call reported ${verdict.missing.join(', ')}, which ${verdict.missing.length === 1 ? 'is' : 'are'} not on the canvas: it did not make what it said it made.`, { result: result.result }, { toolName }), { ...timing, effectCheckMs });
|
|
1369
|
+
}
|
|
1370
|
+
const changed = verdict.changed ?? [];
|
|
1371
|
+
effectMeta = { class: effect, changed: changed.length > 0, signals: changed, checked: verdict.checked ?? [] };
|
|
1372
|
+
if (!changed.length)
|
|
1373
|
+
noChangeNote = NO_CHANGE_NOTE(verdict.checked ?? []);
|
|
1374
|
+
}
|
|
1147
1375
|
}
|
|
1148
|
-
|
|
1376
|
+
let out = withSessionNote(executedResult(code, result.result, result.screenshot, description, result.report), result);
|
|
1377
|
+
if (noChangeNote)
|
|
1378
|
+
out = { ...out, content: [...(out.content ?? []), { type: 'text', text: noChangeNote }] };
|
|
1379
|
+
if (effectMeta)
|
|
1380
|
+
out = { ...out, _meta: { ...(out._meta ?? {}), [EFFECT_META_KEY]: effectMeta } };
|
|
1381
|
+
return withTiming(out, checkEffect ? { ...timing, effectCheckMs } : timing);
|
|
1149
1382
|
}
|
|
1150
1383
|
/**
|
|
1151
1384
|
* The emitted code's own verdict, when it reported one and it was a failure.
|
|
@@ -1334,7 +1567,7 @@ export function ignoredArgumentsNote(toolName, args) {
|
|
|
1334
1567
|
export async function handleToolCall(toolName, args, options = {}) {
|
|
1335
1568
|
const tracker = getPerformanceTracker();
|
|
1336
1569
|
const baseTimerId = `${toolName}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
1337
|
-
const result = await handleToolCallInner(toolName, args, options, tracker, baseTimerId);
|
|
1570
|
+
const result = await callContext.run({ toolName, action: typeof args.action === 'string' ? args.action : undefined }, () => handleToolCallInner(toolName, args, options, tracker, baseTimerId));
|
|
1338
1571
|
const ignoredNote = ignoredArgumentsNote(toolName, args);
|
|
1339
1572
|
if (ignoredNote)
|
|
1340
1573
|
result.content = [...(result.content ?? []), { type: 'text', text: ignoredNote }];
|
|
@@ -1703,6 +1936,28 @@ async function handleToolCallInner(toolName, args, options, tracker, baseTimerId
|
|
|
1703
1936
|
}
|
|
1704
1937
|
case 'pinepaper_validate_scene': {
|
|
1705
1938
|
const input = ValidateSceneInputSchema.parse(args);
|
|
1939
|
+
// The validator reads { from, to, relation } on an addRelation op; the
|
|
1940
|
+
// add_relation tool's own names (source, target, relationType) are what
|
|
1941
|
+
// an agent reaches for, and the tool's example used them, so every
|
|
1942
|
+
// example op failed SOURCE_NOT_FOUND "undefined" (gate D8, 1.6.19).
|
|
1943
|
+
if (Array.isArray(input.ops)) {
|
|
1944
|
+
input.ops = input.ops.map((op) => {
|
|
1945
|
+
const o = { ...op };
|
|
1946
|
+
if (o.from === undefined && o.source !== undefined) {
|
|
1947
|
+
o.from = o.source;
|
|
1948
|
+
delete o.source;
|
|
1949
|
+
}
|
|
1950
|
+
if (o.to === undefined && o.target !== undefined) {
|
|
1951
|
+
o.to = o.target;
|
|
1952
|
+
delete o.target;
|
|
1953
|
+
}
|
|
1954
|
+
if (o.relation === undefined && o.relationType !== undefined) {
|
|
1955
|
+
o.relation = o.relationType;
|
|
1956
|
+
delete o.relationType;
|
|
1957
|
+
}
|
|
1958
|
+
return o;
|
|
1959
|
+
});
|
|
1960
|
+
}
|
|
1706
1961
|
const code = codeGenerator.generateValidateScene(input);
|
|
1707
1962
|
const description = input.ops ? `Pre-validate ${input.ops.length} proposed op(s)` : 'Audit the live scene';
|
|
1708
1963
|
return executeOrGenerate(code, description, options, 'pinepaper_validate_scene');
|
|
@@ -1770,6 +2025,47 @@ async function handleToolCallInner(toolName, args, options, tracker, baseTimerId
|
|
|
1770
2025
|
return executeOrGenerate(code, `Text effect: ${input.action}`, options, 'pinepaper_text_effect');
|
|
1771
2026
|
}
|
|
1772
2027
|
// ─── 1.6.4: agent surface for the Tier-2 engine features ───
|
|
2028
|
+
case 'pinepaper_look': {
|
|
2029
|
+
const input = LookInputSchema.parse(args);
|
|
2030
|
+
return executeOrGenerate(codeGenerator.generateLook(input), `Look: ${input.action}`, options, 'pinepaper_look');
|
|
2031
|
+
}
|
|
2032
|
+
case 'pinepaper_original_character': {
|
|
2033
|
+
const input = OriginalCharacterInputSchema.parse(args);
|
|
2034
|
+
return executeOrGenerate(codeGenerator.generateOriginalCharacter(input), `Original character: ${input.action}`, options, 'pinepaper_original_character');
|
|
2035
|
+
}
|
|
2036
|
+
case 'pinepaper_morph_sequence': {
|
|
2037
|
+
const input = MorphSequenceInputSchema.parse(args);
|
|
2038
|
+
return executeOrGenerate(codeGenerator.generateMorphSequence(input), `Morph sequence: ${input.states.length} states`, options, 'pinepaper_morph_sequence');
|
|
2039
|
+
}
|
|
2040
|
+
case 'pinepaper_emitter': {
|
|
2041
|
+
const input = EmitterInputSchema.parse(args);
|
|
2042
|
+
return executeOrGenerate(codeGenerator.generateEmitter(input), `Emitter: ${input.action}`, options, 'pinepaper_emitter');
|
|
2043
|
+
}
|
|
2044
|
+
case 'pinepaper_render_hook': {
|
|
2045
|
+
const input = RenderHookInputSchema.parse(args);
|
|
2046
|
+
return executeOrGenerate(codeGenerator.generateRenderHook(input), `Render hook: ${input.action}`, options, 'pinepaper_render_hook');
|
|
2047
|
+
}
|
|
2048
|
+
case 'pinepaper_choreograph': {
|
|
2049
|
+
const input = ChoreographInputSchema.parse(args);
|
|
2050
|
+
return executeOrGenerate(codeGenerator.generateChoreograph(input), `Choreograph: ${input.beats.map((b) => b.verb).join(' → ')}`, options, 'pinepaper_choreograph');
|
|
2051
|
+
}
|
|
2052
|
+
case 'pinepaper_relight': {
|
|
2053
|
+
const input = RelightInputSchema.parse(args);
|
|
2054
|
+
return executeOrGenerate(codeGenerator.generateRelight(input), `Relight: ${input.action}`, options, 'pinepaper_relight');
|
|
2055
|
+
}
|
|
2056
|
+
case 'pinepaper_shader_graph': {
|
|
2057
|
+
const input = ShaderGraphInputSchema.parse(args);
|
|
2058
|
+
return executeOrGenerate(codeGenerator.generateShaderGraph(input), `Shader graph: ${input.action}`, options, 'pinepaper_shader_graph');
|
|
2059
|
+
}
|
|
2060
|
+
case 'pinepaper_styled_scene': {
|
|
2061
|
+
const input = StyledSceneInputSchema.parse(args);
|
|
2062
|
+
return executeOrGenerate(codeGenerator.generateStyledScene(input), `Styled scene${input.style ? `: ${input.style}` : ''}`, options, 'pinepaper_styled_scene');
|
|
2063
|
+
}
|
|
2064
|
+
case 'pinepaper_place_on_surface': {
|
|
2065
|
+
const input = PlaceOnSurfaceInputSchema.parse(args);
|
|
2066
|
+
const code = codeGenerator.generatePlaceOnSurface(input);
|
|
2067
|
+
return executeOrGenerate(code, 'Place a UI onto a screen in a photo', options, 'pinepaper_place_on_surface');
|
|
2068
|
+
}
|
|
1773
2069
|
case 'pinepaper_brand_kit': {
|
|
1774
2070
|
const input = BrandKitInputSchema.parse(args);
|
|
1775
2071
|
if (input.action === 'from_url')
|
|
@@ -2031,12 +2327,21 @@ async function handleToolCallInner(toolName, args, options, tracker, baseTimerId
|
|
|
2031
2327
|
// RELATION TOOLS
|
|
2032
2328
|
// -----------------------------------------------------------------------
|
|
2033
2329
|
case 'pinepaper_add_relation': {
|
|
2034
|
-
|
|
2035
|
-
|
|
2330
|
+
// A relation registered with register_custom_relation this session is
|
|
2331
|
+
// a valid type here (gate D2, 1.6.19: the enum refused the name just
|
|
2332
|
+
// registered). Only those names; an unknown one is still refused.
|
|
2333
|
+
const rt = args.relationType;
|
|
2334
|
+
// The schema's refinements check presence, not which type, so a custom
|
|
2335
|
+
// name is validated with a built-in stand-in and then put back.
|
|
2336
|
+
const isCustom = typeof rt === 'string' && CUSTOM_RELATION_NAMES.has(rt);
|
|
2337
|
+
const input = AddRelationInputSchema.parse(isCustom ? { ...args, relationType: 'orbits' } : args);
|
|
2338
|
+
if (isCustom)
|
|
2339
|
+
input.relationType = rt;
|
|
2340
|
+
const code = codeGenerator.generateAddRelation(input, isCustom);
|
|
2036
2341
|
const description = getLocalizedSuccessMessage(i18n, 'relationAdded', {
|
|
2037
2342
|
relationType: input.relationType ?? `preset:${input.presetId}`,
|
|
2038
2343
|
sourceId: input.sourceId,
|
|
2039
|
-
targetId: input.targetId || '
|
|
2344
|
+
targetId: input.targetId || '(no target)',
|
|
2040
2345
|
});
|
|
2041
2346
|
return executeOrGenerate(code, description, options, 'pinepaper_add_relation');
|
|
2042
2347
|
}
|
|
@@ -2056,6 +2361,7 @@ async function handleToolCallInner(toolName, args, options, tracker, baseTimerId
|
|
|
2056
2361
|
}
|
|
2057
2362
|
case 'pinepaper_register_custom_relation': {
|
|
2058
2363
|
const input = RegisterCustomRelationInputSchema.parse(args);
|
|
2364
|
+
CUSTOM_RELATION_NAMES.add(input.name);
|
|
2059
2365
|
const code = codeGenerator.generateRegisterCustomRelation(input);
|
|
2060
2366
|
return executeOrGenerate(code, `Registers custom relation type: ${input.name}`, options, 'pinepaper_register_custom_relation');
|
|
2061
2367
|
}
|
|
@@ -2197,10 +2503,9 @@ async function handleToolCallInner(toolName, args, options, tracker, baseTimerId
|
|
|
2197
2503
|
case 'pinepaper_set_canvas_size': {
|
|
2198
2504
|
const input = SetCanvasSizeInputSchema.parse(args);
|
|
2199
2505
|
const code = codeGenerator.generateSetCanvasSize(input);
|
|
2200
|
-
const description =
|
|
2201
|
-
|
|
2202
|
-
height: input.height
|
|
2203
|
-
});
|
|
2506
|
+
const description = input.preset && input.width === undefined
|
|
2507
|
+
? `Set canvas size to preset ${input.preset}`
|
|
2508
|
+
: getLocalizedSuccessMessage(i18n, 'canvasSizeSet', { width: input.width ?? '', height: input.height ?? '' });
|
|
2204
2509
|
return executeOrGenerate(code, description, options, 'pinepaper_set_canvas_size');
|
|
2205
2510
|
}
|
|
2206
2511
|
case 'pinepaper_get_canvas_size': {
|
|
@@ -2306,6 +2611,15 @@ You can now start creating new items on a clean canvas.${sizeNote}`,
|
|
|
2306
2611
|
// -----------------------------------------------------------------------
|
|
2307
2612
|
case 'pinepaper_add_filter': {
|
|
2308
2613
|
const input = AddFilterInputSchema.parse(args);
|
|
2614
|
+
// Out-of-range parameters are refused with the engine's own range
|
|
2615
|
+
// (gate A, 1.6.19: dither levels 99 was accepted and quietly clamped).
|
|
2616
|
+
const ranges = FILTER_PARAM_RANGES[input.filterType] ?? {};
|
|
2617
|
+
for (const [k, v] of Object.entries((input.params ?? {}))) {
|
|
2618
|
+
const r = ranges[k];
|
|
2619
|
+
if (r && typeof v === 'number' && (v < r[0] || v > r[1])) {
|
|
2620
|
+
return errorResult(ErrorCodes.VALIDATION_ERROR, `${input.filterType} ${k} must be ${r[0]}..${r[1]} (got ${v}).`, { filterType: input.filterType, param: k, range: r });
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2309
2623
|
const code = codeGenerator.generateAddFilter(input.filterType, input.params);
|
|
2310
2624
|
const description = `Adds ${input.filterType} filter to the canvas`;
|
|
2311
2625
|
return executeOrGenerate(code, description, options, 'pinepaper_add_filter');
|
|
@@ -2516,7 +2830,19 @@ You can now start creating new items on a clean canvas.${sizeNote}`,
|
|
|
2516
2830
|
const perfTracker = getPerformanceTracker();
|
|
2517
2831
|
const recentMetrics = perfTracker.exportMetrics('json', { limit: metricsLimit });
|
|
2518
2832
|
const stats = perfTracker.getStats();
|
|
2519
|
-
|
|
2833
|
+
// exportMetrics('json') returns a JSON STRING; embedded as-is it reached
|
|
2834
|
+
// the report as one escaped string, not an array (gate H3).
|
|
2835
|
+
let entries = recentMetrics;
|
|
2836
|
+
if (typeof recentMetrics === 'string') {
|
|
2837
|
+
try {
|
|
2838
|
+
entries = JSON.parse(recentMetrics);
|
|
2839
|
+
}
|
|
2840
|
+
catch { /* keep the string */ }
|
|
2841
|
+
}
|
|
2842
|
+
if (entries && typeof entries === 'object' && !Array.isArray(entries) && Array.isArray(entries.metrics)) {
|
|
2843
|
+
entries = entries.metrics;
|
|
2844
|
+
}
|
|
2845
|
+
metricsSection = { entries, stats };
|
|
2520
2846
|
}
|
|
2521
2847
|
catch { /* metrics not available */ }
|
|
2522
2848
|
}
|
|
@@ -2571,17 +2897,25 @@ You can now start creating new items on a clean canvas.${sizeNote}`,
|
|
|
2571
2897
|
},
|
|
2572
2898
|
recommendations: [],
|
|
2573
2899
|
};
|
|
2574
|
-
//
|
|
2900
|
+
// RECOMMEND ONLY WHAT SAVES (gate H4, 1.6.19). This said "switch to
|
|
2901
|
+
// agent" on full, which saves under 1% (agent omits three small
|
|
2902
|
+
// tools). Each alternative is measured against the active set and
|
|
2903
|
+
// offered only when it saves at least 10%, with the saving stated.
|
|
2575
2904
|
const recs = toolDefsSection.recommendations;
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2905
|
+
const alternatives = [];
|
|
2906
|
+
for (const v of ['compact', 'minimal']) {
|
|
2907
|
+
for (const k of ['full', 'agent', 'minimal']) {
|
|
2908
|
+
if (k === toolkit && v === verbosity)
|
|
2909
|
+
continue;
|
|
2910
|
+
const t = Math.ceil(Buffer.byteLength(JSON.stringify(getToolsForToolkit(getToolsForVerbosity(v), k)), 'utf-8') / 4);
|
|
2911
|
+
alternatives.push({ label: `${k}+${v}`, how: `PINEPAPER_TOOLKIT=${k} PINEPAPER_VERBOSITY=${v}`, tokens: t });
|
|
2912
|
+
}
|
|
2584
2913
|
}
|
|
2914
|
+
alternatives
|
|
2915
|
+
.filter((alt) => alt.tokens <= activeTokens * 0.9)
|
|
2916
|
+
.sort((x, y) => x.tokens - y.tokens)
|
|
2917
|
+
.slice(0, 3)
|
|
2918
|
+
.forEach((alt) => recs.push(`${alt.how}: ~${alt.tokens.toLocaleString()} tokens/turn (${Math.round((1 - alt.tokens / activeTokens) * 100)}% less)${alt.label.startsWith('minimal+') ? ' — a small core set; switch profiles at runtime with pinepaper_set_toolkit' : ''}`));
|
|
2585
2919
|
if (activeTokens > 20000) {
|
|
2586
2920
|
recs.push(`Tool definitions alone consume ~${activeTokens.toLocaleString()} tokens per API turn — this is a major context budget item`);
|
|
2587
2921
|
}
|
|
@@ -2700,11 +3034,19 @@ You can now start creating new items on a clean canvas.${sizeNote}`,
|
|
|
2700
3034
|
// ASSET TOOLS
|
|
2701
3035
|
// -----------------------------------------------------------------------
|
|
2702
3036
|
case 'pinepaper_search_assets': {
|
|
3037
|
+
// Asked for by name, a removed repository says why rather than failing
|
|
3038
|
+
// the enum check with a bare "invalid option".
|
|
3039
|
+
{
|
|
3040
|
+
const { REMOVED_REPOSITORIES, REMOVED_REPOSITORY_REASON } = await import('../assets/index.js');
|
|
3041
|
+
if (typeof args.repository === 'string' && REMOVED_REPOSITORIES.includes(args.repository)) {
|
|
3042
|
+
return errorResult(ErrorCodes.INVALID_PARAMS, `${REMOVED_REPOSITORY_REASON} Search repository 'all', 'iconify' or 'fontawesome'.`, { repository: args.repository }, { toolName: 'pinepaper_search_assets' });
|
|
3043
|
+
}
|
|
3044
|
+
}
|
|
2703
3045
|
const input = SearchAssetsInputSchema.parse(args);
|
|
2704
3046
|
const { getAssetManager } = await import('../assets/index.js');
|
|
2705
3047
|
const assetManager = getAssetManager();
|
|
2706
3048
|
try {
|
|
2707
|
-
const results = await assetManager.
|
|
3049
|
+
const { results, sources } = await assetManager.searchWithStatus(input.query, input.repository || 'all', input.limit || 10);
|
|
2708
3050
|
// If includeSvgContent is true, fetch SVG content for each result
|
|
2709
3051
|
let resultsWithContent = [];
|
|
2710
3052
|
if (input.includeSvgContent) {
|
|
@@ -2776,6 +3118,7 @@ You can now start creating new items on a clean canvas.${sizeNote}`,
|
|
|
2776
3118
|
query: input.query,
|
|
2777
3119
|
repository: input.repository || 'all',
|
|
2778
3120
|
count: resultsWithContent.length,
|
|
3121
|
+
sources,
|
|
2779
3122
|
includeSvgContent: input.includeSvgContent || false,
|
|
2780
3123
|
attributionRequired: requiresAttribution.length > 0,
|
|
2781
3124
|
attributionNotice,
|
|
@@ -2836,6 +3179,15 @@ You can now start creating new items on a clean canvas.${sizeNote}`,
|
|
|
2836
3179
|
svg = body;
|
|
2837
3180
|
metadata = { source: 'url', url: input.url };
|
|
2838
3181
|
}
|
|
3182
|
+
// `color` was accepted and never applied, so a recoloured icon
|
|
3183
|
+
// rendered black (gate D15, 1.6.19). A monochrome icon draws in
|
|
3184
|
+
// currentColor (stroke sets) or in the default fill (fill sets):
|
|
3185
|
+
// both take the colour.
|
|
3186
|
+
if (input.color) {
|
|
3187
|
+
const c = input.color.replace(/"/g, '');
|
|
3188
|
+
svg = svg.replace(/currentColor/g, c);
|
|
3189
|
+
svg = svg.replace(/<svg\b([^>]*)>/i, (m, attrs) => (/\sfill\s*=/.test(attrs) ? m : `<svg${attrs} fill="${c}">`));
|
|
3190
|
+
}
|
|
2839
3191
|
// Import the SVG onto canvas using existing import_svg tool
|
|
2840
3192
|
const code = codeGenerator.generateImportSVG(svg, undefined, input.position, input.scale);
|
|
2841
3193
|
// A URL import has no catalogue metadata (no title, no licence): this
|
|
@@ -2940,11 +3292,20 @@ You can now start creating new items on a clean canvas.${sizeNote}`,
|
|
|
2940
3292
|
}
|
|
2941
3293
|
const code = codeGenerator.generateAgentStartJob(input);
|
|
2942
3294
|
const descriptionText = `Started agent job${input.name ? ` "${input.name}"` : ''} with ${input.screenshotPolicy || 'on_complete'} screenshot policy`;
|
|
3295
|
+
// A STALE STUDIO IS RELOADED HERE, AND ONLY HERE (D92): the canvas is
|
|
3296
|
+
// about to be cleared anyway. The setup below runs on the new page.
|
|
3297
|
+
const gate = await studioJobGate(input, options);
|
|
2943
3298
|
const result = await executeOrGenerate(code, descriptionText, options, 'pinepaper_agent_start_job');
|
|
2944
3299
|
// Append a 1-line workflow hint
|
|
2945
3300
|
if (result.content && result.content.length > 0 && result.content[0].type === 'text') {
|
|
2946
3301
|
result.content[0].text += `\n\nNEXT: Call batch_execute with all operations, then end_job.`;
|
|
3302
|
+
if (gate.note)
|
|
3303
|
+
result.content[0].text = `${gate.note}\n\n${result.content[0].text}`;
|
|
3304
|
+
if (gate.warning)
|
|
3305
|
+
result.content[0].text = `⚠️ ${gate.warning}\n\n${result.content[0].text}`;
|
|
2947
3306
|
}
|
|
3307
|
+
if (gate.build)
|
|
3308
|
+
result._meta = { ...(result._meta ?? {}), 'pinepaper.studio/build': gate.build };
|
|
2948
3309
|
return result;
|
|
2949
3310
|
}
|
|
2950
3311
|
case 'pinepaper_agent_end_job': {
|
|
@@ -3366,10 +3727,18 @@ You can now start creating new items on a clean canvas.${sizeNote}`,
|
|
|
3366
3727
|
`Call action 'list_styles' and use one marked composable.`, { style: input.style }, { toolName: 'pinepaper_design_system' });
|
|
3367
3728
|
}
|
|
3368
3729
|
const ops = designSystems.sceneToOps(scene);
|
|
3730
|
+
// Some vendored styles lay out at their OWN fixed size and ignore
|
|
3731
|
+
// width/height (swiss_typographic is always 1280x720), so a caller
|
|
3732
|
+
// asking for 1080x1080 got a resized canvas (gate run 3). Say so.
|
|
3733
|
+
const sizeIgnored = (input.width !== undefined && input.width !== scene.width)
|
|
3734
|
+
|| (input.height !== undefined && input.height !== scene.height);
|
|
3735
|
+
const sizeWarning = sizeIgnored
|
|
3736
|
+
? `"${scene.style}" lays out at its own size, ${scene.width}x${scene.height}; the requested ${input.width ?? '?'}x${input.height ?? '?'} was not used`
|
|
3737
|
+
: undefined;
|
|
3369
3738
|
if (input.draw === false) {
|
|
3370
|
-
return dataResult({ scene, ops, drawn: false });
|
|
3739
|
+
return dataResult({ scene, ops, drawn: false, ...(sizeWarning ? { warning: sizeWarning } : {}) });
|
|
3371
3740
|
}
|
|
3372
|
-
const code = codeGenerator.generateDesignCompose(scene, ops, scene.style);
|
|
3741
|
+
const code = codeGenerator.generateDesignCompose(scene, ops, scene.style, sizeWarning);
|
|
3373
3742
|
return executeOrGenerate(code, `Composed a ${scene.style} scene (${ops.length} items)`, options, 'pinepaper_design_system');
|
|
3374
3743
|
}
|
|
3375
3744
|
}
|
|
@@ -3387,7 +3756,12 @@ You can now start creating new items on a clean canvas.${sizeNote}`,
|
|
|
3387
3756
|
case 'pinepaper_interchange': {
|
|
3388
3757
|
const input = InterchangeInputSchema.parse(args);
|
|
3389
3758
|
const code = codeGenerator.generateInterchange(input);
|
|
3390
|
-
|
|
3759
|
+
const out = await executeOrGenerate(code, `Interchange: ${input.action}`, options, 'pinepaper_interchange');
|
|
3760
|
+
// AN EXPORT IS A FILE (gate D7, 1.6.19): .lottie, GLB and the PNG-sequence
|
|
3761
|
+
// zip came back as byte-index objects or inline data URLs, and a Lottie
|
|
3762
|
+
// JSON can be large. Written out; the result names the path.
|
|
3763
|
+
const ext = { export_lottie: 'json', export_dotlottie: 'lottie', export_glb: 'glb', export_png_sequence: 'zip', export_bvh: 'bvh' };
|
|
3764
|
+
return ext[input.action] ? await writeResultDataToFile(out, ext[input.action]) : out;
|
|
3391
3765
|
}
|
|
3392
3766
|
case 'pinepaper_sound': {
|
|
3393
3767
|
const input = SoundInputSchema.parse(args);
|