@larkup/workspace-server 0.2.14 → 0.2.16

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.
@@ -1 +1 @@
1
- {"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../../../../src/app/api/chat/route.ts"],"names":[],"mappings":"AAqHA,eAAO,MAAM,WAAW,MAAM,CAAC;AAiuB/B,wBAAsB,IAAI,CAAC,GAAG,EAAE,OAAO,qBA69CtC"}
1
+ {"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../../../../src/app/api/chat/route.ts"],"names":[],"mappings":"AAuHA,eAAO,MAAM,WAAW,MAAM,CAAC;AAquB/B,wBAAsB,IAAI,CAAC,GAAG,EAAE,OAAO,qBA+/CtC"}
@@ -1 +1 @@
1
- {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../../../../src/app/api/chat/tools.ts"],"names":[],"mappings":"AA+WA,wBAAsB,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0I7F;AAyVD,wBAAsB,YAAY,CAAC,OAAO,EAAE;IAC1C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,GAAG,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iGAAiG;IACjG,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC,6EAA6E;IAC7E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,wEAAwE;IACxE,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,gGAAgG;IAChG,iBAAiB,CAAC,EAAE;QAClB,OAAO,EAAE,MAAM,EAAE,CAAC;QAClB,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;KACtC,CAAC;IACF,oFAAoF;IACpF,wBAAwB,CAAC,EAAE;QACzB,OAAO,EAAE,MAAM,EAAE,CAAC;QAClB,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;KACtC,CAAC;IACF,2FAA2F;IAC3F,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;;;;;;GAumFA"}
1
+ {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../../../../src/app/api/chat/tools.ts"],"names":[],"mappings":"AAoXA,wBAAsB,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0I7F;AAyVD,wBAAsB,YAAY,CAAC,OAAO,EAAE;IAC1C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,GAAG,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iGAAiG;IACjG,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC,6EAA6E;IAC7E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,wEAAwE;IACxE,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,gGAAgG;IAChG,iBAAiB,CAAC,EAAE;QAClB,OAAO,EAAE,MAAM,EAAE,CAAC;QAClB,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;KACtC,CAAC;IACF,oFAAoF;IACpF,wBAAwB,CAAC,EAAE;QACzB,OAAO,EAAE,MAAM,EAAE,CAAC;QAClB,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;KACtC,CAAC;IACF,2FAA2F;IAC3F,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;;;;;;GAmnFA"}
@@ -0,0 +1,34 @@
1
+ export interface AnalysisTablePresentation {
2
+ kind: 'table';
3
+ title: string;
4
+ summary?: string;
5
+ columns: string[];
6
+ rows: Array<Record<string, string | number | boolean | null>>;
7
+ totalItems: number;
8
+ requestedItems?: number;
9
+ complete: boolean;
10
+ fileName?: string;
11
+ }
12
+ export interface AnalysisDocumentPage {
13
+ title?: string;
14
+ content: string;
15
+ }
16
+ export interface AnalysisDocumentPresentation {
17
+ kind: 'document';
18
+ title: string;
19
+ summary?: string;
20
+ pages: AnalysisDocumentPage[];
21
+ totalItems: number;
22
+ requestedItems?: number;
23
+ complete: boolean;
24
+ fileName?: string;
25
+ }
26
+ export type AnalysisPresentation = AnalysisTablePresentation | AnalysisDocumentPresentation;
27
+ export declare function extractAnalysisPresentation(stdout: string): {
28
+ stdout: string;
29
+ presentation?: AnalysisPresentation;
30
+ };
31
+ /** Injected before generated Python so long results have one stable UI contract. */
32
+ export declare const ANALYSIS_PRESENTATION_PYTHON: string;
33
+ export declare function withAnalysisPresentationHelpers(code: string, requestedItems?: number): string;
34
+ //# sourceMappingURL=analysis-presentation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analysis-presentation.d.ts","sourceRoot":"","sources":["../../../src/lib/chat/analysis-presentation.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,yBAAyB;IACxC,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC,CAAC,CAAC;IAC9D,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,oBAAoB;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,4BAA4B;IAC3C,IAAI,EAAE,UAAU,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,oBAAoB,EAAE,CAAC;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,MAAM,oBAAoB,GAAG,yBAAyB,GAAG,4BAA4B,CAAC;AAkH5F,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,MAAM,GAAG;IAC3D,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,oBAAoB,CAAC;CACrC,CAqBA;AAED,oFAAoF;AACpF,eAAO,MAAM,4BAA4B,QAmCxC,CAAC;AAEF,wBAAgB,+BAA+B,CAAC,IAAI,EAAE,MAAM,EAAE,cAAc,CAAC,EAAE,MAAM,GAAG,MAAM,CAG7F"}
@@ -21,6 +21,13 @@ export type ReusableTabularEvidence = {
21
21
  rows: Array<Record<string, unknown>>;
22
22
  totalRows: number;
23
23
  };
24
+ export type AnalysisArtifactSummary = {
25
+ kind: 'table' | 'document';
26
+ title: string;
27
+ totalItems: number;
28
+ requestedItems?: number;
29
+ complete: boolean;
30
+ };
24
31
  /** Only reuse table evidence while its source is still present in the live data store. */
25
32
  export declare function tabularEvidenceDatasetIsAvailable(evidence: ReusableTabularEvidence | undefined, availableDatasetIds: ReadonlySet<string>): boolean;
26
33
  export type ConversationEvidence = {
@@ -42,6 +49,8 @@ export type ConversationEvidence = {
42
49
  * answers keep their specialized verification paths and are never reused here.
43
50
  */
44
51
  export declare function findImmediateExactGroundedAnswer(messages: readonly ChatMessage[], currentQuestion: string): ExactGroundedAnswer | undefined;
52
+ /** Read only completion metadata; the full paged artifact remains in the UI message. */
53
+ export declare function extractLatestAnalysisArtifact(messages: readonly ChatMessage[]): AnalysisArtifactSummary | undefined;
45
54
  /** Keep the complete bounded query result for an immediate export follow-up. */
46
55
  export declare function extractLatestTabularExportResult(messages: readonly ChatMessage[]): ReusableTabularEvidence | undefined;
47
56
  /** Keep only enough exact table evidence for a direct follow-up, never raw sheets. */
@@ -1 +1 @@
1
- {"version":3,"file":"conversation-memory.d.ts","sourceRoot":"","sources":["../../../src/lib/chat/conversation-memory.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"conversation-memory.d.ts","sourceRoot":"","sources":["../../../src/lib/chat/conversation-memory.ts"],"names":[],"mappings":"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"}
@@ -1,8 +1,10 @@
1
- import type { TabularDataset, TabularQueryRequest } from '@larkup/core/tabular-store';
1
+ import { type TabularDataset, type TabularQueryRequest } from '@larkup/core/tabular-store';
2
2
  export type InferredTabularPlan = {
3
3
  request: TabularQueryRequest;
4
4
  chartTitle?: string;
5
5
  };
6
+ /** Preserve an explicit long-list size instead of relying on a model default. */
7
+ export declare function requestedTabularResultLimit(text: string): number | undefined;
6
8
  /**
7
9
  * Normalize a clear calendar request from the dataset schema. It never
8
10
  * assumes business columns or domain names: a time series is derived only
@@ -1 +1 @@
1
- {"version":3,"file":"tabular-query-plan.d.ts","sourceRoot":"","sources":["../../../src/lib/chat/tabular-query-plan.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAEtF,MAAM,MAAM,mBAAmB,GAAG;IAChC,OAAO,EAAE,mBAAmB,CAAC;IAC7B,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAiHF;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,OAAO,EAAE,cAAc,EACvB,OAAO,EAAE,mBAAmB,GAC3B,mBAAmB,CAmErB"}
1
+ {"version":3,"file":"tabular-query-plan.d.ts","sourceRoot":"","sources":["../../../src/lib/chat/tabular-query-plan.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,mBAAmB,EACzB,MAAM,4BAA4B,CAAC;AAEpC,MAAM,MAAM,mBAAmB,GAAG;IAChC,OAAO,EAAE,mBAAmB,CAAC;IAC7B,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AA2EF,iFAAiF;AACjF,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAQ5E;AAwCD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,OAAO,EAAE,cAAc,EACvB,OAAO,EAAE,mBAAmB,GAC3B,mBAAmB,CAuErB"}
@@ -28,6 +28,14 @@ export declare function hasCompletedTabularVisualizationStep(steps: Array<{
28
28
  * column names.
29
29
  */
30
30
  export declare function requiresTabularSandbox(text: string): boolean;
31
+ /**
32
+ * Long per-record transformations cannot be completed safely as streamed prose:
33
+ * the model will eventually hit its output ceiling and a later "continue" loses
34
+ * global constraints. Route these through one sandbox run and a paged artifact.
35
+ */
36
+ export declare function requestsLongAnalysisArtifact(text: string): boolean;
37
+ /** Preserve explicit batch sizes for sandbox artifacts (larger than chat prose limits). */
38
+ export declare function requestedLongAnalysisItemCount(text: string): number | undefined;
31
39
  /**
32
40
  * A workspace can contain spreadsheets alongside PDFs, media, and notes.
33
41
  * Only force the table path when the wording actually asks for tabular facts;
@@ -1 +1 @@
1
- {"version":3,"file":"tabular-routing.d.ts","sourceRoot":"","sources":["../../../src/lib/chat/tabular-routing.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE;IACzC,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;;;;;;;;;EAqBA;AAED,gFAAgF;AAChF,wBAAgB,oCAAoC,CAClD,KAAK,EAAE,KAAK,CAAC;IACX,WAAW,CAAC,EAAE,SAAS,OAAO,EAAE,CAAC;CAClC,CAAC,WAgBH;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAI5D;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE;IAC7C,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB,WA8CA"}
1
+ {"version":3,"file":"tabular-routing.d.ts","sourceRoot":"","sources":["../../../src/lib/chat/tabular-routing.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE;IACzC,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;;;;;;;;;EAqBA;AAED,gFAAgF;AAChF,wBAAgB,oCAAoC,CAClD,KAAK,EAAE,KAAK,CAAC;IACX,WAAW,CAAC,EAAE,SAAS,OAAO,EAAE,CAAC;CAClC,CAAC,WAgBH;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAM5D;AAED;;;;GAIG;AACH,wBAAgB,4BAA4B,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAUlE;AAED,2FAA2F;AAC3F,wBAAgB,8BAA8B,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAM/E;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE;IAC7C,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB,WA8CA"}
@@ -1 +1 @@
1
- {"version":3,"file":"tabular-visualization.d.ts","sourceRoot":"","sources":["../../../src/lib/chat/tabular-visualization.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,kBAAkB,GAAG;IAC/B,SAAS,EAAE,KAAK,GAAG,MAAM,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC7C,UAAU,EAAE,OAAO,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,KAAK,0BAA0B,GAAG;IAChC,aAAa,CAAC,EAAE,kBAAkB,CAAC;IACnC,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,CAAC;AAoBF,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAI3D;AA+BD,0EAA0E;AAC1E,wBAAgB,0BAA0B,CACxC,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,MAAM,EAAE;IAAE,OAAO,EAAE,MAAM,EAAE,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAA;CAAE,GAC7D,kBAAkB,GAAG,SAAS,CAsDhC;AAED;;;;GAIG;AACH,wBAAgB,gCAAgC,CAAC,MAAM,EAAE,0BAA0B,GAAG,MAAM,CA4B3F"}
1
+ {"version":3,"file":"tabular-visualization.d.ts","sourceRoot":"","sources":["../../../src/lib/chat/tabular-visualization.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,kBAAkB,GAAG;IAC/B,SAAS,EAAE,KAAK,GAAG,MAAM,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC7C,UAAU,EAAE,OAAO,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,KAAK,0BAA0B,GAAG;IAChC,aAAa,CAAC,EAAE,kBAAkB,CAAC;IACnC,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,CAAC;AAoBF,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAU3D;AA+BD,0EAA0E;AAC1E,wBAAgB,0BAA0B,CACxC,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,MAAM,EAAE;IAAE,OAAO,EAAE,MAAM,EAAE,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAA;CAAE,GAC7D,kBAAkB,GAAG,SAAS,CAsDhC;AAED;;;;GAIG;AACH,wBAAgB,gCAAgC,CAAC,MAAM,EAAE,0BAA0B,GAAG,MAAM,CA4B3F"}
@@ -1 +1 @@
1
- {"version":3,"file":"tool-context.d.ts","sourceRoot":"","sources":["../../../src/lib/chat/tool-context.ts"],"names":[],"mappings":"AAUA;;;;;;GAMG;AACH,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAmBpE;AAED,0EAA0E;AAC1E,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAgB/D;AAED,kFAAkF;AAClF,wBAAgB,gCAAgC,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAmBxE;AAmBD,oEAAoE;AACpE,wBAAgB,kCAAkC,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,CAuB7F;AAED;;;;GAIG;AACH,wBAAgB,iCAAiC,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,EAAE,CA+D1E;AAED;;;;GAIG;AACH,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,EAAE,CAyDrE;AAED;;;;;GAKG;AACH,wBAAgB,kCAAkC,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CA4KrF;AAED;;;;;;GAMG;AACH,wBAAgB,8BAA8B,CAC5C,KAAK,EAAE,OAAO,EACd,aAAa,CAAC,EAAE;IAAE,IAAI,CAAC,EAAE,OAAO,CAAC;IAAC,SAAS,CAAC,EAAE,OAAO,CAAA;CAAE,GACtD,MAAM,GAAG,SAAS,CAwGpB;AAED,sFAAsF;AACtF,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,OAAO,EACd,aAAa,EAAE,2BAA2B,GACzC,MAAM,GAAG,SAAS,CAyHpB;AAED,8FAA8F;AAC9F,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,SAAK,GAAG,MAAM,GAAG,SAAS,CAqE/F;AAED,8EAA8E;AAC9E,wBAAgB,+BAA+B,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CA2BvE;AAED,sGAAsG;AACtG,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CA0D/F;AAYD;;;GAGG;AACH,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CA+IjG;AAED,gGAAgG;AAChG,wBAAgB,2BAA2B,CAAC,YAAY,EAAE,MAAM,mCAwC/D;AAkLD;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,SAAS,GAAG,EAAE,GAAG,GAAG,EAAE,CAapE;AAED,uFAAuF;AACvF,wBAAgB,0BAA0B,CAAC,QAAQ,EAAE,SAAS,GAAG,EAAE,GAAG,GAAG,EAAE,CAiB1E;AACD,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,4CAA4C,CAAC"}
1
+ {"version":3,"file":"tool-context.d.ts","sourceRoot":"","sources":["../../../src/lib/chat/tool-context.ts"],"names":[],"mappings":"AAUA;;;;;;GAMG;AACH,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAmBpE;AAED,0EAA0E;AAC1E,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAgB/D;AAED,kFAAkF;AAClF,wBAAgB,gCAAgC,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAmBxE;AAmBD,oEAAoE;AACpE,wBAAgB,kCAAkC,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,CAuB7F;AAED;;;;GAIG;AACH,wBAAgB,iCAAiC,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,EAAE,CA+D1E;AAED;;;;GAIG;AACH,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,EAAE,CAyDrE;AAED;;;;;GAKG;AACH,wBAAgB,kCAAkC,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CA4KrF;AAED;;;;;;GAMG;AACH,wBAAgB,8BAA8B,CAC5C,KAAK,EAAE,OAAO,EACd,aAAa,CAAC,EAAE;IAAE,IAAI,CAAC,EAAE,OAAO,CAAC;IAAC,SAAS,CAAC,EAAE,OAAO,CAAA;CAAE,GACtD,MAAM,GAAG,SAAS,CAwGpB;AAED,sFAAsF;AACtF,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,OAAO,EACd,aAAa,EAAE,2BAA2B,GACzC,MAAM,GAAG,SAAS,CAyHpB;AAED,8FAA8F;AAC9F,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,SAAK,GAAG,MAAM,GAAG,SAAS,CAqE/F;AAED,8EAA8E;AAC9E,wBAAgB,+BAA+B,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CA2BvE;AAED,sGAAsG;AACtG,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CA0D/F;AAYD;;;GAGG;AACH,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CA+IjG;AAED,gGAAgG;AAChG,wBAAgB,2BAA2B,CAAC,YAAY,EAAE,MAAM,mCAwC/D;AAmMD;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,SAAS,GAAG,EAAE,GAAG,GAAG,EAAE,CAapE;AAED,uFAAuF;AACvF,wBAAgB,0BAA0B,CAAC,QAAQ,EAAE,SAAS,GAAG,EAAE,GAAG,GAAG,EAAE,CAiB1E;AACD,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,4CAA4C,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@larkup/workspace-server",
3
- "version": "0.2.14",
3
+ "version": "0.2.16",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src",
@@ -53,6 +53,11 @@
53
53
  "import": "./src/lib/chat/assistant-source-scope.ts",
54
54
  "default": "./src/lib/chat/assistant-source-scope.ts"
55
55
  },
56
+ "./lib/chat/analysis-presentation": {
57
+ "types": "./dist-types/lib/chat/analysis-presentation.d.ts",
58
+ "import": "./src/lib/chat/analysis-presentation.ts",
59
+ "default": "./src/lib/chat/analysis-presentation.ts"
60
+ },
56
61
  "./lib/chat/conversation-memory": {
57
62
  "types": "./dist-types/lib/chat/conversation-memory.d.ts",
58
63
  "import": "./src/lib/chat/conversation-memory.ts",
@@ -326,8 +331,8 @@
326
331
  "mammoth": "^1.12.0",
327
332
  "@larkup/marketplace": "0.3.11",
328
333
  "@larkup/sandbox": "0.1.8",
329
- "@larkup/scraper": "0.2.2",
330
- "@larkup/vector-stores": "0.1.27"
334
+ "@larkup/vector-stores": "0.1.27",
335
+ "@larkup/scraper": "0.2.2"
331
336
  },
332
337
  "devDependencies": {
333
338
  "typescript": "^7.0.2",
@@ -44,6 +44,7 @@ import { collectExhaustiveVideoEvidencePages } from '../../../lib/chat/video-rag
44
44
  import { reconcileVideoInvestigation } from '../../../lib/chat/video-query-planning';
45
45
  import {
46
46
  extractConversationEvidence,
47
+ extractLatestAnalysisArtifact,
47
48
  extractLatestTabularExportResult,
48
49
  contextualizeKnowledgeFollowUpQuery,
49
50
  formatConversationEvidence,
@@ -78,6 +79,7 @@ import {
78
79
  } from '../../../lib/chat/tabular-visualization';
79
80
  import {
80
81
  isLikelyTabularQuestion,
82
+ requestsLongAnalysisArtifact,
81
83
  requiresTabularSandbox,
82
84
  tabularToolsForStep,
83
85
  } from '../../../lib/chat/tabular-routing';
@@ -176,6 +178,10 @@ ${PERSONALIZED_RESPONSE_STYLE}
176
178
 
177
179
  Keep the answer brief and direct: give the answer first, then only the essential context. Do not repeat tool output, dump rows, or add a table unless the user asks to see data. Use a short list only when it improves clarity.
178
180
 
181
+ When a tabular tool returns a requested long list, the structured table is the complete visible answer. Briefly state what it contains and its coverage; never stream the rows again as prose or Markdown.
182
+
183
+ When the user asks for a transformation repeated across many records (for example matches, recommendations, explanations, profiles, or summaries for the first 25 or for every item), use the sandbox analysis tool. It must prepare the whole requested result in one run with larkup_table or larkup_document. Treat that paged artifact as the answer: state its coverage briefly and never repeat its entries in chat prose. Do not ask the user to say "continue" for an artifact result.
184
+
179
185
  When the user asks for an Excel, CSV, or PDF export, use createDataExport. Never claim that a file was created and never write a Markdown download link unless the tool returned a real file artifact.
180
186
 
181
187
  When the user asks for every item and the media evidence marks its continuation as exhaustive, include every returned item in chronological order. In that case completeness overrides brevity; deduplicate wording but do not summarize items away.
@@ -1197,6 +1203,19 @@ ${fieldLines}`;
1197
1203
  ? precedingNumberedDocumentReference(messagesToProcess)
1198
1204
  : undefined;
1199
1205
  const reusableEvidence = extractConversationEvidence(evidenceMessages);
1206
+ const latestAnalysisArtifact = extractLatestAnalysisArtifact(evidenceMessages);
1207
+ if (
1208
+ latestAnalysisArtifact?.complete &&
1209
+ /^(?:continue|go\s+on|keep\s+going|show\s+the\s+rest|more|تابع|أكمل|اكمل)[.!\s]*$/iu.test(
1210
+ userText.trim(),
1211
+ )
1212
+ ) {
1213
+ const count = latestAnalysisArtifact.requestedItems ?? latestAnalysisArtifact.totalItems;
1214
+ return directAssistantTextResponse(
1215
+ messages,
1216
+ `The complete ${count.toLocaleString()}-item result is already prepared in “${latestAnalysisArtifact.title}”. Use its page controls, search, or download button to inspect the rest without restarting the analysis.`,
1217
+ );
1218
+ }
1200
1219
  const latestTabularExportResult = extractLatestTabularExportResult(evidenceMessages);
1201
1220
  // This compact string is used by retrieval only, not appended to model
1202
1221
  // history. It preserves the immediate topic while keeping the model context
@@ -1217,8 +1236,11 @@ ${fieldLines}`;
1217
1236
  columnNames: tabularColumnNames,
1218
1237
  datasetNames: tabularDatasetNames,
1219
1238
  });
1239
+ const requestsLongArtifact = requestsLongAnalysisArtifact(userText);
1240
+ const requiresCorpusSandboxAnalysis = !tabularQuestion && requestsLongArtifact;
1220
1241
  const requiresSandboxAnalysis =
1221
- hasTabularData && tabularQuestion && requiresTabularSandbox(userText);
1242
+ (hasTabularData && tabularQuestion && requiresTabularSandbox(userText)) ||
1243
+ requiresCorpusSandboxAnalysis;
1222
1244
  const isExplicitVideoCorrection =
1223
1245
  /^(?:no|nah),\s+|\b(?:that(?:'s| is) (?:wrong|incorrect)|correction\s*:|actually\s*,|instead\s*,|should be|not .{0,80}\bbut|i meant)\b/i.test(
1224
1246
  userText,
@@ -1318,6 +1340,9 @@ ${fieldLines}`;
1318
1340
  createDataExport: allTools.createDataExport,
1319
1341
  generateVisualization: allTools.generateVisualization,
1320
1342
  ...(allTools.executeAnalysis ? { executeAnalysis: allTools.executeAnalysis } : {}),
1343
+ ...(allTools.analyzeCorpusWithCode
1344
+ ? { analyzeCorpusWithCode: allTools.analyzeCorpusWithCode }
1345
+ : {}),
1321
1346
  inspectPdfPages: allTools.inspectPdfPages,
1322
1347
  analyzePdfPages: allTools.analyzePdfPages,
1323
1348
  analyzeImageDeeply: allTools.analyzeImageDeeply,
@@ -1340,6 +1365,7 @@ ${fieldLines}`;
1340
1365
  userText.trim() &&
1341
1366
  !isGreeting &&
1342
1367
  !docSessionId &&
1368
+ !(requiresCorpusSandboxAnalysis && builtInTools.analyzeCorpusWithCode) &&
1343
1369
  (!hasTabularData || !tabularQuestion) &&
1344
1370
  builtInTools.searchKnowledgeBase,
1345
1371
  );
@@ -2106,6 +2132,20 @@ ${fieldLines}`;
2106
2132
  };
2107
2133
  }
2108
2134
 
2135
+ if (requiresCorpusSandboxAnalysis && builtInTools.analyzeCorpusWithCode) {
2136
+ return stepNumber === 0
2137
+ ? {
2138
+ toolChoice: { type: 'tool' as const, toolName: 'analyzeCorpusWithCode' },
2139
+ activeTools: ['analyzeCorpusWithCode'],
2140
+ messages: compactToolContextForModel(messages),
2141
+ }
2142
+ : {
2143
+ toolChoice: 'none' as const,
2144
+ activeTools: [],
2145
+ messages: withFinalAnswerNudge(compactToolContextForModel(messages)),
2146
+ };
2147
+ }
2148
+
2109
2149
  if (tabularFollowUp) {
2110
2150
  if (dataExportFormat && stepNumber === 1 && builtInTools.createDataExport) {
2111
2151
  return {
@@ -96,6 +96,7 @@ import { normalizeChartConfigWithEvidence } from '@larkup/core/chart-config';
96
96
  import { createDataExport, selectDataExportSource } from '@larkup/core/data-export';
97
97
  import { assistantSourceScopeFingerprint } from '../../../lib/chat/assistant-source-scope';
98
98
  import { inferTabularPlan } from '../../../lib/chat/tabular-query-plan';
99
+ import { requestedLongAnalysisItemCount } from '../../../lib/chat/tabular-routing';
99
100
  import {
100
101
  resolveTabularDatasetForQuestion,
101
102
  selectTabularDatasetForQuestion,
@@ -104,6 +105,10 @@ import {
104
105
  createTabularSandboxFiles,
105
106
  remapLegacyMultiDatasetReads,
106
107
  } from '../../../lib/chat/tabular-sandbox-files';
108
+ import {
109
+ extractAnalysisPresentation,
110
+ withAnalysisPresentationHelpers,
111
+ } from '../../../lib/chat/analysis-presentation';
107
112
  import { leadingMediaAssetId } from '../../../lib/chat/media-retrieval-routing';
108
113
  import { findIndexedImageSource } from '../../../lib/chat/visual-routing';
109
114
  import { indexedVideoEvidenceIsSufficient } from '../../../lib/chat/video-rag-routing';
@@ -2759,12 +2764,12 @@ export async function getChatTools(context: {
2759
2764
 
2760
2765
  executeAnalysis: tool({
2761
2766
  description:
2762
- 'Execute Python code in a secure sandbox for deep data analysis. Use this for complex statistical computations (correlations, regressions, clustering), data transformations, or when creating custom matplotlib visualizations. The code has access to pandas, numpy, matplotlib, scipy, scikit-learn, and seaborn. Always print results to stdout and use plt.show() for charts. The sandbox already configures headless plotting and its cache directories: do not set MPLBACKEND, MPLCONFIGDIR, or XDG_CACHE_HOME in analysis code.',
2767
+ 'Execute Python code in a secure sandbox for deep data analysis and complete multi-record results. Use this for correlations, matching or recommendations across many rows, constrained ranking, transformations, clustering, and custom visualizations. For a result containing many records, do not print the records as prose. Call larkup_table(title, rows, columns, summary, requested_items, file_name) exactly once so Chat renders the complete result with pagination and download. For long narrative sections, call larkup_document(title, pages, summary, requested_items, file_name), where pages is a list of strings or {title, content} objects. Produce every requested item in this one execution, validate counts and global constraints in Python, and keep stdout to a short diagnostic. The code has pandas, numpy, matplotlib, scipy, scikit-learn, and seaborn. Use plt.show() for charts. The sandbox already configures headless plotting and cache directories.',
2763
2768
  inputSchema: z.object({
2764
2769
  code: z
2765
2770
  .string()
2766
2771
  .describe(
2767
- 'Python code to execute. Has pandas (2.0+), numpy, matplotlib, scipy, sklearn, seaborn available. ALWAYS use "ME" instead of "M" for resample frequency. Print results and use plt.show() for charts. Plot configuration is already prepared by the sandbox; do not set MPLBACKEND, MPLCONFIGDIR, or XDG_CACHE_HOME.',
2772
+ 'Python code to execute. For a list/table longer than a short chat answer, finish with larkup_table(...). For long prose, finish with larkup_document(...). These helpers are preloaded; never redefine them. requested_items must equal the user-requested top/first/all count so completeness is verified. Has pandas (2.0+), numpy, matplotlib, scipy, sklearn, seaborn. ALWAYS use "ME" instead of "M" for resample frequency. Use plt.show() for charts. Plot configuration is already prepared; do not set MPLBACKEND, MPLCONFIGDIR, or XDG_CACHE_HOME.',
2768
2773
  ),
2769
2774
  datasetId: z
2770
2775
  .string()
@@ -2850,6 +2855,10 @@ export async function getChatTools(context: {
2850
2855
  ) {
2851
2856
  finalCode = `import os\n${finalCode}`;
2852
2857
  }
2858
+ finalCode = withAnalysisPresentationHelpers(
2859
+ finalCode,
2860
+ requestedLongAnalysisItemCount(requestText ?? ''),
2861
+ );
2853
2862
 
2854
2863
  let result = await sandboxManager.execute({
2855
2864
  code: finalCode,
@@ -2880,10 +2889,12 @@ export async function getChatTools(context: {
2880
2889
  });
2881
2890
  }
2882
2891
 
2892
+ const extracted = extractAnalysisPresentation(result.stdout);
2883
2893
  return {
2884
- stdout: result.stdout.slice(0, 5000),
2894
+ stdout: extracted.stdout.slice(0, 5000),
2885
2895
  stderr: result.stderr.slice(0, 2000),
2886
2896
  exitCode: result.exitCode,
2897
+ ...(extracted.presentation ? { presentation: extracted.presentation } : {}),
2887
2898
  artifacts: result.artifacts.map((a) => ({
2888
2899
  name: a.name,
2889
2900
  mimeType: a.mimeType,
@@ -3052,12 +3063,12 @@ export async function getChatTools(context: {
3052
3063
 
3053
3064
  analyzeCorpusWithCode: tool({
3054
3065
  description:
3055
- 'Run Python code in a sandbox with the FULL indexed corpus available as a file. Use this for complex analysis of documents: parsing fields from content, computing progress percentages, grouping by patterns, detecting duplicates, generating charts from corpus data, or any question that requires programmatic processing of hundreds/thousands of documents. The corpus is available as "corpus.csv" (or "corpus.jsonl" if format is jsonl). CSV columns: id, title, source, url, charCount, status, createdAt, content, plus any metadata_* columns.',
3066
+ 'Run Python code in a sandbox with the FULL indexed corpus available as a file. Use this for complete long inventories or transformations across documents, parsing fields, grouping, detecting duplicates, and corpus-scale analysis. For many result records call larkup_table(...) exactly once; for long narrative sections call larkup_document(...) exactly once. Produce the complete requested item count in one execution instead of streaming a long answer. The corpus is available as "corpus.csv" (or "corpus.jsonl"). CSV columns: id, title, source, url, charCount, status, createdAt, content, plus metadata_* columns.',
3056
3067
  inputSchema: z.object({
3057
3068
  code: z
3058
3069
  .string()
3059
3070
  .describe(
3060
- 'Python code to execute. The corpus file is in the working directory. Use pandas to load it: df = pd.read_csv("corpus.csv") or for JSONL: df = pd.read_json("corpus.jsonl", lines=True). Has pandas (2.0+), numpy, matplotlib, scipy, sklearn, seaborn available. ALWAYS use "ME" instead of "M" for resample frequency. Print results and use plt.show() for charts.',
3071
+ 'Python code to execute. The corpus file is in the working directory. Use pandas to load it. For long structured results finish with larkup_table(...); for long prose finish with larkup_document(...). These helpers are preloaded. Set requested_items to the requested count. Has pandas, numpy, matplotlib, scipy, sklearn, seaborn. ALWAYS use "ME" instead of "M" for resample frequency.',
3061
3072
  ),
3062
3073
  format: z
3063
3074
  .enum(['csv', 'jsonl'])
@@ -3092,7 +3103,10 @@ export async function getChatTools(context: {
3092
3103
  const fileName = format === 'jsonl' ? 'corpus.jsonl' : 'corpus.csv';
3093
3104
  const files = [{ name: fileName, content: corpusData }];
3094
3105
 
3095
- let finalCode = code;
3106
+ let finalCode = withAnalysisPresentationHelpers(
3107
+ code,
3108
+ requestedLongAnalysisItemCount(requestText ?? ''),
3109
+ );
3096
3110
  let result = await sandboxManager.execute({
3097
3111
  code: finalCode,
3098
3112
  language: 'python',
@@ -3116,10 +3130,12 @@ export async function getChatTools(context: {
3116
3130
  });
3117
3131
  }
3118
3132
 
3133
+ const extracted = extractAnalysisPresentation(result.stdout);
3119
3134
  return {
3120
- stdout: result.stdout.slice(0, 5000),
3135
+ stdout: extracted.stdout.slice(0, 5000),
3121
3136
  stderr: result.stderr.slice(0, 2000),
3122
3137
  exitCode: result.exitCode,
3138
+ ...(extracted.presentation ? { presentation: extracted.presentation } : {}),
3123
3139
  artifacts: result.artifacts.map((a) => ({
3124
3140
  name: a.name,
3125
3141
  mimeType: a.mimeType,
@@ -3389,6 +3405,7 @@ export async function getChatTools(context: {
3389
3405
  'createDataExport',
3390
3406
  'generateVisualization',
3391
3407
  'executeAnalysis',
3408
+ 'analyzeCorpusWithCode',
3392
3409
  // PDF/image hits can require one visual pass after retrieval. This is
3393
3410
  // available only to a follow-up tool step, never auto-run on chat.
3394
3411
  'inspectPdfPages',
@@ -0,0 +1,217 @@
1
+ const PRESENTATION_MARKER = '__LARKUP_PRESENTATION__:';
2
+ const MAX_PRESENTATION_CHARS = 3_000_000;
3
+ const MAX_TABLE_ROWS = 5_000;
4
+ const MAX_TABLE_COLUMNS = 50;
5
+ const MAX_CELL_CHARS = 20_000;
6
+ const MAX_DOCUMENT_PAGES = 1_000;
7
+
8
+ export interface AnalysisTablePresentation {
9
+ kind: 'table';
10
+ title: string;
11
+ summary?: string;
12
+ columns: string[];
13
+ rows: Array<Record<string, string | number | boolean | null>>;
14
+ totalItems: number;
15
+ requestedItems?: number;
16
+ complete: boolean;
17
+ fileName?: string;
18
+ }
19
+
20
+ export interface AnalysisDocumentPage {
21
+ title?: string;
22
+ content: string;
23
+ }
24
+
25
+ export interface AnalysisDocumentPresentation {
26
+ kind: 'document';
27
+ title: string;
28
+ summary?: string;
29
+ pages: AnalysisDocumentPage[];
30
+ totalItems: number;
31
+ requestedItems?: number;
32
+ complete: boolean;
33
+ fileName?: string;
34
+ }
35
+
36
+ export type AnalysisPresentation = AnalysisTablePresentation | AnalysisDocumentPresentation;
37
+
38
+ function boundedText(value: unknown, max: number): string | undefined {
39
+ if (typeof value !== 'string') return undefined;
40
+ const text = value.trim();
41
+ return text ? text.slice(0, max) : undefined;
42
+ }
43
+
44
+ function boundedCount(value: unknown): number | undefined {
45
+ return typeof value === 'number' && Number.isFinite(value) && value >= 0
46
+ ? Math.floor(value)
47
+ : undefined;
48
+ }
49
+
50
+ function normalizeCell(value: unknown): string | number | boolean | null {
51
+ if (value === null || typeof value === 'number' || typeof value === 'boolean') return value;
52
+ if (typeof value === 'string') return value.slice(0, MAX_CELL_CHARS);
53
+ try {
54
+ const serialized = JSON.stringify(value);
55
+ return (serialized ?? String(value)).slice(0, MAX_CELL_CHARS);
56
+ } catch {
57
+ return String(value).slice(0, MAX_CELL_CHARS);
58
+ }
59
+ }
60
+
61
+ function normalizeTable(value: Record<string, unknown>): AnalysisTablePresentation | undefined {
62
+ if (!Array.isArray(value.rows)) return undefined;
63
+ const inferredColumns = value.rows.flatMap((row) =>
64
+ row && typeof row === 'object' && !Array.isArray(row) ? Object.keys(row) : [],
65
+ );
66
+ const columns = [
67
+ ...new Set(
68
+ (Array.isArray(value.columns) ? value.columns : inferredColumns).filter(
69
+ (column): column is string => typeof column === 'string' && column.trim().length > 0,
70
+ ),
71
+ ),
72
+ ]
73
+ .slice(0, MAX_TABLE_COLUMNS)
74
+ .map((column) => column.slice(0, 160));
75
+ if (columns.length === 0) return undefined;
76
+
77
+ const rows: AnalysisTablePresentation['rows'] = [];
78
+ let serializedChars = 0;
79
+ for (const candidate of value.rows) {
80
+ if (rows.length >= MAX_TABLE_ROWS || serializedChars >= MAX_PRESENTATION_CHARS) break;
81
+ if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) continue;
82
+ const source = candidate as Record<string, unknown>;
83
+ const row = Object.fromEntries(
84
+ columns.map((column) => [column, normalizeCell(source[column])]),
85
+ );
86
+ const rowChars = JSON.stringify(row).length;
87
+ if (serializedChars + rowChars > MAX_PRESENTATION_CHARS) break;
88
+ rows.push(row);
89
+ serializedChars += rowChars;
90
+ }
91
+ if (rows.length === 0) return undefined;
92
+
93
+ const requestedItems = boundedCount(value.requestedItems);
94
+ const reportedTotal = boundedCount(value.totalItems) ?? value.rows.length;
95
+ const totalItems = Math.max(reportedTotal, rows.length);
96
+ const retainedEveryRow = rows.length === value.rows.length;
97
+ const satisfiesRequest = requestedItems === undefined || rows.length >= requestedItems;
98
+ return {
99
+ kind: 'table',
100
+ title: boundedText(value.title, 160) ?? 'Analysis results',
101
+ ...(boundedText(value.summary, 800) ? { summary: boundedText(value.summary, 800) } : {}),
102
+ columns,
103
+ rows,
104
+ totalItems,
105
+ ...(requestedItems !== undefined ? { requestedItems } : {}),
106
+ complete: value.complete !== false && retainedEveryRow && satisfiesRequest,
107
+ ...(boundedText(value.fileName, 120) ? { fileName: boundedText(value.fileName, 120) } : {}),
108
+ };
109
+ }
110
+
111
+ function normalizeDocument(
112
+ value: Record<string, unknown>,
113
+ ): AnalysisDocumentPresentation | undefined {
114
+ if (!Array.isArray(value.pages)) return undefined;
115
+ const pages: AnalysisDocumentPage[] = [];
116
+ let serializedChars = 0;
117
+ for (const candidate of value.pages) {
118
+ if (pages.length >= MAX_DOCUMENT_PAGES || serializedChars >= MAX_PRESENTATION_CHARS) break;
119
+ const page =
120
+ typeof candidate === 'string'
121
+ ? { content: candidate }
122
+ : candidate && typeof candidate === 'object' && !Array.isArray(candidate)
123
+ ? (candidate as Record<string, unknown>)
124
+ : undefined;
125
+ const content = boundedText(page?.content, MAX_CELL_CHARS);
126
+ if (!content || serializedChars + content.length > MAX_PRESENTATION_CHARS) break;
127
+ const title = boundedText(page?.title, 160);
128
+ pages.push({ ...(title ? { title } : {}), content });
129
+ serializedChars += content.length;
130
+ }
131
+ if (pages.length === 0) return undefined;
132
+
133
+ const requestedItems = boundedCount(value.requestedItems);
134
+ const reportedTotal = boundedCount(value.totalItems) ?? value.pages.length;
135
+ const totalItems = Math.max(reportedTotal, pages.length);
136
+ const retainedEveryPage = pages.length === value.pages.length;
137
+ const satisfiesRequest = requestedItems === undefined || pages.length >= requestedItems;
138
+ return {
139
+ kind: 'document',
140
+ title: boundedText(value.title, 160) ?? 'Prepared document',
141
+ ...(boundedText(value.summary, 800) ? { summary: boundedText(value.summary, 800) } : {}),
142
+ pages,
143
+ totalItems,
144
+ ...(requestedItems !== undefined ? { requestedItems } : {}),
145
+ complete: value.complete !== false && retainedEveryPage && satisfiesRequest,
146
+ ...(boundedText(value.fileName, 120) ? { fileName: boundedText(value.fileName, 120) } : {}),
147
+ };
148
+ }
149
+
150
+ export function extractAnalysisPresentation(stdout: string): {
151
+ stdout: string;
152
+ presentation?: AnalysisPresentation;
153
+ } {
154
+ const markerIndex = stdout.lastIndexOf(PRESENTATION_MARKER);
155
+ if (markerIndex < 0) return { stdout };
156
+ const payloadStart = markerIndex + PRESENTATION_MARKER.length;
157
+ const lineEnd = stdout.indexOf('\n', payloadStart);
158
+ const payload = stdout.slice(payloadStart, lineEnd < 0 ? undefined : lineEnd).trim();
159
+ const cleanedStdout =
160
+ `${stdout.slice(0, markerIndex)}${lineEnd < 0 ? '' : stdout.slice(lineEnd + 1)}`.trim();
161
+ if (!payload || payload.length > MAX_PRESENTATION_CHARS * 2) return { stdout: cleanedStdout };
162
+ try {
163
+ const value = JSON.parse(payload) as Record<string, unknown>;
164
+ const presentation =
165
+ value?.kind === 'table'
166
+ ? normalizeTable(value)
167
+ : value?.kind === 'document'
168
+ ? normalizeDocument(value)
169
+ : undefined;
170
+ return { stdout: cleanedStdout, ...(presentation ? { presentation } : {}) };
171
+ } catch {
172
+ return { stdout: cleanedStdout };
173
+ }
174
+ }
175
+
176
+ /** Injected before generated Python so long results have one stable UI contract. */
177
+ export const ANALYSIS_PRESENTATION_PYTHON = String.raw`
178
+ import json as _larkup_json
179
+
180
+ def _larkup_json_default(value):
181
+ item = getattr(value, "item", None)
182
+ if callable(item):
183
+ try:
184
+ return item()
185
+ except Exception:
186
+ pass
187
+ return str(value)
188
+
189
+ def larkup_table(title, rows, columns=None, summary=None, requested_items=None, file_name=None):
190
+ if hasattr(rows, "to_dict"):
191
+ rows = rows.to_dict(orient="records")
192
+ rows = list(rows)
193
+ if columns is None:
194
+ columns = list(rows[0].keys()) if rows else []
195
+ requested_items = requested_items if requested_items is not None else _LARKUP_REQUESTED_ITEMS
196
+ payload = {
197
+ "kind": "table", "title": title, "summary": summary,
198
+ "columns": list(columns), "rows": rows, "totalItems": len(rows),
199
+ "requestedItems": requested_items, "complete": True, "fileName": file_name,
200
+ }
201
+ print("${PRESENTATION_MARKER}" + _larkup_json.dumps(payload, ensure_ascii=False, default=_larkup_json_default))
202
+
203
+ def larkup_document(title, pages, summary=None, requested_items=None, file_name=None):
204
+ pages = list(pages)
205
+ requested_items = requested_items if requested_items is not None else _LARKUP_REQUESTED_ITEMS
206
+ payload = {
207
+ "kind": "document", "title": title, "summary": summary,
208
+ "pages": pages, "totalItems": len(pages), "requestedItems": requested_items,
209
+ "complete": True, "fileName": file_name,
210
+ }
211
+ print("${PRESENTATION_MARKER}" + _larkup_json.dumps(payload, ensure_ascii=False, default=_larkup_json_default))
212
+ `;
213
+
214
+ export function withAnalysisPresentationHelpers(code: string, requestedItems?: number): string {
215
+ const requested = requestedItems === undefined ? 'None' : String(requestedItems);
216
+ return `${ANALYSIS_PRESENTATION_PYTHON}\n_LARKUP_REQUESTED_ITEMS = ${requested}\n${code}`;
217
+ }
@@ -40,6 +40,14 @@ export type ReusableTabularEvidence = {
40
40
  totalRows: number;
41
41
  };
42
42
 
43
+ export type AnalysisArtifactSummary = {
44
+ kind: 'table' | 'document';
45
+ title: string;
46
+ totalItems: number;
47
+ requestedItems?: number;
48
+ complete: boolean;
49
+ };
50
+
43
51
  /** Only reuse table evidence while its source is still present in the live data store. */
44
52
  export function tabularEvidenceDatasetIsAvailable(
45
53
  evidence: ReusableTabularEvidence | undefined,
@@ -283,6 +291,44 @@ function completeTabularEvidence(
283
291
  };
284
292
  }
285
293
 
294
+ function analysisPresentation(output: unknown): Record<string, unknown> | undefined {
295
+ const result = unwrapJson(output) as Record<string, unknown> | undefined;
296
+ const presentation = result?.presentation;
297
+ return presentation && typeof presentation === 'object' && !Array.isArray(presentation)
298
+ ? (presentation as Record<string, unknown>)
299
+ : undefined;
300
+ }
301
+
302
+ /** Read only completion metadata; the full paged artifact remains in the UI message. */
303
+ export function extractLatestAnalysisArtifact(
304
+ messages: readonly ChatMessage[],
305
+ ): AnalysisArtifactSummary | undefined {
306
+ for (const message of [...messages].reverse()) {
307
+ if (message.role !== 'assistant') continue;
308
+ for (const part of toolParts(message).reverse()) {
309
+ if (part.name !== 'executeAnalysis' && part.name !== 'analyzeCorpusWithCode') continue;
310
+ const presentation = analysisPresentation(part.output);
311
+ if (
312
+ (presentation?.kind !== 'table' && presentation?.kind !== 'document') ||
313
+ typeof presentation.title !== 'string' ||
314
+ typeof presentation.totalItems !== 'number' ||
315
+ typeof presentation.complete !== 'boolean'
316
+ )
317
+ continue;
318
+ return {
319
+ kind: presentation.kind,
320
+ title: presentation.title,
321
+ totalItems: presentation.totalItems,
322
+ ...(typeof presentation.requestedItems === 'number'
323
+ ? { requestedItems: presentation.requestedItems }
324
+ : {}),
325
+ complete: presentation.complete,
326
+ };
327
+ }
328
+ }
329
+ return undefined;
330
+ }
331
+
286
332
  /** Keep the complete bounded query result for an immediate export follow-up. */
287
333
  export function extractLatestTabularExportResult(
288
334
  messages: readonly ChatMessage[],
@@ -290,8 +336,18 @@ export function extractLatestTabularExportResult(
290
336
  for (const message of [...messages].reverse()) {
291
337
  if (message.role !== 'assistant') continue;
292
338
  const result = toolParts(message)
293
- .filter((part) => part.name === 'queryTabularData')
294
- .map((part) => completeTabularEvidence(part.input, part.output))
339
+ .filter(
340
+ (part) =>
341
+ part.name === 'queryTabularData' ||
342
+ part.name === 'executeAnalysis' ||
343
+ part.name === 'analyzeCorpusWithCode',
344
+ )
345
+ .map((part) =>
346
+ completeTabularEvidence(
347
+ part.input,
348
+ part.name === 'queryTabularData' ? part.output : analysisPresentation(part.output),
349
+ ),
350
+ )
295
351
  .find(Boolean);
296
352
  if (result) return result;
297
353
  }
@@ -348,8 +404,18 @@ export function extractConversationEvidence(
348
404
  if (message.role !== 'assistant') continue;
349
405
  const parts = toolParts(message);
350
406
  const table = parts
351
- .filter((part) => part.name === 'queryTabularData')
352
- .map((part) => tabularEvidence(part.input, part.output))
407
+ .filter(
408
+ (part) =>
409
+ part.name === 'queryTabularData' ||
410
+ part.name === 'executeAnalysis' ||
411
+ part.name === 'analyzeCorpusWithCode',
412
+ )
413
+ .map((part) =>
414
+ tabularEvidence(
415
+ part.input,
416
+ part.name === 'queryTabularData' ? part.output : analysisPresentation(part.output),
417
+ ),
418
+ )
353
419
  .find(Boolean);
354
420
  if (table) {
355
421
  return { sources: [], images: [], mediaAssetIds: [], tabular: table };
@@ -1,4 +1,8 @@
1
- import type { TabularDataset, TabularQueryRequest } from '@larkup/core/tabular-store';
1
+ import {
2
+ MAX_TABULAR_RESULT_LIMIT,
3
+ type TabularDataset,
4
+ type TabularQueryRequest,
5
+ } from '@larkup/core/tabular-store';
2
6
 
3
7
  export type InferredTabularPlan = {
4
8
  request: TabularQueryRequest;
@@ -78,6 +82,17 @@ function requestedSortOrder(text: string) {
78
82
  return undefined;
79
83
  }
80
84
 
85
+ /** Preserve an explicit long-list size instead of relying on a model default. */
86
+ export function requestedTabularResultLimit(text: string): number | undefined {
87
+ const match = text.match(
88
+ /\b(?:top|first|last|show|list|return|give(?:\s+me)?|display)\s+(?:the\s+)?(\d{1,4})\b/iu,
89
+ );
90
+ if (!match) return undefined;
91
+ const requested = Number(match[1]);
92
+ if (!Number.isSafeInteger(requested) || requested < 1) return undefined;
93
+ return Math.min(requested, MAX_TABULAR_RESULT_LIMIT);
94
+ }
95
+
81
96
  function normalizeAggregations(
82
97
  dataset: TabularDataset,
83
98
  request: TabularQueryRequest,
@@ -133,6 +148,7 @@ export function inferTabularPlan(
133
148
  const measureColumns =
134
149
  requestedMeasures.length > 0 ? requestedMeasures : numeric.length === 1 ? [numeric[0]] : [];
135
150
  const year = text.match(/\b(20\d{2})\b/)?.[1];
151
+ const explicitLimit = requestedTabularResultLimit(text);
136
152
 
137
153
  if (
138
154
  dateColumn &&
@@ -184,12 +200,15 @@ export function inferTabularPlan(
184
200
  ...(sortOrder
185
201
  ? { sortBy: `${aggregations[0].op}_${aggregations[0].column}`, sortOrder }
186
202
  : {}),
187
- limit: Math.min(request.limit ?? 120, 120),
203
+ limit: explicitLimit ?? Math.min(request.limit ?? 120, 120),
188
204
  },
189
205
  };
190
206
  }
191
207
 
192
208
  return {
193
- request: normalizeDistinctCountIntent(text, normalizeAggregations(dataset, request)),
209
+ request: {
210
+ ...normalizeDistinctCountIntent(text, normalizeAggregations(dataset, request)),
211
+ ...(explicitLimit ? { limit: explicitLimit } : {}),
212
+ },
194
213
  };
195
214
  }
@@ -59,9 +59,37 @@ export function hasCompletedTabularVisualizationStep(
59
59
  * column names.
60
60
  */
61
61
  export function requiresTabularSandbox(text: string): boolean {
62
- return /\b(?:join|correlat(?:e|ion)|regression|cluster(?:ing)?|forecast(?:ing)?|statistical|sandbox|python|matplotlib|seaborn|scikit(?:-learn)?|custom\s+(?:analysis|chart|visuali[sz]ation)|across\s+(?:sheets?|tables?|datasets?|files?)|between\s+(?:sheets?|tables?|datasets?|files?))\b/i.test(
63
- text,
62
+ return (
63
+ /\b(?:join|correlat(?:e|ion)|regression|cluster(?:ing)?|forecast(?:ing)?|statistical|sandbox|python|matplotlib|seaborn|scikit(?:-learn)?|custom\s+(?:analysis|chart|visuali[sz]ation)|across\s+(?:sheets?|tables?|datasets?|files?)|between\s+(?:sheets?|tables?|datasets?|files?))\b/i.test(
64
+ text,
65
+ ) || requestsLongAnalysisArtifact(text)
66
+ );
67
+ }
68
+
69
+ /**
70
+ * Long per-record transformations cannot be completed safely as streamed prose:
71
+ * the model will eventually hit its output ceiling and a later "continue" loses
72
+ * global constraints. Route these through one sandbox run and a paged artifact.
73
+ */
74
+ export function requestsLongAnalysisArtifact(text: string): boolean {
75
+ const hasManyTargets =
76
+ /\bfor\s+(?:the\s+)?(?:first|last|top)\s+\d+\b|\bfor\s+(?:each|every|all)\b|\b(?:each|every|all)\s+(?:of\s+)?(?:the\s+)?(?:people|participants?|rows?|records?|entries|documents?|items?|responses?)\b/i.test(
77
+ text,
78
+ );
79
+ const transformsEachTarget =
80
+ /\b(?:match(?:es|ed|ing)?|recommend(?:ation|ations|ed|ing)?|explain(?:ed|ing|s)?|summari[sz](?:e|ed|es|ing|ation)|profile(?:d|s|ing)?|assess(?:ed|ment|ments|ing)?|evaluat(?:e|ed|es|ing|ion)|classif(?:y|ied|ies|ication)|score(?:d|s|ing)?|rewrite|generate\s+(?:a\s+)?(?:description|report|answer))\b/i.test(
81
+ text,
82
+ );
83
+ return hasManyTargets && transformsEachTarget;
84
+ }
85
+
86
+ /** Preserve explicit batch sizes for sandbox artifacts (larger than chat prose limits). */
87
+ export function requestedLongAnalysisItemCount(text: string): number | undefined {
88
+ const match = text.match(
89
+ /\b(?:first|last|top)\s+(\d{1,5})\b|\bfor\s+(\d{1,5})\s+(?:people|participants?|rows?|records?|entries|documents?|items?|responses?)\b/i,
64
90
  );
91
+ const value = Number(match?.[1] ?? match?.[2]);
92
+ return Number.isInteger(value) && value > 0 ? Math.min(value, 5_000) : undefined;
65
93
  }
66
94
 
67
95
  /**
@@ -35,9 +35,15 @@ function formatSeriesLabel(value: string) {
35
35
  }
36
36
 
37
37
  export function requestsVisualization(text: string): boolean {
38
- return /\b(chart|graph|plot|visuali[sz]e|distribution|breakdown|trends?|compare|show\s+me)\b/i.test(
39
- text,
40
- );
38
+ const explicitChart =
39
+ /\b(?:charts?|graphs?|plots?|visuali[sz](?:e|ation)|diagram|histogram|scatter\s*plot|trends?)\b/iu.test(
40
+ text,
41
+ );
42
+ const visualPresentation =
43
+ /\b(?:show|display|draw|render|create|make)\b.{0,40}\b(?:distribution|breakdown|trend|comparison)\b/iu.test(
44
+ text,
45
+ );
46
+ return explicitChart || visualPresentation;
41
47
  }
42
48
 
43
49
  function normalizedTerms(value: string) {
@@ -1192,11 +1192,28 @@ function compactValue(value: unknown, toolName?: string): unknown {
1192
1192
  totalRows: result.totalRows,
1193
1193
  aggregationResults: result.aggregationResults,
1194
1194
  };
1195
- } else if (toolName === 'executeAnalysis') {
1195
+ } else if (toolName === 'executeAnalysis' || toolName === 'analyzeCorpusWithCode') {
1196
+ const presentation =
1197
+ result.presentation && typeof result.presentation === 'object'
1198
+ ? (result.presentation as Record<string, any>)
1199
+ : undefined;
1196
1200
  compact = {
1197
1201
  stdout: typeof result.stdout === 'string' ? result.stdout.slice(0, 4_000) : result.stdout,
1198
1202
  stderr: typeof result.stderr === 'string' ? result.stderr.slice(0, 1_000) : result.stderr,
1199
1203
  exitCode: result.exitCode,
1204
+ ...(presentation
1205
+ ? {
1206
+ presentation: {
1207
+ kind: presentation.kind,
1208
+ title: presentation.title,
1209
+ summary: presentation.summary,
1210
+ totalItems: presentation.totalItems,
1211
+ requestedItems: presentation.requestedItems,
1212
+ complete: presentation.complete,
1213
+ ...(Array.isArray(presentation.columns) ? { columns: presentation.columns } : {}),
1214
+ },
1215
+ }
1216
+ : {}),
1200
1217
  };
1201
1218
  } else if (
1202
1219
  (toolName === 'inspectPdfPages' || toolName === 'analyzePdfPages') &&