@jxsuite/studio 10.3.0 → 10.5.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/dist/chunks/2020-7sx2vexa.js +4 -0
- package/dist/chunks/adapter-12dxr3z7.js +4 -0
- package/dist/chunks/{add-new-flow-3tkn72ey.js → add-new-flow-any4pw2q.js} +3 -3
- package/dist/chunks/{cf-account-picker-xb8vvt2h.js → cf-account-picker-ajh60k54.js} +3 -3
- package/dist/chunks/cf-settings-hbj16kh3.js +4 -0
- package/dist/chunks/{color-area-h8dbasp1.js → color-area-gbcftzh3.js} +2 -2
- package/dist/chunks/{color-field-qv5r2b16.js → color-field-85136598.js} +3 -3
- package/dist/chunks/{color-slider-nz2mrzky.js → color-slider-w1s3ndd8.js} +2 -2
- package/dist/chunks/color-thzpppyc.js +4 -0
- package/dist/chunks/{combobox-6hs00996.js → combobox-dk98wc5x.js} +3 -3
- package/dist/chunks/components-a98hzqw9.js +4 -0
- package/dist/chunks/content-source-jq3fjs3w.js +4 -0
- package/dist/chunks/{convert-to-repeater-gwzh9gar.js → convert-to-repeater-g66r6qp1.js} +3 -3
- package/dist/chunks/editor-rth1rzj8.js +4 -0
- package/dist/chunks/entry-commands-02j0s19w.js +4 -0
- package/dist/chunks/{extension-sections-338ee35t.js → extension-sections-rqrs7dr0.js} +3 -3
- package/dist/chunks/file-ops-6a49er4c.js +4 -0
- package/dist/chunks/files-b2p9y0yz.js +4 -0
- package/dist/chunks/format-host-7xe57k1s.js +4 -0
- package/dist/chunks/fs-events-0xsy168g.js +4 -0
- package/dist/chunks/grid-open-h3q4fjy5.js +4 -0
- package/dist/chunks/{grid-view-w9nfy203.js → grid-view-0v4hjspb.js} +17 -17
- package/dist/chunks/{icons-gb967qc8.js → icons-j9eksgew.js} +2 -2
- package/dist/chunks/{iframe-entry-btck0e9s.js → iframe-entry-4zqtn3ry.js} +4 -4
- package/dist/chunks/{iframe-entry-b5qgggyp.js → iframe-entry-54kwwhmg.js} +4 -4
- package/dist/chunks/{iframe-entry-42hpmwz5.js → iframe-entry-m24fza30.js} +4 -4
- package/dist/chunks/iframe-host-8jcmaktg.js +4 -0
- package/dist/chunks/{image-bake-4afr4y6a.js → image-bake-m8gzaan9.js} +3 -3
- package/dist/chunks/{image-editor-x7t8jbjn.js → image-editor-0afyws7c.js} +3 -3
- package/dist/chunks/{index-yjrfeks8.js → index-2q88g57p.js} +3 -3
- package/dist/chunks/index-7nj2375a.js +4 -0
- package/dist/chunks/{javascript-hy2mws8v.js → javascript-5sxtr1g7.js} +3 -3
- package/dist/chunks/jsonMode-m6sf47hq.js +13 -0
- package/dist/chunks/jx-validate-808zgbgy.js +4 -0
- package/dist/chunks/layers-0ffdtqjn.js +4 -0
- package/dist/chunks/layers-panel-rw62wtvf.js +4 -0
- package/dist/chunks/{listbox-7gka1h86.js → listbox-k5p41049.js} +2 -2
- package/dist/chunks/lit-html-9bz9rcfv.js +4 -0
- package/dist/chunks/media-library-2a5hdbek.js +4 -0
- package/dist/chunks/media-open-069kzhj4.js +4 -0
- package/dist/chunks/media-picker-4p4pd28y.js +4 -0
- package/dist/chunks/menu-26nez152.js +4 -0
- package/dist/chunks/{monaco-binding-a51py33c.js → monaco-binding-1s2b8snq.js} +3 -3
- package/dist/chunks/monaco-buffer-m9fd2dka.js +4 -0
- package/dist/chunks/monaco-setup-e0crh3v6.js +4 -0
- package/dist/chunks/notify-re3js157.js +4 -0
- package/dist/chunks/platform-rywy3tkg.js +4 -0
- package/dist/chunks/popover-fhdnt4be.js +4 -0
- package/dist/chunks/publish-panel-czkpg8m4.js +4 -0
- package/dist/chunks/{publish-quick-w3nxd7q4.js → publish-quick-b9wbhqj6.js} +3 -3
- package/dist/chunks/publish-workspace-yy6d23b7.js +4 -0
- package/dist/chunks/quick-search-tp7pjnmv.js +4 -0
- package/dist/chunks/references-wd2yj8s1.js +4 -0
- package/dist/chunks/right-panel-x971k0zc.js +4 -0
- package/dist/chunks/source-control-k4p00hpe.js +4 -0
- package/dist/chunks/{split-m5rwn31z.js → split-qh21j2b0.js} +2 -2
- package/dist/chunks/{studio-v5wktj6f.js → studio-151kgn69.js} +4 -4
- package/dist/chunks/studio-15mfah02.js +10 -0
- package/dist/chunks/studio-1a7zshnv.js +5 -0
- package/dist/chunks/studio-1d8m5s38.js +16 -0
- package/dist/chunks/studio-35pmfjtx.js +5 -0
- package/dist/chunks/{studio-3na1r48j.js → studio-3yr1zdp9.js} +8 -8
- package/dist/chunks/studio-43pynnhz.js +5 -0
- package/dist/chunks/studio-44xqe0b7.js +4 -0
- package/dist/chunks/{studio-hh394phe.js → studio-48yzqzqr.js} +4 -4
- package/dist/chunks/{studio-nskxe9q1.js → studio-4jy967rj.js} +5 -5
- package/dist/chunks/{studio-waekn290.js → studio-4ncxzw5m.js} +4 -4
- package/dist/chunks/{studio-pm17wdc4.js → studio-4s5bka8t.js} +4 -4
- package/dist/chunks/{studio-5tet77de.js → studio-4z0cdh16.js} +3 -3
- package/dist/chunks/studio-5rq3e4q5.js +5 -0
- package/dist/chunks/{studio-2ggfzrrd.js → studio-69g6a0jn.js} +4 -4
- package/dist/chunks/studio-6hcfm9q3.js +5 -0
- package/dist/chunks/studio-6mf31e16.js +5 -0
- package/dist/chunks/studio-6sxnqqw8.js +5 -0
- package/dist/chunks/{studio-0939t1q1.js → studio-7qqb7hb8.js} +4 -4
- package/dist/chunks/{studio-5n193zqd.js → studio-7qr8k0v2.js} +4 -4
- package/dist/chunks/{studio-0x6f3nk9.js → studio-8ncm9zwx.js} +4 -4
- package/dist/chunks/studio-8w5pa88p.js +7 -0
- package/dist/chunks/{studio-3qr08sxq.js → studio-9f5vcb4w.js} +3 -3
- package/dist/chunks/studio-aaw7wxv8.js +5 -0
- package/dist/chunks/studio-afx9brsc.js +5 -0
- package/dist/chunks/studio-ajfq4fmr.js +5 -0
- package/dist/chunks/{studio-nn4f9m9c.js → studio-ber1dgte.js} +4 -4
- package/dist/chunks/{studio-sce30r7b.js → studio-bk0anq3b.js} +3 -3
- package/dist/chunks/{studio-0gvvqj3h.js → studio-bmwgj8ez.js} +4 -4
- package/dist/chunks/{studio-321ym1tt.js → studio-bpzwhn88.js} +4 -4
- package/dist/chunks/{studio-g9brbewz.js → studio-c72v1kwc.js} +4 -4
- package/dist/chunks/{studio-tpxcryf2.js → studio-cv6gnjqd.js} +4 -4
- package/dist/chunks/{studio-90zkgne5.js → studio-czpmzs9r.js} +7 -7
- package/dist/chunks/{studio-3c10v2q1.js → studio-d5f7sskn.js} +4 -4
- package/dist/chunks/studio-dmgh2rrj.js +5 -0
- package/dist/chunks/{studio-b68k0z00.js → studio-dst8y8vh.js} +4 -4
- package/dist/chunks/studio-ehj3csbm.js +6 -0
- package/dist/chunks/{studio-thz5t120.js → studio-ek4fb4eb.js} +4 -4
- package/dist/chunks/studio-ep1384t9.js +5 -0
- package/dist/chunks/{studio-abh9kfdh.js → studio-eq5vqn1q.js} +4 -4
- package/dist/chunks/studio-erjccagy.js +5 -0
- package/dist/chunks/studio-ex1644g2.js +5 -0
- package/dist/chunks/studio-f3awr5nc.js +12 -0
- package/dist/chunks/studio-f69wcajk.js +9 -0
- package/dist/chunks/{studio-p2a03pgm.js → studio-g65t6svn.js} +4 -4
- package/dist/chunks/{studio-3q7mhbgx.js → studio-gw83z798.js} +5 -5
- package/dist/chunks/{studio-nwynxqtb.js → studio-hy936516.js} +4 -4
- package/dist/chunks/studio-j3bq30yz.js +5 -0
- package/dist/chunks/studio-j52ks8q7.js +5 -0
- package/dist/chunks/{studio-g0hxtcga.js → studio-jqvke4ac.js} +4 -4
- package/dist/chunks/{studio-bct64zmj.js → studio-k05g7efh.js} +4 -4
- package/dist/chunks/studio-kefs4sz4.js +5 -0
- package/dist/chunks/{studio-s7t4vp1d.js → studio-ketrgtph.js} +5 -5
- package/dist/chunks/studio-m3jrjfhy.js +14 -0
- package/dist/chunks/{studio-45mj4jqe.js → studio-m8hesdrw.js} +4 -4
- package/dist/chunks/{studio-k0xzrse2.js → studio-n271vdsg.js} +36 -36
- package/dist/chunks/studio-n4pnf0sw.js +5 -0
- package/dist/chunks/{studio-wfct4h6n.js → studio-ncwx602y.js} +4 -4
- package/dist/chunks/{studio-423tfdte.js → studio-nngc45ct.js} +4 -4
- package/dist/chunks/{studio-eqcddvc2.js → studio-nv5eq0f7.js} +173 -173
- package/dist/chunks/{studio-ea6r1zyr.js → studio-nzp0sqjg.js} +4 -4
- package/dist/chunks/{studio-k2evngrj.js → studio-pgze9p34.js} +5 -5
- package/dist/chunks/studio-ph17an19.js +5 -0
- package/dist/chunks/studio-pte5w7rf.js +9 -0
- package/dist/chunks/studio-pv1ts08p.js +6 -0
- package/dist/chunks/studio-q509sqt8.js +5 -0
- package/dist/chunks/{studio-stahswgb.js → studio-qarg1xf9.js} +6 -6
- package/dist/chunks/studio-r5g7fx7q.js +9 -0
- package/dist/chunks/studio-s49y6m5b.js +5 -0
- package/dist/chunks/studio-s4k1v7dt.js +7 -0
- package/dist/chunks/studio-t46rt6gx.js +5 -0
- package/dist/chunks/{studio-y1b86ast.js → studio-td3h1644.js} +4 -4
- package/dist/chunks/{studio-a65kbs2e.js → studio-thgexj3w.js} +7 -7
- package/dist/chunks/{studio-vpevkfxw.js → studio-veh91hjv.js} +4 -4
- package/dist/chunks/studio-vswmz40d.js +6 -0
- package/dist/chunks/{studio-daksw8kh.js → studio-w3aafbpk.js} +4 -4
- package/dist/chunks/studio-w4mfaaam.js +5 -0
- package/dist/chunks/{studio-4zd6an2b.js → studio-wn2cmf9q.js} +4 -4
- package/dist/chunks/studio-x5ffyssw.js +5 -0
- package/dist/chunks/{studio-swedpd82.js → studio-x5py815g.js} +4 -4
- package/dist/chunks/{studio-4fnb5648.js → studio-xh6rf05g.js} +4 -4
- package/dist/chunks/studio-z6s56dzz.js +5 -0
- package/dist/chunks/{studio-nvcn2ykc.js → studio-zav9hgb3.js} +4 -4
- package/dist/chunks/{studio-wse1rs8t.js → studio-ze1sm147.js} +6 -6
- package/dist/chunks/{studio-hfxttkyr.js → studio-zkkt13qd.js} +4 -4
- package/dist/chunks/studio-zn7ysyzb.js +5 -0
- package/dist/chunks/{studio-nf8esxxc.js → studio-ztzf8mce.js} +4 -4
- package/dist/chunks/{studio-1410q5dw.js → studio-zzgp5yka.js} +4 -4
- package/dist/chunks/{tooltip-dj1pvgz7.js → tooltip-gtx40bjg.js} +3 -3
- package/dist/chunks/{tree-j0ebwr5s.js → tree-a19s1c05.js} +2 -2
- package/dist/chunks/tsMode-3f116hth.js +14 -0
- package/dist/chunks/workspace-home-61n1jz3q.js +4 -0
- package/dist/chunks/workspace-sheek6zw.js +4 -0
- package/dist/chunks/{ws-client-arz2qsy9.js → ws-client-cb49m87s.js} +3 -3
- package/dist/iframe-entry.js +14 -14
- package/dist/manifest.json +82 -82
- package/dist/studio.js +97 -96
- package/dist/workers/ts.worker.js +3 -3
- package/package.json +11 -11
- package/src/browse/library-layouts.ts +9 -12
- package/src/browse/library-pane.ts +22 -16
- package/src/browse/library-source.ts +3 -4
- package/src/canvas/canvas-live-render.ts +2 -5
- package/src/canvas/canvas-page-context.ts +5 -0
- package/src/content/entry-commands.ts +32 -27
- package/src/content/entry-editor.ts +46 -44
- package/src/content/entry-inspector.ts +15 -12
- package/src/content/entry-model.ts +3 -3
- package/src/content/entry-summaries.ts +5 -5
- package/src/content/hold-state.ts +172 -0
- package/src/content/page-summaries.ts +10 -14
- package/src/editor/inline-edit.ts +6 -1
- package/src/editor/repeater-scope.ts +333 -74
- package/src/grid/sources/content-source.ts +42 -66
- package/src/panels/block-action-bar.ts +3 -4
- package/src/panels/events-panel.ts +6 -2
- package/src/panels/formula-workspace.ts +9 -0
- package/src/panels/properties-panel.ts +127 -35
- package/src/panels/statement-editor.ts +4 -0
- package/src/panels/tab-strip.ts +29 -26
- package/src/services/ai-project-tools.ts +3 -3
- package/src/services/ai-tools.ts +5 -5
- package/src/services/ai-write-report.ts +22 -4
- package/src/services/context-manager.ts +110 -2
- package/src/services/document-assistant.ts +2 -2
- package/src/services/harness/chat-reducer.ts +9 -4
- package/src/services/render-critic.ts +234 -12
- package/src/services/tool-executor.ts +16 -8
- package/src/surfaces/collections-panel.json +1 -1
- package/src/surfaces/collections-panel.ts +1 -1
- package/src/surfaces/entry-editor.json +10 -10
- package/src/surfaces/entry-editor.ts +22 -19
- package/src/surfaces/entry-inspector.json +11 -11
- package/src/surfaces/entry-inspector.ts +17 -10
- package/src/surfaces/inspector-dock.json +3 -3
- package/src/surfaces/tab-strip.json +5 -5
- package/src/surfaces/tab-strip.ts +2 -2
- package/src/types.ts +2 -0
- package/src/ui/expression-editor.ts +10 -5
- package/src/ui/schema-form.ts +18 -1
- package/src/utils/edit-display.ts +27 -1
- package/dist/chunks/2020-906vx2dq.js +0 -4
- package/dist/chunks/adapter-hnvad61x.js +0 -4
- package/dist/chunks/cf-settings-mkbcz4z2.js +0 -4
- package/dist/chunks/color-04361ttk.js +0 -4
- package/dist/chunks/components-w5zqwva1.js +0 -4
- package/dist/chunks/content-source-1vzxq1ba.js +0 -4
- package/dist/chunks/editor-wvf5e6j9.js +0 -4
- package/dist/chunks/entry-commands-mx2whrjx.js +0 -4
- package/dist/chunks/file-ops-9bxvk59j.js +0 -4
- package/dist/chunks/files-t07yqqej.js +0 -4
- package/dist/chunks/format-host-5ja5gxyk.js +0 -4
- package/dist/chunks/fs-events-3424ezp3.js +0 -4
- package/dist/chunks/grid-open-1mpq9mzb.js +0 -4
- package/dist/chunks/iframe-host-wsg0bn73.js +0 -4
- package/dist/chunks/index-f9qr7kzh.js +0 -4
- package/dist/chunks/jsonMode-vj30te2h.js +0 -13
- package/dist/chunks/jx-validate-ttr5ewf0.js +0 -4
- package/dist/chunks/layers-m49nmwvx.js +0 -4
- package/dist/chunks/layers-panel-hvy2e549.js +0 -4
- package/dist/chunks/lit-html-k2mb9k0r.js +0 -4
- package/dist/chunks/media-library-ae67q4at.js +0 -4
- package/dist/chunks/media-open-yp9jm834.js +0 -4
- package/dist/chunks/media-picker-s41ngdfr.js +0 -4
- package/dist/chunks/menu-d2jxpgma.js +0 -4
- package/dist/chunks/monaco-buffer-pa0g6ax7.js +0 -4
- package/dist/chunks/monaco-setup-g9kterhx.js +0 -4
- package/dist/chunks/notify-bwehrp7q.js +0 -4
- package/dist/chunks/platform-kv0k65cy.js +0 -4
- package/dist/chunks/popover-9tf4cjrk.js +0 -4
- package/dist/chunks/publish-panel-gzfxcm0t.js +0 -4
- package/dist/chunks/publish-workspace-dppd82tc.js +0 -4
- package/dist/chunks/quick-search-tdxaam55.js +0 -4
- package/dist/chunks/references-vqyr5s9x.js +0 -4
- package/dist/chunks/right-panel-0yrw4cgz.js +0 -4
- package/dist/chunks/source-control-6fk72me2.js +0 -4
- package/dist/chunks/studio-0khd8xtx.js +0 -4
- package/dist/chunks/studio-0w3541dg.js +0 -7
- package/dist/chunks/studio-15hwqgd8.js +0 -5
- package/dist/chunks/studio-1ks5d9y4.js +0 -5
- package/dist/chunks/studio-3xk9zdgb.js +0 -5
- package/dist/chunks/studio-4f7thq0p.js +0 -5
- package/dist/chunks/studio-53wy9ek3.js +0 -5
- package/dist/chunks/studio-7rrdbvex.js +0 -5
- package/dist/chunks/studio-8kx6m0rg.js +0 -5
- package/dist/chunks/studio-9dq7rwc7.js +0 -5
- package/dist/chunks/studio-9qx1ywbv.js +0 -9
- package/dist/chunks/studio-aam8zt8z.js +0 -5
- package/dist/chunks/studio-b4e0ftkt.js +0 -5
- package/dist/chunks/studio-bz0b5brj.js +0 -6
- package/dist/chunks/studio-cfnn70h1.js +0 -5
- package/dist/chunks/studio-e7vjyp04.js +0 -10
- package/dist/chunks/studio-egnjpyde.js +0 -9
- package/dist/chunks/studio-ejrypvmq.js +0 -5
- package/dist/chunks/studio-f8tyeyzt.js +0 -5
- package/dist/chunks/studio-fj6c905s.js +0 -5
- package/dist/chunks/studio-fkrr7kgg.js +0 -9
- package/dist/chunks/studio-fspe7m9t.js +0 -5
- package/dist/chunks/studio-ggx3j7nd.js +0 -5
- package/dist/chunks/studio-j0zf5x2x.js +0 -5
- package/dist/chunks/studio-jeb7hth3.js +0 -5
- package/dist/chunks/studio-k1xqrgm5.js +0 -5
- package/dist/chunks/studio-k3we3kqb.js +0 -5
- package/dist/chunks/studio-n9qba0gx.js +0 -5
- package/dist/chunks/studio-nzemfbkk.js +0 -14
- package/dist/chunks/studio-q80fg1d6.js +0 -12
- package/dist/chunks/studio-r050sez0.js +0 -6
- package/dist/chunks/studio-s9fng7m8.js +0 -7
- package/dist/chunks/studio-wgc54sxf.js +0 -5
- package/dist/chunks/studio-wqfaqw4m.js +0 -5
- package/dist/chunks/studio-x0gsfr1d.js +0 -16
- package/dist/chunks/studio-xpx91fby.js +0 -6
- package/dist/chunks/studio-yrcrncev.js +0 -5
- package/dist/chunks/studio-yzpskb0j.js +0 -5
- package/dist/chunks/studio-zfdbmhp3.js +0 -5
- package/dist/chunks/tsMode-jg1dtphj.js +0 -14
- package/dist/chunks/workspace-9btkc8tk.js +0 -4
- package/dist/chunks/workspace-home-qcc4z5ag.js +0 -4
- package/src/content/draft-state.ts +0 -108
|
@@ -19,7 +19,8 @@ import {
|
|
|
19
19
|
toChatMessages,
|
|
20
20
|
toLiveMessages,
|
|
21
21
|
} from "@jxsuite/ai/messages";
|
|
22
|
-
import { isString } from "@jxsuite/schema/guards";
|
|
22
|
+
import { isJsonObject, isString } from "@jxsuite/schema/guards";
|
|
23
|
+
import type { JsonObject, JsonValue } from "@jxsuite/schema/types";
|
|
23
24
|
import { modelContextWindow } from "./ai-models";
|
|
24
25
|
|
|
25
26
|
/** How many tool messages {@link pruneOrphanToolMessages} dropped, moved and sealed. */
|
|
@@ -161,6 +162,17 @@ function contextTokens(
|
|
|
161
162
|
chatState: ReturnType<typeof createChatState>,
|
|
162
163
|
systemTokens: number,
|
|
163
164
|
): number {
|
|
165
|
+
return (
|
|
166
|
+
countedTokens(chatState, systemTokens) ??
|
|
167
|
+
systemTokens + estimateTokens(chatState.toMessagesArray())
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** The provider's count carried forward to this transcript, when there is one it still covers. */
|
|
172
|
+
function countedTokens(
|
|
173
|
+
chatState: ReturnType<typeof createChatState>,
|
|
174
|
+
systemTokens: number,
|
|
175
|
+
): number | null {
|
|
164
176
|
const { usage, messages } = chatState;
|
|
165
177
|
const boundary = usage ? messages[usage.messageCount - 1] : undefined;
|
|
166
178
|
if (usage && boundary && boundary.id === usage.lastMessageId) {
|
|
@@ -180,7 +192,7 @@ function contextTokens(
|
|
|
180
192
|
return reused;
|
|
181
193
|
}
|
|
182
194
|
}
|
|
183
|
-
return
|
|
195
|
+
return null;
|
|
184
196
|
}
|
|
185
197
|
|
|
186
198
|
/**
|
|
@@ -282,6 +294,102 @@ export function trimContext(
|
|
|
282
294
|
return { estimatedTokens: newTotal, droppedCount };
|
|
283
295
|
}
|
|
284
296
|
|
|
297
|
+
// ─── Shed ───────────────────────────────────────────────────────────────────
|
|
298
|
+
|
|
299
|
+
/** What {@link shedToolResults} sheds down to, as a fraction of the window. */
|
|
300
|
+
const SHED_TARGET_FRACTION = 0.6;
|
|
301
|
+
|
|
302
|
+
/** A result smaller than this is mostly its stub already, and is often an answer worth keeping. */
|
|
303
|
+
const SHED_MIN_CHARS = 1024;
|
|
304
|
+
|
|
305
|
+
/** What a shed result tells the model. */
|
|
306
|
+
const SHED_NOTE =
|
|
307
|
+
"This result was removed to save space. Call the tool again if you still need it.";
|
|
308
|
+
|
|
309
|
+
/** A tool's reply as an object, or an empty one when it is not a result this module can read. */
|
|
310
|
+
function parsedReply(content: string): JsonObject {
|
|
311
|
+
try {
|
|
312
|
+
/* SAFETY: JSON.parse without a reviver turns text into nothing but JSON values. */
|
|
313
|
+
const parsed = JSON.parse(content) as JsonValue;
|
|
314
|
+
return isJsonObject(parsed) ? parsed : {};
|
|
315
|
+
} catch {
|
|
316
|
+
return {};
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* What is left of a tool's reply once its output is shed: how it ended and what it said it did, so
|
|
322
|
+
* the model and a restored chip keep the outcome and lose only the bulk.
|
|
323
|
+
*/
|
|
324
|
+
function shedReply(content: string): string {
|
|
325
|
+
const { success = true, summary, error } = parsedReply(content);
|
|
326
|
+
return JSON.stringify({ success, summary, error, shed: SHED_NOTE });
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Keep a turn inside the model's window between its rounds (specs/ai.md §3.8).
|
|
331
|
+
*
|
|
332
|
+
* {@link trimContext} runs when a message is sent, so nothing else bounds what one turn's own tool
|
|
333
|
+
* results add, and a turn with room to work could fill the window and have its request refused.
|
|
334
|
+
* Past the same budget the trim uses, this replaces the oldest large tool results with a stub until
|
|
335
|
+
* the context is back under {@link SHED_TARGET_FRACTION}. The results of the latest round are never
|
|
336
|
+
* touched, because the model has not read them yet, and the pairs are never broken, because only a
|
|
337
|
+
* reply's content changes.
|
|
338
|
+
*
|
|
339
|
+
* It changes the transcript rather than one request, so a shed result stays shed: the next send
|
|
340
|
+
* measures what it will really send, and the provider's prompt cache breaks once for a whole batch
|
|
341
|
+
* rather than on every round after the budget is reached.
|
|
342
|
+
*
|
|
343
|
+
* @param {ReturnType<typeof createChatState>} chatState
|
|
344
|
+
* @param {string} systemPrompt
|
|
345
|
+
* @returns {number} How many results it shed.
|
|
346
|
+
*/
|
|
347
|
+
export function shedToolResults(
|
|
348
|
+
chatState: ReturnType<typeof createChatState>,
|
|
349
|
+
systemPrompt: string,
|
|
350
|
+
): number {
|
|
351
|
+
const window = contextWindowFor(chatState.model);
|
|
352
|
+
const systemTokens = estimateTokens(systemPrompt);
|
|
353
|
+
const { messages } = chatState;
|
|
354
|
+
let total =
|
|
355
|
+
countedTokens(chatState, systemTokens) ??
|
|
356
|
+
systemTokens + estimateTokens(messages.map((message) => wireEntry(message)));
|
|
357
|
+
if (total <= Math.floor(window * BUDGET_FRACTION)) {
|
|
358
|
+
return 0;
|
|
359
|
+
}
|
|
360
|
+
const target = Math.floor(window * SHED_TARGET_FRACTION);
|
|
361
|
+
/* The latest round is the trailing run of replies, with the placeholder of the round about to
|
|
362
|
+
stream after it when there is one. */
|
|
363
|
+
let latest = messages.length;
|
|
364
|
+
while (
|
|
365
|
+
latest > 0 &&
|
|
366
|
+
messages[latest - 1]!.role === "assistant" &&
|
|
367
|
+
!messages[latest - 1]!.toolCalls
|
|
368
|
+
) {
|
|
369
|
+
latest -= 1;
|
|
370
|
+
}
|
|
371
|
+
while (latest > 0 && messages[latest - 1]!.role === "tool") {
|
|
372
|
+
latest -= 1;
|
|
373
|
+
}
|
|
374
|
+
let shed = 0;
|
|
375
|
+
for (let i = 0; i < latest && total > target; i++) {
|
|
376
|
+
const message = messages[i]!;
|
|
377
|
+
if (message.role === "tool" && message.content.length >= SHED_MIN_CHARS) {
|
|
378
|
+
const before = estimateTokens(message.content);
|
|
379
|
+
message.content = shedReply(message.content);
|
|
380
|
+
total -= before - estimateTokens(message.content);
|
|
381
|
+
shed += 1;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
if (shed > 0) {
|
|
385
|
+
// The provider counted the longer transcript; the estimate is all there is until it next counts.
|
|
386
|
+
chatState.clearUsage();
|
|
387
|
+
chatState.setTokenCount(total);
|
|
388
|
+
chatState.setContextWarning(true);
|
|
389
|
+
}
|
|
390
|
+
return shed;
|
|
391
|
+
}
|
|
392
|
+
|
|
285
393
|
// ─── Orphaned tool calls ────────────────────────────────────────────────────
|
|
286
394
|
|
|
287
395
|
/**
|
|
@@ -38,7 +38,7 @@ import { getAiProvider, getBaseUrl, getOpenAiKey } from "./ai-settings";
|
|
|
38
38
|
import { preferredModel } from "./ai-models";
|
|
39
39
|
import { pruneOrphanToolMessages, trimContext } from "./context-manager";
|
|
40
40
|
import { renderCheck } from "./render-critic";
|
|
41
|
-
import type {
|
|
41
|
+
import type { RenderProbe } from "./ai-write-report";
|
|
42
42
|
import { validateDoc } from "./jx-validate";
|
|
43
43
|
import { reloadFileInTab } from "../files/files";
|
|
44
44
|
import { getExtensionCatalog, refreshExtensionUi } from "../format/format-host";
|
|
@@ -68,7 +68,7 @@ function projectConfig(): ProjectConfig | null {
|
|
|
68
68
|
/* SAFETY: renderCheck runs the whole render inside a try and answers `{ ok: false, error }` for
|
|
69
69
|
anything it cannot render, so JSON that is not (yet) a valid document is exactly what it is safe
|
|
70
70
|
to hand; its JxDocument parameter names what renders, not what may be probed. */
|
|
71
|
-
const probeRender = renderCheck as
|
|
71
|
+
const probeRender = renderCheck as RenderProbe;
|
|
72
72
|
|
|
73
73
|
/**
|
|
74
74
|
* Create a document-assistant session bound to the currently active tab.
|
|
@@ -39,10 +39,15 @@ export function isImportProgressEvent(value: unknown): value is ImportProgressEv
|
|
|
39
39
|
function applyOutcome(chat: ChatStore, outcome: TurnOutcome): void {
|
|
40
40
|
switch (outcome.kind) {
|
|
41
41
|
case "cap_partial": {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
/* The model's closing reply has already been drawn, and there is nothing to add. Otherwise a
|
|
43
|
+
partial success is said as the assistant, not as a failure (specs/ai.md §3.2), after
|
|
44
|
+
removing whatever the closing round that gave no reply left behind. */
|
|
45
|
+
if (outcome.cap) {
|
|
46
|
+
chat.cancelStream();
|
|
47
|
+
chat.beginAssistantTurn();
|
|
48
|
+
chat.appendDelta(outcome.cap.text);
|
|
49
|
+
chat.finishStream("length");
|
|
50
|
+
}
|
|
46
51
|
break;
|
|
47
52
|
}
|
|
48
53
|
case "cap_failed":
|
|
@@ -10,19 +10,41 @@
|
|
|
10
10
|
* expressions). Does NOT detect empty/zero-node output or deep-render custom element internals
|
|
11
11
|
* (connectedCallback doesn't fire on detached nodes).
|
|
12
12
|
*
|
|
13
|
+
* A page is drawn as the build would draw it, so a page that builds is not refused for what only
|
|
14
|
+
* the build supplies: its data is awaited, a dynamic route is drawn with its first entry, and
|
|
15
|
+
* `$page`, `$site` and project state are injected. Live data is the exception. A `timing: "server"`
|
|
16
|
+
* function is never called here and a `Request` is never sent (specs/ai.md §3.1), so both read as
|
|
17
|
+
* placeholders.
|
|
18
|
+
*
|
|
13
19
|
* @license MIT
|
|
20
|
+
* @docs studio/ai/document-assistant
|
|
14
21
|
*/
|
|
15
22
|
|
|
16
23
|
import {
|
|
17
24
|
buildScope,
|
|
25
|
+
lookupsSettled,
|
|
18
26
|
renderNode,
|
|
19
27
|
runScoped,
|
|
20
28
|
setResolveToken,
|
|
21
29
|
setSkipServerFunctions,
|
|
22
30
|
setTemplateErrorHandler,
|
|
23
31
|
} from "@jxsuite/runtime";
|
|
24
|
-
import
|
|
25
|
-
import {
|
|
32
|
+
import { isJsonObject, isServerFnDef, isString } from "@jxsuite/schema/guards";
|
|
33
|
+
import { injectContext } from "@jxsuite/site/context";
|
|
34
|
+
import { documentUrlPattern } from "@jxsuite/site/routes";
|
|
35
|
+
import type { JxScope } from "@jxsuite/runtime/types";
|
|
36
|
+
import type { JxDocument, JxMutableNode, JxStateDefinition } from "@jxsuite/schema/types";
|
|
37
|
+
import { documentBase, hostResolveToken } from "../canvas/canvas-origin";
|
|
38
|
+
import { isPagePath, pageContextFor } from "../canvas/canvas-page-context";
|
|
39
|
+
import {
|
|
40
|
+
dynamicRouteParams,
|
|
41
|
+
loadParamValues,
|
|
42
|
+
pagePathsDef,
|
|
43
|
+
paramBoundStateKeys,
|
|
44
|
+
substitutePreviewParams,
|
|
45
|
+
} from "../page-params";
|
|
46
|
+
import { projectState } from "../store";
|
|
47
|
+
import type { RenderSource } from "./ai-write-report";
|
|
26
48
|
|
|
27
49
|
/**
|
|
28
50
|
* Translate a render-time error into an actionable message the model can fix.
|
|
@@ -49,39 +71,239 @@ function translateRenderError(cause: unknown): string {
|
|
|
49
71
|
return `Render error: ${msg}\n → Fix: The document is schema-valid but fails to render. Review the last change for typos in $ref paths, template expressions, or event handler definitions.`;
|
|
50
72
|
}
|
|
51
73
|
|
|
74
|
+
/** How long a check waits for a document's data before it stops judging its templates. */
|
|
75
|
+
const LOOKUP_WAIT_MS = 5000;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Wait for the lookups in flight, for at most `waitMs`.
|
|
79
|
+
*
|
|
80
|
+
* @param {number} waitMs
|
|
81
|
+
* @returns {Promise<boolean>} Whether they all settled in time
|
|
82
|
+
*/
|
|
83
|
+
async function lookupsAnswered(waitMs: number): Promise<boolean> {
|
|
84
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
85
|
+
const expired = new Promise<boolean>((answer) => {
|
|
86
|
+
timer = setTimeout(answer, waitMs, false);
|
|
87
|
+
});
|
|
88
|
+
const answered = await Promise.race([lookupsSettled().then(() => true), expired]);
|
|
89
|
+
clearTimeout(timer);
|
|
90
|
+
return answered;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* What live data reads as during a check: anything may be read from it or called on it, and it
|
|
95
|
+
* prints as nothing, so a template over live data evaluates and every other template on the page is
|
|
96
|
+
* still judged.
|
|
97
|
+
*
|
|
98
|
+
* `then` and the reactivity library's `__v_` flags read as absent: a truthy `then` would make it a
|
|
99
|
+
* thenable that never settles, and a truthy `__v_raw` sends `toRaw` round in a circle.
|
|
100
|
+
*
|
|
101
|
+
* @returns {() => void}
|
|
102
|
+
*/
|
|
103
|
+
function liveDataPlaceholder(): () => void {
|
|
104
|
+
const placeholder: () => void = new Proxy(() => placeholder, {
|
|
105
|
+
get(_target, key) {
|
|
106
|
+
if (key === Symbol.toPrimitive) {
|
|
107
|
+
return () => "";
|
|
108
|
+
}
|
|
109
|
+
if (key === Symbol.iterator) {
|
|
110
|
+
return Array.prototype[Symbol.iterator].bind([]);
|
|
111
|
+
}
|
|
112
|
+
return isString(key) && key !== "then" && !key.startsWith("__v_") ? placeholder : undefined;
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
return placeholder;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Whether an entry's value is live data, which only a person's own action may fetch. */
|
|
119
|
+
function isLiveData(entry: JxStateDefinition): boolean {
|
|
120
|
+
return (
|
|
121
|
+
isServerFnDef(entry) ||
|
|
122
|
+
(isJsonObject(entry) && entry.$prototype === "Request" && !isString(entry.$src))
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The class entry a template reads that came back with no data, if it reads one. The template is
|
|
128
|
+
* what connects them: an unrelated broken template beside a failed lookup is still its own
|
|
129
|
+
* mistake.
|
|
130
|
+
*
|
|
131
|
+
* @param {JxDocument} built
|
|
132
|
+
* @param {JxScope} state
|
|
133
|
+
* @param {string} template
|
|
134
|
+
* @returns {{ key: string; from: string } | null}
|
|
135
|
+
*/
|
|
136
|
+
function emptyLookupRead(
|
|
137
|
+
built: JxDocument,
|
|
138
|
+
state: JxScope,
|
|
139
|
+
template: string,
|
|
140
|
+
): { key: string; from: string } | null {
|
|
141
|
+
const reads = new Set(template.match(/state\.[\w$]+/g));
|
|
142
|
+
for (const [key, entry] of Object.entries(built.state ?? {})) {
|
|
143
|
+
if (
|
|
144
|
+
reads.has(`state.${key}`) &&
|
|
145
|
+
isJsonObject(entry) &&
|
|
146
|
+
isString(entry.$prototype) &&
|
|
147
|
+
entry.$prototype !== "Function" &&
|
|
148
|
+
(state[key] ?? null) === null
|
|
149
|
+
) {
|
|
150
|
+
return { from: isString(entry.$src) ? entry.$src : entry.$prototype, key };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The first entry of each route parameter a document declares, as the preview picker opens with.
|
|
158
|
+
*
|
|
159
|
+
* @param {JxDocument} doc
|
|
160
|
+
* @param {RenderSource} source
|
|
161
|
+
* @returns {Promise<Record<string, string>>}
|
|
162
|
+
*/
|
|
163
|
+
async function sampleRouteParams(
|
|
164
|
+
doc: JxMutableNode,
|
|
165
|
+
{ documentPath, frontmatter }: RenderSource,
|
|
166
|
+
): Promise<Record<string, string>> {
|
|
167
|
+
const paths = pagePathsDef({ document: doc, frontmatter: frontmatter ?? null });
|
|
168
|
+
const params: Record<string, string> = {};
|
|
169
|
+
for (const [name, values] of Object.entries(await loadParamValues(documentPath, paths))) {
|
|
170
|
+
const [first] = values;
|
|
171
|
+
if (first !== undefined) {
|
|
172
|
+
params[name] = first;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return params;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* A document as the build would hand it to the renderer. A rebuild: the document may be the live
|
|
180
|
+
* tree of an open tab, which must not gain keys it would then save.
|
|
181
|
+
*
|
|
182
|
+
* @param {JxDocument} doc
|
|
183
|
+
* @param {RenderSource} source
|
|
184
|
+
* @returns {Promise<{ built: JxDocument; sampled: boolean }>} `sampled` is false when a route
|
|
185
|
+
* parameter has no entry to stand for it, so the data its templates read cannot be looked up
|
|
186
|
+
*/
|
|
187
|
+
async function asBuilt(
|
|
188
|
+
doc: JxDocument,
|
|
189
|
+
source: RenderSource,
|
|
190
|
+
): Promise<{ built: JxDocument; sampled: boolean }> {
|
|
191
|
+
const { documentPath } = source;
|
|
192
|
+
/* SAFETY: a candidate is a JSON tree, and everything below rebuilds it rather than writing to it,
|
|
193
|
+
so the mutable type is only what these shared helpers are declared over. */
|
|
194
|
+
const candidate = doc as JxMutableNode;
|
|
195
|
+
const params = await sampleRouteParams(candidate, source);
|
|
196
|
+
let built = substitutePreviewParams(candidate, params, documentPath);
|
|
197
|
+
|
|
198
|
+
const projectConfig = projectState?.projectConfig;
|
|
199
|
+
const drawnAsPage =
|
|
200
|
+
documentPath && (isPagePath(documentPath) || documentPath.startsWith("layouts/"));
|
|
201
|
+
if (drawnAsPage && projectConfig && projectState?.isSiteProject) {
|
|
202
|
+
built = { ...built, state: { ...built.state } };
|
|
203
|
+
if (built.imports) {
|
|
204
|
+
built.imports = { ...built.imports };
|
|
205
|
+
}
|
|
206
|
+
/* SAFETY: as on the canvas: `injectContext` writes `state`, `$media` and `imports`, all of which
|
|
207
|
+
the copy above owns. */
|
|
208
|
+
injectContext(built as JxDocument, projectConfig, {
|
|
209
|
+
urlPattern: documentUrlPattern(documentPath),
|
|
210
|
+
});
|
|
211
|
+
const context = await pageContextFor(
|
|
212
|
+
{ documentPath, isEntry: false, lang: doc.$lang, params, title: doc.title },
|
|
213
|
+
projectConfig,
|
|
214
|
+
);
|
|
215
|
+
if (context) {
|
|
216
|
+
built = { ...built, state: { ...built.state, ...context } };
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const sampled =
|
|
221
|
+
dynamicRouteParams(documentPath).every((name) => name in params) &&
|
|
222
|
+
paramBoundStateKeys(built.state ?? null).length === 0;
|
|
223
|
+
/* SAFETY: the same document, rebuilt member for member; its schema has not changed. */
|
|
224
|
+
return { built: built as JxDocument, sampled };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* The URL a document's relative references resolve from, when one can be formed. A check is not
|
|
229
|
+
* failed for lacking one: a document with no relative reference does not need it.
|
|
230
|
+
*
|
|
231
|
+
* @param {RenderSource} source
|
|
232
|
+
* @returns {string | undefined}
|
|
233
|
+
*/
|
|
234
|
+
function baseOf({ documentPath }: RenderSource): string | undefined {
|
|
235
|
+
const base = documentBase(projectState?.projectRoot);
|
|
236
|
+
return documentPath && URL.canParse(base) ? new URL(documentPath, base).href : undefined;
|
|
237
|
+
}
|
|
238
|
+
|
|
52
239
|
/**
|
|
53
240
|
* Attempt a detached render of a Jx document. Returns { ok: true } on success or { ok: false,
|
|
54
241
|
* error: string } with an actionable message on failure.
|
|
55
242
|
*
|
|
56
243
|
* @param {import("@jxsuite/schema/types").JxDocument} doc
|
|
244
|
+
* @param {RenderSource} [source] - Where the document lives, when it lives anywhere yet
|
|
245
|
+
* @param {number} [waitMs] - How long to wait for the document's data
|
|
57
246
|
* @returns {Promise<{ ok: true } | { ok: false; error: string }>}
|
|
58
247
|
*/
|
|
59
248
|
export async function renderCheck(
|
|
60
249
|
doc: JxDocument,
|
|
250
|
+
source: RenderSource = {},
|
|
251
|
+
waitMs = LOOKUP_WAIT_MS,
|
|
61
252
|
): Promise<{ ok: true } | { ok: false; error: string }> {
|
|
62
253
|
let stopRender: (() => void) | null = null;
|
|
63
|
-
/* A binding that throws no longer unwinds the render (spec.md §6.1) — it renders empty and waits
|
|
64
|
-
for its data — so the throw is collected here instead. The first one is the verdict: a page
|
|
65
|
-
whose template can never evaluate is exactly what this check exists to refuse. */
|
|
66
|
-
const bindingErrors: Error[] = [];
|
|
67
|
-
setTemplateErrorHandler((error) => {
|
|
68
|
-
bindingErrors.push(error);
|
|
69
|
-
});
|
|
70
254
|
try {
|
|
71
255
|
setSkipServerFunctions(true);
|
|
72
256
|
// The desktop dev proxy refuses a class resolve without the token the canvas carries.
|
|
73
257
|
setResolveToken(hostResolveToken());
|
|
74
258
|
|
|
75
|
-
const
|
|
259
|
+
const { built, sampled } = await asBuilt(doc, source);
|
|
260
|
+
const base = baseOf(source) ?? location.href;
|
|
261
|
+
/* This call's own context rather than `setSkipAutoRequests`, which is realm-wide: a Library
|
|
262
|
+
preview draws in this same realm, and would silently stop fetching. */
|
|
263
|
+
const state = await buildScope(built, {}, base, {
|
|
264
|
+
base,
|
|
265
|
+
media: null,
|
|
266
|
+
root: null,
|
|
267
|
+
skipAutoRequests: true,
|
|
268
|
+
});
|
|
269
|
+
for (const [key, entry] of Object.entries(built.state ?? {})) {
|
|
270
|
+
if (isLiveData(entry)) {
|
|
271
|
+
state[key] = liveDataPlaceholder();
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/* The build resolves a page's data before its templates run, and here the data is a lookup that
|
|
276
|
+
has only just started. Rendering now would read `null` and refuse a page that builds. */
|
|
277
|
+
const answered = await lookupsAnswered(waitMs);
|
|
278
|
+
|
|
279
|
+
/* A binding that throws no longer unwinds the render (spec.md §6.1) — it renders empty and
|
|
280
|
+
waits for its data — so the throw is collected here instead. The first one is the verdict: a
|
|
281
|
+
page whose template can never evaluate is exactly what this check exists to refuse. Only for
|
|
282
|
+
the synchronous render: anything else this realm draws during the wait is not this document. */
|
|
283
|
+
const bindingErrors: { error: Error; template: string }[] = [];
|
|
284
|
+
setTemplateErrorHandler((error, template) => {
|
|
285
|
+
bindingErrors.push({ error, template });
|
|
286
|
+
});
|
|
76
287
|
|
|
77
288
|
const container = document.createElement("div");
|
|
78
289
|
const { stop } = runScoped(() => {
|
|
79
|
-
container.append(renderNode(
|
|
290
|
+
container.append(renderNode(built, state));
|
|
80
291
|
});
|
|
81
292
|
stopRender = stop;
|
|
82
293
|
|
|
294
|
+
/* With a lookup still out, or no entry to look up, a throw may be nothing but the data being
|
|
295
|
+
absent, and refusing would block a valid write. A live binding recovers if that is what it was. */
|
|
83
296
|
const [first] = bindingErrors;
|
|
84
|
-
|
|
297
|
+
if (!first || !answered || !sampled) {
|
|
298
|
+
return { ok: true };
|
|
299
|
+
}
|
|
300
|
+
const empty = emptyLookupRead(built, state, first.template);
|
|
301
|
+
return {
|
|
302
|
+
error: empty
|
|
303
|
+
? `Render error: ${first.error.message}\n → Fix: state.${empty.key} has no data: its lookup through ${empty.from} failed or came back empty, so this template has nothing to read. Check that $src and the entry's config. If the data can legitimately be empty, guard the read, for example state.${empty.key}?.name.`
|
|
304
|
+
: translateRenderError(first.error),
|
|
305
|
+
ok: false,
|
|
306
|
+
};
|
|
85
307
|
} catch (error) {
|
|
86
308
|
return { ok: false, error: translateRenderError(error) };
|
|
87
309
|
} finally {
|
|
@@ -22,15 +22,15 @@
|
|
|
22
22
|
import type { InteractionStore } from "./harness/interaction-store";
|
|
23
23
|
import type { createChatState } from "@jxsuite/ai/chat-state";
|
|
24
24
|
import { DEFAULT_TURN_POLICY, assistantActorId, runTurn } from "@jxsuite/ai/harness";
|
|
25
|
-
import type { TurnInput, TurnRun } from "@jxsuite/ai/harness";
|
|
26
|
-
import { toChatMessages, toOpenAITools } from "@jxsuite/ai/messages";
|
|
25
|
+
import type { TurnInput, TurnPolicy, TurnRun } from "@jxsuite/ai/harness";
|
|
26
|
+
import { toChatMessages, toOpenAIMessages, toOpenAITools } from "@jxsuite/ai/messages";
|
|
27
27
|
import type { StreamingClient } from "@jxsuite/ai/streaming-client";
|
|
28
28
|
import { createSessionFacts } from "@jxsuite/ai/tools";
|
|
29
29
|
import type { SessionFacts, ToolRegistry } from "@jxsuite/ai/tools";
|
|
30
30
|
|
|
31
31
|
import type { Tab } from "../tabs/tab";
|
|
32
32
|
import { openBatchLane } from "../tabs/transact";
|
|
33
|
-
import { estimatePromptTokens } from "./context-manager";
|
|
33
|
+
import { estimatePromptTokens, shedToolResults } from "./context-manager";
|
|
34
34
|
import { fileTurn, openTurnLedger, turnAnchor } from "./ai-writes";
|
|
35
35
|
import { applyHarnessEvent } from "./harness/chat-reducer";
|
|
36
36
|
import { studioTurnHooks } from "./harness/turn-hooks";
|
|
@@ -52,6 +52,8 @@ interface RunAgentLoopOptions {
|
|
|
52
52
|
session?: SessionFacts;
|
|
53
53
|
/** Where a call that needs the author waits on them. Without one, such a call is refused. */
|
|
54
54
|
interactions?: InteractionStore;
|
|
55
|
+
/** Overrides on Studio's policy, for a run with bounds of its own. */
|
|
56
|
+
policy?: Partial<TurnPolicy>;
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
/**
|
|
@@ -67,6 +69,7 @@ export async function runAgentLoop({
|
|
|
67
69
|
getTab,
|
|
68
70
|
session = createSessionFacts(),
|
|
69
71
|
interactions,
|
|
72
|
+
policy,
|
|
70
73
|
}: RunAgentLoopOptions): Promise<void> {
|
|
71
74
|
/* The ledger is filed under the turn's last DRAWN assistant message (see services/ai-writes), so
|
|
72
75
|
the scan starts at the message this turn answers. */
|
|
@@ -103,9 +106,13 @@ export async function runAgentLoop({
|
|
|
103
106
|
system: [{ text: systemPrompt }],
|
|
104
107
|
/* Projected from the chat store, the transcript of record. The streaming client is read on
|
|
105
108
|
every round, and the turn's own signal is the host's, so a request is the one it was. */
|
|
106
|
-
model: (request) =>
|
|
107
|
-
|
|
108
|
-
|
|
109
|
+
model: (request) => {
|
|
110
|
+
/* A long turn must not outgrow the window between two sends: the trim only runs at a send. */
|
|
111
|
+
shedToolResults(chatState, systemPrompt);
|
|
112
|
+
const messages = chatState.toMessagesArray();
|
|
113
|
+
return streamingClient.streamChat(
|
|
114
|
+
/* A closing round's note is on no transcript, so it joins here, for this request only. */
|
|
115
|
+
request.closing ? [...messages, ...toOpenAIMessages([request.closing])] : messages,
|
|
109
116
|
toOpenAITools(request.tools),
|
|
110
117
|
systemPrompt,
|
|
111
118
|
/* SAFETY: the one production caller, `document-assistant.ts`, always passes its turn
|
|
@@ -113,11 +120,12 @@ export async function runAgentLoop({
|
|
|
113
120
|
doubles record the absent signal rather than reading it — which the agent-trace
|
|
114
121
|
goldens (`signal: "none"`) pin. */
|
|
115
122
|
signal as AbortSignal,
|
|
116
|
-
)
|
|
123
|
+
);
|
|
124
|
+
},
|
|
117
125
|
modelInfo: { family: "openai-compat", model: chatState.model },
|
|
118
126
|
tools: toolRegistry,
|
|
119
127
|
turnId: ledger.turnId,
|
|
120
|
-
policy: { estimateTokens: estimatePromptTokens },
|
|
128
|
+
policy: { estimateTokens: estimatePromptTokens, parallelReadOnly: true, ...policy },
|
|
121
129
|
hooks: studioTurnHooks({
|
|
122
130
|
superseded: () =>
|
|
123
131
|
turnUserId !== undefined && !chatState.messages.some((m) => m.id === turnUserId),
|
|
@@ -130,7 +130,7 @@
|
|
|
130
130
|
"& [part=\"entry-locale\"]:empty": {
|
|
131
131
|
"display": "none"
|
|
132
132
|
},
|
|
133
|
-
"& [part=\"entry-status\"][data-status=\"
|
|
133
|
+
"& [part=\"entry-status\"]:not([data-status=\"Published\"], [data-status=\"\"])": {
|
|
134
134
|
"color": "var(--warning)"
|
|
135
135
|
},
|
|
136
136
|
"& [part=\"note\"], & [part=\"no-project\"]": {
|
|
@@ -42,7 +42,7 @@ export interface CollectionRow {
|
|
|
42
42
|
export interface CollectionEntryRow {
|
|
43
43
|
path: string;
|
|
44
44
|
title: string;
|
|
45
|
-
/**
|
|
45
|
+
/** A `hold` rule's label, "Published", or "". */
|
|
46
46
|
status: string;
|
|
47
47
|
/** Whether a thumbnail is drawn, or the document glyph. */
|
|
48
48
|
art: "image" | "glyph";
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://jxsuite.com/schema/v1",
|
|
3
3
|
"$id": "StudioEntryEditor",
|
|
4
|
-
"$description": "The Entry editor (site-architecture.md §7.4) — one content entry's fields, in a pane, as a Jx document over the kit. The adapter (`surfaces/entry-editor.ts`) hands in a projection; `content/entry-editor.ts` decides everything in it — which collection the file belongs to, which record its fields live in, which required keys are absent and what a
|
|
4
|
+
"$description": "The Entry editor (site-architecture.md §7.4) — one content entry's fields, in a pane, as a Jx document over the kit. The adapter (`surfaces/entry-editor.ts`) hands in a projection; `content/entry-editor.ts` decides everything in it — which collection the file belongs to, which record its fields live in, which required keys are absent and what a hold switch commits into. The FORM is an island: a schema is `ui/schema-form.ts`'s subject, so this document renders an empty `fields` node and the flow places the element that engine hands back (studio-ui-guidelines.md §9.3, §9.4). That is the whole of the difference between this and the card the lit version drew, and it is why the engine converting in batch 7 is what unblocked this one.",
|
|
5
5
|
"tagName": "div",
|
|
6
6
|
"attributes": {
|
|
7
7
|
"part": "entry",
|
|
8
8
|
"data-jx-region": "${state.region}"
|
|
9
9
|
},
|
|
10
10
|
"style": {
|
|
11
|
-
"$description": "What `.entry-editor`, `.entry-editor-header`, `.entry-editor-collection`, `.entry-
|
|
11
|
+
"$description": "What `.entry-editor`, `.entry-editor-header`, `.entry-editor-collection`, `.entry-hold-switch`, `.entry-editor-fields`, `.entry-editor-note` and `.entry-editor-empty` said in `styles/overlays.css`, keyed on `part` and re-pointed at the studio token layer. This editor was the only emitter of all seven, so those rules are dead in the stylesheet from here; a central sweep removes them rather than this change, which owns no stylesheet. Every `--spectrum-font-size-*` reference is gone rather than renamed: those names resolve only inside `<sp-theme>`, and a pane editor is mounted outside it, so each would silently have taken its hard-coded fallback.",
|
|
12
12
|
"height": "100%",
|
|
13
13
|
"padding": "var(--jx-space-5)",
|
|
14
14
|
"overflowY": "auto",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"color": "var(--fg-dim)",
|
|
33
33
|
"fontSize": "var(--jx-text-sm)"
|
|
34
34
|
},
|
|
35
|
-
"& [part=\"
|
|
35
|
+
"& [part=\"hold\"]": {
|
|
36
36
|
"$description": "Pushed to the far end of the header rather than placed there: the heading and the collection chip are both content-width, so `margin-left: auto` is what keeps the switch on the right edge whatever the entry is called.",
|
|
37
37
|
"marginLeft": "auto"
|
|
38
38
|
},
|
|
@@ -241,27 +241,27 @@
|
|
|
241
241
|
{
|
|
242
242
|
"tagName": "div",
|
|
243
243
|
"attributes": {
|
|
244
|
-
"part": "
|
|
244
|
+
"part": "hold-slot",
|
|
245
245
|
"role": "none"
|
|
246
246
|
},
|
|
247
247
|
"style": {
|
|
248
248
|
"display": "contents"
|
|
249
249
|
},
|
|
250
250
|
"$switch": {
|
|
251
|
-
"$ref": "#/state/
|
|
251
|
+
"$ref": "#/state/holdSwitch"
|
|
252
252
|
},
|
|
253
253
|
"cases": {
|
|
254
254
|
"shown": {
|
|
255
255
|
"$description": "No `label`: the slotted word is on screen and carries the name, so an aria-label would be read twice. The tooltip is `hint`, which is what DRAFT_MEANING has always been.",
|
|
256
256
|
"tagName": "jx-switch",
|
|
257
257
|
"attributes": {
|
|
258
|
-
"part": "
|
|
258
|
+
"part": "hold"
|
|
259
259
|
},
|
|
260
260
|
"$props": {
|
|
261
261
|
"checked": {
|
|
262
|
-
"$ref": "#/state/
|
|
262
|
+
"$ref": "#/state/holdOn"
|
|
263
263
|
},
|
|
264
|
-
"hint": "${state.
|
|
264
|
+
"hint": "${state.holdHint}",
|
|
265
265
|
"size": "sm"
|
|
266
266
|
},
|
|
267
267
|
"onchange": {
|
|
@@ -270,7 +270,7 @@
|
|
|
270
270
|
{
|
|
271
271
|
"operator": "call",
|
|
272
272
|
"target": {
|
|
273
|
-
"$ref": "#/state/
|
|
273
|
+
"$ref": "#/state/setHold"
|
|
274
274
|
},
|
|
275
275
|
"value": [
|
|
276
276
|
{
|
|
@@ -283,7 +283,7 @@
|
|
|
283
283
|
"children": [
|
|
284
284
|
{
|
|
285
285
|
"tagName": "span",
|
|
286
|
-
"textContent": "
|
|
286
|
+
"textContent": "${state.holdLabel}"
|
|
287
287
|
}
|
|
288
288
|
]
|
|
289
289
|
}
|